diff --git a/.github/translators.txt b/.github/translators.txt index 67eea4874..61a6697fc 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -512,3 +512,10 @@ David Olsen (dawin) :: Danish ltnzr :: French Frank Holler (holler.frank) :: German; German Informal Korab Arifi (korabidev) :: Albanian +Petr Husák (petrhusak) :: Czech +Bernardo Maia (bernardo.bmaia2) :: Portuguese, Brazilian +Amr (amr3k) :: Arabic +Tahsin Ahmed (tahsinahmed2012) :: Bengali +bojan_che :: Serbian (Cyrillic) +setiawan setiawan (culture.setiawan) :: Indonesian +Donald Mac Kenzie (kiuman) :: Norwegian Bokmal diff --git a/app/Access/Mfa/TotpService.php b/app/Access/Mfa/TotpService.php index 1d1af451a..f13637a3f 100644 --- a/app/Access/Mfa/TotpService.php +++ b/app/Access/Mfa/TotpService.php @@ -14,10 +14,9 @@ use PragmaRX\Google2FA\Support\Constants; class TotpService { - protected $google2fa; - - public function __construct(Google2FA $google2fa) - { + public function __construct( + protected Google2FA $google2fa + ) { $this->google2fa = $google2fa; // Use SHA1 as a default, Personal testing of other options in 2021 found // many apps lack support for other algorithms yet still will scan @@ -35,7 +34,7 @@ class TotpService } /** - * Generate a TOTP URL from secret key. + * Generate a TOTP URL from a secret key. */ public function generateUrl(string $secret, User $user): string { diff --git a/app/Activity/Models/MentionHistory.php b/app/Activity/Models/MentionHistory.php new file mode 100644 index 000000000..7386a4d74 --- /dev/null +++ b/app/Activity/Models/MentionHistory.php @@ -0,0 +1,20 @@ +entity instanceof Page)) { + throw new \InvalidArgumentException("Detail for comment mention notifications must be a comment on a page"); + } + + /** @var Page $page */ + $page = $detail->entity; + + $parser = new MentionParser(); + $mentionedUserIds = $parser->parseUserIdsFromHtml($detail->html); + $realMentionedUsers = User::whereIn('id', $mentionedUserIds)->get(); + + $receivingNotifications = $realMentionedUsers->filter(function (User $user) { + $prefs = new UserNotificationPreferences($user); + return $prefs->notifyOnCommentMentions(); + }); + $receivingNotificationsUserIds = $receivingNotifications->pluck('id')->toArray(); + + $userMentionsToLog = $realMentionedUsers; + + // When an edit, we check our history to see if we've already notified the user about this comment before + // so that we can filter them out to avoid double notifications. + if ($activity->type === ActivityType::COMMENT_UPDATE) { + $previouslyNotifiedUserIds = $this->getPreviouslyNotifiedUserIds($detail); + $receivingNotificationsUserIds = array_values(array_diff($receivingNotificationsUserIds, $previouslyNotifiedUserIds)); + $userMentionsToLog = $userMentionsToLog->filter(function (User $user) use ($previouslyNotifiedUserIds) { + return !in_array($user->id, $previouslyNotifiedUserIds); + }); + } + + $this->logMentions($userMentionsToLog, $detail, $user); + $this->sendNotificationToUserIds(CommentMentionNotification::class, $receivingNotificationsUserIds, $user, $detail, $page); + } + + /** + * @param Collection $mentionedUsers + */ + protected function logMentions(Collection $mentionedUsers, Comment $comment, User $fromUser): void + { + $mentions = []; + $now = Carbon::now(); + + foreach ($mentionedUsers as $mentionedUser) { + $mentions[] = [ + 'mentionable_type' => $comment->getMorphClass(), + 'mentionable_id' => $comment->id, + 'from_user_id' => $fromUser->id, + 'to_user_id' => $mentionedUser->id, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + + MentionHistory::query()->insert($mentions); + } + + protected function getPreviouslyNotifiedUserIds(Comment $comment): array + { + return MentionHistory::query() + ->where('mentionable_id', $comment->id) + ->where('mentionable_type', $comment->getMorphClass()) + ->pluck('to_user_id') + ->toArray(); + } +} diff --git a/app/Activity/Notifications/Messages/CommentMentionNotification.php b/app/Activity/Notifications/Messages/CommentMentionNotification.php new file mode 100644 index 000000000..de9e71963 --- /dev/null +++ b/app/Activity/Notifications/Messages/CommentMentionNotification.php @@ -0,0 +1,37 @@ +detail; + /** @var Page $page */ + $page = $comment->entity; + + $locale = $notifiable->getLocale(); + + $listLines = array_filter([ + $locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page), + $locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable), + $locale->trans('notifications.detail_commenter') => $this->user->name, + $locale->trans('notifications.detail_comment') => strip_tags($comment->html), + ]); + + return $this->newMailMessage($locale) + ->subject($locale->trans('notifications.comment_mention_subject', ['pageName' => $page->getShortName()])) + ->line($locale->trans('notifications.comment_mention_intro', ['appName' => setting('app-name')])) + ->line(new ListMessageLine($listLines)) + ->action($locale->trans('notifications.action_view_comment'), $page->getUrl('#comment' . $comment->local_id)) + ->line($this->buildReasonFooterLine($locale)); + } +} diff --git a/app/Activity/Notifications/NotificationManager.php b/app/Activity/Notifications/NotificationManager.php index 294f56ebb..8a6c26ffb 100644 --- a/app/Activity/Notifications/NotificationManager.php +++ b/app/Activity/Notifications/NotificationManager.php @@ -6,6 +6,7 @@ use BookStack\Activity\ActivityType; use BookStack\Activity\Models\Activity; use BookStack\Activity\Models\Loggable; use BookStack\Activity\Notifications\Handlers\CommentCreationNotificationHandler; +use BookStack\Activity\Notifications\Handlers\CommentMentionNotificationHandler; use BookStack\Activity\Notifications\Handlers\NotificationHandler; use BookStack\Activity\Notifications\Handlers\PageCreationNotificationHandler; use BookStack\Activity\Notifications\Handlers\PageUpdateNotificationHandler; @@ -48,5 +49,7 @@ class NotificationManager $this->registerHandler(ActivityType::PAGE_CREATE, PageCreationNotificationHandler::class); $this->registerHandler(ActivityType::PAGE_UPDATE, PageUpdateNotificationHandler::class); $this->registerHandler(ActivityType::COMMENT_CREATE, CommentCreationNotificationHandler::class); + $this->registerHandler(ActivityType::COMMENT_CREATE, CommentMentionNotificationHandler::class); + $this->registerHandler(ActivityType::COMMENT_UPDATE, CommentMentionNotificationHandler::class); } } diff --git a/app/Activity/Tools/MentionParser.php b/app/Activity/Tools/MentionParser.php new file mode 100644 index 000000000..d7bcac5e6 --- /dev/null +++ b/app/Activity/Tools/MentionParser.php @@ -0,0 +1,28 @@ +queryXPath('//a[@data-mention-user-id]'); + + foreach ($mentionLinks as $link) { + if ($link instanceof DOMElement) { + $id = intval($link->getAttribute('data-mention-user-id')); + if ($id > 0) { + $ids[] = $id; + } + } + } + + return array_values(array_unique($ids)); + } +} diff --git a/app/App/HomeController.php b/app/App/HomeController.php index 0585e0af5..00e2db3df 100644 --- a/app/App/HomeController.php +++ b/app/App/HomeController.php @@ -83,7 +83,7 @@ class HomeController extends Controller if ($homepageOption === 'bookshelves') { $shelves = $this->queries->shelves->visibleForListWithCover() ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder()) - ->paginate(18); + ->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000)); $data = array_merge($commonData, ['shelves' => $shelves]); return view('home.shelves', $data); @@ -92,7 +92,7 @@ class HomeController extends Controller if ($homepageOption === 'books') { $books = $this->queries->books->visibleForListWithCover() ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder()) - ->paginate(18); + ->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000)); $data = array_merge($commonData, ['books' => $books]); return view('home.books', $data); diff --git a/app/App/Providers/AppServiceProvider.php b/app/App/Providers/AppServiceProvider.php index 9012a07eb..debba7944 100644 --- a/app/App/Providers/AppServiceProvider.php +++ b/app/App/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace BookStack\App\Providers; use BookStack\Access\SocialDriverManager; +use BookStack\Activity\Models\Comment; use BookStack\Activity\Tools\ActivityLogger; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; @@ -73,6 +74,7 @@ class AppServiceProvider extends ServiceProvider 'book' => Book::class, 'chapter' => Chapter::class, 'page' => Page::class, + 'comment' => Comment::class, ]); } } diff --git a/app/App/SluggableInterface.php b/app/App/SluggableInterface.php index 96af49cd3..dd544f5ed 100644 --- a/app/App/SluggableInterface.php +++ b/app/App/SluggableInterface.php @@ -5,11 +5,9 @@ namespace BookStack\App; /** * Assigned to models that can have slugs. * Must have the below properties. + * + * @property string $slug */ interface SluggableInterface { - /** - * Regenerate the slug for this model. - */ - public function refreshSlug(): string; } diff --git a/app/Config/database.php b/app/Config/database.php index 5edafa777..6fc861312 100644 --- a/app/Config/database.php +++ b/app/Config/database.php @@ -81,7 +81,8 @@ return [ 'strict' => false, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + // @phpstan-ignore class.notFound + (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], diff --git a/app/Config/setting-defaults.php b/app/Config/setting-defaults.php index 88c4612ca..2f270b283 100644 --- a/app/Config/setting-defaults.php +++ b/app/Config/setting-defaults.php @@ -41,6 +41,7 @@ return [ 'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'), 'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'), 'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'), + 'notifications#comment-mentions' => true, ], ]; diff --git a/app/Entities/Controllers/BookController.php b/app/Entities/Controllers/BookController.php index cbf7ffb79..c94057fa9 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -8,6 +8,7 @@ use BookStack\Activity\Models\View; use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Entities\Queries\BookQueries; use BookStack\Entities\Queries\BookshelfQueries; +use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\Cloner; @@ -31,6 +32,7 @@ class BookController extends Controller protected ShelfContext $shelfContext, protected BookRepo $bookRepo, protected BookQueries $queries, + protected EntityQueries $entityQueries, protected BookshelfQueries $shelfQueries, protected ReferenceFetcher $referenceFetcher, ) { @@ -50,7 +52,7 @@ class BookController extends Controller $books = $this->queries->visibleForListWithCover() ->orderBy($listOptions->getSort(), $listOptions->getOrder()) - ->paginate(18); + ->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000)); $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->take(4)->get() : false; $popular = $this->queries->popularForList()->take(4)->get(); $new = $this->queries->visibleForList()->orderBy('created_at', 'desc')->take(4)->get(); @@ -127,7 +129,16 @@ class BookController extends Controller */ public function show(Request $request, ActivityQueries $activities, string $slug) { - $book = $this->queries->findVisibleBySlugOrFail($slug); + try { + $book = $this->queries->findVisibleBySlugOrFail($slug); + } catch (NotFoundException $exception) { + $book = $this->entityQueries->findVisibleByOldSlugs('book', $slug); + if (is_null($book)) { + throw $exception; + } + return redirect($book->getUrl()); + } + $bookChildren = (new BookContents($book))->getTree(true); $bookParentShelves = $book->shelves()->scopes('visible')->get(); diff --git a/app/Entities/Controllers/BookshelfController.php b/app/Entities/Controllers/BookshelfController.php index 8d7ffb8f9..f5f4a90bf 100644 --- a/app/Entities/Controllers/BookshelfController.php +++ b/app/Entities/Controllers/BookshelfController.php @@ -6,6 +6,7 @@ use BookStack\Activity\ActivityQueries; use BookStack\Activity\Models\View; use BookStack\Entities\Queries\BookQueries; use BookStack\Entities\Queries\BookshelfQueries; +use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Repos\BookshelfRepo; use BookStack\Entities\Tools\ShelfContext; use BookStack\Exceptions\ImageUploadException; @@ -23,6 +24,7 @@ class BookshelfController extends Controller public function __construct( protected BookshelfRepo $shelfRepo, protected BookshelfQueries $queries, + protected EntityQueries $entityQueries, protected BookQueries $bookQueries, protected ShelfContext $shelfContext, protected ReferenceFetcher $referenceFetcher, @@ -43,7 +45,7 @@ class BookshelfController extends Controller $shelves = $this->queries->visibleForListWithCover() ->orderBy($listOptions->getSort(), $listOptions->getOrder()) - ->paginate(18); + ->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000)); $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->get() : false; $popular = $this->queries->popularForList()->get(); $new = $this->queries->visibleForList() @@ -105,7 +107,16 @@ class BookshelfController extends Controller */ public function show(Request $request, ActivityQueries $activities, string $slug) { - $shelf = $this->queries->findVisibleBySlugOrFail($slug); + try { + $shelf = $this->queries->findVisibleBySlugOrFail($slug); + } catch (NotFoundException $exception) { + $shelf = $this->entityQueries->findVisibleByOldSlugs('bookshelf', $slug); + if (is_null($shelf)) { + throw $exception; + } + return redirect($shelf->getUrl()); + } + $this->checkOwnablePermission(Permission::BookshelfView, $shelf); $listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([ diff --git a/app/Entities/Controllers/ChapterController.php b/app/Entities/Controllers/ChapterController.php index a1af29de2..878ee42b5 100644 --- a/app/Entities/Controllers/ChapterController.php +++ b/app/Entities/Controllers/ChapterController.php @@ -77,7 +77,15 @@ class ChapterController extends Controller */ public function show(string $bookSlug, string $chapterSlug) { - $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); + try { + $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); + } catch (NotFoundException $exception) { + $chapter = $this->entityQueries->findVisibleByOldSlugs('chapter', $chapterSlug, $bookSlug); + if (is_null($chapter)) { + throw $exception; + } + return redirect($chapter->getUrl()); + } $sidebarTree = (new BookContents($chapter->book))->getTree(); $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get(); diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index 603d015ef..a648bc298 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -17,7 +17,6 @@ use BookStack\Entities\Tools\PageContent; use BookStack\Entities\Tools\PageEditActivity; use BookStack\Entities\Tools\PageEditorData; use BookStack\Exceptions\NotFoundException; -use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\PermissionsException; use BookStack\Http\Controller; use BookStack\Permissions\Permission; @@ -140,9 +139,7 @@ class PageController extends Controller try { $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); } catch (NotFoundException $e) { - $revision = $this->entityQueries->revisions->findLatestVersionBySlugs($bookSlug, $pageSlug); - $page = $revision->page ?? null; - + $page = $this->entityQueries->findVisibleByOldSlugs('page', $pageSlug, $bookSlug); if (is_null($page)) { throw $e; } diff --git a/app/Entities/Models/BookChild.php b/app/Entities/Models/BookChild.php index 4a2e52aed..9a8493c3a 100644 --- a/app/Entities/Models/BookChild.php +++ b/app/Entities/Models/BookChild.php @@ -2,7 +2,6 @@ namespace BookStack\Entities\Models; -use BookStack\References\ReferenceUpdater; use Illuminate\Database\Eloquent\Relations\BelongsTo; /** @@ -17,34 +16,10 @@ abstract class BookChild extends Entity { /** * Get the book this page sits in. + * @return BelongsTo */ public function book(): BelongsTo { return $this->belongsTo(Book::class)->withTrashed(); } - - /** - * Change the book that this entity belongs to. - */ - public function changeBook(int $newBookId): self - { - $oldUrl = $this->getUrl(); - $this->book_id = $newBookId; - $this->unsetRelation('book'); - $this->refreshSlug(); - $this->save(); - - if ($oldUrl !== $this->getUrl()) { - app()->make(ReferenceUpdater::class)->updateEntityReferences($this, $oldUrl); - } - - // Update all child pages if a chapter - if ($this instanceof Chapter) { - foreach ($this->pages()->withTrashed()->get() as $page) { - $page->changeBook($newBookId); - } - } - - return $this; - } } diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 641fe29d5..47e134626 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -13,7 +13,6 @@ use BookStack\Activity\Models\Viewable; use BookStack\Activity\Models\Watch; use BookStack\App\Model; use BookStack\App\SluggableInterface; -use BookStack\Entities\Tools\SlugGenerator; use BookStack\Permissions\JointPermissionBuilder; use BookStack\Permissions\Models\EntityPermission; use BookStack\Permissions\Models\JointPermission; @@ -405,16 +404,6 @@ abstract class Entity extends Model implements app()->make(SearchIndex::class)->indexEntity(clone $this); } - /** - * {@inheritdoc} - */ - public function refreshSlug(): string - { - $this->slug = app()->make(SlugGenerator::class)->generate($this, $this->name); - - return $this->slug; - } - /** * {@inheritdoc} */ @@ -441,6 +430,14 @@ abstract class Entity extends Model implements return $this->morphMany(Watch::class, 'watchable'); } + /** + * Get the related slug history for this entity. + */ + public function slugHistory(): MorphMany + { + return $this->morphMany(SlugHistory::class, 'sluggable'); + } + /** * {@inheritdoc} */ diff --git a/app/Entities/Models/Page.php b/app/Entities/Models/Page.php index 88c59bd1b..a1d3fc7b4 100644 --- a/app/Entities/Models/Page.php +++ b/app/Entities/Models/Page.php @@ -124,6 +124,14 @@ class Page extends BookChild return url('/' . implode('/', $parts)); } + /** + * Get the ID-based permalink for this page. + */ + public function getPermalink(): string + { + return url("/link/{$this->id}"); + } + /** * Get this page for JSON display. */ diff --git a/app/Entities/Models/SlugHistory.php b/app/Entities/Models/SlugHistory.php new file mode 100644 index 000000000..4041cedd9 --- /dev/null +++ b/app/Entities/Models/SlugHistory.php @@ -0,0 +1,28 @@ +hasMany(JointPermission::class, 'entity_id', 'sluggable_id') + ->whereColumn('joint_permissions.entity_type', '=', 'slug_history.sluggable_type'); + } +} diff --git a/app/Entities/Queries/EntityQueries.php b/app/Entities/Queries/EntityQueries.php index 91c6a4363..3ffa0adf3 100644 --- a/app/Entities/Queries/EntityQueries.php +++ b/app/Entities/Queries/EntityQueries.php @@ -4,6 +4,7 @@ namespace BookStack\Entities\Queries; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\EntityTable; +use BookStack\Entities\Tools\SlugHistory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\JoinClause; @@ -18,6 +19,7 @@ class EntityQueries public ChapterQueries $chapters, public PageQueries $pages, public PageRevisionQueries $revisions, + protected SlugHistory $slugHistory, ) { } @@ -31,9 +33,30 @@ class EntityQueries $explodedId = explode(':', $identifier); $entityType = $explodedId[0]; $entityId = intval($explodedId[1]); - $queries = $this->getQueriesForType($entityType); - return $queries->findVisibleById($entityId); + return $this->findVisibleById($entityType, $entityId); + } + + /** + * Find an entity by its ID. + */ + public function findVisibleById(string $type, int $id): ?Entity + { + $queries = $this->getQueriesForType($type); + return $queries->findVisibleById($id); + } + + /** + * Find an entity by looking up old slugs in the slug history. + */ + public function findVisibleByOldSlugs(string $type, string $slug, string $parentSlug = ''): ?Entity + { + $id = $this->slugHistory->lookupEntityIdUsingSlugs($type, $slug, $parentSlug); + if ($id === null) { + return null; + } + + return $this->findVisibleById($type, $id); } /** diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index fd88625cd..717e9c9f8 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -8,6 +8,8 @@ use BookStack\Entities\Models\HasCoverInterface; use BookStack\Entities\Models\HasDescriptionInterface; use BookStack\Entities\Models\Entity; use BookStack\Entities\Queries\PageQueries; +use BookStack\Entities\Tools\SlugGenerator; +use BookStack\Entities\Tools\SlugHistory; use BookStack\Exceptions\ImageUploadException; use BookStack\References\ReferenceStore; use BookStack\References\ReferenceUpdater; @@ -25,6 +27,8 @@ class BaseRepo protected ReferenceStore $referenceStore, protected PageQueries $pageQueries, protected BookSorter $bookSorter, + protected SlugGenerator $slugGenerator, + protected SlugHistory $slugHistory, ) { } @@ -43,7 +47,7 @@ class BaseRepo 'updated_by' => user()->id, 'owned_by' => user()->id, ]); - $entity->refreshSlug(); + $this->refreshSlug($entity); if ($entity instanceof HasDescriptionInterface) { $this->updateDescription($entity, $input); @@ -78,7 +82,7 @@ class BaseRepo $entity->updated_by = user()->id; if ($entity->isDirty('name') || empty($entity->slug)) { - $entity->refreshSlug(); + $this->refreshSlug($entity); } if ($entity instanceof HasDescriptionInterface) { @@ -155,4 +159,13 @@ class BaseRepo $entity->descriptionInfo()->set('', $input['description']); } } + + /** + * Refresh the slug for the given entity. + */ + public function refreshSlug(Entity $entity): void + { + $this->slugHistory->recordForEntity($entity); + $this->slugGenerator->regenerateForEntity($entity); + } } diff --git a/app/Entities/Repos/ChapterRepo.php b/app/Entities/Repos/ChapterRepo.php index d5feb30fd..a528eece0 100644 --- a/app/Entities/Repos/ChapterRepo.php +++ b/app/Entities/Repos/ChapterRepo.php @@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Tools\BookContents; +use BookStack\Entities\Tools\ParentChanger; use BookStack\Entities\Tools\TrashCan; use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\PermissionsException; @@ -21,6 +22,7 @@ class ChapterRepo protected BaseRepo $baseRepo, protected EntityQueries $entityQueries, protected TrashCan $trashCan, + protected ParentChanger $parentChanger, ) { } @@ -97,7 +99,7 @@ class ChapterRepo } return (new DatabaseTransaction(function () use ($chapter, $parent) { - $chapter = $chapter->changeBook($parent->id); + $this->parentChanger->changeBook($chapter, $parent->id); $chapter->rebuildPermissions(); Activity::add(ActivityType::CHAPTER_MOVE, $chapter); diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index f2e558210..bc590785d 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -12,6 +12,7 @@ use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\PageContent; use BookStack\Entities\Tools\PageEditorType; +use BookStack\Entities\Tools\ParentChanger; use BookStack\Entities\Tools\TrashCan; use BookStack\Exceptions\MoveOperationException; use BookStack\Exceptions\PermissionsException; @@ -31,6 +32,7 @@ class PageRepo protected ReferenceStore $referenceStore, protected ReferenceUpdater $referenceUpdater, protected TrashCan $trashCan, + protected ParentChanger $parentChanger, ) { } @@ -242,7 +244,7 @@ class PageRepo } $page->updated_by = user()->id; - $page->refreshSlug(); + $this->baseRepo->refreshSlug($page); $page->save(); $page->indexForSearch(); $this->referenceStore->updateForEntity($page); @@ -284,7 +286,7 @@ class PageRepo return (new DatabaseTransaction(function () use ($page, $parent) { $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null; $newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id; - $page = $page->changeBook($newBookId); + $this->parentChanger->changeBook($page, $newBookId); $page->rebuildPermissions(); Activity::add(ActivityType::PAGE_MOVE, $page); diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php index ff42ae6e4..64c48c351 100644 --- a/app/Entities/Tools/Cloner.php +++ b/app/Entities/Tools/Cloner.php @@ -13,30 +13,47 @@ use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Repos\PageRepo; use BookStack\Permissions\Permission; +use BookStack\References\ReferenceChangeContext; +use BookStack\References\ReferenceUpdater; use BookStack\Uploads\Image; use BookStack\Uploads\ImageService; use Illuminate\Http\UploadedFile; class Cloner { + protected ReferenceChangeContext $referenceChangeContext; + public function __construct( protected PageRepo $pageRepo, protected ChapterRepo $chapterRepo, protected BookRepo $bookRepo, protected ImageService $imageService, + protected ReferenceUpdater $referenceUpdater, ) { + $this->referenceChangeContext = new ReferenceChangeContext(); } /** * Clone the given page into the given parent using the provided name. */ public function clonePage(Page $original, Entity $parent, string $newName): Page + { + $context = $this->newReferenceChangeContext(); + $page = $this->createPageClone($original, $parent, $newName); + $this->referenceUpdater->changeReferencesUsingContext($context); + return $page; + } + + protected function createPageClone(Page $original, Entity $parent, string $newName): Page { $copyPage = $this->pageRepo->getNewDraftPage($parent); $pageData = $this->entityToInputData($original); $pageData['name'] = $newName; - return $this->pageRepo->publishDraft($copyPage, $pageData); + $newPage = $this->pageRepo->publishDraft($copyPage, $pageData); + $this->referenceChangeContext->add($original, $newPage); + + return $newPage; } /** @@ -44,6 +61,14 @@ class Cloner * Clones all child pages. */ public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter + { + $context = $this->newReferenceChangeContext(); + $chapter = $this->createChapterClone($original, $parent, $newName); + $this->referenceUpdater->changeReferencesUsingContext($context); + return $chapter; + } + + protected function createChapterClone(Chapter $original, Book $parent, string $newName): Chapter { $chapterDetails = $this->entityToInputData($original); $chapterDetails['name'] = $newName; @@ -53,10 +78,12 @@ class Cloner if (userCan(Permission::PageCreate, $copyChapter)) { /** @var Page $page */ foreach ($original->getVisiblePages() as $page) { - $this->clonePage($page, $copyChapter, $page->name); + $this->createPageClone($page, $copyChapter, $page->name); } } + $this->referenceChangeContext->add($original, $copyChapter); + return $copyChapter; } @@ -65,6 +92,14 @@ class Cloner * Clones all child chapters and pages. */ public function cloneBook(Book $original, string $newName): Book + { + $context = $this->newReferenceChangeContext(); + $book = $this->createBookClone($original, $newName); + $this->referenceUpdater->changeReferencesUsingContext($context); + return $book; + } + + protected function createBookClone(Book $original, string $newName): Book { $bookDetails = $this->entityToInputData($original); $bookDetails['name'] = $newName; @@ -76,11 +111,11 @@ class Cloner $directChildren = $original->getDirectVisibleChildren(); foreach ($directChildren as $child) { if ($child instanceof Chapter && userCan(Permission::ChapterCreate, $copyBook)) { - $this->cloneChapter($child, $copyBook, $child->name); + $this->createChapterClone($child, $copyBook, $child->name); } if ($child instanceof Page && !$child->draft && userCan(Permission::PageCreate, $copyBook)) { - $this->clonePage($child, $copyBook, $child->name); + $this->createPageClone($child, $copyBook, $child->name); } } @@ -92,6 +127,8 @@ class Cloner } } + $this->referenceChangeContext->add($original, $copyBook); + return $copyBook; } @@ -155,4 +192,10 @@ class Cloner return $tags; } + + protected function newReferenceChangeContext(): ReferenceChangeContext + { + $this->referenceChangeContext = new ReferenceChangeContext(); + return $this->referenceChangeContext; + } } diff --git a/app/Entities/Tools/HierarchyTransformer.php b/app/Entities/Tools/HierarchyTransformer.php index fa45fcd11..c58d29bd0 100644 --- a/app/Entities/Tools/HierarchyTransformer.php +++ b/app/Entities/Tools/HierarchyTransformer.php @@ -17,7 +17,8 @@ class HierarchyTransformer protected BookRepo $bookRepo, protected BookshelfRepo $shelfRepo, protected Cloner $cloner, - protected TrashCan $trashCan + protected TrashCan $trashCan, + protected ParentChanger $parentChanger, ) { } @@ -35,7 +36,7 @@ class HierarchyTransformer foreach ($chapter->pages as $page) { $page->chapter_id = 0; $page->save(); - $page->changeBook($book->id); + $this->parentChanger->changeBook($page, $book->id); } $this->trashCan->destroyEntity($chapter); diff --git a/app/Entities/Tools/ParentChanger.php b/app/Entities/Tools/ParentChanger.php new file mode 100644 index 000000000..00ce42aae --- /dev/null +++ b/app/Entities/Tools/ParentChanger.php @@ -0,0 +1,40 @@ +getUrl(); + + $child->book_id = $newBookId; + $child->unsetRelation('book'); + $this->slugGenerator->regenerateForEntity($child); + $child->save(); + + if ($oldUrl !== $child->getUrl()) { + $this->referenceUpdater->updateEntityReferences($child, $oldUrl); + } + + // Update all child pages if a chapter + if ($child instanceof Chapter) { + foreach ($child->pages()->withTrashed()->get() as $page) { + $this->changeBook($page, $newBookId); + } + } + } +} diff --git a/app/Entities/Tools/SlugGenerator.php b/app/Entities/Tools/SlugGenerator.php index fb9123187..6eec84a91 100644 --- a/app/Entities/Tools/SlugGenerator.php +++ b/app/Entities/Tools/SlugGenerator.php @@ -5,12 +5,14 @@ namespace BookStack\Entities\Tools; use BookStack\App\Model; use BookStack\App\SluggableInterface; use BookStack\Entities\Models\BookChild; +use BookStack\Entities\Models\Entity; +use BookStack\Users\Models\User; use Illuminate\Support\Str; class SlugGenerator { /** - * Generate a fresh slug for the given entity. + * Generate a fresh slug for the given item. * The slug will be generated so that it doesn't conflict within the same parent item. */ public function generate(SluggableInterface&Model $model, string $slugSource): string @@ -23,6 +25,26 @@ class SlugGenerator return $slug; } + /** + * Regenerate the slug for the given entity. + */ + public function regenerateForEntity(Entity $entity): string + { + $entity->slug = $this->generate($entity, $entity->name); + + return $entity->slug; + } + + /** + * Regenerate the slug for a user. + */ + public function regenerateForUser(User $user): string + { + $user->slug = $this->generate($user, $user->name); + + return $user->slug; + } + /** * Format a name as a URL slug. */ diff --git a/app/Entities/Tools/SlugHistory.php b/app/Entities/Tools/SlugHistory.php new file mode 100644 index 000000000..2c8d88129 --- /dev/null +++ b/app/Entities/Tools/SlugHistory.php @@ -0,0 +1,97 @@ +id || !$entity->slug) { + return; + } + + $parentSlug = null; + if ($entity instanceof BookChild) { + $parentSlug = $entity->book()->first()?->slug; + } + + $latest = $this->getLatestEntryForEntity($entity); + if ($latest && $latest->slug === $entity->slug && $latest->parent_slug === $parentSlug) { + return; + } + + $info = [ + 'sluggable_type' => $entity->getMorphClass(), + 'sluggable_id' => $entity->id, + 'slug' => $entity->slug, + 'parent_slug' => $parentSlug, + ]; + + $entry = new SlugHistoryModel(); + $entry->forceFill($info); + $entry->save(); + + if ($entity instanceof Book) { + $this->recordForBookChildren($entity); + } + } + + protected function recordForBookChildren(Book $book): void + { + $query = EntityTable::query() + ->select(['type', 'id', 'slug', DB::raw("'{$book->slug}' as parent_slug"), DB::raw('now() as created_at'), DB::raw('now() as updated_at')]) + ->where('book_id', '=', $book->id) + ->whereNotNull('book_id'); + + SlugHistoryModel::query()->insertUsing( + ['sluggable_type', 'sluggable_id', 'slug', 'parent_slug', 'created_at', 'updated_at'], + $query + ); + } + + /** + * Find the latest visible entry for an entity which uses the given slug(s) in the history. + */ + public function lookupEntityIdUsingSlugs(string $type, string $slug, string $parentSlug = ''): ?int + { + $query = SlugHistoryModel::query() + ->where('sluggable_type', '=', $type) + ->where('slug', '=', $slug); + + if ($parentSlug) { + $query->where('parent_slug', '=', $parentSlug); + } + + $query = $this->permissions->restrictEntityRelationQuery($query, 'slug_history', 'sluggable_id', 'sluggable_type'); + + /** @var SlugHistoryModel|null $result */ + $result = $query->orderBy('created_at', 'desc')->first(); + + return $result?->sluggable_id; + } + + protected function getLatestEntryForEntity(Entity $entity): SlugHistoryModel|null + { + return SlugHistoryModel::query() + ->where('sluggable_type', '=', $entity->getMorphClass()) + ->where('sluggable_id', '=', $entity->id) + ->orderBy('created_at', 'desc') + ->first(); + } +} diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index c298169c3..96645aebf 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -388,7 +388,7 @@ class TrashCan /** * Update entity relations to remove or update outstanding connections. */ - protected function destroyCommonRelations(Entity $entity) + protected function destroyCommonRelations(Entity $entity): void { Activity::removeEntity($entity); $entity->views()->delete(); @@ -402,6 +402,7 @@ class TrashCan $entity->watches()->delete(); $entity->referencesTo()->delete(); $entity->referencesFrom()->delete(); + $entity->slugHistory()->delete(); if ($entity instanceof HasCoverInterface && $entity->coverInfo()->exists()) { $imageService = app()->make(ImageService::class); diff --git a/app/References/ReferenceChangeContext.php b/app/References/ReferenceChangeContext.php new file mode 100644 index 000000000..27de0e2d2 --- /dev/null +++ b/app/References/ReferenceChangeContext.php @@ -0,0 +1,45 @@ + + */ + protected array $changes = []; + + public function add(Entity $oldEntity, Entity $newEntity): void + { + $this->changes[] = [$oldEntity, $newEntity]; + } + + /** + * Get all the new entities from the changes. + */ + public function getNewEntities(): array + { + return array_column($this->changes, 1); + } + + /** + * Get all the old entities from the changes. + */ + public function getOldEntities(): array + { + return array_column($this->changes, 0); + } + + public function getNewForOld(Entity $oldEntity): ?Entity + { + foreach ($this->changes as [$old, $new]) { + if ($old->id === $oldEntity->id && $old->type === $oldEntity->type) { + return $new; + } + } + return null; + } +} diff --git a/app/References/ReferenceUpdater.php b/app/References/ReferenceUpdater.php index 06b3389ba..42de72fde 100644 --- a/app/References/ReferenceUpdater.php +++ b/app/References/ReferenceUpdater.php @@ -5,7 +5,6 @@ namespace BookStack\References; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\HasDescriptionInterface; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\EntityContainerData; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\RevisionRepo; use BookStack\Util\HtmlDocument; @@ -30,6 +29,47 @@ class ReferenceUpdater } } + /** + * Change existing references for a range of entities using the given context. + */ + public function changeReferencesUsingContext(ReferenceChangeContext $context): void + { + $bindings = []; + foreach ($context->getOldEntities() as $old) { + $bindings[] = $old->getMorphClass(); + $bindings[] = $old->id; + } + + // No targets to update within the context, so no need to continue. + if (count($bindings) < 2) { + return; + } + + $toReferenceQuery = '(to_type, to_id) IN (' . rtrim(str_repeat('(?,?),', count($bindings) / 2), ',') . ')'; + + // Cycle each new entity in the context + foreach ($context->getNewEntities() as $new) { + // For each, get all references from it which lead to other items within the context of the change + $newReferencesInContext = $new->referencesFrom()->whereRaw($toReferenceQuery, $bindings)->get(); + // For each reference, update the URL and the reference entry + foreach ($newReferencesInContext as $reference) { + $oldToEntity = $reference->to; + $newToEntity = $context->getNewForOld($oldToEntity); + if ($newToEntity === null) { + continue; + } + + $this->updateReferencesWithinEntity($new, $oldToEntity->getUrl(), $newToEntity->getUrl()); + if ($newToEntity instanceof Page && $oldToEntity instanceof Page) { + $this->updateReferencesWithinEntity($new, $oldToEntity->getPermalink(), $newToEntity->getPermalink()); + } + $reference->to_id = $newToEntity->id; + $reference->to_type = $newToEntity->getMorphClass(); + $reference->save(); + } + } + } + /** * @return Reference[] */ diff --git a/app/Search/SearchController.php b/app/Search/SearchController.php index 9586beffb..8a6a5bbde 100644 --- a/app/Search/SearchController.php +++ b/app/Search/SearchController.php @@ -25,11 +25,12 @@ class SearchController extends Controller $searchOpts = SearchOptions::fromRequest($request); $fullSearchString = $searchOpts->toString(); $page = intval($request->get('page', '0')) ?: 1; + $count = setting()->getInteger('lists-page-count-search', 18, 1, 1000); - $results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, 20); + $results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, $count); $formatter->format($results['results']->all(), $searchOpts); - $paginator = new LengthAwarePaginator($results['results'], $results['total'], 20, $page); - $paginator->setPath('/search'); + $paginator = new LengthAwarePaginator($results['results'], $results['total'], $count, $page); + $paginator->setPath(url('/search')); $paginator->appends($request->except('page')); $this->setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString])); diff --git a/app/Settings/AppSettingsStore.php b/app/Settings/AppSettingsStore.php index e6fc466ba..e098d87f8 100644 --- a/app/Settings/AppSettingsStore.php +++ b/app/Settings/AppSettingsStore.php @@ -14,7 +14,7 @@ class AppSettingsStore ) { } - public function storeFromUpdateRequest(Request $request, string $category) + public function storeFromUpdateRequest(Request $request, string $category): void { $this->storeSimpleSettings($request); if ($category === 'customization') { @@ -76,7 +76,7 @@ class AppSettingsStore protected function storeSimpleSettings(Request $request): void { foreach ($request->all() as $name => $value) { - if (strpos($name, 'setting-') !== 0) { + if (!str_starts_with($name, 'setting-')) { continue; } @@ -85,7 +85,7 @@ class AppSettingsStore } } - protected function destroyExistingSettingImage(string $settingKey) + protected function destroyExistingSettingImage(string $settingKey): void { $existingVal = setting()->get($settingKey); if ($existingVal) { diff --git a/app/Settings/SettingService.php b/app/Settings/SettingService.php index 31debdaea..e0b136180 100644 --- a/app/Settings/SettingService.php +++ b/app/Settings/SettingService.php @@ -28,6 +28,21 @@ class SettingService return $this->formatValue($value, $default); } + /** + * Get a setting from the database as an integer. + * Returns the default value if not found or not an integer, and clamps the value to the given min/max range. + */ + public function getInteger(string $key, int $default, int $min = 0, int $max = PHP_INT_MAX): int + { + $value = $this->get($key, $default); + if (!is_numeric($value)) { + return $default; + } + + $int = intval($value); + return max($min, min($max, $int)); + } + /** * Get a value from the session instead of the main store option. */ diff --git a/app/Settings/UserNotificationPreferences.php b/app/Settings/UserNotificationPreferences.php index 5b267b533..752d92de6 100644 --- a/app/Settings/UserNotificationPreferences.php +++ b/app/Settings/UserNotificationPreferences.php @@ -26,9 +26,14 @@ class UserNotificationPreferences return $this->getNotificationSetting('comment-replies'); } + public function notifyOnCommentMentions(): bool + { + return $this->getNotificationSetting('comment-mentions'); + } + public function updateFromSettingsArray(array $settings) { - $allowList = ['own-page-changes', 'own-page-comments', 'comment-replies']; + $allowList = ['own-page-changes', 'own-page-comments', 'comment-replies', 'comment-mentions']; foreach ($settings as $setting => $status) { if (!in_array($setting, $allowList)) { continue; diff --git a/app/Sorting/BookSorter.php b/app/Sorting/BookSorter.php index 99e307e35..b4f93d47b 100644 --- a/app/Sorting/BookSorter.php +++ b/app/Sorting/BookSorter.php @@ -8,12 +8,14 @@ use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\EntityQueries; +use BookStack\Entities\Tools\ParentChanger; use BookStack\Permissions\Permission; class BookSorter { public function __construct( protected EntityQueries $queries, + protected ParentChanger $parentChanger, ) { } @@ -155,7 +157,7 @@ class BookSorter // Action the required changes if ($bookChanged) { - $model = $model->changeBook($newBook->id); + $this->parentChanger->changeBook($model, $newBook->id); } if ($model instanceof Page && $chapterChanged) { diff --git a/app/Uploads/ImageResizer.php b/app/Uploads/ImageResizer.php index 8d7571c82..9ed0d0f75 100644 --- a/app/Uploads/ImageResizer.php +++ b/app/Uploads/ImageResizer.php @@ -55,7 +55,7 @@ class ImageResizer /** * Get the thumbnail for an image. - * If $keepRatio is true only the width will be used. + * If $keepRatio is true, only the width will be used. * Checks the cache then storage to avoid creating / accessing the filesystem on every check. * * @throws Exception @@ -84,7 +84,7 @@ class ImageResizer return $this->storage->getPublicUrl($cachedThumbPath); } - // If thumbnail has already been generated, serve that and cache path + // If a thumbnail has already been generated, serve that and cache path $disk = $this->storage->getDisk($image->type); if (!$shouldCreate && $disk->exists($thumbFilePath)) { Cache::put($thumbCacheKey, $thumbFilePath, static::THUMBNAIL_CACHE_TIME); @@ -110,7 +110,7 @@ class ImageResizer } /** - * Resize the image of given data to the specified size, and return the new image data. + * Resize the image of given data to the specified size and return the new image data. * Format will remain the same as the input format, unless specified. * * @throws ImageUploadException @@ -125,6 +125,7 @@ class ImageResizer try { $thumb = $this->interventionFromImageData($imageData, $format); } catch (Exception $e) { + Log::error('Failed to resize image with error:' . $e->getMessage()); throw new ImageUploadException(trans('errors.cannot_create_thumbs')); } @@ -154,17 +155,21 @@ class ImageResizer /** * Create an intervention image instance from the given image data. - * Performs some manual library usage to ensure image is specifically loaded + * Performs some manual library usage to ensure the image is specifically loaded * from given binary data instead of data being misinterpreted. */ protected function interventionFromImageData(string $imageData, ?string $fileType): InterventionImage { + if (!extension_loaded('gd')) { + throw new ImageUploadException('The PHP "gd" extension is required to resize images, but is missing.'); + } + $manager = new ImageManager( new Driver(), autoOrientation: false, ); - // Ensure gif images are decoded natively instead of deferring to intervention GIF + // Ensure GIF images are decoded natively instead of deferring to intervention GIF // handling since we don't need the added animation support. $isGif = $fileType === 'gif'; $decoder = $isGif ? NativeObjectDecoder::class : BinaryImageDecoder::class; @@ -223,7 +228,7 @@ class ImageResizer } /** - * Checks if the image is a gif. Returns true if it is, else false. + * Checks if the image is a GIF. Returns true if it is, else false. */ protected function isGif(Image $image): bool { @@ -250,7 +255,7 @@ class ImageResizer /** * Check if the given avif image data represents an animated image. - * This is based up the answer here: https://stackoverflow.com/a/79457313 + * This is based upon the answer here: https://stackoverflow.com/a/79457313 */ protected function isAnimatedAvifData(string &$imageData): bool { diff --git a/app/Users/Controllers/UserSearchController.php b/app/Users/Controllers/UserSearchController.php index a2543b7ee..bc0543cab 100644 --- a/app/Users/Controllers/UserSearchController.php +++ b/app/Users/Controllers/UserSearchController.php @@ -5,6 +5,7 @@ namespace BookStack\Users\Controllers; use BookStack\Http\Controller; use BookStack\Permissions\Permission; use BookStack\Users\Models\User; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; class UserSearchController extends Controller @@ -34,8 +35,43 @@ class UserSearchController extends Controller $query->where('name', 'like', '%' . $search . '%'); } + /** @var Collection $users */ + $users = $query->get(); + return view('form.user-select-list', [ - 'users' => $query->get(), + 'users' => $users, + ]); + } + + /** + * Search users in the system, with the response formatted + * for use in a list of mentions. + */ + public function forMentions(Request $request) + { + $hasPermission = !user()->isGuest() && ( + userCan(Permission::CommentCreateAll) + || userCan(Permission::CommentUpdate) + ); + + if (!$hasPermission) { + $this->showPermissionError(); + } + + $search = $request->get('search', ''); + $query = User::query() + ->orderBy('name', 'asc') + ->take(20); + + if (!empty($search)) { + $query->where('name', 'like', '%' . $search . '%'); + } + + /** @var Collection $users */ + $users = $query->get(); + + return view('form.user-mention-list', [ + 'users' => $users, ]); } } diff --git a/app/Users/Models/User.php b/app/Users/Models/User.php index 8bbf11695..50efdcdad 100644 --- a/app/Users/Models/User.php +++ b/app/Users/Models/User.php @@ -11,7 +11,6 @@ use BookStack\Activity\Models\Watch; use BookStack\Api\ApiToken; use BookStack\App\Model; use BookStack\App\SluggableInterface; -use BookStack\Entities\Tools\SlugGenerator; use BookStack\Permissions\Permission; use BookStack\Translation\LocaleDefinition; use BookStack\Translation\LocaleManager; @@ -358,14 +357,4 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon { return "({$this->id}) {$this->name}"; } - - /** - * {@inheritdoc} - */ - public function refreshSlug(): string - { - $this->slug = app()->make(SlugGenerator::class)->generate($this, $this->name); - - return $this->slug; - } } diff --git a/app/Users/UserRepo.php b/app/Users/UserRepo.php index 894d7c01f..2c0897cef 100644 --- a/app/Users/UserRepo.php +++ b/app/Users/UserRepo.php @@ -5,6 +5,7 @@ namespace BookStack\Users; use BookStack\Access\UserInviteException; use BookStack\Access\UserInviteService; use BookStack\Activity\ActivityType; +use BookStack\Entities\Tools\SlugGenerator; use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\UserUpdateException; use BookStack\Facades\Activity; @@ -21,7 +22,8 @@ class UserRepo { public function __construct( protected UserAvatars $userAvatar, - protected UserInviteService $inviteService + protected UserInviteService $inviteService, + protected SlugGenerator $slugGenerator, ) { } @@ -63,7 +65,7 @@ class UserRepo $user->email_confirmed = $emailConfirmed; $user->external_auth_id = $data['external_auth_id'] ?? ''; - $user->refreshSlug(); + $this->slugGenerator->regenerateForUser($user); $user->save(); if (!empty($data['language'])) { @@ -109,7 +111,7 @@ class UserRepo { if (!empty($data['name'])) { $user->name = $data['name']; - $user->refreshSlug(); + $this->slugGenerator->regenerateForUser($user); } if (!empty($data['email']) && $manageUsersAllowed) { diff --git a/app/Util/HtmlDescriptionFilter.php b/app/Util/HtmlDescriptionFilter.php index d4f7d2c8f..1baa11ffc 100644 --- a/app/Util/HtmlDescriptionFilter.php +++ b/app/Util/HtmlDescriptionFilter.php @@ -19,7 +19,7 @@ class HtmlDescriptionFilter */ protected static array $allowedAttrsByElements = [ 'p' => [], - 'a' => ['href', 'title', 'target'], + 'a' => ['href', 'title', 'target', 'data-mention-user-id'], 'ol' => [], 'ul' => [], 'li' => [], diff --git a/bookstack-system-cli b/bookstack-system-cli index c55c5a8a7..1a518e644 100755 Binary files a/bookstack-system-cli and b/bookstack-system-cli differ diff --git a/composer.json b/composer.json index 7e7412976..9d47f5548 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "league/oauth2-client": "^2.6", "onelogin/php-saml": "^4.3.1", "phpseclib/phpseclib": "^3.0", - "pragmarx/google2fa": "^8.0", + "pragmarx/google2fa": "^9.0", "predis/predis": "^3.2", "socialiteproviders/discord": "^4.1", "socialiteproviders/gitlab": "^4.1", @@ -47,7 +47,7 @@ "nunomaduro/collision": "^8.6", "larastan/larastan": "^v3.0", "phpunit/phpunit": "^11.5", - "squizlabs/php_codesniffer": "^3.7", + "squizlabs/php_codesniffer": "^4.0.1", "ssddanbrown/asserthtml": "^3.1" }, "autoload": { diff --git a/composer.lock b/composer.lock index 98f2d4606..93bf172c6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9f946fb1755acd72dcc63d0c3af637e9", + "content-hash": "556613432c8fb7d8f96bcf637c8c07a9", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.366.4", + "version": "3.369.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "1861cc8eede21cdaab0732fd44f43f19ddf1effd" + "reference": "5e3f541e344d71f3b9591fe1d94d9576530fa795" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1861cc8eede21cdaab0732fd44f43f19ddf1effd", - "reference": "1861cc8eede21cdaab0732fd44f43f19ddf1effd", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5e3f541e344d71f3b9591fe1d94d9576530fa795", + "reference": "5e3f541e344d71f3b9591fe1d94d9576530fa795", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.366.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.2" }, - "time": "2025-12-09T19:21:22+00:00" + "time": "2025-12-23T19:21:43+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1596,16 +1596,16 @@ }, { "name": "intervention/image", - "version": "3.11.5", + "version": "3.11.6", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "76e96d3809d53dd8d597005634a733d4b2f6c2c3" + "reference": "5f6d27d9fd56312c47f347929e7ac15345c605a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/76e96d3809d53dd8d597005634a733d4b2f6c2c3", - "reference": "76e96d3809d53dd8d597005634a733d4b2f6c2c3", + "url": "https://api.github.com/repos/Intervention/image/zipball/5f6d27d9fd56312c47f347929e7ac15345c605a1", + "reference": "5f6d27d9fd56312c47f347929e7ac15345c605a1", "shasum": "" }, "require": { @@ -1652,7 +1652,7 @@ ], "support": { "issues": "https://github.com/Intervention/image/issues", - "source": "https://github.com/Intervention/image/tree/3.11.5" + "source": "https://github.com/Intervention/image/tree/3.11.6" }, "funding": [ { @@ -1668,7 +1668,7 @@ "type": "ko_fi" } ], - "time": "2025-11-29T11:18:34+00:00" + "time": "2025-12-17T13:38:29+00:00" }, { "name": "knplabs/knp-snappy", @@ -1739,16 +1739,16 @@ }, { "name": "laravel/framework", - "version": "v12.42.0", + "version": "v12.44.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "509b33095564c5165366d81bbaa0afaac28abe75" + "reference": "592bbf1c036042958332eb98e3e8131b29102f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/509b33095564c5165366d81bbaa0afaac28abe75", - "reference": "509b33095564c5165366d81bbaa0afaac28abe75", + "url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33", + "reference": "592bbf1c036042958332eb98e3e8131b29102f33", "shasum": "" }, "require": { @@ -1957,7 +1957,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-12-09T15:51:23+00:00" + "time": "2025-12-23T15:29:43+00:00" }, { "name": "laravel/prompts", @@ -3469,16 +3469,16 @@ }, { "name": "nette/utils", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", - "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72", + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72", "shasum": "" }, "require": { @@ -3552,9 +3552,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.0" + "source": "https://github.com/nette/utils/tree/v4.1.1" }, - "time": "2025-12-01T17:49:23+00:00" + "time": "2025-12-22T12:14:32+00:00" }, { "name": "nikic/php-parser", @@ -3961,16 +3961,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.47", + "version": "3.0.48", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d" + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d", - "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89", + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89", "shasum": "" }, "require": { @@ -4051,7 +4051,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.47" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.48" }, "funding": [ { @@ -4067,20 +4067,20 @@ "type": "tidelift" } ], - "time": "2025-10-06T01:07:24+00:00" + "time": "2025-12-15T11:51:42+00:00" }, { "name": "pragmarx/google2fa", - "version": "v8.0.3", + "version": "v9.0.0", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", - "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", "shasum": "" }, "require": { @@ -4117,9 +4117,9 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" }, - "time": "2024-09-05T11:56:40+00:00" + "time": "2025-09-19T22:51:08+00:00" }, { "name": "predis/predis", @@ -4598,16 +4598,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.16", + "version": "v0.12.18", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "ee6d5028be4774f56c6c2c85ec4e6bc9acfe6b67" + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ee6d5028be4774f56c6c2c85ec4e6bc9acfe6b67", - "reference": "ee6d5028be4774f56c6c2c85ec4e6bc9acfe6b67", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196", + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196", "shasum": "" }, "require": { @@ -4671,9 +4671,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.16" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.18" }, - "time": "2025-12-07T03:39:01+00:00" + "time": "2025-12-17T14:35:46+00:00" }, { "name": "ralouphie/getallheaders", @@ -4797,20 +4797,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -4869,9 +4869,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "robrichards/xmlseclibs", @@ -7922,23 +7922,23 @@ }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -7971,9 +7971,9 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "vlucas/phpdotenv", @@ -8439,16 +8439,16 @@ }, { "name": "larastan/larastan", - "version": "v3.8.0", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e" + "reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e", - "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e", + "url": "https://api.github.com/repos/larastan/larastan/zipball/ff3725291bc4c7e6032b5a54776e3e5104c86db9", + "reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9", "shasum": "" }, "require": { @@ -8462,7 +8462,7 @@ "illuminate/pipeline": "^11.44.2 || ^12.4.1", "illuminate/support": "^11.44.2 || ^12.4.1", "php": "^8.2", - "phpstan/phpstan": "^2.1.29" + "phpstan/phpstan": "^2.1.32" }, "require-dev": { "doctrine/coding-standard": "^13", @@ -8517,7 +8517,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.8.0" + "source": "https://github.com/larastan/larastan/tree/v3.8.1" }, "funding": [ { @@ -8525,7 +8525,7 @@ "type": "github" } ], - "time": "2025-10-27T23:09:14+00:00" + "time": "2025-12-11T16:37:35+00:00" }, { "name": "mockery/mockery", @@ -8942,35 +8942,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.11", + "version": "11.0.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", "sebastian/code-unit-reverse-lookup": "^4.0.1", "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/lines-of-code": "^3.0.1", "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^1.3.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.2" + "phpunit/phpunit": "^11.5.46" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -9008,7 +9008,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" }, "funding": [ { @@ -9028,7 +9028,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T14:37:49+00:00" + "time": "2025-12-24T07:01:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10372,26 +10372,26 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.5", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + "reference": "0525c73950de35ded110cffafb9892946d7771b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", - "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5", + "reference": "0525c73950de35ded110cffafb9892946d7771b5", "shasum": "" }, "require": { "ext-simplexml": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": ">=7.2.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31" }, "bin": [ "bin/phpcbf", @@ -10416,7 +10416,7 @@ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "description": "PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.", "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", @@ -10447,7 +10447,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-04T16:30:35+00:00" + "time": "2025-11-10T16:43:36+00:00" }, { "name": "ssddanbrown/asserthtml", diff --git a/database/factories/Entities/Models/SlugHistoryFactory.php b/database/factories/Entities/Models/SlugHistoryFactory.php new file mode 100644 index 000000000..c8c57e09c --- /dev/null +++ b/database/factories/Entities/Models/SlugHistoryFactory.php @@ -0,0 +1,29 @@ + + */ +class SlugHistoryFactory extends Factory +{ + protected $model = \BookStack\Entities\Models\SlugHistory::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'sluggable_id' => Book::factory(), + 'sluggable_type' => 'book', + 'slug' => $this->faker->slug(), + 'parent_slug' => null, + ]; + } +} diff --git a/database/migrations/2025_11_23_161812_create_slug_history_table.php b/database/migrations/2025_11_23_161812_create_slug_history_table.php new file mode 100644 index 000000000..df30bf030 --- /dev/null +++ b/database/migrations/2025_11_23_161812_create_slug_history_table.php @@ -0,0 +1,51 @@ +increments('id'); + $table->string('sluggable_type', 10)->index(); + $table->unsignedBigInteger('sluggable_id')->index(); + $table->string('slug')->index(); + $table->string('parent_slug')->nullable()->index(); + $table->timestamps(); + }); + + // Migrate in slugs from page revisions + $revisionSlugQuery = DB::table('page_revisions') + ->select([ + DB::raw('\'page\' as sluggable_type'), + 'page_id as sluggable_id', + 'slug', + 'book_slug as parent_slug', + DB::raw('min(created_at) as created_at'), + DB::raw('min(updated_at) as updated_at'), + ]) + ->where('type', '=', 'version') + ->groupBy(['sluggable_id', 'slug', 'parent_slug']); + + DB::table('slug_history')->insertUsing( + ['sluggable_type', 'sluggable_id', 'slug', 'parent_slug', 'created_at', 'updated_at'], + $revisionSlugQuery, + ); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('slug_history'); + } +}; diff --git a/database/migrations/2025_12_15_140219_create_mention_history_table.php b/database/migrations/2025_12_15_140219_create_mention_history_table.php new file mode 100644 index 000000000..ff7b6a981 --- /dev/null +++ b/database/migrations/2025_12_15_140219_create_mention_history_table.php @@ -0,0 +1,31 @@ +increments('id'); + $table->string('mentionable_type', 50)->index(); + $table->unsignedBigInteger('mentionable_id')->index(); + $table->unsignedInteger('from_user_id'); + $table->unsignedInteger('to_user_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('mention_history'); + } +}; diff --git a/database/migrations/2025_12_19_103417_add_views_viewable_type_index.php b/database/migrations/2025_12_19_103417_add_views_viewable_type_index.php new file mode 100644 index 000000000..38d1cb450 --- /dev/null +++ b/database/migrations/2025_12_19_103417_add_views_viewable_type_index.php @@ -0,0 +1,28 @@ +index('viewable_type', 'views_viewable_type_index'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('views', function (Blueprint $table) { + $table->dropIndex('views_viewable_type_index'); + }); + } +}; diff --git a/dev/build/esbuild.js b/dev/build/esbuild.mjs similarity index 52% rename from dev/build/esbuild.js rename to dev/build/esbuild.mjs index 63387d612..fa2231157 100644 --- a/dev/build/esbuild.js +++ b/dev/build/esbuild.mjs @@ -1,12 +1,15 @@ #!/usr/bin/env node -const esbuild = require('esbuild'); -const path = require('path'); -const fs = require('fs'); +import * as esbuild from 'esbuild'; +import * as path from 'node:path'; +import * as fs from 'node:fs'; +import * as process from "node:process"; // Check if we're building for production // (Set via passing `production` as first argument) -const isProd = process.argv[2] === 'production'; +const mode = process.argv[2]; +const isProd = mode === 'production'; +const __dirname = import.meta.dirname; // Gather our input files const entryPoints = { @@ -17,11 +20,16 @@ const entryPoints = { wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.ts'), }; +// Watch styles so we can reload on change +if (mode === 'watch') { + entryPoints['styles-dummy'] = path.join(__dirname, '../../public/dist/styles.css'); +} + // Locate our output directory const outdir = path.join(__dirname, '../../public/dist'); -// Build via esbuild -esbuild.build({ +// Define the options for esbuild +const options = { bundle: true, metafile: true, entryPoints, @@ -33,6 +41,7 @@ esbuild.build({ minify: isProd, logLevel: 'info', loader: { + '.html': 'copy', '.svg': 'text', }, absWorkingDir: path.join(__dirname, '../..'), @@ -45,6 +54,34 @@ esbuild.build({ js: '// See the "/licenses" URI for full package license details', css: '/* See the "/licenses" URI for full package license details */', }, -}).then(result => { +}; + +if (mode === 'watch') { + options.inject = [ + path.join(__dirname, './livereload.js'), + ]; +} + +const ctx = await esbuild.context(options); + +if (mode === 'watch') { + // Watch for changes and rebuild on change + ctx.watch({}); + let {hosts, port} = await ctx.serve({ + servedir: path.join(__dirname, '../../public'), + cors: { + origin: '*', + } + }); +} else { + // Build with meta output for analysis + const result = await ctx.rebuild(); + const outputs = result.metafile.outputs; + const files = Object.keys(outputs); + for (const file of files) { + const output = outputs[file]; + console.log(`Written: ${file} @ ${Math.round(output.bytes / 1000)}kB`); + } fs.writeFileSync('esbuild-meta.json', JSON.stringify(result.metafile)); -}).catch(() => process.exit(1)); + process.exit(0); +} diff --git a/dev/build/livereload.js b/dev/build/livereload.js new file mode 100644 index 000000000..c2d8ac620 --- /dev/null +++ b/dev/build/livereload.js @@ -0,0 +1,35 @@ +if (!window.__dev_reload_listening) { + listen(); + window.__dev_reload_listening = true; +} + + +function listen() { + console.log('Listening for livereload events...'); + new EventSource("http://127.0.0.1:8000/esbuild").addEventListener('change', e => { + const { added, removed, updated } = JSON.parse(e.data); + + if (!added.length && !removed.length && updated.length > 0) { + const updatedPath = updated.filter(path => path.endsWith('.css'))[0] + if (!updatedPath) return; + + const links = [...document.querySelectorAll("link[rel='stylesheet']")]; + for (const link of links) { + const url = new URL(link.href); + const name = updatedPath.replace('-dummy', ''); + + if (url.pathname.endsWith(name)) { + const next = link.cloneNode(); + next.href = name + '?version=' + Math.random().toString(36).slice(2); + next.onload = function() { + link.remove(); + }; + link.after(next); + return + } + } + } + + location.reload() + }); +} \ No newline at end of file diff --git a/dev/docker/db-testing/Dockerfile b/dev/docker/db-testing/Dockerfile index 69b07a2b5..618f5bb82 100644 --- a/dev/docker/db-testing/Dockerfile +++ b/dev/docker/db-testing/Dockerfile @@ -19,8 +19,8 @@ ARG BRANCH=development # Download BookStack & install PHP deps RUN mkdir -p /var/www && \ git clone https://github.com/bookstackapp/bookstack.git --branch "$BRANCH" --single-branch /var/www/bookstack && \ - cd /var/www/bookstack && \ - wget https://raw.githubusercontent.com/composer/getcomposer.org/f3108f64b4e1c1ce6eb462b159956461592b3e3e/web/installer -O - -q | php -- --quiet --filename=composer && \ + cd /var/www/bookstack && \ + wget https://raw.githubusercontent.com/composer/getcomposer.org/f3108f64b4e1c1ce6eb462b159956461592b3e3e/web/installer -O - -q | php -- --quiet --filename=composer && \ php composer install # Set the BookStack dir as the default working dir diff --git a/dev/docker/db-testing/readme.md b/dev/docker/db-testing/readme.md new file mode 100644 index 000000000..89892408e --- /dev/null +++ b/dev/docker/db-testing/readme.md @@ -0,0 +1,32 @@ +# Database Testing Suite + +This docker setup is designed to run BookStack's test suite against each major database version we support +across MySQL and MariaDB to ensure compatibility and highlight any potential issues before a release. +This is a fairly slow and heavy process, so is designed to just be run manually before a release which +makes changes to the database schema, or a release which makes significant changes to database queries. + +### Running + +Everything is ran via the `run.sh` script. This will: + +- Optionally, accept a branch of BookStack to use for testing. +- Build the docker image from the `Dockerfile`. + - This will include a built-in copy of the chosen BookStack branch. +- Cycle through each major supported database version: + - Migrate and seed the database. + - Run the full PHP test suite. + +If there's a failure for a database version, the script will prompt if you'd like to continue or stop testing. + +This script should be ran from this `db-testing` directory: + +```bash +# Enter this directory +cd dev/docker/db-testing + +# Runs for the 'development' branch by default +./run.sh + +# Run for a specific branch +./run.sh v25-11 +``` diff --git a/dev/docker/db-testing/run.sh b/dev/docker/db-testing/run.sh index 9898e52ec..fe6ee4508 100755 --- a/dev/docker/db-testing/run.sh +++ b/dev/docker/db-testing/run.sh @@ -3,7 +3,7 @@ BRANCH=${1:-development} # Build the container with a known name -docker build --build-arg BRANCH="$BRANCH" -t bookstack:db-testing . +docker build --no-cache --build-arg BRANCH="$BRANCH" -t bookstack:db-testing . if [ $? -eq 1 ]; then echo "Failed to build app container for testing" exit 1 @@ -11,11 +11,9 @@ fi # List of database containers to test against containers=( - "mysql:5.7" "mysql:8.0" "mysql:8.4" "mysql:9.5" - "mariadb:10.2" "mariadb:10.6" "mariadb:10.11" "mariadb:11.4" diff --git a/dev/docs/development.md b/dev/docs/development.md index 6c250902a..418e9ead6 100644 --- a/dev/docs/development.md +++ b/dev/docs/development.md @@ -3,7 +3,7 @@ All development on BookStack is currently done on the `development` branch. When it's time for a release the `development` branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements: -* [Node.js](https://nodejs.org/en/) v20.0+ +* [Node.js](https://nodejs.org/en/) v22.0+ ## Building CSS & JavaScript Assets diff --git a/dev/docs/javascript-code.md b/dev/docs/javascript-code.md index e5f491839..4de6223d8 100644 --- a/dev/docs/javascript-code.md +++ b/dev/docs/javascript-code.md @@ -161,3 +161,7 @@ window.$components.firstOnElement(element, name); There are a range of available events that are emitted as part of a public & supported API for accessing or extending JavaScript libraries & components used in the system. Details on these events can be found in the [JavaScript Public Events file](javascript-public-events.md). + +## WYSIWYG Editor API + +Details on the API for our custom-built WYSIWYG editor can be found in the [WYSIWYG JavaScript API file](./wysiwyg-js-api.md). \ No newline at end of file diff --git a/dev/docs/javascript-public-events.md b/dev/docs/javascript-public-events.md index 4f68daaeb..e9ea014ef 100644 --- a/dev/docs/javascript-public-events.md +++ b/dev/docs/javascript-public-events.md @@ -60,7 +60,7 @@ This event is called when the markdown editor loads, post configuration but befo #### Event Data -- `markdownIt` - A references to the [MarkdownIt](https://markdown-it.github.io/markdown-it/#MarkdownIt) instance used to render markdown to HTML (Just for the preview). +- `markdownIt` - A reference to the [MarkdownIt](https://markdown-it.github.io/markdown-it/#MarkdownIt) instance used to render markdown to HTML (Just for the preview). - `displayEl` - The IFrame Element that wraps the HTML preview display. - `cmEditorView` - The CodeMirror [EditorView](https://codemirror.net/docs/ref/#view.EditorView) instance used for the markdown input editor. @@ -79,7 +79,7 @@ window.addEventListener('editor-markdown::setup', event => { This event is called as the embedded diagrams.net drawing editor loads, to allow configuration of the diagrams.net interface. See [this diagrams.net page](https://www.diagrams.net/doc/faq/configure-diagram-editor) for details on the available options for the configure event. -If using a custom diagrams.net instance, via the `DRAWIO` option, you will need to ensure your DRAWIO option URL has the `configure=1` query parameter. +If using a custom diagrams.net instance, via the `DRAWIO` option, you will need to ensure your DRAWIO option URL has the `configure=1` query parameter. #### Event Data @@ -134,6 +134,47 @@ window.addEventListener('editor-tinymce::setup', event => { }); ``` +### `editor-wysiwyg::post-init` + +This is called after the (new custom-built Lexical-based) WYSIWYG editor has been initialised. + +#### Event Data + +- `usage` - A string label to identify the usage type of the WYSIWYG editor in BookStack. +- `api` - An instance to the WYSIWYG editor API, as documented in the [WYSIWYG JavaScript API file](./wysiwyg-js-api.md). + +##### Example + +The below example shows how you'd use this API to create a button, with that button added to the main toolbar of the page editor, which inserts bold "Hello!" text on press: + +
+Show Example + +```javascript +window.addEventListener('editor-wysiwyg::post-init', event => { + const {usage, api} = event.detail; + // Check that it's the page editor which is being loaded + if (usage !== 'page-editor') { + return; + } + + // Create a custom button which inserts bold hello text on press + const button = api.ui.createButton({ + label: 'Greet', + action: () => { + api.content.insertHtml(`Hello!`); + } + }); + + // Add the button to the start of the first section within the main toolbar + const toolbar = api.ui.getMainToolbar(); + if (toolbar) { + toolbar.getSections()[0]?.addButton(button, 0); + } +}); +``` +
+ ### `library-cm6::configure-theme` This event is called whenever a CodeMirror instance is loaded, as a method to configure the theme used by CodeMirror. This applies to all CodeMirror instances including in-page code blocks, editors using in BookStack settings, and the Page markdown editor. @@ -142,7 +183,7 @@ This event is called whenever a CodeMirror instance is loaded, as a method to co - `darkModeActive` - A boolean to indicate if the current view/page is being loaded with dark mode active. - `registerViewTheme(builder)` - A method that can be called to register a new view (CodeMirror UI) theme. - - `builder` - A function that will return an object that will be passed into the CodeMirror [EditorView.theme()](https://codemirror.net/docs/ref/#view.EditorView^theme) function as a StyleSpec. + - `builder` - A function that will return an object that will be passed into the CodeMirror [EditorView.theme()](https://codemirror.net/docs/ref/#view.EditorView^theme) function as a StyleSpec. - `registerHighlightStyle(builder)` - A method that can be called to register a new HighlightStyle (code highlighting) theme. - `builder` - A function, that receives a reference to [Tag.tags](https://lezer.codemirror.net/docs/ref/#highlight.tags) and returns an array of [TagStyle](https://codemirror.net/docs/ref/#language.TagStyle) objects. @@ -301,7 +342,7 @@ This event is called just after any CodeMirror instances are initialised so that ##### Example -The below shows how you'd prepend some default text to all content (page) code blocks. +The below example shows how you'd prepend some default text to all content (page) code blocks.
Show Example @@ -318,4 +359,4 @@ window.addEventListener('library-cm6::post-init', event => { } }); ``` -
\ No newline at end of file + diff --git a/dev/docs/wysiwyg-js-api.md b/dev/docs/wysiwyg-js-api.md new file mode 100644 index 000000000..4b4fafe56 --- /dev/null +++ b/dev/docs/wysiwyg-js-api.md @@ -0,0 +1,136 @@ +# WYSIWYG JavaScript API + +**Warning: This API is currently in development and may change without notice.** + +Feedback is very much welcomed via this issue: https://github.com/BookStackApp/BookStack/issues/5937 + +This document covers the JavaScript API for the (newer Lexical-based) WYSIWYG editor. +This API is built and designed to abstract the internals of the editor away +to provide a stable interface for performing common customizations. + +Only the methods and properties documented here are guaranteed to be stable **once this API +is out of initial development**. +Other elements may be accessible but are not designed to be used directly, and therefore may change +without notice. +Stable parts of the API may still change where needed, but such changes would be noted as part of BookStack update advisories. + +The methods shown here are documented using standard TypeScript notation. + +## Overview + +The API is provided as an object, which itself provides a number of modules +via its properties: + +- `ui` - Provides methods related to the UI of the editor, like buttons and toolbars. +- `content` - Provides methods related to the live user content being edited upon. + +Each of these modules, and the relevant types used within, are documented in detail below. + +The API object itself is provided via the [editor-wysiwyg::post-init](./javascript-public-events.md#editor-wysiwygpost-init) +JavaScript public event, so you can access it like so: + +```javascript +window.addEventListener('editor-wysiwyg::post-init', event => { + const {api} = event.detail; +}); +``` + +--- + +## UI Module + +This module provides methods related to the UI of the editor, like buttons and toolbars. + +### Methods + +#### createButton(options: object): EditorApiButton + +Creates a new button which can be used by other methods. +This takes an option object with the following properties: + +- `label` - string, optional - Used for the button text if no icon provided, or the button tooltip if an icon is provided. +- `icon` - string, optional - The icon to use for the button. Expected to be an SVG string. +- `action` - callback, required - The action to perform when the button is clicked. + +The function returns an [EditorApiButton](#editorapibutton) object. + +**Example** + +```javascript +const button = api.ui.createButton({ + label: 'Warn', + icon: '...', + action: () => { + window.alert('You clicked the button!'); + } +}); +``` + +### getMainToolbar(): EditorApiToolbar + +Get the main editor toolbar. This is typically the toolbar at the top of the editor. +The function returns an [EditorApiToolbar](#editorapitoolbar) object, or null if no toolbar is found. + +**Example** + +```javascript +const toolbar = api.ui.getMainToolbar(); +const sections = toolbar?.getSections() || []; +if (sections.length > 0) { + sections[0].addButton(button); +} +``` + +### Types + +These are types which may be provided from UI module methods. + +#### EditorApiButton + +Represents a button created via the `createButton` method. +This has the following methods: + +- `setActive(isActive: boolean): void` - Sets whether the button should be in an active state or not (typically active buttons appear as pressed). + +#### EditorApiToolbar + +Represents a toolbar within the editor. This is a bar typically containing sets of buttons. +This has the following methods: + +- `getSections(): EditorApiToolbarSection[]` - Provides the main [EditorApiToolbarSections](#editorapitoolbarsection) contained within this toolbar. + +#### EditorApiToolbarSection + +Represents a section of the main editor toolbar, which contains a set of buttons. +This has the following methods: + +- `getLabel(): string` - Provides the string label of the section. +- `addButton(button: EditorApiButton, targetIndex: number = -1): void` - Adds a button to the section. + - By default, this will append the button, although a target index can be provided to insert at a specific position. + +--- + +## Content Module + +This module provides methods related to the live user content being edited within the editor. + +### Methods + +#### insertHtml(html: string, position: string = 'selection'): void + +Inserts the given HTML string at the given position string. +The position, if not provided, will default to `'selection'`, replacing any existing selected content (or inserting at the selection if there's no active selection range). +Valid position string values are: `selection`, `start` and `end`. `start` & `end` are relative to the whole editor document. +The HTML is not assured to be added to the editor exactly as provided, since it will be parsed and serialised to fit the editor's internal known model format. Different parts of the HTML content may be handled differently depending on if it's block or inline content. + +The function does not return anything. + +**Example** + +```javascript +// Basic insert at selection +api.content.insertHtml('

Hello world!

'); + +// Insert at the start of the editor content +api.content.insertHtml('

I\'m at the start!

', 'start'); +``` \ No newline at end of file diff --git a/dev/licensing/js-library-licenses.txt b/dev/licensing/js-library-licenses.txt index 95acfa6af..d7ad4ecc8 100644 --- a/dev/licensing/js-library-licenses.txt +++ b/dev/licensing/js-library-licenses.txt @@ -1619,20 +1619,6 @@ Copyright: Copyright (c) 2015 Vitaly Puzrin. Source: markdown-it/linkify-it Link: markdown-it/linkify-it ----------- -livereload-js -License: MIT -License File: node_modules/livereload-js/LICENSE -Copyright: Copyright (c) 2010-2012 Andrey Tarantsov -Source: git://github.com/livereload/livereload-js.git -Link: https://github.com/livereload/livereload-js ------------ -livereload -License: MIT -License File: node_modules/livereload/LICENSE -Copyright: Copyright (c) 2010 Joshua Peek -Source: http://github.com/napcs/node-livereload.git -Link: http://github.com/napcs/node-livereload.git ------------ load-json-file License: MIT License File: node_modules/load-json-file/license @@ -1928,14 +1914,6 @@ Copyright: Copyright (c) George Zahariev Source: git://github.com/gkz/optionator.git Link: https://github.com/gkz/optionator ----------- -opts -License: BSD-2-Clause -License File: node_modules/opts/LICENSE.txt -Copyright: Copyright (c) 2010, Joey Mazzarelli -All rights reserved. -Source: github:khtdr/opts -Link: http://khtdr.com/opts ------------ own-keys License: MIT License File: node_modules/own-keys/LICENSE @@ -2318,9 +2296,9 @@ Link: https://github.com/ljharb/side-channel#readme signal-exit License: ISC License File: node_modules/signal-exit/LICENSE.txt -Copyright: Copyright (c) 2015, Contributors +Copyright: Copyright (c) 2015-2023 Benjamin Coe, Isaac Z. Schlueter, and Contributors Source: https://github.com/tapjs/signal-exit.git -Link: https://github.com/tapjs/signal-exit +Link: https://github.com/tapjs/signal-exit.git ----------- slash License: MIT @@ -3343,6 +3321,20 @@ Copyright: Copyright 2022 Romain Menke, Antonio Laguna <*******@******.**> Source: git+https://github.com/csstools/postcss-plugins.git Link: https://github.com/csstools/postcss-plugins/tree/main/packages/css-tokenizer#readme ----------- +@emnapi/core +License: MIT +License File: node_modules/@emnapi/core/LICENSE +Copyright: Copyright (c) 2021-present Toyobayashi +Source: git+https://github.com/toyobayashi/emnapi.git +Link: https://github.com/toyobayashi/emnapi#readme +----------- +@emnapi/runtime +License: MIT +License File: node_modules/@emnapi/runtime/LICENSE +Copyright: Copyright (c) 2021-present Toyobayashi +Source: git+https://github.com/toyobayashi/emnapi.git +Link: https://github.com/toyobayashi/emnapi#readme +----------- @esbuild/linux-x64 License: MIT Source: git+https://github.com/evanw/esbuild.git @@ -3396,7 +3388,7 @@ Link: https://eslint.org License: Apache-2.0 License File: node_modules/@eslint/object-schema/LICENSE Source: git+https://github.com/eslint/rewrite.git -Link: https://github.com/eslint/rewrite#readme +Link: https://github.com/eslint/rewrite/tree/main/packages/object-schema#readme ----------- @eslint/plugin-kit License: Apache-2.0 @@ -3792,6 +3784,11 @@ Copyright: Copyright (c) Microsoft Corporation. Source: https://github.com/tsconfig/bases.git Link: https://github.com/tsconfig/bases.git ----------- +@tybys/wasm-util +License: MIT +Source: https://github.com/toyobayashi/wasm-util.git +Link: https://github.com/toyobayashi/wasm-util.git +----------- @types/babel__core License: MIT License File: node_modules/@types/babel__core/LICENSE diff --git a/lang/ar/notifications.php b/lang/ar/notifications.php index 30a49a631..69d5dfdcf 100644 --- a/lang/ar/notifications.php +++ b/lang/ar/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'تم تحديث الصفحة: :pageName', 'updated_page_intro' => 'تم تحديث الصفحة في :appName:', 'updated_page_debounce' => 'لمنع تلقي عدد كبير من الإشعارات، لن يتم إرسال إشعارات إليك لفترة من الوقت لإجراء المزيد من التعديلات على هذه الصفحة بواسطة نفس المحرر.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'اسم الصفحة:', 'detail_page_path' => 'مسار الصفحة:', diff --git a/lang/ar/preferences.php b/lang/ar/preferences.php index 9158b4b37..92733c998 100644 --- a/lang/ar/preferences.php +++ b/lang/ar/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'التحكم في إشعارات البريد الإلكتروني الذي تتلقاها عند إجراء نشاط معين داخل النظام.', 'notifications_opt_own_page_changes' => 'إشعاري عند حدوث تغييرات في الصفحات التي أملكها', 'notifications_opt_own_page_comments' => 'إشعاري بشأن التعليقات على الصفحات التي أملكها', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'إشعاري عند الردود على تعليقاتي', 'notifications_save' => 'حفظ اﻹعدادات', 'notifications_update_success' => 'تم تحديث إعدادات الإشعارات!', diff --git a/lang/ar/settings.php b/lang/ar/settings.php index 9006ac227..dc95ac846 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -75,7 +75,7 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'لم يتم اختيار أي قيود', // Sorting Settings - 'sorting' => 'طريقة الترتيب', + 'sorting' => 'القوائم و الفرز', 'sorting_book_default' => 'ترتيب الكتاب الافتراضي', 'sorting_book_default_desc' => 'حدد قاعدة الترتيب الافتراضية لتطبيقها على الكتب الجديدة. لن يؤثر هذا على الكتب الحالية، ويمكن تجاوزه لكل كتاب على حدة.', 'sorting_rules' => 'قواعد الترتيب', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'تاريخ التحديث', 'sort_rule_op_chapters_first' => 'الفصول الأولى', 'sort_rule_op_chapters_last' => 'الفصول الأخيرة', + 'sorting_page_limits' => 'حدود العرض لكل صفحة', + 'sorting_page_limits_desc' => 'تعيين عدد العناصر لإظهار كل صفحة في قوائم مختلفة داخل النظام. عادةً ما يكون الرقم الأقل هو الأكثر أداء، بينما وضع رقم أعلى يغني عن النقر على صفحات متعددة. يوصى باستخدام مضاعفات رقم ٣ (18 و 24 و 30 و إلخ...).', // Maintenance settings 'maint' => 'الصيانة', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'استيراد المحتوى', 'role_editor_change' => 'تغيير محرر الصفحة', 'role_notifications' => 'تلقي الإشعارات وإدارتها', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'أذونات الأصول', 'roles_system_warning' => 'اعلم أن الوصول إلى أي من الأذونات الثلاثة المذكورة أعلاه يمكن أن يسمح للمستخدم بتغيير امتيازاته الخاصة أو امتيازات الآخرين في النظام. قم بتعيين الأدوار مع هذه الأذونات فقط للمستخدمين الموثوق بهم.', 'role_asset_desc' => 'تتحكم هذه الأذونات في الوصول الافتراضي إلى الأصول داخل النظام. ستتجاوز الأذونات الخاصة بالكتب والفصول والصفحات هذه الأذونات.', 'role_asset_admins' => 'يُمنح المسؤولين حق الوصول تلقائيًا إلى جميع المحتويات ولكن هذه الخيارات قد تعرض خيارات واجهة المستخدم أو تخفيها.', 'role_asset_image_view_note' => 'يتعلق هذا بالرؤية داخل مدير الصور. يعتمد الوصول الفعلي لملفات الصور المُحمّلة على خِيار تخزين الصور في النظام.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'الكل', 'role_own' => 'ما يخص', 'role_controlled_by_asset' => 'يتحكم فيها الأصول التي يتم رفعها إلى', diff --git a/lang/bg/notifications.php b/lang/bg/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/bg/notifications.php +++ b/lang/bg/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/bg/preferences.php b/lang/bg/preferences.php index f954340e2..8f5aaa07e 100644 --- a/lang/bg/preferences.php +++ b/lang/bg/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/bg/settings.php b/lang/bg/settings.php index 6dd27d74a..ae770c559 100644 --- a/lang/bg/settings.php +++ b/lang/bg/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Няма наложени ограничения', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Поддръжка', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Настройки за достъп до активи', 'roles_system_warning' => 'Важно: Добавянето на потребител в някое от горните три роли може да му позволи да промени собствените си права или правата на другите в системата. Възлагайте тези роли само на доверени потребители.', 'role_asset_desc' => 'Тези настройки за достъп контролират достъпа по подразбиране до активите в системата. Настройките за достъп до книги, глави и страници ще отменят тези настройки.', 'role_asset_admins' => 'Администраторите автоматично получават достъп до цялото съдържание, но тези опции могат да показват или скриват опциите за потребителския интерфейс.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Всички', 'role_own' => 'Собствени', 'role_controlled_by_asset' => 'Контролирани от актива, към който са качени', diff --git a/lang/bn/activities.php b/lang/bn/activities.php index 7e4ba8229..c9268f8ab 100644 --- a/lang/bn/activities.php +++ b/lang/bn/activities.php @@ -131,7 +131,7 @@ return [ 'sort_rule_create' => 'created sort rule', 'sort_rule_create_notification' => 'Sort rule successfully created', 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', + 'sort_rule_update_notification' => 'রোলটি সার্থকভাবে হালনাগাদ করা হয়েছে', 'sort_rule_delete' => 'deleted sort rule', 'sort_rule_delete_notification' => 'Sort rule successfully deleted', diff --git a/lang/bn/auth.php b/lang/bn/auth.php index 06ed376a7..30879fcb6 100644 --- a/lang/bn/auth.php +++ b/lang/bn/auth.php @@ -24,8 +24,8 @@ return [ 'password_hint' => 'ন্যূনতম ৮ অক্ষরের হতে হবে', 'forgot_password' => 'পাসওয়ার্ড ভুলে গেছেন?', 'remember_me' => 'লগইন স্থায়িত্ব ধরে রাখুন', - 'ldap_email_hint' => 'Please enter an email to use for this account.', - 'create_account' => 'Create Account', + 'ldap_email_hint' => 'অনুগ্রহ করে এই অ্যাকাউন্টের জন্য ব্যবহার করার জন্য একটি ইমেইল ঠিকানা লিখুন।', + 'create_account' => 'অ্যাকাউন্ট তৈরি করুন', 'already_have_account' => 'Already have an account?', 'dont_have_account' => 'Don\'t have an account?', 'social_login' => 'Social Login', @@ -39,16 +39,16 @@ return [ 'register_success' => 'Thanks for signing up! You are now registered and signed in.', // Login auto-initiation - 'auto_init_starting' => 'Attempting Login', + 'auto_init_starting' => 'লগইন করার চেষ্টা করা হচ্ছে', 'auto_init_starting_desc' => 'We\'re contacting your authentication system to start the login process. If there\'s no progress after 5 seconds you can try clicking the link below.', 'auto_init_start_link' => 'Proceed with authentication', // Password Reset - 'reset_password' => 'Reset Password', + 'reset_password' => 'পাসওয়ার্ড রিসেট করুন', 'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.', 'reset_password_send_button' => 'Send Reset Link', 'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.', - 'reset_password_success' => 'Your password has been successfully reset.', + 'reset_password_success' => 'আপনার পাসওয়ার্ড সফলভাবে রিসেট করা হয়েছে.', 'email_reset_subject' => 'Reset your :appName password', 'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.', 'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.', diff --git a/lang/bn/notifications.php b/lang/bn/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/bn/notifications.php +++ b/lang/bn/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/bn/preferences.php b/lang/bn/preferences.php index 2e47604e4..c59ec62da 100644 --- a/lang/bn/preferences.php +++ b/lang/bn/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/bn/settings.php b/lang/bn/settings.php index 04aca4f2e..6d0f4ab88 100644 --- a/lang/bn/settings.php +++ b/lang/bn/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'No restriction set', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Maintenance', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/bs/notifications.php b/lang/bs/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/bs/notifications.php +++ b/lang/bs/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/bs/preferences.php b/lang/bs/preferences.php index 2872f5f3c..f4459d738 100644 --- a/lang/bs/preferences.php +++ b/lang/bs/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/bs/settings.php b/lang/bs/settings.php index 81c2c0a93..c68605fe1 100644 --- a/lang/bs/settings.php +++ b/lang/bs/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'No restriction set', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Maintenance', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/ca/notifications.php b/lang/ca/notifications.php index 49b986ad0..e3fb928a1 100644 --- a/lang/ca/notifications.php +++ b/lang/ca/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'S’ha actualitzat la pàgina :pageName', 'updated_page_intro' => 'S’ha actualitzat una pàgina a :appName.', 'updated_page_debounce' => 'Per a evitar que s’acumulin les notificacions, durant un temps no se us notificarà cap canvi fet en aquesta pàgina pel mateix usuari.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Nom de la pàgina:', 'detail_page_path' => 'Ruta de la pàgina:', diff --git a/lang/ca/preferences.php b/lang/ca/preferences.php index 25206726e..09fdaa4e5 100644 --- a/lang/ca/preferences.php +++ b/lang/ca/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Gestioneu les notificacions de correu electrònic que rebreu quan es facin certes activitats.', 'notifications_opt_own_page_changes' => 'Notifica’m els canvis a les meves pàgines.', 'notifications_opt_own_page_comments' => 'Notifica’m la creació de comentaris a les meves pàgines.', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notifica’m les respostes als meus comentaris.', 'notifications_save' => 'Desa les preferències', 'notifications_update_success' => 'S’han actualitzat les preferències de notificació', diff --git a/lang/ca/settings.php b/lang/ca/settings.php index 63352147e..352291fe5 100644 --- a/lang/ca/settings.php +++ b/lang/ca/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'No hi ha cap restricció', // Sorting Settings - 'sorting' => 'Ordenar', - 'sorting_book_default' => 'Ordre predeterminat del llibre', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Selecciona la regla d\'ordenació predeterminada per aplicar a nous llibres. Això no afectarà els llibres existents, i pot ser anul·lat per llibre.', 'sorting_rules' => 'Regles d\'ordenació', 'sorting_rules_desc' => 'Són operacions d\'ordenació predefinides que es poden aplicar al contingut en el sistema.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Data d\'actualització', 'sort_rule_op_chapters_first' => 'Capítols a l\'inici', 'sort_rule_op_chapters_last' => 'Capítols al final', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Manteniment', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Importar contingut', 'role_editor_change' => 'Canvi de l’editor de pàgina', 'role_notifications' => 'Recepció i gestió de notificacions', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Permisos de recursos', 'roles_system_warning' => 'Tingueu en compte que l’accés a qualsevol dels tres permisos de dalt permeten que l’usuari canviï els seus privilegis i els privilegis d’altres usuaris. Assigneu rols d’usuari amb aquests permisos només a usuaris de confiança.', 'role_asset_desc' => 'Aquests permisos controlen l’accés per defecte als recursos del sistema. El permisos dels llibres, capítols i pàgines sobreescriuran aquests permisos.', 'role_asset_admins' => 'Als administradors se’ls dona accés automàticament a tot el contingut però aquestes opcions mostren o amaguen opcions de la interfície d’usuari.', 'role_asset_image_view_note' => 'Això té relació amb la visibilitat al gestor d’imatges. L’accés a les imatges pujades dependrà de l’opció d’emmagatzematge d’imatges dels sistema.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Tot', 'role_own' => 'Propi', 'role_controlled_by_asset' => 'Controlat pel recurs a què estan pujats', diff --git a/lang/cs/notifications.php b/lang/cs/notifications.php index caf75f179..a6c9e88b5 100644 --- a/lang/cs/notifications.php +++ b/lang/cs/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Aktualizovaná stránka: :pageName', 'updated_page_intro' => 'V :appName byla aktualizována stránka:', 'updated_page_debounce' => 'Po nějakou dobu neobdržíte další oznámení o aktualizaci této stránky stejným editorem, aby se omezil počet stejných zpráv.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Název stránky:', 'detail_page_path' => 'Umístění:', diff --git a/lang/cs/preferences.php b/lang/cs/preferences.php index ec7afaf27..4a9bf3ad7 100644 --- a/lang/cs/preferences.php +++ b/lang/cs/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Nastavte si e-mailová oznámení, která dostanete při provedení určitých akcí v systému.', 'notifications_opt_own_page_changes' => 'Upozornit na změny stránek u kterých jsem vlastníkem', 'notifications_opt_own_page_comments' => 'Upozornit na komentáře na stránkách, které vlastním', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Upozornit na odpovědi na mé komentáře', 'notifications_save' => 'Uložit nastavení', 'notifications_update_success' => 'Nastavení oznámení byla aktualizována!', diff --git a/lang/cs/settings.php b/lang/cs/settings.php index e72d4b403..f856b64cf 100644 --- a/lang/cs/settings.php +++ b/lang/cs/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Žádná omezení nebyla nastavena', // Sorting Settings - 'sorting' => 'Řazení', - 'sorting_book_default' => 'Výchozí řazení knih', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Vybere výchozí pravidlo řazení pro nové knihy. Řazení neovlivní existující knihy a může být upraveno u konkrétní knihy.', 'sorting_rules' => 'Pravidla řazení', 'sorting_rules_desc' => 'Toto jsou předem definovaná pravidla řazení, která mohou být použita na webu.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Datum aktualizace', 'sort_rule_op_chapters_first' => 'Kapitoly jako první', 'sort_rule_op_chapters_last' => 'Kapitoly jako poslední', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Údržba', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Importovat obsah', 'role_editor_change' => 'Změnit editor stránek', 'role_notifications' => 'Přijímat a spravovat oznámení', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Obsahová oprávnění', 'roles_system_warning' => 'Berte na vědomí, že přístup k některému ze tří výše uvedených oprávnění může uživateli umožnit změnit svá vlastní oprávnění nebo oprávnění ostatních uživatelů v systému. Přiřazujte role s těmito oprávněními pouze důvěryhodným uživatelům.', 'role_asset_desc' => 'Tato oprávnění řídí přístup k obsahu napříč systémem. Specifická oprávnění na knihách, kapitolách a stránkách převáží tato nastavení.', 'role_asset_admins' => 'Administrátoři automaticky dostávají přístup k veškerému obsahu, ale tyto volby mohou ukázat nebo skrýt volby v uživatelském rozhraní.', 'role_asset_image_view_note' => 'To se týká viditelnosti ve správci obrázků. Skutečný přístup k nahraným souborům obrázků bude záviset na možnosti uložení systémových obrázků.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Vše', 'role_own' => 'Vlastní', 'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány', diff --git a/lang/cy/notifications.php b/lang/cy/notifications.php index 26f65f144..766e666b9 100644 --- a/lang/cy/notifications.php +++ b/lang/cy/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Tudalen wedi\'i diweddaru :pageName', 'updated_page_intro' => 'Mae tudalen newydd wedi cael ei diweddaru yn :appName:', 'updated_page_debounce' => 'Er mwyn atal llu o hysbysiadau, am gyfnod ni fyddwch yn cael hysbysiadau am ragor o olygiadau i\'r dudalen hon gan yr un golygydd.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Enw\'r dudalen:', 'detail_page_path' => 'Llwybr Tudalen:', diff --git a/lang/cy/preferences.php b/lang/cy/preferences.php index 6e00adfe8..4a08ca498 100644 --- a/lang/cy/preferences.php +++ b/lang/cy/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Rheoli’r hysbysiadau e-bost a gewch pan fydd gweithgaredd penodol yn cael ei gyflawni o fewn y system.', 'notifications_opt_own_page_changes' => 'Hysbysu am newidiadau i dudalennau yr wyf yn berchen arnynt', 'notifications_opt_own_page_comments' => 'Hysbysu am sylwadau ar dudalennau yr wyf yn berchen arnynt', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Hysbysu am atebion i\'m sylwadau', 'notifications_save' => 'Dewisiadau Cadw', 'notifications_update_success' => 'Mae’r dewisiadau hysbysu wedi\'u diweddaru!', diff --git a/lang/cy/settings.php b/lang/cy/settings.php index cb08e8c90..29e86e28b 100644 --- a/lang/cy/settings.php +++ b/lang/cy/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Ni osodwyd cyfyngiad', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Cynnal', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Mewnforio Cynnwys', 'role_editor_change' => 'Newid golygydd tudalen', 'role_notifications' => 'Derbyn a rheoli hysbysiadau', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Caniatâd Asedau', 'roles_system_warning' => 'Byddwch yn ymwybodol y gall mynediad i unrhyw un o\'r tri chaniatâd uchod ganiatáu i ddefnyddiwr newid eu breintiau eu hunain neu freintiau eraill yn y system. Neilltuo rolau gyda\'r caniatâd hyn i ddefnyddwyr dibynadwy yn unig.', 'role_asset_desc' => 'Mae\'r caniatâd hwn yn rheoli mynediad diofyn i\'r asedau o fewn y system. Bydd caniatâd ar Lyfrau, Penodau a Thudalennau yn diystyru\'r caniatâd hwn.', 'role_asset_admins' => 'Mae gweinyddwyr yn cael mynediad awtomatig i\'r holl gynnwys ond gall yr opsiynau hyn ddangos neu guddio opsiynau UI.', 'role_asset_image_view_note' => 'Mae hyn yn ymwneud â gwelededd o fewn y rheolwr delweddau. Bydd mynediad gwirioneddol i ffeiliau delwedd wedi\'u huwchlwytho yn dibynnu ar opsiwn storio delwedd y system.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Popeth', 'role_own' => 'Meddu', 'role_controlled_by_asset' => 'Wedi\'u rheoli gan yr ased y maent yn cael eu huwchlwytho iddo', diff --git a/lang/da/notifications.php b/lang/da/notifications.php index 8074c03c4..3aaf2ff18 100644 --- a/lang/da/notifications.php +++ b/lang/da/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Opdateret side: :pageName', 'updated_page_intro' => 'En side er blevet opdateret i :appName:', 'updated_page_debounce' => 'For at forhindre en masse af notifikationer, i et stykke tid vil du ikke blive sendt notifikationer for yderligere redigeringer til denne side af den samme editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Sidens navn:', 'detail_page_path' => 'Sidesti:', diff --git a/lang/da/preferences.php b/lang/da/preferences.php index 143405a16..5ba4f450c 100644 --- a/lang/da/preferences.php +++ b/lang/da/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Administrer de e-mail-notifikationer, du modtager, når visse aktiviteter udføres i systemet.', 'notifications_opt_own_page_changes' => 'Adviser ved ændringer af sider, jeg ejer', 'notifications_opt_own_page_comments' => 'Adviser ved kommentarer på sider, jeg ejer', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Adviser ved svar på mine kommentarer', 'notifications_save' => 'Gem indstillinger', 'notifications_update_success' => 'Indstillinger for notifikationer er blevet opdateret!', diff --git a/lang/da/settings.php b/lang/da/settings.php index 4f194545b..8d89c30af 100644 --- a/lang/da/settings.php +++ b/lang/da/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Ingen restriktion opsat', // Sorting Settings - 'sorting' => 'Sortering', - 'sorting_book_default' => 'Standard bog-sortering', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Vælg den standardsorteringsregel, der skal gælde for nye bøger. Dette påvirker ikke eksisterende bøger og kan tilsidesættes for hver enkelt bog.', 'sorting_rules' => 'Regler for sortering', 'sorting_rules_desc' => 'Det er foruddefinerede sorteringsoperationer, som kan anvendes på indhold i systemet.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Opdateret dato', 'sort_rule_op_chapters_first' => 'Kapitler først', 'sort_rule_op_chapters_last' => 'De sidste kapitler', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Vedligeholdelse', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Importer indhold', 'role_editor_change' => 'Skift side editor', 'role_notifications' => 'Modtag og administrer notifikationer', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Tilladelser for medier og "assets"', 'roles_system_warning' => 'Vær opmærksom på, at adgang til alle af de ovennævnte tre tilladelser, kan give en bruger mulighed for at ændre deres egne brugerrettigheder eller brugerrettigheder for andre i systemet. Tildel kun roller med disse tilladelser til betroede brugere.', 'role_asset_desc' => 'Disse tilladelser kontrollerer standardadgang til medier og "assets" i systemet. Tilladelser til bøger, kapitler og sider tilsidesætter disse tilladelser.', 'role_asset_admins' => 'Administratorer får automatisk adgang til alt indhold, men disse indstillinger kan vise eller skjule UI-indstillinger.', 'role_asset_image_view_note' => 'Dette vedrører synlighed i billedhåndteringen. Den faktiske adgang til uploadede billedfiler vil afhænge af systemets billedlagringsindstilling.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Alle', 'role_own' => 'Eget', 'role_controlled_by_asset' => 'Styres af det medie/"asset", de uploades til', diff --git a/lang/de/notifications.php b/lang/de/notifications.php index f3b86b502..71b71d1b6 100644 --- a/lang/de/notifications.php +++ b/lang/de/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Aktualisierte Seite: :pageName', 'updated_page_intro' => 'Eine Seite wurde in :appName aktualisiert:', 'updated_page_debounce' => 'Um eine Flut von Benachrichtigungen zu vermeiden, werden Sie für eine gewisse Zeit keine Benachrichtigungen für weitere Bearbeitungen dieser Seite durch denselben Bearbeiter erhalten.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Name der Seite:', 'detail_page_path' => 'Seitenpfad:', diff --git a/lang/de/preferences.php b/lang/de/preferences.php index 26f0b05da..1f74f3d3e 100644 --- a/lang/de/preferences.php +++ b/lang/de/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Legen Sie fest, welche E-Mail-Benachrichtigungen Sie erhalten, wenn bestimmte Aktivitäten im System durchgeführt werden.', 'notifications_opt_own_page_changes' => 'Benachrichtigung bei Änderungen an eigenen Seiten', 'notifications_opt_own_page_comments' => 'Benachrichtigung bei Kommentaren an eigenen Seiten', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Bei Antworten auf meine Kommentare benachrichtigen', 'notifications_save' => 'Einstellungen speichern', 'notifications_update_success' => 'Benachrichtigungseinstellungen wurden aktualisiert!', diff --git a/lang/de/settings.php b/lang/de/settings.php index 250668e8a..a87408995 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -76,8 +76,8 @@ Hinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt', // Sorting Settings - 'sorting' => 'Sortierung', - 'sorting_book_default' => 'Standard-Buchsortierung', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Wählen Sie die Standard-Sortierregel aus, die auf neue Bücher angewendet werden soll. Dies wirkt sich nicht auf bestehende Bücher aus und kann pro Buch überschrieben werden.', 'sorting_rules' => 'Sortierregeln', 'sorting_rules_desc' => 'Dies sind vordefinierte Sortieraktionen, die auf Inhalte im System angewendet werden können.', @@ -104,6 +104,8 @@ Hinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung 'sort_rule_op_updated_date' => 'Aktualisierungsdatum', 'sort_rule_op_chapters_first' => 'Kapitel zuerst', 'sort_rule_op_chapters_last' => 'Kapitel zuletzt', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Wartung', @@ -196,11 +198,13 @@ Hinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung 'role_import_content' => 'Inhalt importieren', 'role_editor_change' => 'Seiten-Editor ändern', 'role_notifications' => 'Empfangen und Verwalten von Benachrichtigungen', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Berechtigungen', 'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.', 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.', 'role_asset_admins' => 'Administratoren erhalten automatisch Zugriff auf alle Inhalte, aber diese Optionen können Oberflächenoptionen ein- oder ausblenden.', 'role_asset_image_view_note' => 'Das bezieht sich auf die Sichtbarkeit innerhalb des Bildmanagers. Der tatsächliche Zugriff auf hochgeladene Bilddateien hängt von der Speicheroption des Systems für Bilder ab.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Alle', 'role_own' => 'Eigene', 'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt', diff --git a/lang/de_informal/notifications.php b/lang/de_informal/notifications.php index 0bf7739f4..99c270ec1 100644 --- a/lang/de_informal/notifications.php +++ b/lang/de_informal/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Aktualisierte Seite: :pageName', 'updated_page_intro' => 'Eine Seite wurde in :appName aktualisiert:', 'updated_page_debounce' => 'Um eine Flut von Benachrichtigungen zu vermeiden, wirst du für eine gewisse Zeit keine Benachrichtigungen für weitere Bearbeitungen dieser Seite durch denselben Bearbeiter erhalten.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Seitenname:', 'detail_page_path' => 'Seitenpfad:', diff --git a/lang/de_informal/preferences.php b/lang/de_informal/preferences.php index 07b83a8da..bfb57b2f4 100644 --- a/lang/de_informal/preferences.php +++ b/lang/de_informal/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Lege fest, welche E-Mail-Benachrichtigungen du erhältst, wenn bestimmte Aktivitäten im System durchgeführt werden.', 'notifications_opt_own_page_changes' => 'Benachrichtigung bei Änderungen an eigenen Seiten', 'notifications_opt_own_page_comments' => 'Benachrichtigung bei Kommentaren an eigenen Seiten', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Bei Antworten auf meine Kommentare benachrichtigen', 'notifications_save' => 'Einstellungen speichern', 'notifications_update_success' => 'Benachrichtigungseinstellungen wurden aktualisiert!', diff --git a/lang/de_informal/settings.php b/lang/de_informal/settings.php index 50003c58e..ab9a075a0 100644 --- a/lang/de_informal/settings.php +++ b/lang/de_informal/settings.php @@ -76,8 +76,8 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt', // Sorting Settings - 'sorting' => 'Sortierung', - 'sorting_book_default' => 'Standard-Buchsortierung', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Wähle die Standard-Sortierregel aus, die auf neue Bücher angewendet werden soll. Dies wirkt sich nicht auf bestehende Bücher aus und kann pro Buch überschrieben werden.', 'sorting_rules' => 'Sortierregeln', 'sorting_rules_desc' => 'Dies sind vordefinierte Sortieraktionen, die auf Inhalte im System angewendet werden können.', @@ -104,6 +104,8 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'sort_rule_op_updated_date' => 'Aktualisierungsdatum', 'sort_rule_op_chapters_first' => 'Kapitel zuerst', 'sort_rule_op_chapters_last' => 'Kapitel zuletzt', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Wartung', @@ -196,11 +198,13 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'role_import_content' => 'Inhalt importieren', 'role_editor_change' => 'Seiteneditor ändern', 'role_notifications' => 'Empfangen und Verwalten von Benachrichtigungen', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Berechtigungen', 'roles_system_warning' => 'Beachte, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weise nur Rollen mit diesen Berechtigungen vertrauenswürdigen Benutzern zu.', 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungen.', 'role_asset_admins' => 'Administratoren erhalten automatisch Zugriff auf alle Inhalte, aber diese Optionen können Oberflächenoptionen ein- oder ausblenden.', 'role_asset_image_view_note' => 'Das bezieht sich auf die Sichtbarkeit innerhalb des Bildmanagers. Der tatsächliche Zugriff auf hochgeladene Bilddateien hängt von der Speicheroption des Systems für Bilder ab.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Alle', 'role_own' => 'Eigene', 'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt', diff --git a/lang/el/notifications.php b/lang/el/notifications.php index 3924cab3b..e9a9fe15d 100644 --- a/lang/el/notifications.php +++ b/lang/el/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Ενημερωμένη σελίδα: :pageName', 'updated_page_intro' => 'Μια σελίδα έχει ενημερωθεί στο :appName:', 'updated_page_debounce' => 'Για να αποτρέψετε μαζικές ειδοποιήσεις, για κάποιο διάστημα δε θα σας αποστέλλονται ειδοποιήσεις για περαιτέρω αλλαγές σε αυτήν τη σελίδα από τον ίδιο συντάκτη.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Όνομα σελίδας:', 'detail_page_path' => 'Διαδρομή σελίδας:', diff --git a/lang/el/preferences.php b/lang/el/preferences.php index 4f7e97d52..2c2ce8fe9 100644 --- a/lang/el/preferences.php +++ b/lang/el/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/el/settings.php b/lang/el/settings.php index 172038fd0..67461604e 100644 --- a/lang/el/settings.php +++ b/lang/el/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Δε έχουν ρυθμιστεί περιορισμοί ακόμα', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Συντήρηση', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Εισαγωγή περιεχομένου', 'role_editor_change' => 'Αλλαγή προγράμματος επεξεργασίας σελίδας', 'role_notifications' => 'Λήψη & διαχείριση ειδοποιήσεων', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Δικαιώματα Συστήματος', 'roles_system_warning' => 'Λάβετε υπόψη ότι η πρόσβαση σε οποιοδήποτε από τις τρεις παραπάνω άδειες (δικαιώματα) μπορεί να επιτρέψει σε έναν χρήστη να αλλάξει τα δικά του προνόμια ή τα προνόμια άλλων στο σύστημα. Εκχωρήστε ρόλους με αυτά τα δικαιώματα μόνο σε αξιόπιστους χρήστες.', 'role_asset_desc' => 'Αυτά τα δικαιώματα ελέγχουν την προεπιλεγμένη πρόσβαση στα στοιχεία (άδειες) εντός του συστήματος. Τα δικαιώματα σε Βιβλία, Κεφάλαια και Σελίδες θα παρακάμψουν αυτές τις άδειες.', 'role_asset_admins' => 'Οι διαχειριστές έχουν αυτόματα πρόσβαση σε όλο το περιεχόμενο, αλλά αυτές οι επιλογές ενδέχεται να εμφανίζουν ή να αποκρύπτουν τις επιλογές διεπαφής χρήστη.', 'role_asset_image_view_note' => 'Αυτό σχετίζεται με την ορατότητα εντός του διαχειριστή εικόνων. Η πραγματική πρόσβαση των μεταφορτωμένων αρχείων εικόνας θα εξαρτηθεί από την επιλογή αποθήκευσης εικόνας συστήματος.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Ολα', 'role_own' => 'Τα δικά του', 'role_controlled_by_asset' => 'Ελέγχονται από το στοιχείο στο οποίο ανεβαίνουν (Ράφια, Βιβλία)', diff --git a/lang/en/notifications.php b/lang/en/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/en/notifications.php +++ b/lang/en/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/en/preferences.php b/lang/en/preferences.php index 2872f5f3c..f4459d738 100644 --- a/lang/en/preferences.php +++ b/lang/en/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/en/settings.php b/lang/en/settings.php index 81c2c0a93..c68605fe1 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'No restriction set', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Maintenance', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/es/notifications.php b/lang/es/notifications.php index 5ebc42129..38c770888 100644 --- a/lang/es/notifications.php +++ b/lang/es/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Página actualizada: :pageName', 'updated_page_intro' => 'Una página ha sido actualizada en :appName:', 'updated_page_debounce' => 'Para prevenir notificaciones en masa, durante un tiempo no se enviarán notificaciones para futuras ediciones de esta página por el mismo editor.', + 'comment_mention_subject' => 'Ha sido mencionado en un comentario en la página: :pageName', + 'comment_mention_intro' => 'Fue mencionado en un comentario en :appName:', 'detail_page_name' => 'Nombre de página:', 'detail_page_path' => 'Ruta de la página:', diff --git a/lang/es/preferences.php b/lang/es/preferences.php index 13af93d29..0328a572a 100644 --- a/lang/es/preferences.php +++ b/lang/es/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Controle las notificaciones por correo electrónico que recibe cuando se realiza cierta actividad dentro del sistema.', 'notifications_opt_own_page_changes' => 'Notificar sobre los cambios en las páginas en las que soy propietario', 'notifications_opt_own_page_comments' => 'Notificar sobre comentarios en las páginas en las que soy propietario', + 'notifications_opt_comment_mentions' => 'Notificarme cuando he sido mencionado en un comentario', 'notifications_opt_comment_replies' => 'Notificar sobre respuestas a mis comentarios', 'notifications_save' => 'Guardar preferencias', 'notifications_update_success' => '¡Se han actualizado las preferencias de notificaciones!', diff --git a/lang/es/settings.php b/lang/es/settings.php index 55eb498d6..1a9927c8e 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida', // Sorting Settings - 'sorting' => 'Ordenación', - 'sorting_book_default' => 'Orden por defecto del libro', + 'sorting' => 'Listas y ordenación', + 'sorting_book_default' => 'Orden de libros por defecto', 'sorting_book_default_desc' => 'Seleccione la regla de ordenación predeterminada para aplicar a nuevos libros. Esto no afectará a los libros existentes, y puede ser anulado por libro.', 'sorting_rules' => 'Reglas de ordenación', 'sorting_rules_desc' => 'Son operaciones de ordenación predefinidas que se pueden aplicar al contenido en el sistema.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Fecha de actualización', 'sort_rule_op_chapters_first' => 'Capítulos al inicio', 'sort_rule_op_chapters_last' => 'Capítulos al final', + 'sorting_page_limits' => 'Límites de visualización por página', + 'sorting_page_limits_desc' => 'Establecer cuántos elementos a mostrar por página en varias listas dentro del sistema. Normalmente una cantidad más baja rendirá mejor, mientras que una cantidad más alta evita la necesidad de hacer clic a través de varias páginas. Se recomienda utilizar un múltiplo par de 3 (18, 24, 30, etc).', // Maintenance settings 'maint' => 'Mantenimiento', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Importar contenido', 'role_editor_change' => 'Cambiar editor de página', 'role_notifications' => 'Recibir y gestionar notificaciones', + 'role_permission_note_users_and_roles' => 'Estos permisos proporcionarán también visibilidad y búsqueda de usuarios y roles en el sistema.', 'role_asset' => 'Permisos de contenido', 'roles_system_warning' => 'Tenga en cuenta que el acceso a cualquiera de los tres permisos anteriores puede permitir a un usuario alterar sus propios privilegios o los privilegios de otros en el sistema. Sólo asignar roles con estos permisos a usuarios de confianza.', 'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los contenidos del sistema. Los permisos de Libros, Capítulos y Páginas sobreescribiran estos permisos.', 'role_asset_admins' => 'A los administradores se les asigna automáticamente permisos para acceder a todo el contenido pero estas opciones podrían mostrar u ocultar opciones de la interfaz.', 'role_asset_image_view_note' => 'Esto se refiere a la visibilidad dentro del gestor de imágenes. El acceso a los archivos de imagen subidos dependerá de la opción de almacenamiento de imágenes del sistema.', + 'role_asset_users_note' => 'Estos permisos proporcionarán también visibilidad y búsqueda de usuarios en el sistema.', 'role_all' => 'Todo', 'role_own' => 'Propio', 'role_controlled_by_asset' => 'Controlado por el contenido al que ha sido subido', diff --git a/lang/es_AR/notifications.php b/lang/es_AR/notifications.php index fc89b2c25..af9f5332b 100644 --- a/lang/es_AR/notifications.php +++ b/lang/es_AR/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Página actualizada: :pageName', 'updated_page_intro' => 'Se actualizó una página en :appName:', 'updated_page_debounce' => 'Para evitar una avalancha de notificaciones, durante un tiempo no se enviarán notificaciones sobre más ediciones de esta página por el mismo editor.', + 'comment_mention_subject' => 'Ha sido mencionado en un comentario en la página: :pageName', + 'comment_mention_intro' => 'Fue mencionado en un comentario en :appName:', 'detail_page_name' => 'Nombre de la página:', 'detail_page_path' => 'Ruta de la página:', diff --git a/lang/es_AR/preferences.php b/lang/es_AR/preferences.php index 11c872021..deca55466 100644 --- a/lang/es_AR/preferences.php +++ b/lang/es_AR/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Controle las notificaciones por correo electrónico que recibe cuando se realiza cierta actividad dentro del sistema.', 'notifications_opt_own_page_changes' => 'Notificar sobre los cambios en las páginas de las que soy propietario', 'notifications_opt_own_page_comments' => 'Notificar sobre comentarios en las páginas de las que soy propietario', + 'notifications_opt_comment_mentions' => 'Notificarme cuando he sido mencionado en un comentario', 'notifications_opt_comment_replies' => 'Notificar sobre respuestas a mis comentarios', 'notifications_save' => 'Guardar preferencias', 'notifications_update_success' => '¡Se actualizaron las preferencias de notificaciones!', diff --git a/lang/es_AR/settings.php b/lang/es_AR/settings.php index 5cbcff86e..3eb41d2cc 100644 --- a/lang/es_AR/settings.php +++ b/lang/es_AR/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida', // Sorting Settings - 'sorting' => 'Ordenando', - 'sorting_book_default' => 'Orden predeterminado del libro', + 'sorting' => 'Listas y ordenación', + 'sorting_book_default' => 'Orden de libros por defecto', 'sorting_book_default_desc' => 'Seleccione la regla de ordenación predeterminada para aplicar a nuevos libros. Esto no afectará a los libros existentes, y puede ser anulado por libro.', 'sorting_rules' => 'Reglas de Ordenación', 'sorting_rules_desc' => 'Son operaciones de ordenación predefinidas que se pueden aplicar al contenido en el sistema.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Fecha de actualización', 'sort_rule_op_chapters_first' => 'Capítulos al inicio', 'sort_rule_op_chapters_last' => 'Capítulos al final', + 'sorting_page_limits' => 'Límites de visualización por página', + 'sorting_page_limits_desc' => 'Establecer cuántos elementos a mostrar por página en varias listas dentro del sistema. Normalmente una cantidad más baja rendirá mejor, mientras que una cantidad más alta evita la necesidad de hacer clic a través de varias páginas. Se recomienda utilizar un múltiplo par de 3 (18, 24, 30, etc).', // Maintenance settings 'maint' => 'Mantenimiento', @@ -196,11 +198,13 @@ return [ 'role_import_content' => 'Importar contenido', 'role_editor_change' => 'Cambiar editor de página', 'role_notifications' => 'Recibir y gestionar notificaciones', + 'role_permission_note_users_and_roles' => 'Estos permisos proporcionarán también visibilidad y búsqueda de usuarios y roles en el sistema.', 'role_asset' => 'Permisos de activos', 'roles_system_warning' => 'Tenga en cuenta que el acceso a cualquiera de los tres permisos anteriores puede permitir a un usuario modificar sus propios privilegios o los privilegios de otros usuarios en el sistema. Asignar roles con estos permisos sólo a usuarios de comfianza.', 'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos definidos en Libros, Capítulos y Páginas ignorarán estos permisos.', 'role_asset_admins' => 'Los administradores reciben automáticamente acceso a todo el contenido pero estas opciones pueden mostrar u ocultar opciones de UI.', 'role_asset_image_view_note' => 'Esto se refiere a la visibilidad dentro del gestor de imágenes. El acceso real a los archivos de imágenes subidos, dependerá de la opción de almacenamiento de imágenes del sistema.', + 'role_asset_users_note' => 'Estos permisos proporcionarán también visibilidad y búsqueda de usuarios en el sistema.', 'role_all' => 'Todo', 'role_own' => 'Propio', 'role_controlled_by_asset' => 'Controlado por el activo al que ha sido subido', diff --git a/lang/et/notifications.php b/lang/et/notifications.php index 0b5fc814c..2843d5799 100644 --- a/lang/et/notifications.php +++ b/lang/et/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Muudetud leht: :pageName', 'updated_page_intro' => 'Rakenduses :appName muudeti lehte:', 'updated_page_debounce' => 'Et vältida liigseid teavitusi, ei saadeta sulle mõnda aega teavitusi selle lehe muutmiste kohta sama kasutaja poolt.', + 'comment_mention_subject' => 'Sind mainiti kommentaaris lehel: :pageName', + 'comment_mention_intro' => 'Sind mainiti kommentaaris rakenduses :appName:', 'detail_page_name' => 'Lehe nimetus:', 'detail_page_path' => 'Lehe asukoht:', diff --git a/lang/et/preferences.php b/lang/et/preferences.php index f4ba5e6ae..a4003463c 100644 --- a/lang/et/preferences.php +++ b/lang/et/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Halda e-posti teavitusi, mis saadetakse teatud tegevuste puhul.', 'notifications_opt_own_page_changes' => 'Teavita muudatustest minu lehtedel', 'notifications_opt_own_page_comments' => 'Teavita kommentaaridest minu lehtedel', + 'notifications_opt_comment_mentions' => 'Teavita mind, kui mind kommentaaris mainitakse', 'notifications_opt_comment_replies' => 'Teavita vastustest minu kommentaaridele', 'notifications_save' => 'Salvesta eelistused', 'notifications_update_success' => 'Teavituste eelistused on salvestatud!', diff --git a/lang/et/settings.php b/lang/et/settings.php index 003238cfe..fbd9d9c7e 100644 --- a/lang/et/settings.php +++ b/lang/et/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Piirangut ei ole', // Sorting Settings - 'sorting' => 'Sorteerimine', - 'sorting_book_default' => 'Vaikimisi raamatu sorteerimine', + 'sorting' => 'Loendid ja järjestamine', + 'sorting_book_default' => 'Vaikimisi raamatute sorteerimise reegel', 'sorting_book_default_desc' => 'Vali vaikimisi uutele raamatutele rakenduv sorteerimisreegel. See ei mõjuta olemasolevaid raamatuid ning seda saab raamatupõhiselt muuta.', 'sorting_rules' => 'Sorteerimisreeglid', 'sorting_rules_desc' => 'Need on eeldefineeritud sorteerimistoimingud, mida saab süsteemis olevale sisule rakendada.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Muutmise aeg', 'sort_rule_op_chapters_first' => 'Peatükid eespool', 'sort_rule_op_chapters_last' => 'Peatükid tagapool', + 'sorting_page_limits' => 'Leheküljepõhised kuvalimiidid', + 'sorting_page_limits_desc' => 'Seadista, mitut objekti erinevates loendites ühel leheküljel kuvada. Väiksem väärtus tähendab reeglina paremat jõudlust, samas kui suurem väärtus vähendab vajadust mitut lehekülge läbi klikkida. Soovituslik on kasutada 3-ga jaguvat väärtust (18, 24, 30 jne).', // Maintenance settings 'maint' => 'Hooldus', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Imporditud sisu', 'role_editor_change' => 'Lehe redaktori muutmine', 'role_notifications' => 'Võta vastu ja halda teavitusi', + 'role_permission_note_users_and_roles' => 'Need õigused lubavad ka süsteemis olevaid kasutajaid ja rolle vaadata ja otsida.', 'role_asset' => 'Sisu õigused', 'roles_system_warning' => 'Pane tähele, et ülalolevad kolm õigust võimaldavad kasutajal enda või teiste kasutajate õiguseid muuta. Määra nende õigustega roll ainult usaldusväärsetele kasutajatele.', 'role_asset_desc' => 'Need load kontrollivad vaikimisi ligipääsu süsteemis olevale sisule. Raamatute, peatükkide ja lehtede õigused rakenduvad esmajärjekorras.', 'role_asset_admins' => 'Administraatoritel on automaatselt ligipääs kogu sisule, aga need valikud võivad peida või näidata kasutajaliidese elemente.', 'role_asset_image_view_note' => 'See käib nähtavuse kohta pildifailide halduris. Tegelik ligipääs üleslaaditud pildifailidele sõltub süsteemsest piltide salvestamise valikust.', + 'role_asset_users_note' => 'Need õigused lubavad ka süsteemis olevaid kasutajaid vaadata ja otsida.', 'role_all' => 'Kõik', 'role_own' => 'Enda omad', 'role_controlled_by_asset' => 'Õigused määratud seotud objekti kaudu', diff --git a/lang/eu/notifications.php b/lang/eu/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/eu/notifications.php +++ b/lang/eu/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/eu/preferences.php b/lang/eu/preferences.php index da7638593..3818932d4 100644 --- a/lang/eu/preferences.php +++ b/lang/eu/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/eu/settings.php b/lang/eu/settings.php index e93f3c964..9a9227b83 100644 --- a/lang/eu/settings.php +++ b/lang/eu/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Mugarik gabe', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Mantentze-lanak', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Fitxategi baimenak', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Guztiak', 'role_own' => 'Norberarenak', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/fa/notifications.php b/lang/fa/notifications.php index 4595fd825..d216b04fe 100644 --- a/lang/fa/notifications.php +++ b/lang/fa/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'صفحه جدید: :pageName', 'updated_page_intro' => 'یک صفحه جدید ایجاد شده است در :appName:', 'updated_page_debounce' => 'برای جلوگیری از انبوه اعلان‌ها، برای مدتی اعلان‌ ویرایش‌هایی که توسط همان ویرایشگر در این صفحه انجام می‌شود، ارسال نخواهد شد.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'نام صفحه:', 'detail_page_path' => 'نام میسر صفحه:', diff --git a/lang/fa/preferences.php b/lang/fa/preferences.php index ceecedc80..00d277bdf 100644 --- a/lang/fa/preferences.php +++ b/lang/fa/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'تنظیمات اطلاعیه‌های ایمیلی هنگام انجام فعالیت‌های خاص در سیستم.', 'notifications_opt_own_page_changes' => 'در صورت تغییرات در صفحاتی که متعلق به من است، اطلاع بده', 'notifications_opt_own_page_comments' => 'در صورت ثبت نظر در صفحاتی که متعلق به من است، اطلاع بده', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'پس از درج پاسخ به روی نظراتی که من ثبت کرده‌ام، اطلاع بده', 'notifications_save' => 'ذخیره تنظیمات', 'notifications_update_success' => 'تنظیمات اعلان‌ها به روز شده است!', diff --git a/lang/fa/settings.php b/lang/fa/settings.php index e4cb9018e..abbfce470 100644 --- a/lang/fa/settings.php +++ b/lang/fa/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'بدون محدودیت', // Sorting Settings - 'sorting' => 'مرتب‌سازی', - 'sorting_book_default' => 'مرتب‌سازی پیش‌فرض کتاب', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'قانون پیش‌فرض مرتب‌سازی را برای کتاب‌های جدید انتخاب کنید. تغییر قانون بر ترتیب کتاب‌های موجود تأثیری ندارد و می‌تواند برای هر کتاب به‌صورت جداگانه تغییر یابد.', 'sorting_rules' => 'قوانین مرتب‌سازی', 'sorting_rules_desc' => 'این‌ها عملیات مرتب‌سازی از پیش تعریف‌شده‌ای هستند که می‌توانید آن‌ها را بر محتوای سیستم اعمال کنید.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'تاریخ به‌روزرسانی', 'sort_rule_op_chapters_first' => 'ابتدا فصل‌ها', 'sort_rule_op_chapters_last' => 'فصل‌ها در آخر', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'نگهداری', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'وارد کردن محتوا', 'role_editor_change' => 'تغییر ویرایشگر صفحه', 'role_notifications' => 'دریافت و مدیریت اعلان‌ها', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'مجوزهای دارایی', 'roles_system_warning' => 'توجه داشته باشید که دسترسی به هر یک از سه مجوز فوق می‌تواند به کاربر اجازه دهد تا امتیازات خود یا امتیازات دیگران را در سیستم تغییر دهد. فقط نقش هایی را با این مجوزها به کاربران مورد اعتماد اختصاص دهید.', 'role_asset_desc' => 'این مجوزها دسترسی پیش‌فرض به دارایی‌های درون سیستم را کنترل می‌کنند. مجوزهای مربوط به کتاب‌ها، فصل‌ها و صفحات این مجوزها را لغو می‌کنند.', 'role_asset_admins' => 'به ادمین‌ها به‌طور خودکار به همه محتوا دسترسی داده می‌شود، اما این گزینه‌ها ممکن است گزینه‌های UI را نشان داده یا پنهان کنند.', 'role_asset_image_view_note' => 'این مربوط به مرئی بودن در بخش مدیر تصاویر است. دسترسی عملی به تصاویر آپلود شده بستگی به گزینه ذخیره‌سازی تصویر سیستم دارد.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'همه', 'role_own' => 'صاحب', 'role_controlled_by_asset' => 'توسط دارایی که در آن آپلود می شود کنترل می شود', diff --git a/lang/fi/notifications.php b/lang/fi/notifications.php index a737ad7d9..647378ded 100644 --- a/lang/fi/notifications.php +++ b/lang/fi/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Päivitetty sivu: :pageName', 'updated_page_intro' => 'Sivu on päivitetty sivustolla :appName:', 'updated_page_debounce' => 'Useiden ilmoitusten välttämiseksi sinulle ei toistaiseksi lähetetä ilmoituksia saman toimittajan tekemistä uusista muokkauksista tälle sivulle.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Sivun nimi:', 'detail_page_path' => 'Sivun polku:', diff --git a/lang/fi/preferences.php b/lang/fi/preferences.php index 25cd76bf7..05afc111f 100644 --- a/lang/fi/preferences.php +++ b/lang/fi/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Hallitse järjestelmän toimintoihin liittyviä sähköposti-ilmoituksia.', 'notifications_opt_own_page_changes' => 'Ilmoita omistamilleni sivuille tehdyistä muutoksista', 'notifications_opt_own_page_comments' => 'Ilmoita omistamilleni sivuille tehdyistä kommenteista', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Ilmoita vastauksista kommentteihini', 'notifications_save' => 'Tallenna asetukset', 'notifications_update_success' => 'Ilmoitusasetukset on päivitetty!', diff --git a/lang/fi/settings.php b/lang/fi/settings.php index d9e91d4fa..499122fe3 100644 --- a/lang/fi/settings.php +++ b/lang/fi/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Ei rajoituksia', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Huolto', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Vaihda sivun editoria', 'role_notifications' => 'Vastaanota ja hallinnoi ilmoituksia', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Sisältöjen oikeudet', 'roles_system_warning' => 'Huomaa, että minkä tahansa edellä mainituista kolmesta käyttöoikeudesta voi antaa käyttäjälle mahdollisuuden muuttaa omia tai muiden järjestelmän käyttäjien oikeuksia. Anna näitä oikeuksia sisältävät roolit vain luotetuille käyttäjille.', 'role_asset_desc' => 'Näillä asetuksilla hallitaan oletuksena annettavia käyttöoikeuksia järjestelmässä oleviin sisältöihin. Yksittäisten kirjojen, lukujen ja sivujen käyttöoikeudet kumoavat nämä käyttöoikeudet.', 'role_asset_admins' => 'Ylläpitäjät saavat automaattisesti pääsyn kaikkeen sisältöön, mutta nämä vaihtoehdot voivat näyttää tai piilottaa käyttöliittymävalintoja.', 'role_asset_image_view_note' => 'Tämä tarkoittaa näkyvyyttä kuvien hallinnassa. Pääsy ladattuihin kuvatiedostoihin riippuu asetetusta kuvien tallennusvaihtoehdosta.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Kaikki', 'role_own' => 'Omat', 'role_controlled_by_asset' => 'Määräytyy sen sisällön mukaan, johon ne on ladattu', diff --git a/lang/fr/notifications.php b/lang/fr/notifications.php index 5d7ae88f7..b82f82bd0 100644 --- a/lang/fr/notifications.php +++ b/lang/fr/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Page mise à jour: :pageName', 'updated_page_intro' => 'Une page a été mise à jour dans :appName:', 'updated_page_debounce' => 'Pour éviter de nombreuses notifications, pendant un certain temps, vous ne recevrez pas de notifications pour d\'autres modifications de cette page par le même éditeur.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Nom de la page :', 'detail_page_path' => 'Chemin de la page :', diff --git a/lang/fr/preferences.php b/lang/fr/preferences.php index 2180ed70c..c595222f1 100644 --- a/lang/fr/preferences.php +++ b/lang/fr/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Contrôlez les notifications par e-mail que vous recevez lorsque certaines activités sont effectuées dans le système.', 'notifications_opt_own_page_changes' => 'Notifier lors des modifications des pages que je possède', 'notifications_opt_own_page_comments' => 'Notifier lorsque les pages que je possède sont commentées', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notifier les réponses à mes commentaires', 'notifications_save' => 'Enregistrer les préférences', 'notifications_update_success' => 'Les préférences de notification ont été mises à jour !', diff --git a/lang/fr/settings.php b/lang/fr/settings.php index e73d8a4b1..7ce2312f3 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Aucune restriction en place', // Sorting Settings - 'sorting' => 'Tri', - 'sorting_book_default' => 'Tri des livres par défaut', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Sélectionnez le tri par défaut à mettre en place sur les nouveaux livres. Cela n’affectera pas les livres existants, et peut être redéfini dans les livres.', 'sorting_rules' => 'Règles de tri', 'sorting_rules_desc' => 'Ce sont les opérations de tri qui peuvent être appliquées au contenu du système.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Date de mise à jour', 'sort_rule_op_chapters_first' => 'Chapitres en premier', 'sort_rule_op_chapters_last' => 'Chapitres en dernier', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Maintenance', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Importer le contenu', 'role_editor_change' => 'Changer l\'éditeur de page', 'role_notifications' => 'Recevoir et gérer les notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Permissions des ressources', 'roles_system_warning' => 'Sachez que l\'accès à l\'une des trois permissions ci-dessus peut permettre à un utilisateur de modifier ses propres privilèges ou les privilèges des autres utilisateurs du système. N\'attribuez uniquement des rôles avec ces permissions qu\'à des utilisateurs de confiance.', 'role_asset_desc' => 'Ces permissions contrôlent l\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions', 'role_asset_admins' => 'Les administrateurs ont automatiquement accès à tous les contenus mais les options suivantes peuvent afficher ou masquer certaines options de l\'interface.', 'role_asset_image_view_note' => 'Cela concerne la visibilité dans le gestionnaire d\'images. L\'accès réel des fichiers d\'image téléchargés dépendra de l\'option de stockage d\'images du système.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Tous', 'role_own' => 'Propres', 'role_controlled_by_asset' => 'Contrôlé par les ressources les ayant envoyés', diff --git a/lang/he/notifications.php b/lang/he/notifications.php index 8385c0a6d..cfab57117 100644 --- a/lang/he/notifications.php +++ b/lang/he/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'עמוד עודכן :pageName', 'updated_page_intro' => 'דף עודכן ב:appName:', 'updated_page_debounce' => 'על מנת לעצור הצפת התראות, לזמן מסוים אתה לא תקבל התראות על שינויים עתידיים בדף זה על ידי אותו עורך.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'שם עמוד:', 'detail_page_path' => 'נתיב לעמוד:', diff --git a/lang/he/preferences.php b/lang/he/preferences.php index 9e05eefb3..fa0c89ea5 100644 --- a/lang/he/preferences.php +++ b/lang/he/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'העדפות קבלת מייל והתראות כאשר מבוצעת פעולה מסויימת במערכת.', 'notifications_opt_own_page_changes' => 'עדכן אותי כאשר מתבצעים שינויים לדפים שבבעלותי', 'notifications_opt_own_page_comments' => 'עדכן אותי כאשר נוספות הערות לדפים שבבעלותי', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'עדכן אותי כאשר מתקבלות תגובות להערות שלי', 'notifications_save' => 'שמור העדפות', 'notifications_update_success' => 'הגדרת התראות עודכנו!', diff --git a/lang/he/settings.php b/lang/he/settings.php index 5b405072f..0b5034475 100644 --- a/lang/he/settings.php +++ b/lang/he/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'אין הגבלה לדומיין', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'תחזוקה', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'שנה עורך עמודים', 'role_notifications' => 'ניהול התראות', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'הרשאות משאבים', 'roles_system_warning' => 'שימו לב לכך שגישה לכל אחת משלושת ההרשאות הנ"ל יכולה לאפשר למשתמש לשנות את הפריווילגיות שלהם או של אחרים במערכת. הגדירו תפקידים להרשאות אלה למשתמשים בהם אתם בוטחים בלבד.', 'role_asset_desc' => 'הרשאות אלו שולטות בגישת ברירת המחדל למשאבים בתוך המערכת. הרשאות של ספרים, פרקים ודפים יגברו על הרשאות אלו.', 'role_asset_admins' => 'מנהלים מקבלים הרשאה מלאה לכל התוכן אך אפשרויות אלו עלולות להציג או להסתיר אפשרויות בממשק', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'הכל', 'role_own' => 'שלי', 'role_controlled_by_asset' => 'נשלטים על ידי המשאב אליו הועלו', diff --git a/lang/hr/notifications.php b/lang/hr/notifications.php index c3a38cdfa..c9428e2dc 100644 --- a/lang/hr/notifications.php +++ b/lang/hr/notifications.php @@ -13,6 +13,8 @@ return [ Ažurirana stranica: :pageName', 'updated_page_intro' => 'Stranica je ažurirana u :appName:', 'updated_page_debounce' => 'Kako biste spriječili velik broj obavijesti, nećete primati obavijesti o daljnjim izmjenama ove stranice od istog urednika neko vrijeme.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Naziv Stranice:', 'detail_page_path' => 'Page Path:', diff --git a/lang/hr/preferences.php b/lang/hr/preferences.php index 2f409f8d3..5da5a72fb 100644 --- a/lang/hr/preferences.php +++ b/lang/hr/preferences.php @@ -25,6 +25,7 @@ return [ 'notifications_opt_own_page_comments' => 'ChatGPT Obavijesti o komentarima na stranicama koje posjedujem', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Obavijesti o odgovorima na moje komentare', 'notifications_save' => 'Spremi Postavke', 'notifications_update_success' => 'Postavke obavijesti su ažurirane!', diff --git a/lang/hr/settings.php b/lang/hr/settings.php index 5f0007426..6465d0ea7 100644 --- a/lang/hr/settings.php +++ b/lang/hr/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Bez ograničenja', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Održavanje', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Promijeni uređivač stranica', 'role_notifications' => 'Primanje i upravljanje obavijestima', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Upravljanje vlasništvom', 'roles_system_warning' => 'Uzmite u obzir da pristup bilo kojem od ovih dopuštenja dozvoljavate korisniku upravljanje dopuštenjima ostalih u sustavu. Ova dopuštenja dodijelite pouzdanim korisnicima.', 'role_asset_desc' => 'Ova dopuštenja kontroliraju zadane pristupe. Dopuštenja za knjige, poglavlja i stranice ih poništavaju.', 'role_asset_admins' => 'Administratori automatski imaju pristup svim sadržajima, ali ove opcije mogu prikazati ili sakriti korisnička sučelja.', 'role_asset_image_view_note' => 'Ovo se odnosi na vidljivost unutar upravitelja slika. Stvarni pristup uploadiranim slikovnim datotekama ovisit će o odabranim opcijama pohrane slika u sustavu.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Sve', 'role_own' => 'Vlastito', 'role_controlled_by_asset' => 'Kontrolirano od strane vlasnika', diff --git a/lang/hu/notifications.php b/lang/hu/notifications.php index c8cae9143..d8a29688a 100644 --- a/lang/hu/notifications.php +++ b/lang/hu/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Frissített oldal: :pageName', 'updated_page_intro' => 'Az oldal frissítése sikeres volt itt: :appName:', 'updated_page_debounce' => 'Az értesítések tömegének elkerülése érdekében egy ideig nem kap értesítést az oldal további szerkesztéseiről ugyanaz a szerkesztő.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Oldal neve:', 'detail_page_path' => 'Oldal helye:', diff --git a/lang/hu/preferences.php b/lang/hu/preferences.php index 4251a28fb..fb3c335e8 100644 --- a/lang/hu/preferences.php +++ b/lang/hu/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Állítsd be az e-mail értesítéseket, amelyeket akkor kapsz, ha bizonyos tevékenység történik a rendszeren belül.', 'notifications_opt_own_page_changes' => 'Értesítsen változásokról az általam tulajdonolt oldalakon', 'notifications_opt_own_page_comments' => 'Értesítés a hozzászólásokról az általam tulajdonolt oldalakon', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Értesítsen válaszokról a hozzászólásaimra', 'notifications_save' => 'Beállítások mentése', 'notifications_update_success' => 'Az értesítési beállítások frissítve lettek!', diff --git a/lang/hu/settings.php b/lang/hu/settings.php index 813fee7b0..c6810ed68 100644 --- a/lang/hu/settings.php +++ b/lang/hu/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Nincs beállítva korlátozás', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Karbantartás', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Oldalszerkesztő módosítása', 'role_notifications' => 'Értesítések fogadása és kezelése', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Eszköz jogosultságok', 'roles_system_warning' => 'Ne feledje, hogy a fenti három engedély bármelyikéhez való hozzáférés lehetővé teszi a felhasználó számára, hogy módosítsa saját vagy a rendszerben mások jogosultságait. Csak megbízható felhasználókhoz rendeljen szerepeket ezekkel az engedélyekkel.', 'role_asset_desc' => 'Ezek a jogosultságok vezérlik az alapértelmezés szerinti hozzáférést a rendszerben található eszközökhöz. A könyvek, fejezetek és oldalak jogosultságai felülírják ezeket a jogosultságokat.', 'role_asset_admins' => 'Az adminisztrátorok automatikusan hozzáférést kapnak minden tartalomhoz, de ezek a beállítások megjeleníthetnek vagy elrejthetnek felhasználói felület beállításokat.', 'role_asset_image_view_note' => 'Ez a képkezelőn belüli láthatóságra vonatkozik. A feltöltött képfájlok tényleges elérése a rendszerkép tárolási beállításától függ.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Összes', 'role_own' => 'Saját', 'role_controlled_by_asset' => 'Az általuk feltöltött eszköz által ellenőrzött', diff --git a/lang/id/notifications.php b/lang/id/notifications.php index b22af1f73..7b9d2181a 100644 --- a/lang/id/notifications.php +++ b/lang/id/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Halaman yang diperbarui: :pageName', 'updated_page_intro' => 'Halaman telah diperbarui di :appName:', 'updated_page_debounce' => 'Untuk mencegah banyaknya pemberitahuan, untuk sementara Anda tidak akan dikirimi pemberitahuan untuk pengeditan lebih lanjut pada halaman ini oleh editor yang sama.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Nama Halaman:', 'detail_page_path' => 'Jalur Halaman:', diff --git a/lang/id/passwords.php b/lang/id/passwords.php index 3ee2e4d57..2559b2651 100644 --- a/lang/id/passwords.php +++ b/lang/id/passwords.php @@ -6,7 +6,7 @@ */ return [ - 'password' => 'Kata sandi harus setidaknya delapan karakter dan sesuai dengan konfirmasi.', + 'password' => 'Passwords must be at least eight characters and match the confirmation.', 'user' => "Kami tidak dapat menemukan pengguna dengan alamat email tersebut.", 'token' => 'Token setel ulang sandi tidak valid untuk alamat email ini.', 'sent' => 'Kami telah mengirimkan email tautan pengaturan ulang kata sandi Anda!', diff --git a/lang/id/preferences.php b/lang/id/preferences.php index a1bb346b8..1c7d4248d 100644 --- a/lang/id/preferences.php +++ b/lang/id/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/id/settings.php b/lang/id/settings.php index 51c0613cb..cc9426683 100644 --- a/lang/id/settings.php +++ b/lang/id/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Tidak ada batasan yang ditetapkan', // Sorting Settings - 'sorting' => 'Menyortir', - 'sorting_book_default' => 'Penyortiran Buku Default', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Pilih aturan sortir default yang akan diterapkan pada buku baru. Aturan ini tidak akan memengaruhi buku yang sudah ada, dan dapat diganti per buku.', 'sorting_rules' => 'Aturan Penyortiran', 'sorting_rules_desc' => 'Ini adalah operasi penyortiran yang telah ditetapkan sebelumnya yang dapat diterapkan pada konten dalam sistem.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Tanggal Pembaruan', 'sort_rule_op_chapters_first' => 'Bab di Urutan Pertama', 'sort_rule_op_chapters_last' => 'Bab di Urutan Terakhir', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Pemeliharaan', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Impor Konten', 'role_editor_change' => 'Ubah editor halaman', 'role_notifications' => 'Terima dan kelola notifikasi', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Izin Aset', 'roles_system_warning' => 'Ketahuilah bahwa akses ke salah satu dari tiga izin di atas dapat memungkinkan pengguna untuk mengubah hak mereka sendiri atau orang lain dalam sistem. Hanya tetapkan peran dengan izin ini untuk pengguna tepercaya.', 'role_asset_desc' => 'Izin ini mengontrol akses default ke aset dalam sistem. Izin pada Buku, Bab, dan Halaman akan menggantikan izin ini.', 'role_asset_admins' => 'Admin secara otomatis diberi akses ke semua konten tetapi opsi ini dapat menampilkan atau menyembunyikan opsi UI.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Semua', 'role_own' => 'Sendiri', 'role_controlled_by_asset' => 'Dikendalikan oleh aset tempat mereka diunggah', diff --git a/lang/is/notifications.php b/lang/is/notifications.php index b4fe01ad7..956fc7432 100644 --- a/lang/is/notifications.php +++ b/lang/is/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Uppfærð síða á: :pageName', 'updated_page_intro' => 'Síða hefur verið uppfærð á :appName:', 'updated_page_debounce' => 'Til að fyrirbyggja fjöldatilkynningar verður þér ekki sendar tilkynningar í smá stund um uppfærslu á þessari síðu frá sama höfundi.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Síðunafn:', 'detail_page_path' => 'Síðuslóð:', diff --git a/lang/is/preferences.php b/lang/is/preferences.php index b7ebc2df0..b6f261deb 100644 --- a/lang/is/preferences.php +++ b/lang/is/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Stýrðu þeim tölvupóst tilkynningum sem þú færð þegar ákveðnar aðgerðir eru gerðar af kerfinu.', 'notifications_opt_own_page_changes' => 'Láta vita þegar gerðar eru breytingar á síðum sem ég á', 'notifications_opt_own_page_comments' => 'Láta vita þegar gerðar eru athugasmedir við síður sem ég á', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Láta vita þegar athugasemdum mínum er svarað', 'notifications_save' => 'Vista stillingar', 'notifications_update_success' => 'Stillingar á tilkynningum hafa verið uppfærðar!', diff --git a/lang/is/settings.php b/lang/is/settings.php index 3765db061..5699c88d6 100644 --- a/lang/is/settings.php +++ b/lang/is/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Engin skilyrði sett', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Viðhald', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Flytja inn efni', 'role_editor_change' => 'Skipta um ritil síðu', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Allt', 'role_own' => 'Eigin', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/it/notifications.php b/lang/it/notifications.php index 6b8932ebe..a4e57abdf 100644 --- a/lang/it/notifications.php +++ b/lang/it/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Pagina aggiornata: :pageName', 'updated_page_intro' => 'Una pagina è stata aggiornata in :appName:', 'updated_page_debounce' => 'Per evitare una massa di notifiche, per un po\' non ti verranno inviate notifiche per ulteriori modifiche a questa pagina dallo stesso editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Nome della pagina:', 'detail_page_path' => 'Percorso della pagina:', diff --git a/lang/it/preferences.php b/lang/it/preferences.php index f25138401..db6b90754 100644 --- a/lang/it/preferences.php +++ b/lang/it/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Controlla le notifiche email che ricevi quando viene eseguita una determinata attività all\'interno del sistema.', 'notifications_opt_own_page_changes' => 'Notifica in caso di modifiche alle pagine che possiedo', 'notifications_opt_own_page_comments' => 'Notifica i commenti sulle pagine che possiedo', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notificare le risposte ai miei commenti', 'notifications_save' => 'Salva preferenze', 'notifications_update_success' => 'Le preferenze di notifica sono state aggiornate!', diff --git a/lang/it/settings.php b/lang/it/settings.php index 8bda7f8ff..9f2d882af 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Nessuna restrizione impostata', // Sorting Settings - 'sorting' => 'Ordinamento', - 'sorting_book_default' => 'Ordinamento libri predefinito', + 'sorting' => 'Elenchi E Ordinamento', + 'sorting_book_default' => 'Regola Di Ordinamento Libro Predefinita', 'sorting_book_default_desc' => 'Selezionare la regola di ordinamento predefinita da applicare ai nuovi libri. Questa regola non influisce sui libri esistenti e può essere modificata per ogni libro.', 'sorting_rules' => 'Regole di ordinamento', 'sorting_rules_desc' => 'Si tratta di operazioni di ordinamento predefinite applicabili ai contenuti del sistema.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Data di aggiornamento', 'sort_rule_op_chapters_first' => 'Capitoli Prima', 'sort_rule_op_chapters_last' => 'Capitoli dopo', + 'sorting_page_limits' => 'Limiti Visualizzazione Per Pagina', + 'sorting_page_limits_desc' => 'Imposta il numero di elementi da visualizzare per pagina nei vari elenchi all\'interno del sistema. In genere, un numero inferiore garantisce prestazioni migliori, mentre un numero più elevato evita la necessità di cliccare su più pagine. Si consiglia di utilizzare un multiplo pari di 3 (18, 24, 30, ecc...).', // Maintenance settings 'maint' => 'Manutenzione', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Importa contenuto', 'role_editor_change' => 'Cambiare editor di pagina', 'role_notifications' => 'Ricevere e gestire le notifiche', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Permessi entità', 'roles_system_warning' => 'Siate consapevoli che l\'accesso a uno dei tre permessi qui sopra può consentire a un utente di modificare i propri privilegi o i privilegi di altri nel sistema. Assegna ruoli con questi permessi solo ad utenti fidati.', 'role_asset_desc' => 'Questi permessi controllano l\'accesso predefinito alle entità. I permessi in libri, capitoli e pagine sovrascriveranno questi.', 'role_asset_admins' => 'Gli amministratori hanno automaticamente accesso a tutti i contenuti ma queste opzioni possono mostrare o nascondere le opzioni della UI.', 'role_asset_image_view_note' => 'Questo si riferisce alla visibilità all\'interno del gestore delle immagini. L\'accesso effettivo ai file di immagine caricati dipenderà dall\'opzione di archiviazione delle immagini di sistema.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Tutti', 'role_own' => 'Propri', 'role_controlled_by_asset' => 'Controllato dall\'entità in cui sono caricati', diff --git a/lang/ja/notifications.php b/lang/ja/notifications.php index 6fc0321d3..981932178 100644 --- a/lang/ja/notifications.php +++ b/lang/ja/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'ページの更新: :pageName', 'updated_page_intro' => ':appName でページが更新されました', 'updated_page_debounce' => '大量の通知を防ぐために、しばらくの間は同じユーザがこのページをさらに編集しても通知は送信されません。', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'ページ名:', 'detail_page_path' => 'ページパス:', diff --git a/lang/ja/preferences.php b/lang/ja/preferences.php index 0207596a9..a47906929 100644 --- a/lang/ja/preferences.php +++ b/lang/ja/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'システム内で特定のアクティビティが実行されたときに受信する電子メール通知を制御します。', 'notifications_opt_own_page_changes' => '自分が所有するページの変更を通知する', 'notifications_opt_own_page_comments' => '自分が所有するページへのコメントを通知する', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => '自分のコメントへの返信を通知する', 'notifications_save' => '設定を保存', 'notifications_update_success' => '通知設定を更新しました。', diff --git a/lang/ja/settings.php b/lang/ja/settings.php index db14f3ccc..a19c942b2 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => '制限しない', // Sorting Settings - 'sorting' => 'ソート', - 'sorting_book_default' => 'ブックのデフォルトソート', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => '新しいブックに適用するデフォルトのソートルールを選択します。これは既存のブックには影響しません。ルールはブックごとに上書きすることができます。', 'sorting_rules' => 'ソートルール', 'sorting_rules_desc' => 'これらはシステム内のコンテンツに適用できる事前定義のソート操作です。', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => '更新日時', 'sort_rule_op_chapters_first' => 'チャプタを最初に', 'sort_rule_op_chapters_last' => 'チャプタを最後に', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'メンテナンス', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'コンテンツのインポート', 'role_editor_change' => 'ページエディタの変更', 'role_notifications' => '通知の受信と管理', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'アセット権限', 'roles_system_warning' => '上記の3つの権限のいずれかを付与することは、ユーザーが自分の特権またはシステム内の他のユーザーの特権を変更できる可能性があることに注意してください。これらの権限は信頼できるユーザーにのみ割り当ててください。', 'role_asset_desc' => '各アセットに対するデフォルトの権限を設定します。ここで設定した権限が優先されます。', 'role_asset_admins' => '管理者にはすべてのコンテンツへのアクセス権が自動的に付与されますが、これらのオプションはUIオプションを表示または非表示にする場合があります。', 'role_asset_image_view_note' => 'これは画像マネージャー内の可視性に関連しています。アップロードされた画像ファイルへの実際のアクセスは、システムの画像保存オプションに依存します。', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => '全て', 'role_own' => '自身', 'role_controlled_by_asset' => 'このアセットに対し、右記の操作を許可:', diff --git a/lang/ka/notifications.php b/lang/ka/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/ka/notifications.php +++ b/lang/ka/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/ka/preferences.php b/lang/ka/preferences.php index 2872f5f3c..f4459d738 100644 --- a/lang/ka/preferences.php +++ b/lang/ka/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/ka/settings.php b/lang/ka/settings.php index 81c2c0a93..c68605fe1 100644 --- a/lang/ka/settings.php +++ b/lang/ka/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'No restriction set', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Maintenance', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/ko/notifications.php b/lang/ko/notifications.php index b337fa86d..a5abcf0f9 100644 --- a/lang/ko/notifications.php +++ b/lang/ko/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => '페이지 업데이트됨: :pageName', 'updated_page_intro' => ':appName: 에서 페이지가 업데이트되었습니다:', 'updated_page_debounce' => '알림이 한꺼번에 몰리는 것을 방지하기 위해 당분간 동일한 편집자가 이 페이지를 추가로 편집할 경우 알림이 전송되지 않습니다.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => '페이지 이름:', 'detail_page_path' => '페이지 경로:', diff --git a/lang/ko/preferences.php b/lang/ko/preferences.php index 79692f564..1979a23e4 100644 --- a/lang/ko/preferences.php +++ b/lang/ko/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => '시스템 내에서 특정 활동이 수행될 때 수신하는 이메일 알림을 제어합니다.', 'notifications_opt_own_page_changes' => '내가 소유한 페이지가 변경되면 알림 받기', 'notifications_opt_own_page_comments' => '내가 소유한 페이지에 댓글이 달렸을 때 알림 받기', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => '내 댓글에 대한 답글 알림 받기', 'notifications_save' => '환경설정 저장', 'notifications_update_success' => '알림 환경설정이 업데이트되었습니다!', diff --git a/lang/ko/settings.php b/lang/ko/settings.php index 797c12043..1138b0ece 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => '차단한 도메인 없음', // Sorting Settings - 'sorting' => '정렬', - 'sorting_book_default' => '기본 책 정렬', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => '새로운 책에 적용할 기본 정렬 규칙을 선택하세요. 이 선택은 기존 책에는 영향을 주지 않고, 기존 책의 설정은 책마다 변경할 수 있습니다.', 'sorting_rules' => '정렬 규칙', 'sorting_rules_desc' => '현재 시스템에 미리 정의된 정렬 규칙의 목록입니다.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => '수정일', 'sort_rule_op_chapters_first' => '챕터 우선 정렬', 'sort_rule_op_chapters_last' => '챕터 나중 정렬', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => '유지관리', @@ -195,11 +197,13 @@ return [ 'role_import_content' => '내용 가져오기', 'role_editor_change' => '페이지 편집기 변경', 'role_notifications' => '알림 수신 및 관리', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => '권한 항목', 'roles_system_warning' => '위 세 권한은 자신의 권한이나 다른 유저의 권한을 바꿀 수 있습니다.', 'role_asset_desc' => '책, 챕터, 문서별 권한은 이 설정에 우선합니다.', 'role_asset_admins' => '관리자 권한은 어디든 접근할 수 있지만 이 설정은 사용자 인터페이스에서 해당 활동을 표시할지 결정합니다.', 'role_asset_image_view_note' => '이는 이미지 관리자 내 가시성과 관련이 있습니다. 업로드된 이미지 파일의 실제 접근은 시스템의 이미지 저장 설정에 따라 달라집니다.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => '모든 항목', 'role_own' => '직접 만든 항목', 'role_controlled_by_asset' => '저마다 다름', diff --git a/lang/ku/notifications.php b/lang/ku/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/ku/notifications.php +++ b/lang/ku/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/ku/preferences.php b/lang/ku/preferences.php index 2872f5f3c..f4459d738 100644 --- a/lang/ku/preferences.php +++ b/lang/ku/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/ku/settings.php b/lang/ku/settings.php index 81c2c0a93..c68605fe1 100644 --- a/lang/ku/settings.php +++ b/lang/ku/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'No restriction set', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Maintenance', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/lt/notifications.php b/lang/lt/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/lt/notifications.php +++ b/lang/lt/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/lt/preferences.php b/lang/lt/preferences.php index 5c38d191c..6258fcae7 100644 --- a/lang/lt/preferences.php +++ b/lang/lt/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/lt/settings.php b/lang/lt/settings.php index 563038bf2..8120d85fe 100644 --- a/lang/lt/settings.php +++ b/lang/lt/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Nėra jokių apribojimų', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Priežiūra', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Nuosavybės leidimai', 'roles_system_warning' => 'Būkite sąmoningi, kad prieiga prie bet kurio iš trijų leidimų viršuje gali leisti naudotojui pakeisti jų pačių privilegijas arba kitų privilegijas sistemoje. Paskirkite vaidmenis su šiais leidimais tik patikimiems naudotojams.', 'role_asset_desc' => 'Šie leidimai kontroliuoja numatytą prieigą į nuosavybę, esančią sistemoje. Knygų, skyrių ir puslapių leidimai nepaisys šių leidimų.', 'role_asset_admins' => 'Administratoriams automatiškai yra suteikiama prieiga prie viso turinio, tačiau šie pasirinkimai gali rodyti arba slėpti vartotojo sąsajos parinktis.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Visi', 'role_own' => 'Nuosavi', 'role_controlled_by_asset' => 'Kontroliuojami nuosavybės, į kurią yra įkelti', diff --git a/lang/lv/notifications.php b/lang/lv/notifications.php index 9fd60222a..7233822a4 100644 --- a/lang/lv/notifications.php +++ b/lang/lv/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Atjaunināta lapa: :pageName', 'updated_page_intro' => 'Lapa atjaunināta :appName:', 'updated_page_debounce' => 'Lai novērstu pārliecīgu paziņojumu sūtīšanu, uz laiku jums tiks pārtraukti paziņojumi par turpmākiem šī lietotāja labojumiem šai lapai.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Lapas nosaukums:', 'detail_page_path' => 'Ceļš uz lapu:', diff --git a/lang/lv/preferences.php b/lang/lv/preferences.php index b91cc2e08..3c107d5fa 100644 --- a/lang/lv/preferences.php +++ b/lang/lv/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Pārvaldiet epasta paziņojumus, ko saņemsiet, kad sistēmā tiek veiktas noteiktas darbības.', 'notifications_opt_own_page_changes' => 'Paziņot par izmaiņām manās lapās', 'notifications_opt_own_page_comments' => 'Paziņot par komentāriem manās lapās', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Paziņot par atbildēm uz maniem komentāriem', 'notifications_save' => 'Saglabāt iestatījumus', 'notifications_update_success' => 'Paziņojumu iestatījumi ir atjaunoti!', diff --git a/lang/lv/settings.php b/lang/lv/settings.php index 9901f7811..fb5cf13dd 100644 --- a/lang/lv/settings.php +++ b/lang/lv/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Nav ierobežojumu', // Sorting Settings - 'sorting' => 'Kārtošana', - 'sorting_book_default' => 'Noklusētā grāmatu kārtošana', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Izvēlieties noklusēto kārtošanas nosacījumu, ko pielietot jaunām grāmatām. Šis neskars jau esošas grāmatas, un to var izmainīt grāmatas iestatījumos.', 'sorting_rules' => 'Kārtošanas noteikumi', 'sorting_rules_desc' => 'Šīs ir iepriekš noteiktas kārtošanas darbības, ko var pielietot saturam šajā sistēmā.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Atjaunināšanas datums', 'sort_rule_op_chapters_first' => 'Nodaļas pirmās', 'sort_rule_op_chapters_last' => 'Nodaļas pēdējās', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Apkope', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Importēt saturu', 'role_editor_change' => 'Mainīt lapu redaktoru', 'role_notifications' => 'Saņemt un pārvaldīt paziņojumus', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Resursa piekļuves tiesības', 'roles_system_warning' => 'Jebkuras no trīs augstāk redzamajām atļaujām dod iespēju lietotājam mainīt savas un citu lietotāju sistēmas atļaujas. Pievieno šīs grupu atļaujas tikai tiem lietotājiem, kuriem uzticies.', 'role_asset_desc' => 'Šīs piekļuves tiesības kontrolē noklusēto piekļuvi sistēmas resursiem. Grāmatām, nodaļām un lapām norādītās tiesības būs pārākas par šīm.', 'role_asset_admins' => 'Administratoriem automātiski ir piekļuve visam saturam, bet šie uzstādījumi var noslēpt vai parādīt lietotāja saskarnes iespējas.', 'role_asset_image_view_note' => 'Šis ir saistīts ar redzamību attēlu pārvaldniekā. Faktiskā piekļuve augšupielādēto attēlu failiem būs atkarīga no sistēmas attēlu glabātuves uzstādījuma.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Visi', 'role_own' => 'Savi', 'role_controlled_by_asset' => 'Kontrolē resurss, uz ko tie ir augšupielādēti', diff --git a/lang/nb/editor.php b/lang/nb/editor.php index 77b01f17b..23a788d71 100644 --- a/lang/nb/editor.php +++ b/lang/nb/editor.php @@ -48,7 +48,7 @@ return [ 'superscript' => 'Hevet skrift', 'subscript' => 'Senket skrift', 'text_color' => 'Tekstfarge', - 'highlight_color' => 'Highlight color', + 'highlight_color' => 'Uthevingsfarge', 'custom_color' => 'Egenvalgt farge', 'remove_color' => 'Fjern farge', 'background_color' => 'Bakgrunnsfarge', diff --git a/lang/nb/notifications.php b/lang/nb/notifications.php index bb46e09db..1e16229f3 100644 --- a/lang/nb/notifications.php +++ b/lang/nb/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Oppdatert side: :pageName', 'updated_page_intro' => 'En side er oppdatert i :appName:', 'updated_page_debounce' => 'For å forhindre mange varslinger, vil du ikke få nye varslinger for endringer på denne siden fra samme forfatter.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Sidenavn:', 'detail_page_path' => 'Side bane:', diff --git a/lang/nb/preferences.php b/lang/nb/preferences.php index 245c9c954..6a6850b83 100644 --- a/lang/nb/preferences.php +++ b/lang/nb/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Kontroller e-postvarslene du mottar når en bestemt aktivitet utføres i systemet.', 'notifications_opt_own_page_changes' => 'Varsle ved endringer til sider jeg eier', 'notifications_opt_own_page_comments' => 'Varsle om kommentarer på sider jeg eier', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Varsle ved svar på mine kommentarer', 'notifications_save' => 'Lagre innstillinger', 'notifications_update_success' => 'Varslingsinnstillingene er oppdatert!', diff --git a/lang/nb/settings.php b/lang/nb/settings.php index 68bab6f30..d1f40814e 100644 --- a/lang/nb/settings.php +++ b/lang/nb/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Ingen begrensninger er satt', // Sorting Settings - 'sorting' => 'Sortering', - 'sorting_book_default' => 'Standard boksortering', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Velg standard sorteringsregelen som skal brukes for nye bøker. Dette vil ikke påvirke eksisterende bøker, og kan overstyres per bok.', 'sorting_rules' => 'Sorteringsregler', 'sorting_rules_desc' => 'Dette er forhåndsdefinerte sorteringsoperasjoner som kan brukes på innhold i systemet.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Kapitler først', 'sort_rule_op_chapters_last' => 'Kapitler sist', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Vedlikehold', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import innhold', 'role_editor_change' => 'Endre sideredigering', 'role_notifications' => 'Motta og administrere varslinger', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Eiendomstillatelser', 'roles_system_warning' => 'Vær oppmerksom på at tilgang til noen av de ovennevnte tre tillatelsene kan tillate en bruker å endre sine egne rettigheter eller rettighetene til andre i systemet. Bare tildel roller med disse tillatelsene til pålitelige brukere.', 'role_asset_desc' => 'Disse tillatelsene kontrollerer standard tilgang til eiendelene i systemet. Tillatelser til bøker, kapitler og sider overstyrer disse tillatelsene.', 'role_asset_admins' => 'Administratorer får automatisk tilgang til alt innhold, men disse alternativene kan vise eller skjule UI-alternativer.', 'role_asset_image_view_note' => 'Dette gjelder synlighet innenfor bilde-administrasjonen. Faktisk tilgang på opplastede bildefiler vil være avhengig av valget for systemlagring av bildet.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Alle', 'role_own' => 'Egne', 'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til', diff --git a/lang/ne/notifications.php b/lang/ne/notifications.php index 1e9c57221..a4eb69832 100644 --- a/lang/ne/notifications.php +++ b/lang/ne/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'पाना अपडेट भयो: :pageName', 'updated_page_intro' => ':appName मा पाना अपडेट गरिएको छ', 'updated_page_debounce' => 'धेरै सूचना नपरोस् भनेर, केही समयको लागि एउटै सम्पादकबाट हुने थप सम्पादनहरूका सूचना तपाईंलाई पठाइने छैन।', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'पानाको नाम:', 'detail_page_path' => 'पानाको स्थान:', diff --git a/lang/ne/preferences.php b/lang/ne/preferences.php index 70388e8ba..2efdafc9f 100644 --- a/lang/ne/preferences.php +++ b/lang/ne/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'प्रणालीमा केही क्रियाकलापहरू गर्दा तपाईंलाई प्राप्त हुने इमेल सूचनाहरू नियन्त्रण गर्नुहोस्।', 'notifications_opt_own_page_changes' => 'मैले स्वामित्व राख्ने पृष्ठहरूमा परिवर्तन हुँदा सूचित गर्नुहोस्', 'notifications_opt_own_page_comments' => 'मैले स्वामित्व राख्ने पृष्ठहरूमा टिप्पणी हुँदा सूचित गर्नुहोस्', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'मेरो टिप्पणीहरूमा उत्तर आएको बेला सूचित गर्नुहोस्', 'notifications_save' => 'प्राथमिकताहरू बचत गर्नुहोस्', 'notifications_update_success' => 'सूचना प्राथमिकताहरू अपडेट गरिएका छन्!', diff --git a/lang/ne/settings.php b/lang/ne/settings.php index eb99f26bd..37e59978e 100644 --- a/lang/ne/settings.php +++ b/lang/ne/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'कुनै प्रतिबन्ध छैन', // Sorting Settings - 'sorting' => 'क्रमबद्धता', - 'sorting_book_default' => 'डिफल्ट पुस्तक क्रम', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'नयाँ पुस्तकहरूमा लागु गर्न डिफल्ट क्रम नियम चयन गर्नुहोस्। यो अस्तित्वमा रहेका पुस्तकहरूमा असर पार्दैन र पुस्तक अनुसार ओभरराइड गर्न सकिन्छ।', 'sorting_rules' => 'क्रम नियमहरू', 'sorting_rules_desc' => 'यी पूर्वनिर्धारित क्रम सञ्चालनहरू हुन् जुन प्रणालीमा सामग्रीमा लागू गर्न सकिन्छ।', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'अपडेट मिति', 'sort_rule_op_chapters_first' => 'पहिले अध्यायहरू', 'sort_rule_op_chapters_last' => 'अन्त्यमा अध्यायहरू', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'सम्भार', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'सामग्री आयात गर्नुहोस्', 'role_editor_change' => 'पृष्ठ सम्पादक परिवर्तन गर्नुहोस्', 'role_notifications' => 'सूचनाहरू प्राप्त र व्यवस्थापन गर्नुहोस्', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'संपत्ति अनुमति', 'roles_system_warning' => 'माथिका कुनै पनि तीन अनुमति प्रयोगकर्ताले आफैं वा अरूका अधिकार परिवर्तन गर्न सक्छन्। यी अनुमति भएको भूमिका मात्र भरपर्दो प्रयोगकर्तालाई दिनुहोस्।', 'role_asset_desc' => 'यी अनुमतिले प्रणालीभित्र सम्पत्तिमा डिफल्ट पहुँच नियन्त्रण गर्छ। पुस्तक, अध्याय र पृष्ठमा अनुमति यी भन्दा प्राथमिक हुन्छ।', 'role_asset_admins' => 'प्रशासनकर्ताहरूलाई सबै सामग्रीमा स्वतः पहुँच दिइन्छ, यी विकल्पहरूले UI मा देखिने वा लुकेका विकल्पहरू मात्र प्रभाव पार्न सक्छ।', 'role_asset_image_view_note' => 'यो छवि व्यवस्थापक भित्रको दृश्यता सम्बन्धि हो। अपलोड गरिएको छविमा वास्तविक पहुँच प्रणालीको छवि भण्डारण विकल्प अनुसार हुन्छ।', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'सबै', 'role_own' => 'आफ्नो', 'role_controlled_by_asset' => 'अपलोड गरिएको सम्पत्तिले नियन्त्रण गरेको', diff --git a/lang/nl/notifications.php b/lang/nl/notifications.php index 1e7035670..ce2a3ebff 100644 --- a/lang/nl/notifications.php +++ b/lang/nl/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Aangepaste pagina: :pageName', 'updated_page_intro' => 'Een pagina werd aangepast in :appName:', 'updated_page_debounce' => 'Om een stortvloed aan meldingen te voorkomen, zul je een tijdje geen meldingen ontvangen voor verdere bewerkingen van deze pagina door dezelfde redacteur.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Pagina Naam:', 'detail_page_path' => 'Paginapad:', diff --git a/lang/nl/preferences.php b/lang/nl/preferences.php index d0e3623b9..1fb56af07 100644 --- a/lang/nl/preferences.php +++ b/lang/nl/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Bepaal welke e-mailmeldingen je ontvangt wanneer bepaalde activiteiten in het systeem worden uitgevoerd.', 'notifications_opt_own_page_changes' => 'Geef melding bij wijzigingen aan pagina\'s waarvan ik de eigenaar ben', 'notifications_opt_own_page_comments' => 'Geef melding van opmerkingen op pagina\'s waarvan ik de eigenaar ben', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Geef melding van reacties op mijn opmerkingen', 'notifications_save' => 'Voorkeuren opslaan', 'notifications_update_success' => 'Voorkeuren voor meldingen zijn bijgewerkt!', diff --git a/lang/nl/settings.php b/lang/nl/settings.php index 3b4da75a9..2041669eb 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -75,7 +75,7 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Geen beperkingen ingesteld', // Sorting Settings - 'sorting' => 'Sorteren', + 'sorting' => 'Lijsten & Sorteren', 'sorting_book_default' => 'Standaard Sorteerregel Boek', 'sorting_book_default_desc' => 'Selecteer de standaard sorteerregel om toe te passen op nieuwe boeken. Dit heeft geen invloed op bestaande boeken, en kan per boek worden overschreven.', 'sorting_rules' => 'Sorteerregels', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Bijwerkdatum', 'sort_rule_op_chapters_first' => 'Hoofdstukken Eerst', 'sort_rule_op_chapters_last' => 'Hoofdstukken Laatst', + 'sorting_page_limits' => 'Weergavelimiet Per Pagina', + 'sorting_page_limits_desc' => 'Stel in hoeveel items er op een pagina worden laten zien in de verschillende lijstweergaves. Een lager aantal verbeterd de snelheid, een hoger aantal verminderd het doorklikken door pagina\'s. Een even veelvoud van 3 (18, 24, 30, etc...) wordt aanbevolen.', // Maintenance settings 'maint' => 'Onderhoud', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Importeer inhoud', 'role_editor_change' => 'Wijzig pagina bewerker', 'role_notifications' => 'Meldingen ontvangen & beheren', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Asset Machtigingen', 'roles_system_warning' => 'Wees ervan bewust dat toegang tot een van de bovengenoemde drie machtigingen een gebruiker in staat kan stellen zijn eigen machtigingen of de machtigingen van anderen in het systeem kan wijzigen. Wijs alleen rollen toe met deze machtigingen aan vertrouwde gebruikers.', 'role_asset_desc' => 'Deze machtigingen bepalen de standaard toegang tot de assets binnen het systeem. Machtigingen op boeken, hoofdstukken en pagina\'s overschrijven deze instelling.', 'role_asset_admins' => 'Beheerders krijgen automatisch toegang tot alle inhoud, maar deze opties kunnen gebruikersinterface opties tonen of verbergen.', 'role_asset_image_view_note' => 'Dit heeft betrekking op de zichtbaarheid binnen de afbeeldingsbeheerder. De werkelijke toegang tot geüploade afbeeldingsbestanden hangt af van de gekozen opslagmethode.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Alles', 'role_own' => 'Eigen', 'role_controlled_by_asset' => 'Gecontroleerd door de asset waar deze is geüpload', diff --git a/lang/nn/notifications.php b/lang/nn/notifications.php index 247d8d105..25f0f30c8 100644 --- a/lang/nn/notifications.php +++ b/lang/nn/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Oppdatert side: :pageName', 'updated_page_intro' => 'Ei side vart oppdatert i :appName:', 'updated_page_debounce' => 'For å forhindre mange varslingar, vil du ikkje få nye varslinger for endringar på denne siden frå same forfattar.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Sidenamn:', 'detail_page_path' => 'Sidenamn:', diff --git a/lang/nn/preferences.php b/lang/nn/preferences.php index ac6dc1b77..17d1fca42 100644 --- a/lang/nn/preferences.php +++ b/lang/nn/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Kontroller e-postvarslene du mottar når en bestemt aktivitet utføres i systemet.', 'notifications_opt_own_page_changes' => 'Varsle ved endringer til sider jeg eier', 'notifications_opt_own_page_comments' => 'Varsle om kommentarer på sider jeg eier', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Varsle ved svar på mine kommentarer', 'notifications_save' => 'Lagre innstillinger', 'notifications_update_success' => 'Varslingsinnstillingene er oppdatert!', diff --git a/lang/nn/settings.php b/lang/nn/settings.php index c098ac753..6d2259bd8 100644 --- a/lang/nn/settings.php +++ b/lang/nn/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Ingen begrensninger er satt', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Vedlikehold', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Endre sideredigering', 'role_notifications' => 'Motta og administrere varslinger', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Eiendomstillatelser', 'roles_system_warning' => 'Vær oppmerksom på at tilgang til noen av de ovennevnte tre tillatelsene kan tillate en bruker å endre sine egne rettigheter eller rettighetene til andre i systemet. Bare tildel roller med disse tillatelsene til pålitelige brukere.', 'role_asset_desc' => 'Disse tillatelsene kontrollerer standard tilgang til eiendelene i systemet. Tillatelser til bøker, kapitler og sider overstyrer disse tillatelsene.', 'role_asset_admins' => 'Administratorer får automatisk tilgang til alt innhold, men disse alternativene kan vise eller skjule UI-alternativer.', 'role_asset_image_view_note' => 'Dette gjelder synlighet innenfor bilde-administrasjonen. Faktisk tilgang på opplastede bildefiler vil være avhengig av valget for systemlagring av bildet.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Alle', 'role_own' => 'Egne', 'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til', diff --git a/lang/pl/notifications.php b/lang/pl/notifications.php index 661149994..a2c9ff1c0 100644 --- a/lang/pl/notifications.php +++ b/lang/pl/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Zaktualizowano stronę: :pageName', 'updated_page_intro' => 'Strona została zaktualizowana w :appName:', 'updated_page_debounce' => 'Aby zapobiec nadmiarowi powiadomień, przez jakiś czas nie będziesz otrzymywać powiadomień o dalszych edycjach tej strony przez tego samego edytora.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Nazwa strony:', 'detail_page_path' => 'Ścieżka strony:', diff --git a/lang/pl/preferences.php b/lang/pl/preferences.php index dd3482102..372b8eda6 100644 --- a/lang/pl/preferences.php +++ b/lang/pl/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Kontroluj otrzymywane powiadomienia e-mail, gdy określona aktywność jest wykonywana w systemie.', 'notifications_opt_own_page_changes' => 'Powiadom o zmianach na stronach, których jestem właścicielem', 'notifications_opt_own_page_comments' => 'Powiadom o komentarzach na stronach, których jestem właścicielem', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Powiadom o odpowiedziach na moje komentarze', 'notifications_save' => 'Zapisz preferencje', 'notifications_update_success' => 'Preferencje powiadomień zostały zaktualizowane!', diff --git a/lang/pl/settings.php b/lang/pl/settings.php index 7c84ce34b..c32797f2f 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Brak restrykcji', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Konserwacja', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Zmień edytor strony', 'role_notifications' => 'Odbieranie i zarządzanie powiadomieniami', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Zarządzanie zasobami', 'roles_system_warning' => 'Pamiętaj, że dostęp do trzech powyższych uprawnień może pozwolić użytkownikowi na zmianę własnych uprawnień lub uprawnień innych osób w systemie. Przypisz tylko role z tymi uprawnieniami do zaufanych użytkowników.', 'role_asset_desc' => 'Te ustawienia kontrolują zarządzanie zasobami systemu. Uprawnienia książek, rozdziałów i stron nadpisują te ustawienia.', 'role_asset_admins' => 'Administratorzy mają automatycznie dostęp do wszystkich treści, ale te opcję mogą być pokazywać lub ukrywać opcje interfejsu użytkownika.', 'role_asset_image_view_note' => 'To odnosi się do widoczności w ramach menedżera obrazów. Rzeczywista możliwość dostępu do przesłanych plików obrazów będzie zależeć od systemowej opcji przechowywania obrazów.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Wszyscy', 'role_own' => 'Własne', 'role_controlled_by_asset' => 'Kontrolowane przez zasób, do którego zostały udostępnione', diff --git a/lang/pt/notifications.php b/lang/pt/notifications.php index 1243c6680..cbe3a511c 100644 --- a/lang/pt/notifications.php +++ b/lang/pt/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Página atualizada: :pageName', 'updated_page_intro' => 'Uma página foi atualizada em :appName:', 'updated_page_debounce' => 'Para evitar um grande volume de notificações, durante algum tempo não serão enviadas notificações de edições futuras para esta página através do mesmo editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Nome da Página:', 'detail_page_path' => 'Page Path:', diff --git a/lang/pt/preferences.php b/lang/pt/preferences.php index 860eec645..b7308aaf9 100644 --- a/lang/pt/preferences.php +++ b/lang/pt/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Controlar as notificações via correio eletrónico quando certas atividades são executadas pelo sistema.', 'notifications_opt_own_page_changes' => 'Notificar quando páginas que possuo sofrem alterações', 'notifications_opt_own_page_comments' => 'Notificar quando comentam páginas que possuo', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notificar respostas aos meus comentários', 'notifications_save' => 'Guardar preferências', 'notifications_update_success' => 'Preferências de notificação foram atualizadas!', diff --git a/lang/pt/settings.php b/lang/pt/settings.php index 0c765fb98..c2179e640 100644 --- a/lang/pt/settings.php +++ b/lang/pt/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Nenhuma restrição definida', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Manutenção', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Alterar editor de página', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Permissões de Ativos', 'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um utilizador altere os seus próprios privilégios ou privilégios de outros no sistema. Apenas atribua cargos com essas permissões a utilizadores de confiança.', 'role_asset_desc' => 'Estas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por estas permissões.', 'role_asset_admins' => 'Os administradores recebem automaticamente acesso a todo o conteúdo, mas estas opções podem mostrar ou ocultar as opções da Interface de Usuário.', 'role_asset_image_view_note' => 'Isto está relacionado com a visibilidade do gerenciador de imagens. O acesso real dos arquivos de imagem enviados dependerá da opção de armazenamento de imagens do sistema.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Todos', 'role_own' => 'Próprio', 'role_controlled_by_asset' => 'Controlado pelo ativo para o qual eles são enviados', diff --git a/lang/pt_BR/notifications.php b/lang/pt_BR/notifications.php index 6397b20e5..8c98467c8 100644 --- a/lang/pt_BR/notifications.php +++ b/lang/pt_BR/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Página atualizada: :pageName', 'updated_page_intro' => 'Uma página foi atualizada em :appName:', 'updated_page_debounce' => 'Para prevenir notificações em massa, por enquanto notificações não serão enviadas para você para próximas edições nessa página pelo mesmo editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Nome da Página:', 'detail_page_path' => 'Caminho da Página:', diff --git a/lang/pt_BR/preferences.php b/lang/pt_BR/preferences.php index e0b79ff06..d2b7fc540 100644 --- a/lang/pt_BR/preferences.php +++ b/lang/pt_BR/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Controle as notificações por e-mail que você recebe quando uma determinada atividade é executada no sistema.', 'notifications_opt_own_page_changes' => 'Notificar quando houver alterações em páginas que eu possuo', 'notifications_opt_own_page_comments' => 'Notificar comentários nas páginas que eu possuo', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notificar ao responder aos meus comentários', 'notifications_save' => 'Salvar Preferências', 'notifications_update_success' => 'Preferências de notificação foram atualizadas!', diff --git a/lang/pt_BR/settings.php b/lang/pt_BR/settings.php index b940e57d4..53947a36d 100644 --- a/lang/pt_BR/settings.php +++ b/lang/pt_BR/settings.php @@ -16,7 +16,7 @@ return [ 'app_customization' => 'Customização', 'app_features_security' => 'Recursos & Segurança', 'app_name' => 'Nome da Aplicação', - 'app_name_desc' => 'Esse nome será mostrado no cabeçalho e em e-mails.', + 'app_name_desc' => 'Esse nome será mostrado no cabeçalho e nos e-mails.', 'app_name_header' => 'Mostrar o nome no cabeçalho', 'app_public_access' => 'Acesso Público', 'app_public_access_desc' => 'Habilitar esta opção irá permitir que visitantes, que não estão logados, acessem o conteúdo em sua instância do BookStack.', @@ -29,14 +29,14 @@ return [ 'app_default_editor' => 'Editor de Página Padrão', 'app_default_editor_desc' => 'Selecione qual editor será usado por padrão ao editar novas páginas. Isso pode ser substituído em um nível de página onde é permitido.', 'app_custom_html' => 'Conteúdo customizado para HTML', - 'app_custom_html_desc' => 'Quaisquer conteúdos aqui adicionados serão inseridos no final da seção de cada página. Essa é uma maneira útil de sobrescrever estilos e adicionar códigos de análise de site.', - 'app_custom_html_disabled_notice' => 'O conteúdo customizado do HTML está desabilitado nesta página de configurações, para garantir que quaisquer alterações danosas possam ser revertidas.', + 'app_custom_html_desc' => 'Qualquer conteúdo adicionado aqui será inserido ao final do HTML de todas as páginas. Isso é útil para sobrescrever estilos e adicionar códigos de análise e estatística do site.', + 'app_custom_html_disabled_notice' => 'O conteúdo customizado do HTML está desabilitado nesta página de configurações para garantir que quaisquer alterações danosas possam ser revertidas.', 'app_logo' => 'Logo da Aplicação', 'app_logo_desc' => 'Isto é usado na barra de cabeçalho do aplicativo, entre outras áreas. Esta imagem deve ter 86px de altura. Imagens grandes serão reduzidas.', 'app_icon' => 'Ícone do Aplicativo', 'app_icon_desc' => 'Este ícone é usado para guias e ícones de atalhos do navegador. Deve ser uma imagem PNG quadrada de 256px.', 'app_homepage' => 'Página Inicial', - 'app_homepage_desc' => 'Selecione uma opção para ser exibida como página inicial em vez da padrão. Permissões de página serão ignoradas para as páginas selecionadas.', + 'app_homepage_desc' => 'Selecione uma opção para ser exibida como página inicial no lugar da página padrão. Permissões de página serão ignoradas para as páginas selecionadas.', 'app_homepage_select' => 'Selecione uma página', 'app_footer_links' => 'Links do Rodapé', 'app_footer_links_desc' => 'Adicionar links para mostrar dentro do rodapé do site. Estes serão exibidos na parte inferior da maioria das páginas, incluindo aqueles que não necessitam de login. Você pode usar uma etiqueta de "trans::" para usar traduções definidas pelo sistema. Por exemplo: Usando "trans::common.privacy_policy" fornecerá o texto traduzido "Política de Privacidade" e "trans::common.terms_of_service" fornecerá o texto traduzido "Termos de Serviço".', @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Nenhuma restrição definida', // Sorting Settings - 'sorting' => 'Ordenação', - 'sorting_book_default' => 'Ordenação padrão de livros', + 'sorting' => 'Listas e classificações', + 'sorting_book_default' => 'Regra padrão de classificação de livros', 'sorting_book_default_desc' => 'Selecione a regra de ordenação padrão a ser aplicada a novos livros. Isso não afetará os livros existentes e pode ser substituído para cada livro individualmente.', 'sorting_rules' => 'Regras de ordenação', 'sorting_rules_desc' => 'Estas são operações de ordenação pré-definidas que podem ser aplicadas a conteúdos no sistema.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Data de Atualização', 'sort_rule_op_chapters_first' => 'Capítulos Primeiro', 'sort_rule_op_chapters_last' => 'Capítulos por Último', + 'sorting_page_limits' => 'Limites de exibição por página', + 'sorting_page_limits_desc' => 'Defina quantos itens serão exibidos por página em diferentes listas do sistema. Normalmente, um número menor proporciona melhor desempenho, enquanto um número maior evita a necessidade de clicar em várias páginas. É recomendado o uso de um múltiplo par de 3 (18, 24, 30, etc.).', // Maintenance settings 'maint' => 'Manutenção', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Importar conteúdo', 'role_editor_change' => 'Alterar página de edição', 'role_notifications' => 'Receber e gerenciar notificações', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Permissões de Ativos', 'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um usuário altere seus próprios privilégios ou privilégios de outros usuários no sistema. Apenas atribua perfis com essas permissões para usuários confiáveis.', 'role_asset_desc' => 'Essas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por essas permissões.', 'role_asset_admins' => 'Administradores recebem automaticamente acesso a todo o conteúdo, mas essas opções podem mostrar ou ocultar as opções da Interface de Usuário.', 'role_asset_image_view_note' => 'Isso está relacionado à visibilidade no gerenciador de imagens. O acesso real dos arquivos de imagem carregados dependerá da opção de armazenamento de imagem do sistema.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Todos', 'role_own' => 'Próprio', 'role_controlled_by_asset' => 'Controlado pelos ativos nos quais o upload foi realizado', diff --git a/lang/ro/notifications.php b/lang/ro/notifications.php index 676eeb814..da7f590fd 100644 --- a/lang/ro/notifications.php +++ b/lang/ro/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Pagina actualizată: :pageName', 'updated_page_intro' => 'O nouă pagină a fost creată în :appName:', 'updated_page_debounce' => 'Pentru a preveni notificări în masă, pentru un timp nu veți primi notificări suplimentare la această pagină de către același editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Nume pagină:', 'detail_page_path' => 'Page Path:', diff --git a/lang/ro/preferences.php b/lang/ro/preferences.php index f7529305c..93db2f66a 100644 --- a/lang/ro/preferences.php +++ b/lang/ro/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Controlați notificările prin e-mail pe care le primiți atunci când o anumită activitate este efectuată în sistem.', 'notifications_opt_own_page_changes' => 'Notifică la comentarii pe paginile pe care le dețin', 'notifications_opt_own_page_comments' => 'Notifică la comentarii pe paginile pe care le dețin', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notifică la răspunsurile la comentariile mele', 'notifications_save' => 'Salvează Preferințe', 'notifications_update_success' => 'Preferințele de notificare au fost actualizate!', diff --git a/lang/ro/settings.php b/lang/ro/settings.php index a28dca22c..02052ef3c 100644 --- a/lang/ro/settings.php +++ b/lang/ro/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Nicio restricție setată', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Mentenanţă', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Schimbă editorul de pagină', 'role_notifications' => 'Primire și gestionare notificări', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Permisiuni active', 'roles_system_warning' => 'Fi conștient de faptul că accesul la oricare dintre cele trei permisiuni de mai sus poate permite unui utilizator să își modifice propriile privilegii sau privilegiile altor persoane din sistem. Atribuie doar roluri cu aceste permisiuni utilizatorilor de încredere.', 'role_asset_desc' => 'Aceste permisiuni controlează accesul implicit la activele din sistem. Permisiunile pe Cărți, Capitole și Pagini vor suprascrie aceste permisiuni.', 'role_asset_admins' => 'Administratorilor li se acordă automat acces la tot conținutul, dar aceste opțiuni pot afișa sau ascunde opțiunile UI.', 'role_asset_image_view_note' => 'Acest lucru se referă la vizibilitatea în managerul de imagini. Accesul efectiv al fișierelor de imagine încărcate va depinde de opțiunea de stocare a imaginilor din sistem.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Tot', 'role_own' => 'Propriu', 'role_controlled_by_asset' => 'Controlat de activele pe care sunt încărcate', diff --git a/lang/ru/notifications.php b/lang/ru/notifications.php index c5e98da80..289de42b6 100644 --- a/lang/ru/notifications.php +++ b/lang/ru/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Обновлена страница: :pageName', 'updated_page_intro' => 'Страница была обновлена в :appName:', 'updated_page_debounce' => 'Чтобы предотвратить массовые уведомления, в течение некоторого времени вы не будете получать уведомления о дальнейших правках этой страницы этим же редактором.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Имя страницы:', 'detail_page_path' => 'Путь страницы:', diff --git a/lang/ru/preferences.php b/lang/ru/preferences.php index 27217815d..b61b252c8 100644 --- a/lang/ru/preferences.php +++ b/lang/ru/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Управляйте полученными по электронной почте уведомлениями при выполнении определенных действий в системе.', 'notifications_opt_own_page_changes' => 'Уведомлять об изменениях в собственных страницах', 'notifications_opt_own_page_comments' => 'Уведомлять о комментариях на собственных страницах', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Уведомлять об ответах на мои комментарии', 'notifications_save' => 'Сохранить настройки', 'notifications_update_success' => 'Настройки уведомлений были обновлены!', diff --git a/lang/ru/settings.php b/lang/ru/settings.php index 69c13e46d..47839d520 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Без ограничений', // Sorting Settings - 'sorting' => 'Сортировка', - 'sorting_book_default' => 'Сортировка книг по умолчанию', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Выберите правило сортировки по умолчанию для новых книг. Это не повлияет на существующие книги, и может быть изменено для каждой книги отдельно.', 'sorting_rules' => 'Правила сортировки', 'sorting_rules_desc' => 'Выберите правило сортировки по умолчанию для новых книг. Это не повлияет на существующие книги и может быть изменено для каждой книги отдельно.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Главы в конце', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Обслуживание', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Изменение редактора страниц', 'role_notifications' => 'Получение и управление уведомлениями', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Права доступа к материалам', 'roles_system_warning' => 'Имейте в виду, что доступ к любому из указанных выше трех разрешений может позволить пользователю изменить свои собственные привилегии или привилегии других пользователей системы. Назначать роли с этими правами можно только доверенным пользователям.', 'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.', 'role_asset_admins' => 'Администраторы автоматически получают доступ ко всему контенту, но эти опции могут отображать или скрывать параметры пользовательского интерфейса.', 'role_asset_image_view_note' => 'Это относится к видимости в менеджере изображений. Фактический доступ к загруженным файлам изображений будет зависеть от опции хранения системных изображений.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Все', 'role_own' => 'Владелец', 'role_controlled_by_asset' => 'Контролируется активом, в который они загружены', diff --git a/lang/sk/notifications.php b/lang/sk/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/sk/notifications.php +++ b/lang/sk/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/sk/preferences.php b/lang/sk/preferences.php index 85da4f331..54fbdb677 100644 --- a/lang/sk/preferences.php +++ b/lang/sk/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/sk/settings.php b/lang/sk/settings.php index 2731fd01a..04855a7f9 100644 --- a/lang/sk/settings.php +++ b/lang/sk/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Nie sú nastavené žiadne obmedzenia', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Údržba', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Zmeniť editor stránky', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Oprávnenia majetku', 'roles_system_warning' => 'Uvedomte si, že prístup ku ktorémukoľvek z vyššie uvedených troch povolení môže používateľovi umožniť zmeniť svoje vlastné privilégiá alebo privilégiá ostatných v systéme. Roly s týmito povoleniami priraďujte iba dôveryhodným používateľom.', 'role_asset_desc' => 'Tieto oprávnenia regulujú prednastavený prístup k zdroju v systéme. Oprávnenia pre knihy, kapitoly a stránky majú vyššiu prioritu.', 'role_asset_admins' => 'Správcovia majú automaticky prístup ku všetkému obsahu, ale tieto možnosti môžu zobraziť alebo skryť možnosti používateľského rozhrania.', 'role_asset_image_view_note' => 'Toto sa týka viditeľnosti v rámci správcu obrázkov. Skutočný prístup k nahratým súborom obrázkov bude závisieť od možnosti ukladania obrázkov systému.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Všetko', 'role_own' => 'Vlastné', 'role_controlled_by_asset' => 'Regulované zdrojom, do ktorého sú nahrané', diff --git a/lang/sl/notifications.php b/lang/sl/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/sl/notifications.php +++ b/lang/sl/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/sl/preferences.php b/lang/sl/preferences.php index 2872f5f3c..f4459d738 100644 --- a/lang/sl/preferences.php +++ b/lang/sl/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/sl/settings.php b/lang/sl/settings.php index 904978f47..6eaed0a17 100644 --- a/lang/sl/settings.php +++ b/lang/sl/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Brez omejitev', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Vzdrževanje', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Sistemska dovoljenja', 'roles_system_warning' => 'Zavedajte se, da lahko dostop do kateregakoli od zgornjih treh dovoljenj uporabniku omogoči, da spremeni lastne privilegije ali privilegije drugih v sistemu. Vloge s temi dovoljenji dodelite samo zaupanja vrednim uporabnikom.', 'role_asset_desc' => 'Ta dovoljenja nadzorujejo privzeti dostop do sredstev v sistemu. Dovoljenja za knjige, poglavja in strani bodo razveljavila ta dovoljenja.', 'role_asset_admins' => 'Skrbniki samodejno pridobijo dostop do vseh vsebin, vendar lahko te možnosti prikažejo ali pa skrijejo možnosti uporabniškega vmesnika.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Vse', 'role_own' => 'Lasten', 'role_controlled_by_asset' => 'Nadzira ga sredstvo, v katerega so naloženi', diff --git a/lang/sq/notifications.php b/lang/sq/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/sq/notifications.php +++ b/lang/sq/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/sq/preferences.php b/lang/sq/preferences.php index 2872f5f3c..f4459d738 100644 --- a/lang/sq/preferences.php +++ b/lang/sq/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/sq/settings.php b/lang/sq/settings.php index 81c2c0a93..c68605fe1 100644 --- a/lang/sq/settings.php +++ b/lang/sq/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'No restriction set', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Maintenance', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/sr/activities.php b/lang/sr/activities.php index ad594a220..b4aa5e2a7 100644 --- a/lang/sr/activities.php +++ b/lang/sr/activities.php @@ -85,9 +85,9 @@ return [ 'webhook_delete_notification' => 'Вебхоок је успешно обрисан', // Imports - 'import_create' => 'created import', + 'import_create' => 'креиран увоз', 'import_create_notification' => 'Import successfully uploaded', - 'import_run' => 'updated import', + 'import_run' => 'ажуриран увоз', 'import_run_notification' => 'Content successfully imported', 'import_delete' => 'deleted import', 'import_delete_notification' => 'Import successfully deleted', diff --git a/lang/sr/common.php b/lang/sr/common.php index e185c99f5..c5c62db65 100644 --- a/lang/sr/common.php +++ b/lang/sr/common.php @@ -30,7 +30,7 @@ return [ 'create' => 'Креирај', 'update' => 'Ажурирање', 'edit' => 'Уреди', - 'archive' => 'Archive', + 'archive' => 'Архивирај', 'unarchive' => 'Un-Archive', 'sort' => 'Разврстај', 'move' => 'Премести', diff --git a/lang/sr/editor.php b/lang/sr/editor.php index e5052595c..2756775a9 100644 --- a/lang/sr/editor.php +++ b/lang/sr/editor.php @@ -13,7 +13,7 @@ return [ 'cancel' => 'Поништи', 'save' => 'Сачувај', 'close' => 'Затвори', - 'apply' => 'Apply', + 'apply' => 'Примени', 'undo' => 'Опозови', 'redo' => 'Понови', 'left' => 'Лево', diff --git a/lang/sr/entities.php b/lang/sr/entities.php index 456a76ceb..151edee40 100644 --- a/lang/sr/entities.php +++ b/lang/sr/entities.php @@ -50,7 +50,7 @@ return [ 'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:', 'import_pending' => 'Pending Imports', 'import_pending_none' => 'No imports have been started.', - 'import_continue' => 'Continue Import', + 'import_continue' => 'Настави увоз', 'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.', 'import_details' => 'Import Details', 'import_run' => 'Run Import', @@ -109,7 +109,7 @@ return [ // Shelves 'shelf' => 'Shelf', - 'shelves' => 'Shelves', + 'shelves' => 'Полице', 'x_shelves' => ':count Shelf|:count Shelves', 'shelves_empty' => 'No shelves have been created', 'shelves_create' => 'Create New Shelf', diff --git a/lang/sr/notifications.php b/lang/sr/notifications.php index 6aa3f2abb..4cc499fdd 100644 --- a/lang/sr/notifications.php +++ b/lang/sr/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Ажурирана страница: :pageName', 'updated_page_intro' => 'Страница је ажурирана у :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Назив странице:', 'detail_page_path' => 'Путања странице:', diff --git a/lang/sr/preferences.php b/lang/sr/preferences.php index 2872f5f3c..f4459d738 100644 --- a/lang/sr/preferences.php +++ b/lang/sr/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/sr/settings.php b/lang/sr/settings.php index b7213e905..d34ff3f3b 100644 --- a/lang/sr/settings.php +++ b/lang/sr/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Нема постављених ограничења', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Одржавање', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/sr/validation.php b/lang/sr/validation.php index d9b982d1e..a2a617633 100644 --- a/lang/sr/validation.php +++ b/lang/sr/validation.php @@ -14,7 +14,7 @@ return [ 'alpha' => 'The :attribute may only contain letters.', 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', 'alpha_num' => 'The :attribute may only contain letters and numbers.', - 'array' => 'The :attribute must be an array.', + 'array' => ':attribute мора бити низ.', 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => 'The :attribute must be a date before :date.', 'between' => [ diff --git a/lang/sv/notifications.php b/lang/sv/notifications.php index 58418ddb0..19933c049 100644 --- a/lang/sv/notifications.php +++ b/lang/sv/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Uppdaterad sida: :pageName', 'updated_page_intro' => 'En sida har blivit uppdaterad i :appName:', 'updated_page_debounce' => 'För att förhindra en massa notiser, så kommer det inte skickas nya notiser på ett tag för ytterligare ändringar till denna sida av samma skribent.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Sidonamn:', 'detail_page_path' => 'Sidosökväg:', diff --git a/lang/sv/preferences.php b/lang/sv/preferences.php index 945099151..492081e59 100644 --- a/lang/sv/preferences.php +++ b/lang/sv/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/sv/settings.php b/lang/sv/settings.php index 8f391d3d7..2e86241da 100644 --- a/lang/sv/settings.php +++ b/lang/sv/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Ingen begränsning inställd', // Sorting Settings - 'sorting' => 'Sorterar', - 'sorting_book_default' => 'Standard boksortering', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Välj standard sorteringsregel som skall tillämpas på nya böcker. Detta påverkar inte befintliga böcker och kan åsidosättas per bok.', 'sorting_rules' => 'Sorteringsregler', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Datum uppdaterat', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Underhåll', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Ändra sidredigerare', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Tillgång till innehåll', 'roles_system_warning' => 'Var medveten om att åtkomst till någon av ovanstående tre behörigheter kan tillåta en användare att ändra sina egna rättigheter eller andras rättigheter i systemet. Tilldela endast roller med dessa behörigheter till betrodda användare.', 'role_asset_desc' => 'Det här är standardinställningarna för allt innehåll i systemet. Eventuella anpassade rättigheter på böcker, kapitel och sidor skriver över dessa inställningar.', 'role_asset_admins' => 'Administratörer har automatisk tillgång till allt innehåll men dessa alternativ kan visa och dölja vissa gränssnittselement', 'role_asset_image_view_note' => 'Detta avser synlighet inom bildhanteraren. Faktisk åtkomst för uppladdade bildfiler kommer att bero på alternativ för bildlagring.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Alla', 'role_own' => 'Egna', 'role_controlled_by_asset' => 'Kontrolleras av den sida de laddas upp till', diff --git a/lang/tk/notifications.php b/lang/tk/notifications.php index 1afd23f1d..563ac24e8 100644 --- a/lang/tk/notifications.php +++ b/lang/tk/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/tk/preferences.php b/lang/tk/preferences.php index 2872f5f3c..f4459d738 100644 --- a/lang/tk/preferences.php +++ b/lang/tk/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/tk/settings.php b/lang/tk/settings.php index 81c2c0a93..c68605fe1 100644 --- a/lang/tk/settings.php +++ b/lang/tk/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'No restriction set', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Maintenance', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Change page editor', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', '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.', 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', diff --git a/lang/tr/notifications.php b/lang/tr/notifications.php index c26a90b32..5dfb71978 100644 --- a/lang/tr/notifications.php +++ b/lang/tr/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Updated page: :pageName', 'updated_page_intro' => 'A page has been updated in :appName:', 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', 'detail_page_path' => 'Page Path:', diff --git a/lang/tr/preferences.php b/lang/tr/preferences.php index 1ce7b01fd..f4ec771b2 100644 --- a/lang/tr/preferences.php +++ b/lang/tr/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/tr/settings.php b/lang/tr/settings.php index 1eb15d9d4..71d56000f 100644 --- a/lang/tr/settings.php +++ b/lang/tr/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Hiçbir kısıtlama tanımlanmamış', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Bakım', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Yazı editörünü değiştir', 'role_notifications' => 'Receive & manage notifications', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Varlık Yetkileri', 'roles_system_warning' => 'Yukarıdaki üç izinden herhangi birine erişimin, kullanıcının kendi ayrıcalıklarını veya sistemdeki diğerlerinin ayrıcalıklarını değiştirmesine izin verebileceğini unutmayın. Yalnızca bu izinlere sahip rolleri güvenilir kullanıcılara atayın.', 'role_asset_desc' => 'Bu izinler, sistem içindeki varlıklara varsayılan erişim izinlerini ayarlar. Kitaplar, bölümler ve sayfalar üzerindeki izinler, buradaki izinleri geçersiz kılar.', 'role_asset_admins' => 'Yöneticilere otomatik olarak bütün içeriğe erişim yetkisi verilir ancak bu seçenekler, kullanıcı arayüzündeki bazı seçeneklerin gösterilmesine veya gizlenmesine neden olabilir.', 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Hepsi', 'role_own' => 'Kendine Ait', 'role_controlled_by_asset' => 'Yüklendikleri varlık tarafından kontrol ediliyor', diff --git a/lang/uk/notifications.php b/lang/uk/notifications.php index a08b9a100..d40457f98 100644 --- a/lang/uk/notifications.php +++ b/lang/uk/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Оновлено сторінку: :pageName', 'updated_page_intro' => 'Оновлено сторінку у :appName:', 'updated_page_debounce' => 'Для запобігання кількості сповіщень, деякий час ви не будете відправлені повідомлення для подальших змін на цій сторінці тим самим редактором.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Назва сторінки:', 'detail_page_path' => 'Шлях до сторінки:', diff --git a/lang/uk/preferences.php b/lang/uk/preferences.php index 14989d2a7..8af3a8d9e 100644 --- a/lang/uk/preferences.php +++ b/lang/uk/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Контролюйте сповіщення по електронній пошті, які ви отримуєте, коли виконується певна активність у системі.', 'notifications_opt_own_page_changes' => 'Повідомляти при змінах сторінок якими я володію', 'notifications_opt_own_page_comments' => 'Повідомляти при коментарях на моїх сторінках', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Повідомляти про відповіді на мої коментарі', 'notifications_save' => 'Зберегти налаштування', 'notifications_update_success' => 'Налаштування сповіщень було оновлено!', diff --git a/lang/uk/settings.php b/lang/uk/settings.php index 3798f1b60..633582ca8 100644 --- a/lang/uk/settings.php +++ b/lang/uk/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Не встановлено обмежень', // Sorting Settings - 'sorting' => 'Сортування', - 'sorting_book_default' => 'Типовий порядок сортування', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Виберіть правило сортування за замовчуванням для застосування нових книг. Це не вплине на існуючі книги, і може бути перевизначено для кожної книги.', 'sorting_rules' => 'Сортувати правила', 'sorting_rules_desc' => 'Це попередньо визначені операції сортування, які можуть бути застосовані до вмісту в системі.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Дата оновлення', 'sort_rule_op_chapters_first' => 'Спочатку розділи', 'sort_rule_op_chapters_last' => 'Розділи останні', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Обслуговування', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Імпортувати вміст', 'role_editor_change' => 'Змінити редактор сторінок', 'role_notifications' => 'Отримувати та керувати повідомленнями', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Дозволи', 'roles_system_warning' => 'Майте на увазі, що доступ до будь-якого з вищезазначених трьох дозволів може дозволити користувачеві змінювати власні привілеї або привілеї інших в системі. Ролі з цими дозволами призначайте лише довіреним користувачам.', 'role_asset_desc' => 'Ці дозволи контролюють стандартні доступи всередині системи. Права на книги, розділи та сторінки перевизначать ці дозволи.', 'role_asset_admins' => 'Адміністратори автоматично отримують доступ до всього вмісту, але ці параметри можуть відображати або приховувати параметри інтерфейсу користувача.', 'role_asset_image_view_note' => 'Це стосується видимості в менеджері зображень. Фактичний доступ завантажуваних зображень буде залежний від опції зберігання системних зображень.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Все', 'role_own' => 'Власне', 'role_controlled_by_asset' => 'Контролюється за об\'єктом, до якого вони завантажуються', diff --git a/lang/uz/notifications.php b/lang/uz/notifications.php index bec9b3925..ece09441e 100644 --- a/lang/uz/notifications.php +++ b/lang/uz/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => ':pageName sahifasi yangilandi', 'updated_page_intro' => ':appName ichida sahifa yangilandi:', 'updated_page_debounce' => 'Xabarnomalar koʻp boʻlishining oldini olish uchun bir muncha vaqt oʻsha muharrir tomonidan ushbu sahifaga keyingi tahrirlar haqida bildirishnomalar yuborilmaydi.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Sahifa nomi:', 'detail_page_path' => 'Page Path:', diff --git a/lang/uz/preferences.php b/lang/uz/preferences.php index de36b953e..996f9d3c4 100644 --- a/lang/uz/preferences.php +++ b/lang/uz/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Tizimda muayyan harakatlar amalga oshirilganda qabul qilinadigan elektron pochta xabarnomalarini boshqaring.', 'notifications_opt_own_page_changes' => 'Menga tegishli boʻlgan sahifalarimdagi oʻzgarishlar haqida xabar bering', 'notifications_opt_own_page_comments' => 'Menga tegishli sahifalardagi sharhlar haqida xabar bering', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Fikrlarimga javoblarim haqida xabar bering', 'notifications_save' => 'Afzalliklarni saqlash', 'notifications_update_success' => 'Bildirishnoma sozlamalari yangilandi!', diff --git a/lang/uz/settings.php b/lang/uz/settings.php index 83fedc569..ad191143f 100644 --- a/lang/uz/settings.php +++ b/lang/uz/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Cheklov oʻrnatilmagan', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_rules' => 'Sort Rules', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Updated Date', 'sort_rule_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_last' => 'Chapters Last', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Xizmat', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Import content', 'role_editor_change' => 'Sahifa muharririni o\'zgartirish', 'role_notifications' => 'Bildirishnomalarni qabul qilish va boshqarish', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Obyektga ruxsatlar', 'roles_system_warning' => 'Shuni yodda tutingki, yuqoridagi uchta ruxsatdan birortasiga kirish foydalanuvchiga o\'z imtiyozlarini yoki tizimdagi boshqalarning imtiyozlarini o\'zgartirishi mumkin. Ishonchli foydalanuvchilarga faqat ushbu ruxsatlarga ega rollarni tayinlang.', 'role_asset_desc' => 'Bu ruxsatlar tizim ichidagi aktivlarga standart kirishni nazorat qiladi. Kitoblar, boblar va sahifalardagi ruxsatlar bu ruxsatlarni bekor qiladi.', 'role_asset_admins' => 'Administratorlarga avtomatik ravishda barcha kontentga kirish huquqi beriladi, lekin bu parametrlar UI parametrlarini koʻrsatishi yoki yashirishi mumkin.', 'role_asset_image_view_note' => 'Bu tasvir menejeridagi ko\'rinishga tegishli. Yuklangan rasm fayllariga haqiqiy kirish tizim tasvirini saqlash opsiyasiga bog\'liq bo\'ladi.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Hammasi', 'role_own' => 'Shaxsiy', 'role_controlled_by_asset' => 'Ular yuklangan obyekt tomonidan nazorat qilinadi', diff --git a/lang/vi/notifications.php b/lang/vi/notifications.php index a18695b23..45fb4434f 100644 --- a/lang/vi/notifications.php +++ b/lang/vi/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => 'Trang đã cập nhật: :pageName', 'updated_page_intro' => 'Một trang mới đã được cập nhật trong :appName:', 'updated_page_debounce' => 'Để tránh việc nhận quá nhiều thông báo, trong một thời gian, bạn sẽ không nhận được thông báo về những chỉnh sửa tiếp theo cho trang này từ cùng một biên tập viên.', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Tên Trang:', 'detail_page_path' => 'Đường dẫn trang:', diff --git a/lang/vi/preferences.php b/lang/vi/preferences.php index 76bc0be16..3e06ccd30 100644 --- a/lang/vi/preferences.php +++ b/lang/vi/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', 'notifications_save' => 'Save Preferences', 'notifications_update_success' => 'Notification preferences have been updated!', diff --git a/lang/vi/settings.php b/lang/vi/settings.php index 1ff252702..69eadddd3 100644 --- a/lang/vi/settings.php +++ b/lang/vi/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'Không có giới hạn nào được thiết lập', // Sorting Settings - 'sorting' => 'Sắp xếp', - 'sorting_book_default' => 'Sắp xếp sách mặc định', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => 'Chọn quy tắc sắp xếp mặc định để áp dụng cho sách mới. Điều này sẽ không ảnh hưởng đến các sách hiện có và có thể được ghi đè cho từng sách.', 'sorting_rules' => 'Quy tắc sắp xếp', 'sorting_rules_desc' => 'Đây là các thao tác sắp xếp được xác định trước có thể được áp dụng cho nội dung trong hệ thống.', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => 'Ngày cập nhật', 'sort_rule_op_chapters_first' => 'Chương trước', 'sort_rule_op_chapters_last' => 'Chương sau', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => 'Bảo trì', @@ -195,11 +197,13 @@ return [ 'role_import_content' => 'Nhập nội dung', 'role_editor_change' => 'Thay đổi trình soạn thảo trang', 'role_notifications' => 'Nhận & quản lý thông báo', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => 'Quyền tài sản (asset)', 'roles_system_warning' => 'Cần lưu ý rằng việc truy cập vào bất kỳ ba quyền trên có thể cho phép người dùng thay đổi đặc quyền của chính họ hoặc đặc quyền của những người khác trong hệ thống. Chỉ gán các vai trò có các quyền này cho những người dùng đáng tin cậy.', 'role_asset_desc' => 'Các quyền này điều khiển truy cập mặc định tới tài sản (asset) nằm trong hệ thống. Quyền tại Sách, Chương và Trang sẽ ghi đè các quyền này.', 'role_asset_admins' => 'Quản trị viên được tự động cấp quyền truy cập đến toàn bộ nội dung, tuy nhiên các tùy chọn đó có thể hiện hoặc ẩn tùy chọn giao diện.', 'role_asset_image_view_note' => 'Điều này liên quan đến khả năng hiển thị trong trình quản lý hình ảnh. Quyền truy cập thực tế vào các tệp hình ảnh đã tải lên sẽ phụ thuộc vào tùy chọn lưu trữ hình ảnh của hệ thống.', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => 'Tất cả', 'role_own' => 'Sở hữu', 'role_controlled_by_asset' => 'Kiểm soát các tài sản (asset) người dùng tải lên', diff --git a/lang/zh_CN/notifications.php b/lang/zh_CN/notifications.php index 52c9822bc..e4eebf5cc 100644 --- a/lang/zh_CN/notifications.php +++ b/lang/zh_CN/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => '页面更新::pageName', 'updated_page_intro' => ':appName: 中的一个页面已被更新', 'updated_page_debounce' => '为了防止出现大量通知,一段时间内您不会收到同一编辑者再次编辑本页面的通知。', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => '页面名称:', 'detail_page_path' => '页面路径:', diff --git a/lang/zh_CN/preferences.php b/lang/zh_CN/preferences.php index f1ef3957d..f89448dd3 100644 --- a/lang/zh_CN/preferences.php +++ b/lang/zh_CN/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => '控制在系统内发生某些活动时您会收到的电子邮件通知。', 'notifications_opt_own_page_changes' => '在我拥有的页面被修改时通知我', 'notifications_opt_own_page_comments' => '在我拥有的页面上有新评论时通知我', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => '在有人回复我的频率时通知我', 'notifications_save' => '保存偏好设置', 'notifications_update_success' => '通知偏好设置已更新!', diff --git a/lang/zh_CN/settings.php b/lang/zh_CN/settings.php index 2114c3648..93f3076c5 100644 --- a/lang/zh_CN/settings.php +++ b/lang/zh_CN/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => '尚未设置限制', // Sorting Settings - 'sorting' => '排序', - 'sorting_book_default' => '默认书卷排序', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => '选择要应用于新书的默认排序规则。这不会影响现有书,并且可以每本书覆盖。', 'sorting_rules' => '排序规则', 'sorting_rules_desc' => '这些是预定义的排序操作,可应用于系统中的内容。', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => '更新时间', 'sort_rule_op_chapters_first' => '章节正序', 'sort_rule_op_chapters_last' => '章节倒序', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => '维护', @@ -195,11 +197,13 @@ return [ 'role_import_content' => '导入内容', 'role_editor_change' => '更改页面编辑器', 'role_notifications' => '管理和接收通知', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => '资源许可', 'roles_system_warning' => '请注意,拥有以上三个权限中的任何一个都会允许用户更改自己的权限或系统中其他人的权限。 请只将拥有这些权限的角色分配给你信任的用户。', 'role_asset_desc' => '对系统内资源的默认访问许可将由这些权限控制。单独设置在书籍、章节和页面上的权限将覆盖这里的权限设定。', 'role_asset_admins' => '管理员可自动获得对所有内容的访问权限,但这些选项可能会显示或隐藏UI选项。', 'role_asset_image_view_note' => '这与图像管理器中的可见性有关。已经上传的图片的实际访问取决于系统图像存储选项。', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => '全部的', 'role_own' => '拥有的', 'role_controlled_by_asset' => '由其所在的资源来控制', diff --git a/lang/zh_TW/notifications.php b/lang/zh_TW/notifications.php index 7fcfa6400..f5deae328 100644 --- a/lang/zh_TW/notifications.php +++ b/lang/zh_TW/notifications.php @@ -11,6 +11,8 @@ return [ 'updated_page_subject' => '頁面更新::pageName', 'updated_page_intro' => ':appName: 中的一個頁面已被更新', 'updated_page_debounce' => '為了防止出現大量通知,一段時間內您不會收到同一編輯者再次編輯本頁面的通知。', + 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', + 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => '頁面名稱:', 'detail_page_path' => '頁面路徑:', diff --git a/lang/zh_TW/preferences.php b/lang/zh_TW/preferences.php index 4900ca152..0bfce9d83 100644 --- a/lang/zh_TW/preferences.php +++ b/lang/zh_TW/preferences.php @@ -23,6 +23,7 @@ return [ 'notifications_desc' => '控制在系統有特定活動時,是否要接收電子郵件通知', 'notifications_opt_own_page_changes' => '當我的頁面有異動時發送通知', 'notifications_opt_own_page_comments' => '當我的頁面有評論時發送通知', + 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', 'notifications_opt_comment_replies' => '當我的評論有新的回覆時發送通知', 'notifications_save' => '儲存偏好設定', 'notifications_update_success' => '通知設定已更新', diff --git a/lang/zh_TW/settings.php b/lang/zh_TW/settings.php index 752d3f2a7..9b5efa09e 100644 --- a/lang/zh_TW/settings.php +++ b/lang/zh_TW/settings.php @@ -75,8 +75,8 @@ return [ 'reg_confirm_restrict_domain_placeholder' => '尚未設定限制', // Sorting Settings - 'sorting' => '排序', - 'sorting_book_default' => '預設書籍排序', + 'sorting' => 'Lists & Sorting', + 'sorting_book_default' => 'Default Book Sort Rule', 'sorting_book_default_desc' => '選取要套用至新書籍的預設排序規則。這不會影響現有書籍,並可按書籍覆寫。', 'sorting_rules' => '排序規則', 'sorting_rules_desc' => '這些是預先定義的排序作業,可套用於系統中的內容。', @@ -103,6 +103,8 @@ return [ 'sort_rule_op_updated_date' => '更新日期', 'sort_rule_op_chapters_first' => '第一章', 'sort_rule_op_chapters_last' => '最後一章', + 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using an even multiple of 3 (18, 24, 30, etc...) is recommended.', // Maintenance settings 'maint' => '維護', @@ -196,11 +198,13 @@ return [ 'role_import_content' => '匯入內容', 'role_editor_change' => '重設頁面編輯器', 'role_notifications' => '管理和接收通知', + 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', 'role_asset' => '資源權限', 'roles_system_warning' => '請注意,有上述三項權限中的任一項的使用者都可以更改自己或系統中其他人的權限。有這些權限的角色只應分配給受信任的使用者。', 'role_asset_desc' => '對系統內資源的預設權限將由這裡的權限控制。若有單獨設定在書本、章節和頁面上的權限,將會覆寫這裡的權限設定。', 'role_asset_admins' => '管理員會自動取得對所有內容的存取權,但這些選項可能會顯示或隱藏使用者介面的選項。', 'role_asset_image_view_note' => '這與圖像管理器中的可見性有關。已經上傳的圖片的實際訪問取決於系統圖像存儲選項。', + 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', 'role_all' => '全部', 'role_own' => '擁有', 'role_controlled_by_asset' => '依據隸屬的資源來決定', diff --git a/package-lock.json b/package-lock.json index fb6a1d4fa..e8a1493d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,20 +5,20 @@ "packages": { "": { "dependencies": { - "@codemirror/commands": "^6.8.1", + "@codemirror/commands": "^6.10.0", "@codemirror/lang-css": "^6.3.1", - "@codemirror/lang-html": "^6.4.9", + "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-markdown": "^6.3.4", + "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", "@codemirror/language": "^6.11.3", - "@codemirror/legacy-modes": "^6.5.1", + "@codemirror/legacy-modes": "^6.5.2", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.38.1", - "@lezer/highlight": "^1.2.1", + "@codemirror/view": "^6.38.8", + "@lezer/highlight": "^1.2.3", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", "@types/jest": "^30.0.0", @@ -26,24 +26,23 @@ "idb-keyval": "^6.2.2", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", - "snabbdom": "^3.6.2", + "snabbdom": "^3.6.3", "sortablejs": "^1.15.6" }, "devDependencies": { - "@eslint/js": "^9.34.0", + "@eslint/js": "^9.39.1", "@lezer/generator": "^1.8.0", "@types/markdown-it": "^14.1.2", - "@types/sortablejs": "^1.15.8", + "@types/sortablejs": "^1.15.9", "chokidar-cli": "^3.0", - "esbuild": "^0.25.9", - "eslint": "^9.34.0", + "esbuild": "^0.27.0", + "eslint": "^9.39.1", "eslint-plugin-import": "^2.32.0", - "jest": "^30.1.1", - "jest-environment-jsdom": "^30.1.1", - "livereload": "^0.10.3", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", "npm-run-all": "^4.1.5", - "sass": "^1.91.0", - "ts-jest": "^29.4.1", + "sass": "^1.94.2", + "ts-jest": "^29.4.5", "ts-node": "^10.9.2", "typescript": "5.9.*" } @@ -590,9 +589,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", - "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", + "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -615,9 +614,9 @@ } }, "node_modules/@codemirror/lang-html": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", - "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -628,7 +627,7 @@ "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", - "@lezer/html": "^1.3.0" + "@lezer/html": "^1.3.12" } }, "node_modules/@codemirror/lang-javascript": { @@ -657,9 +656,9 @@ } }, "node_modules/@codemirror/lang-markdown": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.4.tgz", - "integrity": "sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.7.1", @@ -713,9 +712,9 @@ } }, "node_modules/@codemirror/legacy-modes": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz", - "integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", + "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0" @@ -765,9 +764,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.38.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", - "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "version": "6.38.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", + "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -916,9 +915,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "dev": true, "license": "MIT", "optional": true, @@ -928,9 +927,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "dev": true, "license": "MIT", "optional": true, @@ -950,9 +949,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", "cpu": [ "ppc64" ], @@ -967,9 +966,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "cpu": [ "arm" ], @@ -984,9 +983,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "cpu": [ "arm64" ], @@ -1001,9 +1000,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "cpu": [ "x64" ], @@ -1018,9 +1017,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "cpu": [ "arm64" ], @@ -1035,9 +1034,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "cpu": [ "x64" ], @@ -1052,9 +1051,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "cpu": [ "arm64" ], @@ -1069,9 +1068,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "cpu": [ "x64" ], @@ -1086,9 +1085,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "cpu": [ "arm" ], @@ -1103,9 +1102,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "cpu": [ "arm64" ], @@ -1120,9 +1119,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "cpu": [ "ia32" ], @@ -1137,9 +1136,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "cpu": [ "loong64" ], @@ -1154,9 +1153,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "cpu": [ "mips64el" ], @@ -1171,9 +1170,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", "cpu": [ "ppc64" ], @@ -1188,9 +1187,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", "cpu": [ "riscv64" ], @@ -1205,9 +1204,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", "cpu": [ "s390x" ], @@ -1222,9 +1221,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", "cpu": [ "x64" ], @@ -1239,9 +1238,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", "cpu": [ "arm64" ], @@ -1256,9 +1255,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", "cpu": [ "x64" ], @@ -1273,9 +1272,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", "cpu": [ "arm64" ], @@ -1290,9 +1289,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", "cpu": [ "x64" ], @@ -1307,9 +1306,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", "cpu": [ "arm64" ], @@ -1324,9 +1323,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", "cpu": [ "x64" ], @@ -1341,9 +1340,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", "cpu": [ "arm64" ], @@ -1358,9 +1357,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", "cpu": [ "ia32" ], @@ -1375,9 +1374,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", "cpu": [ "x64" ], @@ -1392,9 +1391,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1434,13 +1433,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1449,19 +1448,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1496,9 +1498,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { @@ -1509,9 +1511,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1519,13 +1521,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1617,9 +1619,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -1714,9 +1716,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -1790,17 +1792,17 @@ } }, "node_modules/@jest/console": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.1.tgz", - "integrity": "sha512-f7TGqR1k4GtN5pyFrKmq+ZVndesiwLU33yDpJIGMS9aW+j6hKjue7ljeAdznBsH9kAnxUWe2Y+Y3fLV/FJt3gA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -1808,39 +1810,39 @@ } }, "node_modules/@jest/core": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.1.tgz", - "integrity": "sha512-3ncU9peZ3D2VdgRkdZtUceTrDgX5yiDRwAFjtxNfU22IiZrpVWlv/FogzDLYSJQptQGfFo3PcHK86a2oG6WUGg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.1", + "@jest/console": "30.2.0", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.1.1", - "@jest/test-result": "30.1.1", - "@jest/transform": "30.1.1", - "@jest/types": "30.0.5", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.0.5", - "jest-config": "30.1.1", - "jest-haste-map": "30.1.0", - "jest-message-util": "30.1.0", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.0", - "jest-resolve-dependencies": "30.1.1", - "jest-runner": "30.1.1", - "jest-runtime": "30.1.1", - "jest-snapshot": "30.1.1", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", - "jest-watcher": "30.1.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -1865,35 +1867,35 @@ } }, "node_modules/@jest/environment": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.1.tgz", - "integrity": "sha512-yWHbU+3j7ehQE+NRpnxRvHvpUhoohIjMePBbIr8lfe0cWVb0WeTf80DNux1GPJa18CDHiIU5DtksGUfxcDE+Rw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.1.1", - "@jest/types": "30.0.5", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5" + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.1.1.tgz", - "integrity": "sha512-d7pP9SeIOI6qnrNIS/ds1hlS9jpqh8EywHK0dALSLODZKo2QEGnDNvnPvhRKI0FHWDnE2EMl8CDTP0jM9lhlOA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", + "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.1", - "@jest/fake-timers": "30.1.1", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/jsdom": "^21.1.7", "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1909,23 +1911,23 @@ } }, "node_modules/@jest/expect": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.1.tgz", - "integrity": "sha512-3vHIHsF+qd3D8FU2c7U5l3rg1fhDwAYcGyHyZAi94YIlTwcJ+boNhRyJf373cl4wxbOX+0Q7dF40RTrTFTSuig==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.1.1", - "jest-snapshot": "30.1.1" + "expect": "30.2.0", + "jest-snapshot": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.1.tgz", - "integrity": "sha512-5YUHr27fpJ64dnvtu+tt11ewATynrHkGYD+uSFgRr8V2eFJis/vEXgToyLwccIwqBihVfz9jwio+Zr1ab1Zihw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0" @@ -1935,18 +1937,18 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.1.tgz", - "integrity": "sha512-fK/25dNgBNYPw3eLi2CRs57g1H04qBAFNMsUY3IRzkfx/m4THe0E1zF+yGQBOMKKc2XQVdc9EYbJ4hEm7/2UtA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1962,16 +1964,16 @@ } }, "node_modules/@jest/globals": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.1.1.tgz", - "integrity": "sha512-NNUUkHT2TU/xztZl6r1UXvJL+zvCwmZsQDmK69fVHHcB9fBtlu3FInnzOve/ZoyKnWY8JXWJNT+Lkmu1+ubXUA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.1", - "@jest/expect": "30.1.1", - "@jest/types": "30.0.5", - "jest-mock": "30.0.5" + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1991,17 +1993,17 @@ } }, "node_modules/@jest/reporters": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.1.tgz", - "integrity": "sha512-Hb2Bq80kahOC6Sv2waEaH1rEU6VdFcM6WHaRBWQF9tf30+nJHxhl/Upbgo9+25f0mOgbphxvbwSMjSgy9gW/FA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.1.1", - "@jest/test-result": "30.1.1", - "@jest/transform": "30.1.1", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", @@ -2014,9 +2016,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", - "jest-worker": "30.1.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -2046,13 +2048,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.1.tgz", - "integrity": "sha512-TkVBc9wuN22TT8hESRFmjjg/xIMu7z0J3UDYtIRydzCqlLPTB7jK1DDBKdnTUZ4zL3z3rnPpzV6rL1Uzh87sXg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -2077,14 +2079,14 @@ } }, "node_modules/@jest/test-result": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.1.tgz", - "integrity": "sha512-bMdj7fNu8iZuBPSnbVir5ezvWmVo4jrw7xDE+A33Yb3ENCoiJK9XgOLgal+rJ9XSKjsL7aPUMIo87zhN7I5o2w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.1", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -2093,15 +2095,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.1.tgz", - "integrity": "sha512-yruRdLXSA3HYD/MTNykgJ6VYEacNcXDFRMqKVAwlYegmxICUiT/B++CNuhJnYJzKYks61iYnjVsMwbUqmmAYJg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.1.1", + "@jest/test-result": "30.2.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -2109,23 +2111,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.1.tgz", - "integrity": "sha512-PHIA2AbAASBfk6evkNifvmx9lkOSkmvaQoO6VSpuL8+kQqDMHeDoJ7RU3YP1wWAMD7AyQn9UL5iheuFYCC4lqQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.0", + "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", @@ -2136,9 +2138,9 @@ } }, "node_modules/@jest/types": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", - "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "license": "MIT", "dependencies": { "@jest/pattern": "30.0.1", @@ -2193,9 +2195,9 @@ } }, "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz", + "integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==", "license": "MIT" }, "node_modules/@lezer/css": { @@ -2224,18 +2226,18 @@ } }, "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@lezer/common": "^1.3.0" } }, "node_modules/@lezer/html": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", - "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz", + "integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2738,9 +2740,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, @@ -2895,9 +2897,9 @@ } }, "node_modules/@types/sortablejs": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz", - "integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", "dev": true, "license": "MIT" }, @@ -3285,9 +3287,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -3488,16 +3490,16 @@ } }, "node_modules/babel-jest": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.1.tgz", - "integrity": "sha512-1bZfC/V03qBCzASvZpNFhx3Ouj6LgOd4KFJm4br/fYOS+tSSvVCE61QmcAVbMTwq/GoB7KN4pzGMoyr9cMxSvQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.1.1", + "@jest/transform": "30.2.0", "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.0", - "babel-preset-jest": "30.0.1", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -3506,15 +3508,18 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-0" } }, "node_modules/babel-plugin-istanbul": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", - "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", "dev": true, "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -3527,14 +3532,12 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", - "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", "@types/babel__core": "^7.20.5" }, "engines": { @@ -3542,9 +3545,9 @@ } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -3565,24 +3568,24 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", - "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0" + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, "node_modules/balanced-match": { @@ -3868,9 +3871,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz", + "integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==", "dev": true, "license": "MIT" }, @@ -3936,9 +3939,9 @@ } }, "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, "license": "MIT" }, @@ -4120,9 +4123,9 @@ "license": "MIT" }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4455,9 +4458,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4468,32 +4471,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" } }, "node_modules/escalade": { @@ -4520,25 +4523,24 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -4819,6 +4821,13 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/exit-x": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", @@ -4830,17 +4839,17 @@ } }, "node_modules/expect": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.1.tgz", - "integrity": "sha512-OKe7cdic4qbfWd/CcgwJvvCrNX2KWfuMZee9AfJHL1gTYmvqjBjZG1a2NwfhspBzxzlXwsN75WWpKTYfsJpBxg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.1.1", + "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.1.1", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4973,19 +4982,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5150,9 +5146,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -6120,16 +6116,16 @@ } }, "node_modules/jest": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.1.tgz", - "integrity": "sha512-yC3JvpP/ZcAZX5rYCtXO/g9k6VTCQz0VFE2v1FpxytWzUqfDtu0XL/pwnNvptzYItvGwomh1ehomRNMOyhCJKw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.1.1", - "@jest/types": "30.0.5", + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", "import-local": "^3.2.0", - "jest-cli": "30.1.1" + "jest-cli": "30.2.0" }, "bin": { "jest": "bin/jest.js" @@ -6147,14 +6143,14 @@ } }, "node_modules/jest-changed-files": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", - "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "p-limit": "^3.1.0" }, "engines": { @@ -6162,29 +6158,29 @@ } }, "node_modules/jest-circus": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.1.tgz", - "integrity": "sha512-M3Vd4x5wD7eSJspuTvRF55AkOOBndRxgW3gqQBDlFvbH3X+ASdi8jc+EqXEeAFd/UHulVYIlC4XKJABOhLw6UA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.1", - "@jest/expect": "30.1.1", - "@jest/test-result": "30.1.1", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.1.0", - "jest-matcher-utils": "30.1.1", - "jest-message-util": "30.1.0", - "jest-runtime": "30.1.1", - "jest-snapshot": "30.1.1", - "jest-util": "30.0.5", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", "p-limit": "^3.1.0", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -6194,21 +6190,21 @@ } }, "node_modules/jest-cli": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.1.tgz", - "integrity": "sha512-xm9llxuh5OoI5KZaYzlMhklryHBwg9LZy/gEaaMlXlxb+cZekGNzukU0iblbDo3XOBuN6N0CgK4ykgNRYSEb6g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.1.1", - "@jest/test-result": "30.1.1", - "@jest/types": "30.0.5", + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.1.1", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "yargs": "^17.7.2" }, "bin": { @@ -6344,34 +6340,34 @@ } }, "node_modules/jest-config": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.1.tgz", - "integrity": "sha512-xuPGUGDw+9fPPnGmddnLnHS/mhKUiJOW7K65vErYmglEPKq65NKwSRchkQ7iv6gqjs2l+YNEsAtbsplxozdOWg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.1.1", - "@jest/types": "30.0.5", - "babel-jest": "30.1.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-circus": "30.1.1", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.1.1", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.0", - "jest-runner": "30.1.1", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "micromatch": "^4.0.8", "parse-json": "^5.2.0", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -6396,24 +6392,24 @@ } }, "node_modules/jest-diff": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.1.tgz", - "integrity": "sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", - "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", "dev": true, "license": "MIT", "dependencies": { @@ -6424,31 +6420,31 @@ } }, "node_modules/jest-each": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", - "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "chalk": "^4.1.2", - "jest-util": "30.0.5", - "pretty-format": "30.0.5" + "jest-util": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-jsdom": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.1.1.tgz", - "integrity": "sha512-fInyXsHSuPaERmRiub4V6jl6KERXowGqY8AISJrXZjOq7vdP46qecm+GnTngjcUPeHFqrxp1PfP0XuFfKTzA2A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.1", - "@jest/environment-jsdom-abstract": "30.1.1", + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", "@types/jsdom": "^21.1.7", "@types/node": "*", "jsdom": "^26.1.0" @@ -6466,39 +6462,39 @@ } }, "node_modules/jest-environment-node": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.1.tgz", - "integrity": "sha512-IaMoaA6saxnJimqCppUDqKck+LKM0Jg+OxyMUIvs1yGd2neiC22o8zXo90k04+tO+49OmgMR4jTgM5e4B0S62Q==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.1", - "@jest/fake-timers": "30.1.1", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.1.0" + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", - "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", - "jest-worker": "30.1.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", "micromatch": "^4.0.8", "walker": "^1.0.8" }, @@ -6510,47 +6506,47 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", - "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.1.tgz", - "integrity": "sha512-SuH2QVemK48BNTqReti6FtjsMPFsSOD/ZzRxU1TttR7RiRsRSe78d03bb4Cx6D4bQC/80Q8U4VnaaAH9FlbZ9w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.1.1", - "pretty-format": "30.0.5" + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", - "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -6559,14 +6555,14 @@ } }, "node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-util": "30.0.5" + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -6600,18 +6596,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.0.tgz", - "integrity": "sha512-hASe7D/wRtZw8Cm607NrlF7fi3HWC5wmA5jCVc2QjQAB2pTwP9eVZILGEi6OeSLNUtE1zb04sXRowsdh5CUjwA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -6620,46 +6616,46 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.1.tgz", - "integrity": "sha512-tRtaaoH8Ws1Gn1o/9pedt19dvVgr81WwdmvJSP9Ow3amOUOP2nN9j94u5jC9XlIfa2Q1FQKIWWQwL4ajqsjCGQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.1.1" + "jest-snapshot": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.1.tgz", - "integrity": "sha512-ATe6372SOfJvCRExtCAr06I4rGujwFdKg44b6i7/aOgFnULwjxzugJ0Y4AnG+jeSeQi8dU7R6oqLGmsxRUbErQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.1", - "@jest/environment": "30.1.1", - "@jest/test-result": "30.1.1", - "@jest/transform": "30.1.1", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.1.1", - "jest-haste-map": "30.1.0", - "jest-leak-detector": "30.1.0", - "jest-message-util": "30.1.0", - "jest-resolve": "30.1.0", - "jest-runtime": "30.1.1", - "jest-util": "30.0.5", - "jest-watcher": "30.1.1", - "jest-worker": "30.1.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -6668,32 +6664,32 @@ } }, "node_modules/jest-runtime": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.1.tgz", - "integrity": "sha512-7sOyR0Oekw4OesQqqBHuYJRB52QtXiq0NNgLRzVogiMSxKCMiliUd6RrXHCnG5f12Age/ggidCBiQftzcA9XKw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.1", - "@jest/fake-timers": "30.1.1", - "@jest/globals": "30.1.1", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.1.1", - "@jest/transform": "30.1.1", - "@jest/types": "30.0.5", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.0", - "jest-snapshot": "30.1.1", - "jest-util": "30.0.5", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -6702,9 +6698,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.1.tgz", - "integrity": "sha512-7/iBEzoJqEt2TjkQY+mPLHP8cbPhLReZVkkxjTMzIzoTC4cZufg7HzKo/n9cIkXKj2LG0x3mmBHsZto+7TOmFg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", "dev": true, "license": "MIT", "dependencies": { @@ -6713,20 +6709,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.1.1", + "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.1.1", - "@jest/transform": "30.1.1", - "@jest/types": "30.0.5", - "babel-preset-current-node-syntax": "^1.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.1.1", + "expect": "30.2.0", "graceful-fs": "^4.2.11", - "jest-diff": "30.1.1", - "jest-matcher-utils": "30.1.1", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", - "pretty-format": "30.0.5", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -6735,9 +6731,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -6748,12 +6744,12 @@ } }, "node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -6777,18 +6773,18 @@ } }, "node_modules/jest-validate": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", - "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -6808,19 +6804,19 @@ } }, "node_modules/jest-watcher": { - "version": "30.1.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.1.tgz", - "integrity": "sha512-CrAQ73LlaS6KGQQw6NBi71g7qvP7scy+4+2c0jKX6+CWaYg85lZiig5nQQVTsS5a5sffNPL3uxXnaE9d7v9eQg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.1.1", - "@jest/types": "30.0.5", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "string-length": "^4.0.2" }, "engines": { @@ -6828,15 +6824,15 @@ } }, "node_modules/jest-worker": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", - "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -6867,9 +6863,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -7030,62 +7026,6 @@ "uc.micro": "^2.0.0" } }, - "node_modules/livereload": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.10.3.tgz", - "integrity": "sha512-llSb8HrtSH7ByPFMc8WTTeW3oy++smwgSA8JVGzEn8KiDPESq6jt1M4ZKKkhKTrhn2wvUOadQq4ip10E5daZ3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.3", - "livereload-js": "^4.0.2", - "opts": "^2.0.2", - "ws": "^8.4.3" - }, - "bin": { - "livereload": "bin/livereload.js" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/livereload-js": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-4.0.2.tgz", - "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/livereload/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/livereload/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -7197,9 +7137,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -7345,9 +7285,9 @@ "license": "MIT" }, "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, "license": "MIT", "bin": { @@ -7627,9 +7567,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "dev": true, "license": "MIT" }, @@ -7774,13 +7714,6 @@ "node": ">= 0.8.0" } }, - "node_modules/opts": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", - "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -8121,9 +8054,9 @@ } }, "node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", @@ -8401,9 +8334,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.91.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", - "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", + "version": "1.94.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz", + "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==", "dev": true, "license": "MIT", "dependencies": { @@ -8643,11 +8576,17 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/slash": { "version": "3.0.0", @@ -8659,9 +8598,9 @@ } }, "node_modules/snabbdom": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.6.2.tgz", - "integrity": "sha512-ig5qOnCDbugFntKi6c7Xlib8bA6xiJVk8O+WdFrV3wxbMqeHO0hXFQC4nAhPVWfZfi8255lcZkNhtIBINCc4+Q==", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.6.3.tgz", + "integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==", "license": "MIT", "engines": { "node": ">=12.17.0" @@ -8992,9 +8931,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -9221,9 +9160,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", - "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9233,7 +9172,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.2", + "semver": "^7.7.3", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -9274,9 +9213,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -10037,19 +9976,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/write-file-atomic/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", diff --git a/package.json b/package.json index 9ec94acc2..624ff876a 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,13 @@ "build:css:dev": "sass ./resources/sass:./public/dist --embed-sources", "build:css:watch": "sass ./resources/sass:./public/dist --watch --embed-sources", "build:css:production": "sass ./resources/sass:./public/dist -s compressed", - "build:js:dev": "node dev/build/esbuild.js", - "build:js:watch": "chokidar --initial \"./resources/**/*.js\" \"./resources/**/*.mjs\" \"./resources/**/*.ts\" -c \"npm run build:js:dev\"", - "build:js:production": "node dev/build/esbuild.js production", + "build:js:dev": "node dev/build/esbuild.mjs", + "build:js:watch": "node dev/build/esbuild.mjs watch", + "build:js:production": "node dev/build/esbuild.mjs production", "build": "npm-run-all --parallel build:*:dev", "production": "npm-run-all --parallel build:*:production", - "dev": "npm-run-all --parallel watch livereload", + "dev": "npm-run-all --parallel build:*:watch", "watch": "npm-run-all --parallel build:*:watch", - "livereload": "livereload ./public/dist/", "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads", "lint": "eslint \"resources/**/*.js\" \"resources/**/*.mjs\"", "fix": "eslint --fix \"resources/**/*.js\" \"resources/**/*.mjs\"", @@ -19,38 +18,37 @@ "test": "jest" }, "devDependencies": { - "@eslint/js": "^9.34.0", + "@eslint/js": "^9.39.1", "@lezer/generator": "^1.8.0", "@types/markdown-it": "^14.1.2", - "@types/sortablejs": "^1.15.8", + "@types/sortablejs": "^1.15.9", "chokidar-cli": "^3.0", - "esbuild": "^0.25.9", - "eslint": "^9.34.0", + "esbuild": "^0.27.0", + "eslint": "^9.39.1", "eslint-plugin-import": "^2.32.0", - "jest": "^30.1.1", - "jest-environment-jsdom": "^30.1.1", - "livereload": "^0.10.3", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", "npm-run-all": "^4.1.5", - "sass": "^1.91.0", - "ts-jest": "^29.4.1", + "sass": "^1.94.2", + "ts-jest": "^29.4.5", "ts-node": "^10.9.2", "typescript": "5.9.*" }, "dependencies": { - "@codemirror/commands": "^6.8.1", + "@codemirror/commands": "^6.10.0", "@codemirror/lang-css": "^6.3.1", - "@codemirror/lang-html": "^6.4.9", + "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-markdown": "^6.3.4", + "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", "@codemirror/language": "^6.11.3", - "@codemirror/legacy-modes": "^6.5.1", + "@codemirror/legacy-modes": "^6.5.2", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.38.1", - "@lezer/highlight": "^1.2.1", + "@codemirror/view": "^6.38.8", + "@lezer/highlight": "^1.2.3", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", "@types/jest": "^30.0.0", @@ -58,7 +56,7 @@ "idb-keyval": "^6.2.2", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", - "snabbdom": "^3.6.2", + "snabbdom": "^3.6.3", "sortablejs": "^1.15.6" } } diff --git a/public/web.config b/public/web.config index 474eb6898..b08c89c9a 100644 --- a/public/web.config +++ b/public/web.config @@ -1,6 +1,6 @@ diff --git a/readme.md b/readme.md index f4b67992c..3ee5f242a 100644 --- a/readme.md +++ b/readme.md @@ -88,13 +88,16 @@ Big thanks to these companies for supporting the project. Route4Me - Route Optimizer and Route Planner Software - phamos + phamos - + SiteSpeakAI + + Admin Intelligence + diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index 8334ebb8a..68cd46f04 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -2,7 +2,7 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom'; import {PageCommentReference} from "./page-comment-reference"; import {HttpError} from "../services/http"; -import {SimpleWysiwygEditorInterface} from "../wysiwyg"; +import {createCommentEditorInstance, SimpleWysiwygEditorInterface} from "../wysiwyg"; import {el} from "../wysiwyg/utils/dom"; export interface PageCommentReplyEventData { @@ -104,7 +104,7 @@ export class PageComment extends Component { this.input.parentElement?.appendChild(container); this.input.hidden = true; - this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, editorContent, { + this.wysiwygEditor = wysiwygModule.createCommentEditorInstance(container as HTMLElement, editorContent, { darkMode: document.documentElement.classList.contains('dark-mode'), textDirection: this.$opts.textDirection, translations: (window as unknown as Record).editor_translations, diff --git a/resources/js/components/page-comments.ts b/resources/js/components/page-comments.ts index a1eeda1f9..707ca3f69 100644 --- a/resources/js/components/page-comments.ts +++ b/resources/js/components/page-comments.ts @@ -5,7 +5,7 @@ import {PageCommentReference} from "./page-comment-reference"; import {scrollAndHighlightElement} from "../services/util"; import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment"; import {el} from "../wysiwyg/utils/dom"; -import {SimpleWysiwygEditorInterface} from "../wysiwyg"; +import {createCommentEditorInstance, SimpleWysiwygEditorInterface} from "../wysiwyg"; export class PageComments extends Component { @@ -200,7 +200,7 @@ export class PageComments extends Component { this.formInput.parentElement?.appendChild(container); this.formInput.hidden = true; - this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, '

', { + this.wysiwygEditor = wysiwygModule.createCommentEditorInstance(container as HTMLElement, '

', { darkMode: document.documentElement.classList.contains('dark-mode'), textDirection: this.wysiwygTextDirection, translations: (window as unknown as Record).editor_translations, diff --git a/resources/js/wysiwyg/api/__tests__/api-test-utils.ts b/resources/js/wysiwyg/api/__tests__/api-test-utils.ts new file mode 100644 index 000000000..dacec3392 --- /dev/null +++ b/resources/js/wysiwyg/api/__tests__/api-test-utils.ts @@ -0,0 +1,14 @@ +import {createTestContext} from "lexical/__tests__/utils"; +import {EditorApi} from "../api"; +import {EditorUiContext} from "../../ui/framework/core"; +import {LexicalEditor} from "lexical"; + + +/** + * Create an instance of the EditorApi and EditorUiContext. + */ +export function createEditorApiInstance(): { api: EditorApi; context: EditorUiContext, editor: LexicalEditor} { + const context = createTestContext(); + const api = new EditorApi(context); + return {api, context, editor: context.editor}; +} \ No newline at end of file diff --git a/resources/js/wysiwyg/api/__tests__/content.test.ts b/resources/js/wysiwyg/api/__tests__/content.test.ts new file mode 100644 index 000000000..0915b6685 --- /dev/null +++ b/resources/js/wysiwyg/api/__tests__/content.test.ts @@ -0,0 +1,93 @@ +import {createEditorApiInstance} from "./api-test-utils"; +import {$createParagraphNode, $createTextNode, $getRoot, IS_BOLD, LexicalEditor} from "lexical"; +import {expectNodeShapeToMatch} from "lexical/__tests__/utils"; + + +describe('Editor API: Content Module', () => { + + describe('insertHtml()', () => { + it('should insert html at selection by default', () => { + const {api, editor} = createEditorApiInstance(); + insertAndSelectSampleBlock(editor); + + api.content.insertHtml('pp'); + editor.commitUpdates(); + + expectNodeShapeToMatch(editor, [ + {type: 'paragraph', children: [ + {text: 'He'}, + {text: 'pp', format: IS_BOLD}, + {text: 'o World'} + ]} + ]); + }); + + it('should handle a mix of inline and block elements', () => { + const {api, editor} = createEditorApiInstance(); + insertAndSelectSampleBlock(editor); + + api.content.insertHtml('

cat

pp

dog

'); + editor.commitUpdates(); + + expectNodeShapeToMatch(editor, [ + {type: 'paragraph', children: [{text: 'cat'}]}, + {type: 'paragraph', children: [ + {text: 'He'}, + {text: 'pp', format: IS_BOLD}, + {text: 'o World'} + ]}, + {type: 'paragraph', children: [{text: 'dog'}]}, + ]); + }); + + it('should throw and error if an invalid position is provided', () => { + const {api, editor} = createEditorApiInstance(); + insertAndSelectSampleBlock(editor); + + + expect(() => { + api.content.insertHtml('happy

cat

', 'near-the-end'); + }).toThrow('Invalid position: near-the-end. Valid positions are: start, end, selection'); + }); + + it('should append html if end provided as a position', () => { + const {api, editor} = createEditorApiInstance(); + insertAndSelectSampleBlock(editor); + + api.content.insertHtml('happy

cat

', 'end'); + editor.commitUpdates(); + + expectNodeShapeToMatch(editor, [ + {type: 'paragraph', children: [{text: 'Hello World'}]}, + {type: 'paragraph', children: [{text: 'happy'}]}, + {type: 'paragraph', children: [{text: 'cat'}]}, + ]); + }); + + it('should prepend html if start provided as a position', () => { + const {api, editor} = createEditorApiInstance(); + insertAndSelectSampleBlock(editor); + + api.content.insertHtml('happy

cat

', 'start'); + editor.commitUpdates(); + + expectNodeShapeToMatch(editor, [ + {type: 'paragraph', children: [{text: 'happy'}]}, + {type: 'paragraph', children: [{text: 'cat'}]}, + {type: 'paragraph', children: [{text: 'Hello World'}]}, + ]); + }); + }); + + function insertAndSelectSampleBlock(editor: LexicalEditor) { + editor.updateAndCommit(() => { + const p = $createParagraphNode(); + const text = $createTextNode('Hello World'); + p.append(text); + $getRoot().append(p); + + text.select(2, 4); + }); + } + +}); \ No newline at end of file diff --git a/resources/js/wysiwyg/api/__tests__/ui.test.ts b/resources/js/wysiwyg/api/__tests__/ui.test.ts new file mode 100644 index 000000000..80045bb48 --- /dev/null +++ b/resources/js/wysiwyg/api/__tests__/ui.test.ts @@ -0,0 +1,123 @@ +import {createEditorApiInstance} from "./api-test-utils"; +import {EditorApiButton, EditorApiToolbar, EditorApiToolbarSection} from "../ui"; +import {getMainEditorFullToolbar} from "../../ui/defaults/toolbars"; +import {EditorContainerUiElement} from "../../ui/framework/core"; +import {EditorOverflowContainer} from "../../ui/framework/blocks/overflow-container"; + + +describe('Editor API: UI Module', () => { + + describe('createButton()', () => { + it('should return a button', () => { + const {api} = createEditorApiInstance(); + const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => ''}); + expect(button).toBeInstanceOf(EditorApiButton); + }); + + it('should only need action to be required', () => { + const {api} = createEditorApiInstance(); + const button = api.ui.createButton({action: () => ''}); + expect(button).toBeInstanceOf(EditorApiButton); + }); + + it('should pass the label and icon to the button', () => { + const {api} = createEditorApiInstance(); + const button = api.ui.createButton({label: 'TestLabel', icon: 'cat', action: () => ''}); + const html = button._getOriginalModel().getDOMElement().outerHTML; + expect(html).toContain('TestLabel'); + expect(html).toContain('cat'); + }) + }); + + describe('EditorApiButton', () => { + + describe('setActive()', () => { + it('should update the active state of the button', () => { + const {api} = createEditorApiInstance(); + const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => ''}); + + button.setActive(true); + expect(button._getOriginalModel().isActive()).toBe(true); + + button.setActive(false); + expect(button._getOriginalModel().isActive()).toBe(false); + }) + }); + + it('should call the provided action on click', () => { + const {api} = createEditorApiInstance(); + let count = 0; + const button = api.ui.createButton({label: 'Test', icon: 'test', action: () => { + count++; + }}); + + const dom = button._getOriginalModel().getDOMElement(); + dom.click(); + dom.click(); + expect(count).toBe(2); + }); + + }); + + describe('getMainToolbar()', () => { + it('should return the main editor toolbar', () => { + const {api, context} = createEditorApiInstance(); + context.manager.setToolbar(getMainEditorFullToolbar(context)); + + const toolbar = api.ui.getMainToolbar(); + + expect(toolbar).toBeInstanceOf(EditorApiToolbar); + }); + }); + + describe('EditorApiToolbar', () => { + describe('getSections()', () => { + it('should return the sections of the toolbar', () => { + const {api, context} = createEditorApiInstance(); + context.manager.setToolbar(testToolbar()); + const toolbar = api.ui.getMainToolbar(); + + const sections = toolbar?.getSections() || []; + + expect(sections.length).toBe(2); + expect(sections[0]).toBeInstanceOf(EditorApiToolbarSection); + }) + }) + }) + + describe('EditorApiToolbarSection', () => { + + describe('getLabel()', () => { + it('should return the label of the section', () => { + const {api, context} = createEditorApiInstance(); + context.manager.setToolbar(testToolbar()); + const section = api.ui.getMainToolbar()?.getSections()[0] as EditorApiToolbarSection; + expect(section.getLabel()).toBe('section-a'); + }) + }); + + describe('addButton()', () => { + it('should add a button to the section', () => { + const {api, context} = createEditorApiInstance(); + const toolbar = testToolbar(); + context.manager.setToolbar(toolbar); + const section = api.ui.getMainToolbar()?.getSections()[0] as EditorApiToolbarSection; + + const button = api.ui.createButton({label: 'TestButtonText!', action: () => ''}); + section.addButton(button); + + const toolbarRendered = toolbar.getDOMElement().innerHTML; + expect(toolbarRendered).toContain('TestButtonText!'); + }); + }); + + }); + + function testToolbar(): EditorContainerUiElement { + return new EditorContainerUiElement([ + new EditorOverflowContainer('section-a', 1, []), + new EditorOverflowContainer('section-b', 1, []), + ]); + } + +}); \ No newline at end of file diff --git a/resources/js/wysiwyg/api/api.ts b/resources/js/wysiwyg/api/api.ts new file mode 100644 index 000000000..f732170b3 --- /dev/null +++ b/resources/js/wysiwyg/api/api.ts @@ -0,0 +1,14 @@ +import {EditorApiUiModule} from "./ui"; +import {EditorUiContext} from "../ui/framework/core"; +import {EditorApiContentModule} from "./content"; + +export class EditorApi { + + public ui: EditorApiUiModule; + public content: EditorApiContentModule; + + constructor(context: EditorUiContext) { + this.ui = new EditorApiUiModule(context); + this.content = new EditorApiContentModule(context); + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/api/content.ts b/resources/js/wysiwyg/api/content.ts new file mode 100644 index 000000000..167ec7905 --- /dev/null +++ b/resources/js/wysiwyg/api/content.ts @@ -0,0 +1,26 @@ +import {EditorUiContext} from "../ui/framework/core"; +import {appendHtmlToEditor, insertHtmlIntoEditor, prependHtmlToEditor} from "../utils/actions"; + + +export class EditorApiContentModule { + readonly #context: EditorUiContext; + + constructor(context: EditorUiContext) { + this.#context = context; + } + + insertHtml(html: string, position: string = 'selection'): void { + const validPositions = ['start', 'end', 'selection']; + if (!validPositions.includes(position)) { + throw new Error(`Invalid position: ${position}. Valid positions are: ${validPositions.join(', ')}`); + } + + if (position === 'start') { + prependHtmlToEditor(this.#context.editor, html); + } else if (position === 'end') { + appendHtmlToEditor(this.#context.editor, html); + } else { + insertHtmlIntoEditor(this.#context.editor, html); + } + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/api/ui.ts b/resources/js/wysiwyg/api/ui.ts new file mode 100644 index 000000000..c5b822a46 --- /dev/null +++ b/resources/js/wysiwyg/api/ui.ts @@ -0,0 +1,89 @@ +import {EditorButton} from "../ui/framework/buttons"; +import {EditorContainerUiElement, EditorUiContext} from "../ui/framework/core"; +import {EditorOverflowContainer} from "../ui/framework/blocks/overflow-container"; + +type EditorApiButtonOptions = { + label?: string; + icon?: string; + action: () => void; +}; + +export class EditorApiButton { + readonly #button: EditorButton; + #isActive: boolean = false; + + constructor(options: EditorApiButtonOptions, context: EditorUiContext) { + this.#button = new EditorButton({ + label: options.label || '', + icon: options.icon || '', + action: () => { + options.action(); + }, + isActive: () => this.#isActive, + }); + this.#button.setContext(context); + } + + setActive(active: boolean = true): void { + this.#isActive = active; + this.#button.setActiveState(active); + } + + _getOriginalModel() { + return this.#button; + } +} + +export class EditorApiToolbar { + readonly #toolbar: EditorContainerUiElement; + + constructor(toolbar: EditorContainerUiElement) { + this.#toolbar = toolbar; + } + + getSections(): EditorApiToolbarSection[] { + const sections = this.#toolbar.getChildren(); + return sections.filter(section => { + return section instanceof EditorOverflowContainer; + }).map(section => new EditorApiToolbarSection(section)); + } +} + +export class EditorApiToolbarSection { + readonly #section: EditorOverflowContainer; + + constructor(section: EditorOverflowContainer) { + this.#section = section; + } + + getLabel(): string { + return this.#section.getLabel(); + } + + addButton(button: EditorApiButton, targetIndex: number = -1): void { + this.#section.addChild(button._getOriginalModel(), targetIndex); + this.#section.rebuildDOM(); + } +} + + +export class EditorApiUiModule { + readonly #context: EditorUiContext; + + constructor(context: EditorUiContext) { + this.#context = context; + } + + createButton(options: EditorApiButtonOptions): EditorApiButton { + return new EditorApiButton(options, this.#context); + } + + getMainToolbar(): EditorApiToolbar|null { + const toolbar = this.#context.manager.getToolbar(); + if (!toolbar) { + return null; + } + + return new EditorApiToolbar(toolbar); + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index 2c8c3b952..01964b066 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -2,7 +2,12 @@ import {createEditor} from 'lexical'; import {createEmptyHistoryState, registerHistory} from '@lexical/history'; import {registerRichText} from '@lexical/rich-text'; import {mergeRegister} from '@lexical/utils'; -import {getNodesForBasicEditor, getNodesForPageEditor, registerCommonNodeMutationListeners} from './nodes'; +import { + getNodesForBasicEditor, + getNodesForCommentEditor, + getNodesForPageEditor, + registerCommonNodeMutationListeners +} from './nodes'; import {buildEditorUI} from "./ui"; import {focusEditor, getEditorContentAsHtml, setEditorContentFromHtml} from "./utils/actions"; import {registerTableResizer} from "./ui/framework/helpers/table-resizer"; @@ -17,10 +22,13 @@ import {registerKeyboardHandling} from "./services/keyboard-handling"; import {registerAutoLinks} from "./services/auto-links"; import {contextToolbars, getBasicEditorToolbar, getMainEditorFullToolbar} from "./ui/defaults/toolbars"; import {modals} from "./ui/defaults/modals"; -import {CodeBlockDecorator} from "./ui/decorators/code-block"; -import {DiagramDecorator} from "./ui/decorators/diagram"; +import {CodeBlockDecorator} from "./ui/decorators/CodeBlockDecorator"; +import {DiagramDecorator} from "./ui/decorators/DiagramDecorator"; import {registerMouseHandling} from "./services/mouse-handling"; import {registerSelectionHandling} from "./services/selection-handling"; +import {EditorApi} from "./api/api"; +import {registerMentions} from "./services/mentions"; +import {MentionDecorator} from "./ui/decorators/MentionDecorator"; const theme = { text: { @@ -94,6 +102,11 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st registerCommonNodeMutationListeners(context); + window.$events.emitPublic(container, 'editor-wysiwyg::post-init', { + usage: 'page-editor', + api: new EditorApi(context), + }); + return new SimpleWysiwygEditorInterface(context); } @@ -122,6 +135,47 @@ export function createBasicEditorInstance(container: HTMLElement, htmlContent: s setEditorContentFromHtml(editor, htmlContent); + window.$events.emitPublic(container, 'editor-wysiwyg::post-init', { + usage: 'description-editor', + api: new EditorApi(context), + }); + + return new SimpleWysiwygEditorInterface(context); +} + +export function createCommentEditorInstance(container: HTMLElement, htmlContent: string, options: Record = {}): SimpleWysiwygEditorInterface { + const editor = createEditor({ + namespace: 'BookStackCommentEditor', + nodes: getNodesForCommentEditor(), + onError: console.error, + theme: theme, + }); + + const context: EditorUiContext = buildEditorUI(container, editor, options); + editor.setRootElement(context.editorDOM); + + const editorTeardown = mergeRegister( + registerRichText(editor), + registerHistory(editor, createEmptyHistoryState(), 300), + registerShortcuts(context), + registerAutoLinks(editor), + registerMentions(context), + ); + + // Register toolbars, modals & decorators + context.manager.setToolbar(getBasicEditorToolbar(context)); + context.manager.registerContextToolbar('link', contextToolbars.link); + context.manager.registerModal('link', modals.link); + context.manager.onTeardown(editorTeardown); + context.manager.registerDecoratorType('mention', MentionDecorator); + + setEditorContentFromHtml(editor, htmlContent); + + window.$events.emitPublic(container, 'editor-wysiwyg::post-init', { + usage: 'comment-editor', + api: new EditorApi(context), + }); + return new SimpleWysiwygEditorInterface(context); } diff --git a/resources/js/wysiwyg/lexical/core/LexicalCommands.ts b/resources/js/wysiwyg/lexical/core/LexicalCommands.ts index f995237a0..1b378b4a0 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalCommands.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalCommands.ts @@ -78,6 +78,8 @@ export const KEY_ESCAPE_COMMAND: LexicalCommand = createCommand('KEY_ESCAPE_COMMAND'); export const KEY_DELETE_COMMAND: LexicalCommand = createCommand('KEY_DELETE_COMMAND'); +export const KEY_AT_COMMAND: LexicalCommand = + createCommand('KEY_AT_COMMAND'); export const KEY_TAB_COMMAND: LexicalCommand = createCommand('KEY_TAB_COMMAND'); export const INSERT_TAB_COMMAND: LexicalCommand = diff --git a/resources/js/wysiwyg/lexical/core/LexicalEvents.ts b/resources/js/wysiwyg/lexical/core/LexicalEvents.ts index 26cf25a80..2d197ccc2 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalEvents.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalEvents.ts @@ -67,7 +67,7 @@ import { SELECTION_CHANGE_COMMAND, UNDO_COMMAND, } from '.'; -import {KEY_MODIFIER_COMMAND, SELECT_ALL_COMMAND} from './LexicalCommands'; +import {KEY_AT_COMMAND, KEY_MODIFIER_COMMAND, SELECT_ALL_COMMAND} from './LexicalCommands'; import { COMPOSITION_START_CHAR, DOM_ELEMENT_TYPE, @@ -97,7 +97,7 @@ import { getEditorPropertyFromDOMNode, getEditorsToPropagate, getNearestEditorFromDOMNode, - getWindow, + getWindow, isAt, isBackspace, isBold, isCopy, @@ -1062,6 +1062,8 @@ function onKeyDown(event: KeyboardEvent, editor: LexicalEditor): void { } else if (isDeleteLineForward(key, metaKey)) { event.preventDefault(); dispatchCommand(editor, DELETE_LINE_COMMAND, false); + } else if (isAt(key)) { + dispatchCommand(editor, KEY_AT_COMMAND, event); } else if (isBold(key, altKey, metaKey, ctrlKey)) { event.preventDefault(); dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold'); diff --git a/resources/js/wysiwyg/lexical/core/LexicalUtils.ts b/resources/js/wysiwyg/lexical/core/LexicalUtils.ts index 71096b19d..b0bf2f180 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalUtils.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalUtils.ts @@ -1056,6 +1056,10 @@ export function isDelete(key: string): boolean { return key === 'Delete'; } +export function isAt(key: string): boolean { + return key === '@'; +} + export function isSelectAll( key: string, metaKey: boolean, diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts index 00c5ec796..ab54bdb31 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts @@ -504,7 +504,7 @@ export function createTestContext(): EditorUiContext { options: {}, scrollDOM: scrollWrap, translate(text: string): string { - return ""; + return text; } }; @@ -769,6 +769,7 @@ export function expectHtmlToBeEqual(expected: string, actual: string): void { type nodeTextShape = { text: string; + format?: number; }; type nodeShape = { @@ -786,7 +787,13 @@ export function getNodeShape(node: SerializedLexicalNode): nodeShape|nodeTextSha if (shape.type === 'text') { // @ts-ignore - return {text: node.text} + const shape: nodeTextShape = {text: node.text} + // @ts-ignore + if (node && node.format) { + // @ts-ignore + shape.format = node.format; + } + return shape; } if (children.length > 0) { diff --git a/resources/js/wysiwyg/lexical/link/LexicalMentionNode.ts b/resources/js/wysiwyg/lexical/link/LexicalMentionNode.ts new file mode 100644 index 000000000..9010b3c78 --- /dev/null +++ b/resources/js/wysiwyg/lexical/link/LexicalMentionNode.ts @@ -0,0 +1,125 @@ +import { + DecoratorNode, + DOMConversion, + DOMConversionMap, DOMConversionOutput, + type EditorConfig, + LexicalEditor, LexicalNode, + SerializedLexicalNode, + Spread +} from "lexical"; +import {EditorDecoratorAdapter} from "../../ui/framework/decorator"; + +export type SerializedMentionNode = Spread<{ + user_id: number; + user_name: string; + user_slug: string; +}, SerializedLexicalNode> + +export class MentionNode extends DecoratorNode { + __user_id: number = 0; + __user_name: string = ''; + __user_slug: string = ''; + + static getType(): string { + return 'mention'; + } + static clone(node: MentionNode): MentionNode { + const newNode = new MentionNode(node.__key); + newNode.__user_id = node.__user_id; + newNode.__user_name = node.__user_name; + newNode.__user_slug = node.__user_slug; + return newNode; + } + + setUserDetails(userId: number, userName: string, userSlug: string): void { + const self = this.getWritable(); + self.__user_id = userId; + self.__user_name = userName; + self.__user_slug = userSlug; + } + + hasUserSet(): boolean { + return this.__user_id > 0; + } + + isInline(): boolean { + return true; + } + + isParentRequired(): boolean { + return true; + } + + decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter { + return { + type: 'mention', + getNode: () => this, + }; + } + + createDOM(_config: EditorConfig, _editor: LexicalEditor) { + const element = document.createElement('a'); + element.setAttribute('target', '_blank'); + element.setAttribute('href', window.baseUrl('/user/' + this.__user_slug)); + element.setAttribute('data-mention-user-id', String(this.__user_id)); + element.setAttribute('title', '@' + this.__user_name); + element.textContent = '@' + this.__user_name; + return element; + } + + updateDOM(prevNode: MentionNode): boolean { + return prevNode.__user_id !== this.__user_id; + } + + static importDOM(): DOMConversionMap|null { + return { + a(node: HTMLElement): DOMConversion|null { + if (node.hasAttribute('data-mention-user-id')) { + return { + conversion: (element: HTMLElement): DOMConversionOutput|null => { + const node = new MentionNode(); + node.setUserDetails( + Number(element.getAttribute('data-mention-user-id') || '0'), + element.innerText.replace(/^@/, ''), + element.getAttribute('href')?.split('/user/')[1] || '' + ); + + return { + node, + after(childNodes): LexicalNode[] { + return []; + } + }; + }, + priority: 4, + }; + } + return null; + }, + }; + } + + exportJSON(): SerializedMentionNode { + return { + type: 'mention', + version: 1, + user_id: this.__user_id, + user_name: this.__user_name, + user_slug: this.__user_slug, + }; + } + + static importJSON(serializedNode: SerializedMentionNode): MentionNode { + return $createMentionNode(serializedNode.user_id, serializedNode.user_name, serializedNode.user_slug); + } +} + +export function $createMentionNode(userId: number, userName: string, userSlug: string) { + const node = new MentionNode(); + node.setUserDetails(userId, userName, userSlug); + return node; +} + +export function $isMentionNode(node: LexicalNode | null | undefined): node is MentionNode { + return node instanceof MentionNode; +} \ No newline at end of file diff --git a/resources/js/wysiwyg/nodes.ts b/resources/js/wysiwyg/nodes.ts index 413e2c4cd..7c1a71579 100644 --- a/resources/js/wysiwyg/nodes.ts +++ b/resources/js/wysiwyg/nodes.ts @@ -19,6 +19,7 @@ import {MediaNode} from "@lexical/rich-text/LexicalMediaNode"; import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode"; import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode"; import {CaptionNode} from "@lexical/table/LexicalCaptionNode"; +import {MentionNode} from "@lexical/link/LexicalMentionNode"; export function getNodesForPageEditor(): (KlassConstructor | LexicalNodeReplacement)[] { return [ @@ -51,6 +52,13 @@ export function getNodesForBasicEditor(): (KlassConstructor ]; } +export function getNodesForCommentEditor(): (KlassConstructor | LexicalNodeReplacement)[] { + return [ + ...getNodesForBasicEditor(), + MentionNode, + ]; +} + export function registerCommonNodeMutationListeners(context: EditorUiContext): void { const decorated = [ImageNode, CodeBlockNode, DiagramNode]; diff --git a/resources/js/wysiwyg/services/common-events.ts b/resources/js/wysiwyg/services/common-events.ts index 2ffa722e4..f7fc81cb3 100644 --- a/resources/js/wysiwyg/services/common-events.ts +++ b/resources/js/wysiwyg/services/common-events.ts @@ -1,4 +1,4 @@ -import {$getSelection, LexicalEditor} from "lexical"; +import {LexicalEditor} from "lexical"; import { appendHtmlToEditor, focusEditor, diff --git a/resources/js/wysiwyg/services/mentions.ts b/resources/js/wysiwyg/services/mentions.ts new file mode 100644 index 000000000..ba6739f32 --- /dev/null +++ b/resources/js/wysiwyg/services/mentions.ts @@ -0,0 +1,82 @@ +import { + $getSelection, $isRangeSelection, + COMMAND_PRIORITY_NORMAL, KEY_ENTER_COMMAND, RangeSelection, TextNode +} from "lexical"; +import {KEY_AT_COMMAND} from "lexical/LexicalCommands"; +import {$createMentionNode, $isMentionNode, MentionNode} from "@lexical/link/LexicalMentionNode"; +import {EditorUiContext} from "../ui/framework/core"; +import {MentionDecorator} from "../ui/decorators/MentionDecorator"; +import {$selectSingleNode} from "../utils/selection"; + + +function enterUserSelectMode(context: EditorUiContext, selection: RangeSelection) { + const textNode = selection.getNodes()[0] as TextNode; + const selectionPos = selection.getStartEndPoints(); + if (!selectionPos) { + return; + } + + const offset = selectionPos[0].offset; + + // Ignore if the @ sign is not after a space or the start of the line + const atStart = offset === 0; + const afterSpace = textNode.getTextContent().charAt(offset - 1) === ' '; + if (!atStart && !afterSpace) { + return; + } + + const split = textNode.splitText(offset); + const priorTextNode = split[0]; + const afterTextNode = split[atStart ? 0 : 1]; + + const mention = $createMentionNode(0, '', ''); + priorTextNode.insertAfter(mention); + afterTextNode.spliceText(0, 1, '', false); + $selectSingleNode(mention); + + requestAnimationFrame(() => { + const mentionDecorator = context.manager.getDecoratorByNodeKey(mention.getKey()); + if (mentionDecorator instanceof MentionDecorator) { + mentionDecorator.showSelection() + } + }); +} + +function selectMention(context: EditorUiContext, event: KeyboardEvent): boolean { + const selected = $getSelection()?.getNodes() || []; + if (selected.length === 1 && $isMentionNode(selected[0])) { + const mention = selected[0] as MentionNode; + const decorator = context.manager.getDecoratorByNodeKey(mention.getKey()) as MentionDecorator; + decorator.showSelection(); + event.preventDefault(); + event.stopPropagation(); + return true; + } + + return false; +} + +export function registerMentions(context: EditorUiContext): () => void { + const editor = context.editor; + + const unregisterCommand = editor.registerCommand(KEY_AT_COMMAND, function (event: KeyboardEvent): boolean { + const selection = $getSelection(); + if ($isRangeSelection(selection) && selection.isCollapsed()) { + window.setTimeout(() => { + editor.update(() => { + enterUserSelectMode(context, selection); + }); + }, 1); + } + return false; + }, COMMAND_PRIORITY_NORMAL); + + const unregisterEnter = editor.registerCommand(KEY_ENTER_COMMAND, function (event: KeyboardEvent): boolean { + return selectMention(context, event); + }, COMMAND_PRIORITY_NORMAL); + + return (): void => { + unregisterCommand(); + unregisterEnter(); + }; +} \ No newline at end of file diff --git a/resources/js/wysiwyg/services/shortcuts.ts b/resources/js/wysiwyg/services/shortcuts.ts index ead4c38d4..c4be0f3cf 100644 --- a/resources/js/wysiwyg/services/shortcuts.ts +++ b/resources/js/wysiwyg/services/shortcuts.ts @@ -71,13 +71,17 @@ const actionsByKeys: Record = { return true; }, 'meta+shift+k': (editor, context) => { - showLinkSelector(entity => { - insertOrUpdateLink(editor, { - text: entity.name, - title: entity.link, - target: '', - url: entity.link, - }); + editor.getEditorState().read(() => { + const selection = $getSelection(); + const selectionText = selection?.getTextContent() || ''; + showLinkSelector(entity => { + insertOrUpdateLink(editor, { + text: entity.name, + title: entity.link, + target: '', + url: entity.link, + }); + }, selectionText); }); return true; }, diff --git a/resources/js/wysiwyg/ui/decorators/code-block.ts b/resources/js/wysiwyg/ui/decorators/CodeBlockDecorator.ts similarity index 84% rename from resources/js/wysiwyg/ui/decorators/code-block.ts rename to resources/js/wysiwyg/ui/decorators/CodeBlockDecorator.ts index daae32e19..d95185e0b 100644 --- a/resources/js/wysiwyg/ui/decorators/code-block.ts +++ b/resources/js/wysiwyg/ui/decorators/CodeBlockDecorator.ts @@ -14,7 +14,7 @@ export class CodeBlockDecorator extends EditorDecorator { // @ts-ignore protected editor: any = null; - setup(context: EditorUiContext, element: HTMLElement) { + setup(element: HTMLElement) { const codeNode = this.getNode() as CodeBlockNode; const preEl = element.querySelector('pre'); if (!preEl) { @@ -35,24 +35,24 @@ export class CodeBlockDecorator extends EditorDecorator { element.addEventListener('click', event => { requestAnimationFrame(() => { - context.editor.update(() => { + this.context.editor.update(() => { $selectSingleNode(this.getNode()); }); }); }); element.addEventListener('dblclick', event => { - context.editor.getEditorState().read(() => { - $openCodeEditorForNode(context.editor, (this.getNode() as CodeBlockNode)); + this.context.editor.getEditorState().read(() => { + $openCodeEditorForNode(this.context.editor, (this.getNode() as CodeBlockNode)); }); }); const selectionChange = (selection: BaseSelection|null): void => { element.classList.toggle('selected', $selectionContainsNode(selection, codeNode)); }; - context.manager.onSelectionChange(selectionChange); + this.context.manager.onSelectionChange(selectionChange); this.onDestroy(() => { - context.manager.offSelectionChange(selectionChange); + this.context.manager.offSelectionChange(selectionChange); }); // @ts-ignore @@ -89,11 +89,11 @@ export class CodeBlockDecorator extends EditorDecorator { } } - render(context: EditorUiContext, element: HTMLElement): void { + render(element: HTMLElement): void { if (this.completedSetup) { this.update(); } else { - this.setup(context, element); + this.setup(element); } } } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/decorators/diagram.ts b/resources/js/wysiwyg/ui/decorators/DiagramDecorator.ts similarity index 70% rename from resources/js/wysiwyg/ui/decorators/diagram.ts rename to resources/js/wysiwyg/ui/decorators/DiagramDecorator.ts index 52a73ad72..e46dcc312 100644 --- a/resources/js/wysiwyg/ui/decorators/diagram.ts +++ b/resources/js/wysiwyg/ui/decorators/DiagramDecorator.ts @@ -9,33 +9,33 @@ import {$openDrawingEditorForNode} from "../../utils/diagrams"; export class DiagramDecorator extends EditorDecorator { protected completedSetup: boolean = false; - setup(context: EditorUiContext, element: HTMLElement) { + setup(element: HTMLElement) { const diagramNode = this.getNode(); element.classList.add('editor-diagram'); - context.editor.registerCommand(CLICK_COMMAND, (event: MouseEvent): boolean => { + this.context.editor.registerCommand(CLICK_COMMAND, (event: MouseEvent): boolean => { if (!element.contains(event.target as HTMLElement)) { return false; } - context.editor.update(() => { + this.context.editor.update(() => { $selectSingleNode(this.getNode()); }); return true; }, COMMAND_PRIORITY_NORMAL); element.addEventListener('dblclick', event => { - context.editor.getEditorState().read(() => { - $openDrawingEditorForNode(context, (this.getNode() as DiagramNode)); + this.context.editor.getEditorState().read(() => { + $openDrawingEditorForNode(this.context, (this.getNode() as DiagramNode)); }); }); const selectionChange = (selection: BaseSelection|null): void => { element.classList.toggle('selected', $selectionContainsNode(selection, diagramNode)); }; - context.manager.onSelectionChange(selectionChange); + this.context.manager.onSelectionChange(selectionChange); this.onDestroy(() => { - context.manager.offSelectionChange(selectionChange); + this.context.manager.offSelectionChange(selectionChange); }); this.completedSetup = true; @@ -45,11 +45,11 @@ export class DiagramDecorator extends EditorDecorator { // } - render(context: EditorUiContext, element: HTMLElement): void { + render(element: HTMLElement): void { if (this.completedSetup) { this.update(); } else { - this.setup(context, element); + this.setup(element); } } } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/decorators/MentionDecorator.ts b/resources/js/wysiwyg/ui/decorators/MentionDecorator.ts new file mode 100644 index 000000000..d6cafba43 --- /dev/null +++ b/resources/js/wysiwyg/ui/decorators/MentionDecorator.ts @@ -0,0 +1,197 @@ +import {EditorDecorator} from "../framework/decorator"; +import {EditorUiContext} from "../framework/core"; +import {el, htmlToDom} from "../../utils/dom"; +import {showLoading} from "../../../services/dom"; +import {MentionNode} from "@lexical/link/LexicalMentionNode"; +import {debounce} from "../../../services/util"; +import {$createTextNode} from "lexical"; +import {KeyboardNavigationHandler} from "../../../services/keyboard-navigation"; + +import searchIcon from "@icons/search.svg"; + +function userClickHandler(onSelect: (id: number, name: string, slug: string)=>void): (event: PointerEvent) => void { + return (event: PointerEvent) => { + const userItem = (event.target as HTMLElement).closest('a[data-id]') as HTMLAnchorElement | null; + if (!userItem) { + return; + } + + const id = Number(userItem.dataset.id || '0'); + const name = userItem.dataset.name || ''; + const slug = userItem.dataset.slug || ''; + + onSelect(id, name, slug); + event.preventDefault(); + }; +} + +function handleUserSelectCancel(context: EditorUiContext, selectList: HTMLElement, controller: AbortController, onCancel: () => void): void { + selectList.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + onCancel(); + } + }, {signal: controller.signal}); + + const input = selectList.querySelector('input') as HTMLInputElement; + input.addEventListener('keydown', (event) => { + if (event.key === 'Backspace' && input.value === '') { + onCancel(); + event.preventDefault(); + event.stopPropagation(); + } + }, {signal: controller.signal}); + + context.editorDOM.addEventListener('click', (event) => { + onCancel() + }, {signal: controller.signal}); + context.editorDOM.addEventListener('keydown', (event) => { + onCancel(); + }, {signal: controller.signal}); +} + +function handleUserListLoading(selectList: HTMLElement) { + const cache = new Map(); + + const updateUserList = async (searchTerm: string) => { + // Empty list + for (const child of [...selectList.children]) { + child.remove(); + } + + // Fetch new content + let responseHtml = ''; + if (cache.has(searchTerm)) { + responseHtml = cache.get(searchTerm) || ''; + } else { + const loadingWrap = el('div', {class: 'flex-container-row items-center dropdown-search-item'}); + showLoading(loadingWrap); + selectList.appendChild(loadingWrap); + + const resp = await window.$http.get(`/search/users/mention?search=${searchTerm}`); + responseHtml = resp.data as string; + cache.set(searchTerm, responseHtml); + loadingWrap.remove(); + } + + const doc = htmlToDom(responseHtml); + const toInsert = [...doc.body.children]; + for (const listEl of toInsert) { + const adopted = window.document.adoptNode(listEl) as HTMLElement; + selectList.appendChild(adopted); + } + }; + + // Initial load + updateUserList(''); + + const input = selectList.parentElement?.querySelector('input') as HTMLInputElement; + const updateUserListDebounced = debounce(updateUserList, 200, false); + input.addEventListener('input', () => { + const searchTerm = input.value; + updateUserListDebounced(searchTerm); + }); +} + +function buildAndShowUserSelectorAtElement(context: EditorUiContext, mentionDOM: HTMLElement): HTMLElement { + const searchInput = el('input', {type: 'text'}); + const list = el('div', {class: 'dropdown-search-list'}); + const iconWrap = el('div'); + iconWrap.innerHTML = searchIcon; + const icon = iconWrap.children[0] as HTMLElement; + icon.classList.add('svg-icon'); + const userSelect = el('div', {class: 'dropdown-search-dropdown compact card'}, [ + el('div', {class: 'dropdown-search-search'}, [icon, searchInput]), + list, + ]); + + context.containerDOM.appendChild(userSelect); + + userSelect.style.display = 'block'; + userSelect.style.top = '0'; + userSelect.style.left = '0'; + const mentionPos = mentionDOM.getBoundingClientRect(); + const userSelectPos = userSelect.getBoundingClientRect(); + userSelect.style.top = `${mentionPos.bottom - userSelectPos.top + 3}px`; + userSelect.style.left = `${mentionPos.left - userSelectPos.left}px`; + + searchInput.focus(); + + return userSelect; +} + +export class MentionDecorator extends EditorDecorator { + protected abortController: AbortController | null = null; + protected dropdownContainer: HTMLElement | null = null; + protected mentionElement: HTMLElement | null = null; + + setup(element: HTMLElement) { + this.mentionElement = element; + + element.addEventListener('click', (event: PointerEvent) => { + this.showSelection(); + event.preventDefault(); + event.stopPropagation(); + }); + } + + showSelection() { + if (!this.mentionElement || this.dropdownContainer) { + return; + } + + this.hideSelection(); + this.abortController = new AbortController(); + + this.dropdownContainer = buildAndShowUserSelectorAtElement(this.context, this.mentionElement); + handleUserListLoading(this.dropdownContainer.querySelector('.dropdown-search-list') as HTMLElement); + + this.dropdownContainer.addEventListener('click', userClickHandler((id, name, slug) => { + this.context.editor.update(() => { + const mentionNode = this.getNode() as MentionNode; + this.hideSelection(); + mentionNode.setUserDetails(id, name, slug); + mentionNode.selectNext(); + }); + }), {signal: this.abortController.signal}); + + handleUserSelectCancel(this.context, this.dropdownContainer, this.abortController, () => { + if ((this.getNode() as MentionNode).hasUserSet()) { + this.hideSelection() + } else { + this.revertMention(); + } + }); + + new KeyboardNavigationHandler(this.dropdownContainer); + } + + hideSelection() { + this.abortController?.abort(); + this.dropdownContainer?.remove(); + this.abortController = null; + this.dropdownContainer = null; + this.context.manager.focus(); + } + + revertMention() { + this.hideSelection(); + this.context.editor.update(() => { + const text = $createTextNode('@'); + const before = this.getNode().getPreviousSibling(); + this.getNode().replace(text); + requestAnimationFrame(() => { + this.context.editor.update(() => { + if (text.isAttached()) { + text.selectEnd(); + } else if (before?.isAttached()) { + before?.selectEnd(); + } + }); + }); + }); + } + + render(element: HTMLElement): void { + this.setup(element); + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/defaults/toolbars.ts b/resources/js/wysiwyg/ui/defaults/toolbars.ts index 33468e0a2..d6af99638 100644 --- a/resources/js/wysiwyg/ui/defaults/toolbars.ts +++ b/resources/js/wysiwyg/ui/defaults/toolbars.ts @@ -88,7 +88,7 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai return new EditorSimpleClassContainer('editor-toolbar-main', [ // History state - new EditorOverflowContainer(2, [ + new EditorOverflowContainer('history', 2, [ new EditorButton(undo), new EditorButton(redo), ]), @@ -110,7 +110,7 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai ]), // Inline formats - new EditorOverflowContainer(6, [ + new EditorOverflowContainer('inline_formats', 6, [ new EditorButton(bold), new EditorButton(italic), new EditorButton(underline), @@ -128,7 +128,7 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai ]), // Alignment - new EditorOverflowContainer(6, [ + new EditorOverflowContainer('alignment', 6, [ new EditorButton(alignLeft), new EditorButton(alignCenter), new EditorButton(alignRight), @@ -138,7 +138,7 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai ].filter(x => x !== null)), // Lists - new EditorOverflowContainer(3, [ + new EditorOverflowContainer('lists', 3, [ new EditorButton(bulletList), new EditorButton(numberList), new EditorButton(taskList), @@ -147,7 +147,7 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai ]), // Insert types - new EditorOverflowContainer(4, [ + new EditorOverflowContainer('inserts', 4, [ new EditorButton(link), new EditorDropdownButton({button: table, direction: 'vertical', showAside: false}, [ @@ -200,7 +200,7 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai ]), // Meta elements - new EditorOverflowContainer(3, [ + new EditorOverflowContainer('meta', 3, [ new EditorButton(source), new EditorButton(about), new EditorButton(fullscreen), @@ -223,11 +223,13 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai export function getBasicEditorToolbar(context: EditorUiContext): EditorContainerUiElement { return new EditorSimpleClassContainer('editor-toolbar-main', [ - new EditorButton(bold), - new EditorButton(italic), - new EditorButton(link), - new EditorButton(bulletList), - new EditorButton(numberList), + new EditorOverflowContainer('formats', 7, [ + new EditorButton(bold), + new EditorButton(italic), + new EditorButton(link), + new EditorButton(bulletList), + new EditorButton(numberList), + ]) ]); } @@ -241,7 +243,7 @@ export const contextToolbars: Record = { content: () => [new EditorButton(media)], }, link: { - selector: 'a', + selector: 'a:not([data-mention-user-id])', content() { return [ new EditorButton(link), @@ -261,16 +263,16 @@ export const contextToolbars: Record = { selector: 'td,th', content() { return [ - new EditorOverflowContainer(2, [ + new EditorOverflowContainer('table', 2, [ new EditorButton(tableProperties), new EditorButton(deleteTable), ]), - new EditorOverflowContainer(3, [ + new EditorOverflowContainer('table_row',3, [ new EditorButton(insertRowAbove), new EditorButton(insertRowBelow), new EditorButton(deleteRow), ]), - new EditorOverflowContainer(3, [ + new EditorOverflowContainer('table_column', 3, [ new EditorButton(insertColumnBefore), new EditorButton(insertColumnAfter), new EditorButton(deleteColumn), diff --git a/resources/js/wysiwyg/ui/framework/blocks/overflow-container.ts b/resources/js/wysiwyg/ui/framework/blocks/overflow-container.ts index 1c9664505..3b582eee6 100644 --- a/resources/js/wysiwyg/ui/framework/blocks/overflow-container.ts +++ b/resources/js/wysiwyg/ui/framework/blocks/overflow-container.ts @@ -9,9 +9,11 @@ export class EditorOverflowContainer extends EditorContainerUiElement { protected size: number; protected overflowButton: EditorDropdownButton; protected content: EditorUiElement[]; + protected label: string; - constructor(size: number, children: EditorUiElement[]) { + constructor(label: string, size: number, children: EditorUiElement[]) { super(children); + this.label = label; this.size = size; this.content = children; this.overflowButton = new EditorDropdownButton({ @@ -24,6 +26,11 @@ export class EditorOverflowContainer extends EditorContainerUiElement { this.addChildren(this.overflowButton); } + addChild(child: EditorUiElement, targetIndex: number = -1): void { + this.content.splice(targetIndex, 0, child); + this.addChildren(child); + } + protected buildDOM(): HTMLElement { const slicePosition = this.content.length > this.size ? this.size - 1 : this.size; const visibleChildren = this.content.slice(0, slicePosition); @@ -41,5 +48,8 @@ export class EditorOverflowContainer extends EditorContainerUiElement { }, visibleElements); } + getLabel(): string { + return this.label; + } } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/framework/decorator.ts b/resources/js/wysiwyg/ui/framework/decorator.ts index 6ea0b8b39..2f46a19ef 100644 --- a/resources/js/wysiwyg/ui/framework/decorator.ts +++ b/resources/js/wysiwyg/ui/framework/decorator.ts @@ -42,7 +42,7 @@ export abstract class EditorDecorator { * If an element is returned, this will be appended to the element * that is being decorated. */ - abstract render(context: EditorUiContext, decorated: HTMLElement): HTMLElement|void; + abstract render(decorated: HTMLElement): HTMLElement|void; /** * Destroy this decorator. Used for tear-down operations upon destruction diff --git a/resources/js/wysiwyg/ui/framework/forms.ts b/resources/js/wysiwyg/ui/framework/forms.ts index b12d9f692..3d8c53d46 100644 --- a/resources/js/wysiwyg/ui/framework/forms.ts +++ b/resources/js/wysiwyg/ui/framework/forms.ts @@ -98,6 +98,13 @@ export class EditorForm extends EditorContainerUiElement { this.definition = definition; } + focusOnFirst() { + const focusable = this.getDOMElement().querySelector('input,select,textarea'); + if (focusable) { + (focusable as HTMLElement).focus(); + } + } + setValues(values: Record) { for (const name of Object.keys(values)) { const field = this.getFieldByName(name); diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index 3f46455da..78d0cc9a2 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -90,7 +90,7 @@ export class EditorUIManager { } // @ts-ignore - const decorator = new decoratorClass(nodeKey); + const decorator = new decoratorClass(this.getContext()); this.decoratorInstancesByNodeKey[nodeKey] = decorator; return decorator; } @@ -109,6 +109,10 @@ export class EditorUIManager { this.getContext().containerDOM.prepend(toolbar.getDOMElement()); } + getToolbar(): EditorContainerUiElement|null { + return this.toolbar; + } + registerContextToolbar(key: string, definition: EditorContextToolbarDefinition) { this.contextToolbarDefinitionsByKey[key] = definition; } @@ -202,6 +206,14 @@ export class EditorUIManager { } } + /** + * Set the UI focus to the editor. + */ + focus(): void { + this.getContext().editorDOM.focus(); + this.getContext().editor.focus(); + } + protected updateContextToolbars(update: EditorUiStateUpdate): void { for (let i = this.activeContextToolbars.length - 1; i >= 0; i--) { const toolbar = this.activeContextToolbars[i]; @@ -258,7 +270,7 @@ export class EditorUIManager { const adapter = decorators[key]; const decorator = this.getDecorator(adapter.type, key); decorator.setNode(adapter.getNode()); - const decoratorEl = decorator.render(this.getContext(), decoratedEl); + const decoratorEl = decorator.render(decoratedEl); if (decoratorEl) { decoratedEl.append(decoratorEl); } diff --git a/resources/js/wysiwyg/ui/framework/modals.ts b/resources/js/wysiwyg/ui/framework/modals.ts index 4dbe9d962..3f5a5881f 100644 --- a/resources/js/wysiwyg/ui/framework/modals.ts +++ b/resources/js/wysiwyg/ui/framework/modals.ts @@ -14,6 +14,7 @@ export interface EditorFormModalDefinition extends EditorModalDefinition { export class EditorFormModal extends EditorContainerUiElement { protected definition: EditorFormModalDefinition; protected key: string; + protected originalFocus: Element|null = null; constructor(definition: EditorFormModalDefinition, key: string) { super([new EditorForm(definition.form)]); @@ -22,6 +23,7 @@ export class EditorFormModal extends EditorContainerUiElement { } show(defaultValues: Record) { + this.originalFocus = document.activeElement as Element; const dom = this.getDOMElement(); document.body.append(dom); @@ -31,11 +33,15 @@ export class EditorFormModal extends EditorContainerUiElement { form.setOnSuccessfulSubmit(this.hide.bind(this)); this.getContext().manager.setModalActive(this.key, this); + form.focusOnFirst(); } hide() { this.getContext().manager.setModalInactive(this.key); this.teardown(); + if (this.originalFocus instanceof HTMLElement && this.originalFocus.isConnected) { + this.originalFocus.focus(); + } } getForm(): EditorForm { @@ -69,6 +75,12 @@ export class EditorFormModal extends EditorContainerUiElement { } }); + wrapper.addEventListener('keydown', event => { + if (event.key === 'Escape') { + this.hide(); + } + }); + return wrapper; } } \ No newline at end of file diff --git a/resources/js/wysiwyg/utils/actions.ts b/resources/js/wysiwyg/utils/actions.ts index b7ce65eeb..e18ac515f 100644 --- a/resources/js/wysiwyg/utils/actions.ts +++ b/resources/js/wysiwyg/utils/actions.ts @@ -1,6 +1,6 @@ -import {$getRoot, $getSelection, LexicalEditor} from "lexical"; +import {$getRoot, $getSelection, $insertNodes, $isBlockElementNode, LexicalEditor} from "lexical"; import {$generateHtmlFromNodes} from "@lexical/html"; -import {$htmlToBlockNodes} from "./nodes"; +import {$getNearestNodeBlockParent, $htmlToBlockNodes, $htmlToNodes} from "./nodes"; export function setEditorContentFromHtml(editor: LexicalEditor, html: string) { editor.update(() => { @@ -42,14 +42,34 @@ export function prependHtmlToEditor(editor: LexicalEditor, html: string) { export function insertHtmlIntoEditor(editor: LexicalEditor, html: string) { editor.update(() => { const selection = $getSelection(); - const nodes = $htmlToBlockNodes(editor, html); + const nodes = $htmlToNodes(editor, html); - const reference = selection?.getNodes()[0]; - const referencesParents = reference?.getParents() || []; - const topLevel = referencesParents[referencesParents.length - 1]; - if (topLevel && reference) { - for (let i = nodes.length - 1; i >= 0; i--) { - reference.insertAfter(nodes[i]); + let reference = selection?.getNodes()[0]; + let replacedReference = false; + let parentBlock = reference ? $getNearestNodeBlockParent(reference) : null; + + for (let i = nodes.length - 1; i >= 0; i--) { + const toInsert = nodes[i]; + if ($isBlockElementNode(toInsert) && parentBlock) { + // Insert at a block level, before or after the referenced block + // depending on if the reference has been replaced. + if (replacedReference) { + parentBlock.insertBefore(toInsert); + } else { + parentBlock.insertAfter(toInsert); + } + } else if ($isBlockElementNode(toInsert)) { + // Otherwise append blocks to the root + $getRoot().append(toInsert); + } else if (!replacedReference) { + // First inline node, replacing existing selection + $insertNodes([toInsert]); + reference = toInsert; + parentBlock = $getNearestNodeBlockParent(reference); + replacedReference = true; + } else { + // For other inline nodes, insert before the reference node + reference?.insertBefore(toInsert) } } }); diff --git a/resources/js/wysiwyg/utils/links.ts b/resources/js/wysiwyg/utils/links.ts index 03c4a5ef0..a7d999d0c 100644 --- a/resources/js/wysiwyg/utils/links.ts +++ b/resources/js/wysiwyg/utils/links.ts @@ -8,7 +8,7 @@ type EditorEntityData = { export function showLinkSelector(callback: (entity: EditorEntityData) => any, selectionText?: string) { const selector: EntitySelectorPopup = window.$components.first('entity-selector-popup') as EntitySelectorPopup; selector.show((entity: EditorEntityData) => callback(entity), { - initialValue: selectionText, + initialValue: selectionText || '', searchEndpoint: '/search/entity-selector', entityTypes: 'page,book,chapter,bookshelf', entityPermission: 'view', diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index 116a3f4e5..ed70bf699 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -25,10 +25,13 @@ function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] { }); } -export function $htmlToBlockNodes(editor: LexicalEditor, html: string): LexicalNode[] { +export function $htmlToNodes(editor: LexicalEditor, html: string): LexicalNode[] { const dom = htmlToDom(html); - const nodes = $generateNodesFromDOM(editor, dom); - return wrapTextNodes(nodes); + return $generateNodesFromDOM(editor, dom); +} + +export function $htmlToBlockNodes(editor: LexicalEditor, html: string): LexicalNode[] { + return wrapTextNodes($htmlToNodes(editor, html)); } export function $getParentOfType(node: LexicalNode, matcher: LexicalNodeMatcher): LexicalNode | null { diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 8ea15de80..8608427d8 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -746,7 +746,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { @include mixins.lightDark(border-color, #DDD, #444); margin-inline-start: vars.$xs; width: vars.$l; - height: calc(100% - vars.$m); + height: calc(100% - #{vars.$m}); } .comment-reference-indicator-wrap a { @@ -982,8 +982,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } .dropdown-search-item { padding: vars.$s vars.$m; + font-size: 0.8rem; &:hover,&:focus { background-color: #F2F2F2; + @include mixins.lightDark(background-color, #F2F2F2, #444); text-decoration: none; } } @@ -996,6 +998,21 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { input:focus { outline: 0; } + .svg-icon { + font-size: vars.$fs-m; + } + &.compact { + .dropdown-search-list { + max-height: 320px; + } + .dropdown-search-item { + padding: vars.$xs vars.$s; + } + .avatar { + width: 22px; + height: 22px; + } + } } @include mixins.smaller-than(vars.$bp-l) { diff --git a/resources/sass/_content.scss b/resources/sass/_content.scss index aba1556a9..e77ea6330 100644 --- a/resources/sass/_content.scss +++ b/resources/sass/_content.scss @@ -198,4 +198,41 @@ body .page-content img, color: inherit; text-decoration: underline; } +} + +/** + * Mention Links + */ + +a[data-mention-user-id] { + display: inline-block; + position: relative; + color: var(--color-link); + padding: 0.1em 0.4em; + display: -webkit-inline-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.92em; + margin-inline: 0.2em; + vertical-align: middle; + border-radius: 3px; + border: 1px solid transparent; + &:hover { + text-decoration: none; + border-color: currentColor; + } + &:after { + content: ''; + background-color: currentColor; + opacity: 0.2; + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 100%; + display: block; + } } \ No newline at end of file diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index 12fb3385f..13a4232fc 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -348,6 +348,10 @@ input[type=color] { } } +.small-inputs input { + width: 150px; +} + .simple-code-input { background-color: #F8F8F8; font-family: monospace; diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index 1e503dd0f..d9a1195aa 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -366,9 +366,10 @@ details.sort-box[open] summary .caret-container svg { ul.pagination { display: inline-flex; + flex-wrap: wrap; list-style: none; margin: vars.$m 0; - padding-inline-start: 1px; + padding-inline: 1px; li:first-child { a, span { border-radius: 3px 0 0 3px; @@ -384,6 +385,7 @@ ul.pagination { padding: vars.$xxs vars.$s; border: 1px solid #CCC; margin-inline-start: -1px; + margin-block-end: -1px; user-select: none; @include mixins.lightDark(color, #555, #eee); @include mixins.lightDark(border-color, #ccc, #666); diff --git a/resources/views/form/number.blade.php b/resources/views/form/number.blade.php index a37cd3694..b70b7e8db 100644 --- a/resources/views/form/number.blade.php +++ b/resources/views/form/number.blade.php @@ -6,7 +6,8 @@ @if($readonly ?? false) readonly="readonly" @endif @if($min ?? false) min="{{ $min }}" @endif @if($max ?? false) max="{{ $max }}" @endif - @if(isset($model) || old($name)) value="{{ old($name) ? old($name) : $model->$name}}" @endif> + @if($step ?? false) step="{{ $step }}" @endif + @if(isset($model) || old($name) || isset($value)) value="{{ old($name) ?? $model->$name ?? $value }}" @endif> @if($errors->has($name))
{{ $errors->first($name) }}
@endif diff --git a/resources/views/form/user-mention-list.blade.php b/resources/views/form/user-mention-list.blade.php new file mode 100644 index 000000000..020cfb35b --- /dev/null +++ b/resources/views/form/user-mention-list.blade.php @@ -0,0 +1,14 @@ +@if($users->isEmpty()) + +@endif +@foreach($users as $user) + + {{ $user->name }} + {{ $user->name }} + +@endforeach \ No newline at end of file diff --git a/resources/views/layouts/parts/custom-styles.blade.php b/resources/views/layouts/parts/custom-styles.blade.php index bfdcc8512..b31334e73 100644 --- a/resources/views/layouts/parts/custom-styles.blade.php +++ b/resources/views/layouts/parts/custom-styles.blade.php @@ -1,15 +1,22 @@ -@php - $settingSuffix = setting()->getForCurrentUser('dark-mode-enabled') ? '-dark' : ''; -@endphp diff --git a/resources/views/settings/categories/sorting.blade.php b/resources/views/settings/categories/sorting.blade.php index 9d1d9814b..0c9dc1f95 100644 --- a/resources/views/settings/categories/sorting.blade.php +++ b/resources/views/settings/categories/sorting.blade.php @@ -11,6 +11,29 @@
+
+
+ +

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

+
+
+ @php + $labelByKey = ['shelves' => trans('entities.shelves'), 'books' => trans('entities.books'), 'search' => trans('entities.search_results')]; + @endphp + @foreach($labelByKey as $key => $label) +
+ + @include('form.number', [ + 'name' => 'setting-lists-page-count-' . $key, + 'value' => setting()->getInteger('lists-page-count-' . $key, 18, 1, 1000), + 'min' => 1, + 'step' => 1, + ]) +
+ @endforeach +
+
+
{{ trans('common.toggle_all') }} -
+
-
@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])
-
@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])
+
@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')]) 1
+
@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')]) 1
@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])
@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])
@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])
@@ -48,6 +48,10 @@

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

+ +

+ 1 {{ trans('settings.role_permission_note_users_and_roles') }} +

@@ -75,7 +79,7 @@ @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.books'), 'permissionPrefix' => 'book']) @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.chapters'), 'permissionPrefix' => 'chapter']) @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.pages'), 'permissionPrefix' => 'page']) - @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.images'), 'permissionPrefix' => 'image', 'refMark' => '1']) + @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.images'), 'permissionPrefix' => 'image']) @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.attachments'), 'permissionPrefix' => 'attachment']) @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.comments'), 'permissionPrefix' => 'comment'])
@@ -83,6 +87,8 @@

1 {{ trans('settings.role_asset_image_view_note') }} +
+ 2 {{ trans('settings.role_asset_users_note') }}

diff --git a/resources/views/settings/roles/parts/related-asset-permissions-row.blade.php b/resources/views/settings/roles/parts/related-asset-permissions-row.blade.php index 62fdd6b74..b9bf5427f 100644 --- a/resources/views/settings/roles/parts/related-asset-permissions-row.blade.php +++ b/resources/views/settings/roles/parts/related-asset-permissions-row.blade.php @@ -6,16 +6,19 @@
{{ trans('common.create') }}
@include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-all', 'label' => '']) + @if($permissionPrefix === 'comment')2@endif
{{ trans('common.view') }}
- {{ trans('settings.role_controlled_by_asset') }}@if($refMark ?? false){{ $refMark }}@endif + {{ trans('settings.role_controlled_by_asset') }}@if($permissionPrefix === 'image')1@endif
{{ trans('common.edit') }}
@include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-own', 'label' => trans('settings.role_own')]) + @if($permissionPrefix === 'comment')2@endif
@include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-all', 'label' => trans('settings.role_all')]) + @if($permissionPrefix === 'comment')2@endif
{{ trans('common.delete') }}
diff --git a/resources/views/users/account/notifications.blade.php b/resources/views/users/account/notifications.blade.php index b3b082bd7..c61cf4af8 100644 --- a/resources/views/users/account/notifications.blade.php +++ b/resources/views/users/account/notifications.blade.php @@ -33,6 +33,13 @@ 'label' => trans('preferences.notifications_opt_comment_replies'), ])
+
+ @include('form.toggle-switch', [ + 'name' => 'preferences[comment-mentions]', + 'value' => $preferences->notifyOnCommentMentions(), + 'label' => trans('preferences.notifications_opt_comment_mentions'), + ]) +
@endif
diff --git a/routes/web.php b/routes/web.php index ea3efe1ac..a20c0a3d3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -198,6 +198,7 @@ Route::middleware('auth')->group(function () { // User Search Route::get('/search/users/select', [UserControllers\UserSearchController::class, 'forSelect']); + Route::get('/search/users/mention', [UserControllers\UserSearchController::class, 'forMentions']); // Template System Route::get('/templates', [EntityControllers\PageTemplateController::class, 'list']); diff --git a/tests/Entity/CommentDisplayTest.php b/tests/Activity/CommentDisplayTest.php similarity index 98% rename from tests/Entity/CommentDisplayTest.php rename to tests/Activity/CommentDisplayTest.php index 80664890a..798ea16ee 100644 --- a/tests/Entity/CommentDisplayTest.php +++ b/tests/Activity/CommentDisplayTest.php @@ -1,10 +1,8 @@ users->viewer(); + $this->permissions->grantUserRolePermissions($userToMention, [Permission::ReceiveNotifications]); + $editor = $this->users->editor(); + $page = $this->entities->pageWithinChapter(); + $notifications = Notification::fake(); + + $this->actingAs($editor)->post("/comment/{$page->id}", [ + 'html' => '

Hello @user

' + ])->assertOk(); + + $notifications->assertSentTo($userToMention, function (CommentMentionNotification $notification) use ($userToMention, $editor, $page) { + $mail = $notification->toMail($userToMention); + $mailContent = html_entity_decode(strip_tags($mail->render()), ENT_QUOTES); + $subjectPrefix = 'You have been mentioned in a comment on page: ' . mb_substr($page->name, 0, 20); + return str_starts_with($mail->subject, $subjectPrefix) + && str_contains($mailContent, 'View Comment') + && str_contains($mailContent, 'Page Name: ' . $page->name) + && str_contains($mailContent, 'Page Path: ' . $page->book->getShortName(24) . ' > ' . $page->chapter->getShortName(24)) + && str_contains($mailContent, 'Commenter: ' . $editor->name) + && str_contains($mailContent, 'Comment: Hello @user'); + }); + } + + public function test_mentions_are_not_notified_if_mentioned_by_same_user() + { + $editor = $this->users->editor(); + $this->permissions->grantUserRolePermissions($editor, [Permission::ReceiveNotifications]); + $page = $this->entities->page(); + $notifications = Notification::fake(); + + $this->actingAs($editor)->post("/comment/{$page->id}", [ + 'html' => '

Hello

' + ])->assertOk(); + + $notifications->assertNothingSent(); + } + + public function test_mentions_are_logged_to_the_database_even_if_not_notified() + { + $editor = $this->users->editor(); + $otherUser = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($editor, [Permission::ReceiveNotifications]); + $page = $this->entities->page(); + $notifications = Notification::fake(); + + $this->actingAs($editor)->post("/comment/{$page->id}", [ + 'html' => '

Hello and

' + ])->assertOk(); + + $notifications->assertNothingSent(); + + $comment = $page->comments()->latest()->first(); + $this->assertDatabaseHas('mention_history', [ + 'mentionable_id' => $comment->id, + 'mentionable_type' => 'comment', + 'from_user_id' => $editor->id, + 'to_user_id' => $otherUser->id, + ]); + $this->assertDatabaseHas('mention_history', [ + 'mentionable_id' => $comment->id, + 'mentionable_type' => 'comment', + 'from_user_id' => $editor->id, + 'to_user_id' => $editor->id, + ]); + } + + public function test_comment_updates_will_send_notifications_only_if_mention_is_new() + { + $userToMention = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($userToMention, [Permission::ReceiveNotifications]); + $editor = $this->users->editor(); + $this->permissions->grantUserRolePermissions($editor, [Permission::CommentUpdateOwn]); + $page = $this->entities->page(); + $notifications = Notification::fake(); + + $this->actingAs($editor)->post("/comment/{$page->id}", [ + 'html' => '

Hello there

' + ])->assertOk(); + $comment = $page->comments()->latest()->first(); + + $notifications->assertNothingSent(); + + $this->put("/comment/{$comment->id}", [ + 'html' => '

Hello

' + ])->assertOk(); + + $notifications->assertSentTo($userToMention, CommentMentionNotification::class); + $notifications->assertCount(1); + + $this->put("/comment/{$comment->id}", [ + 'html' => '

Hello again

' + ])->assertOk(); + + $notifications->assertCount(1); + } + + public function test_notification_limited_to_those_with_view_permissions() + { + $userA = $this->users->newUser(); + $userB = $this->users->newUser(); + $this->permissions->grantUserRolePermissions($userA, [Permission::ReceiveNotifications]); + $this->permissions->grantUserRolePermissions($userB, [Permission::ReceiveNotifications]); + $notifications = Notification::fake(); + $page = $this->entities->page(); + + $this->permissions->disableEntityInheritedPermissions($page); + $this->permissions->addEntityPermission($page, ['view'], $userA->roles()->first()); + + $this->asAdmin()->post("/comment/{$page->id}", [ + 'html' => '

Hello and

' + ])->assertOk(); + + $notifications->assertCount(1); + $notifications->assertSentTo($userA, CommentMentionNotification::class); + } +} diff --git a/tests/Entity/CommentSettingTest.php b/tests/Activity/CommentSettingTest.php similarity index 96% rename from tests/Entity/CommentSettingTest.php rename to tests/Activity/CommentSettingTest.php index 7de457441..f8210114c 100644 --- a/tests/Entity/CommentSettingTest.php +++ b/tests/Activity/CommentSettingTest.php @@ -1,6 +1,6 @@ Hello @User

'; + $result = $parser->parseUserIdsFromHtml($html); + $this->assertEquals([5], $result); + + // Test multiple mentions + $html = '

@Alice and @Bob

'; + $result = $parser->parseUserIdsFromHtml($html); + $this->assertEquals([1, 2], $result); + + // Test filtering out invalid IDs (zero and negative) + $html = '

@Invalid @Negative @Valid

'; + $result = $parser->parseUserIdsFromHtml($html); + $this->assertEquals([3], $result); + + // Test non-mention links are ignored + $html = '

Normal Link @User

'; + $result = $parser->parseUserIdsFromHtml($html); + $this->assertEquals([7], $result); + + // Test empty HTML + $result = $parser->parseUserIdsFromHtml(''); + $this->assertEquals([], $result); + + // Test duplicate user IDs + $html = '

@User mentioned @User again

'; + $result = $parser->parseUserIdsFromHtml($html); + $this->assertEquals([4], $result); + } +} diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php index 543c4e8bb..a7142f037 100644 --- a/tests/Entity/BookTest.php +++ b/tests/Entity/BookTest.php @@ -238,30 +238,6 @@ class BookTest extends TestCase $this->assertEquals('list', setting()->getUser($editor, 'books_view_type')); } - public function test_slug_multi_byte_url_safe() - { - $book = $this->entities->newBook([ - 'name' => 'информация', - ]); - - $this->assertEquals('informaciia', $book->slug); - - $book = $this->entities->newBook([ - 'name' => '¿Qué?', - ]); - - $this->assertEquals('que', $book->slug); - } - - public function test_slug_format() - { - $book = $this->entities->newBook([ - 'name' => 'PartA / PartB / PartC', - ]); - - $this->assertEquals('parta-partb-partc', $book->slug); - } - public function test_description_limited_to_specific_html() { $book = $this->entities->book(); @@ -288,108 +264,4 @@ class BookTest extends TestCase $resp = $this->asEditor()->get($book->getUrl()); $resp->assertSee("

My great
\ndescription
\n
\nwith newlines

", false); } - - public function test_show_view_has_copy_button() - { - $book = $this->entities->book(); - $resp = $this->asEditor()->get($book->getUrl()); - - $this->withHtml($resp)->assertElementContains("a[href=\"{$book->getUrl('/copy')}\"]", 'Copy'); - } - - public function test_copy_view() - { - $book = $this->entities->book(); - $resp = $this->asEditor()->get($book->getUrl('/copy')); - - $resp->assertOk(); - $resp->assertSee('Copy Book'); - $this->withHtml($resp)->assertElementExists("input[name=\"name\"][value=\"{$book->name}\"]"); - } - - public function test_copy() - { - /** @var Book $book */ - $book = Book::query()->whereHas('chapters')->whereHas('pages')->first(); - $resp = $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); - - /** @var Book $copy */ - $copy = Book::query()->where('name', '=', 'My copy book')->first(); - - $resp->assertRedirect($copy->getUrl()); - $this->assertEquals($book->getDirectVisibleChildren()->count(), $copy->getDirectVisibleChildren()->count()); - - $this->get($copy->getUrl())->assertSee($book->description_html, false); - } - - public function test_copy_does_not_copy_non_visible_content() - { - /** @var Book $book */ - $book = Book::query()->whereHas('chapters')->whereHas('pages')->first(); - - // Hide child content - /** @var BookChild $page */ - foreach ($book->getDirectVisibleChildren() as $child) { - $this->permissions->setEntityPermissions($child, [], []); - } - - $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); - /** @var Book $copy */ - $copy = Book::query()->where('name', '=', 'My copy book')->first(); - - $this->assertEquals(0, $copy->getDirectVisibleChildren()->count()); - } - - public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create() - { - /** @var Book $book */ - $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first(); - $viewer = $this->users->viewer(); - $this->permissions->grantUserRolePermissions($viewer, ['book-create-all']); - - $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']); - /** @var Book $copy */ - $copy = Book::query()->where('name', '=', 'My copy book')->first(); - - $this->assertEquals(0, $copy->pages()->count()); - $this->assertEquals(0, $copy->chapters()->count()); - } - - public function test_copy_clones_cover_image_if_existing() - { - $book = $this->entities->book(); - $bookRepo = $this->app->make(BookRepo::class); - $coverImageFile = $this->files->uploadedImage('cover.png'); - $bookRepo->updateCoverImage($book, $coverImageFile); - - $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book'])->assertRedirect(); - /** @var Book $copy */ - $copy = Book::query()->where('name', '=', 'My copy book')->first(); - - $this->assertNotNull($copy->coverInfo()->getImage()); - $this->assertNotEquals($book->coverInfo()->getImage()->id, $copy->coverInfo()->getImage()->id); - } - - public function test_copy_adds_book_to_shelves_if_edit_permissions_allows() - { - /** @var Bookshelf $shelfA */ - /** @var Bookshelf $shelfB */ - [$shelfA, $shelfB] = Bookshelf::query()->take(2)->get(); - $book = $this->entities->book(); - - $shelfA->appendBook($book); - $shelfB->appendBook($book); - - $viewer = $this->users->viewer(); - $this->permissions->grantUserRolePermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']); - $this->permissions->setEntityPermissions($shelfB); - - - $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); - /** @var Book $copy */ - $copy = Book::query()->where('name', '=', 'My copy book')->first(); - - $this->assertTrue($copy->shelves()->where('id', '=', $shelfA->id)->exists()); - $this->assertFalse($copy->shelves()->where('id', '=', $shelfB->id)->exists()); - } } diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php index 1577cee76..0c0ec7841 100644 --- a/tests/Entity/ChapterTest.php +++ b/tests/Entity/ChapterTest.php @@ -66,90 +66,7 @@ class ChapterTest extends TestCase $this->assertNotificationContains($redirectReq, 'Chapter Successfully Deleted'); } - public function test_show_view_has_copy_button() - { - $chapter = $this->entities->chapter(); - $resp = $this->asEditor()->get($chapter->getUrl()); - $this->withHtml($resp)->assertElementContains("a[href$=\"{$chapter->getUrl('/copy')}\"]", 'Copy'); - } - - public function test_copy_view() - { - $chapter = $this->entities->chapter(); - - $resp = $this->asEditor()->get($chapter->getUrl('/copy')); - $resp->assertOk(); - $resp->assertSee('Copy Chapter'); - $this->withHtml($resp)->assertElementExists("input[name=\"name\"][value=\"{$chapter->name}\"]"); - $this->withHtml($resp)->assertElementExists('input[name="entity_selection"]'); - } - - public function test_copy() - { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->whereHas('pages')->first(); - /** @var Book $otherBook */ - $otherBook = Book::query()->where('id', '!=', $chapter->book_id)->first(); - - $resp = $this->asEditor()->post($chapter->getUrl('/copy'), [ - 'name' => 'My copied chapter', - 'entity_selection' => 'book:' . $otherBook->id, - ]); - - /** @var Chapter $newChapter */ - $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); - - $resp->assertRedirect($newChapter->getUrl()); - $this->assertEquals($otherBook->id, $newChapter->book_id); - $this->assertEquals($chapter->pages->count(), $newChapter->pages->count()); - } - - public function test_copy_does_not_copy_non_visible_pages() - { - $chapter = $this->entities->chapterHasPages(); - - // Hide pages to all non-admin roles - /** @var Page $page */ - foreach ($chapter->pages as $page) { - $this->permissions->setEntityPermissions($page, [], []); - } - - $this->asEditor()->post($chapter->getUrl('/copy'), [ - 'name' => 'My copied chapter', - ]); - - /** @var Chapter $newChapter */ - $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); - $this->assertEquals(0, $newChapter->pages()->count()); - } - - public function test_copy_does_not_copy_pages_if_user_cant_page_create() - { - $chapter = $this->entities->chapterHasPages(); - $viewer = $this->users->viewer(); - $this->permissions->grantUserRolePermissions($viewer, ['chapter-create-all']); - - // Lacking permission results in no copied pages - $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [ - 'name' => 'My copied chapter', - ]); - - /** @var Chapter $newChapter */ - $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); - $this->assertEquals(0, $newChapter->pages()->count()); - - $this->permissions->grantUserRolePermissions($viewer, ['page-create-all']); - - // Having permission rules in copied pages - $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [ - 'name' => 'My copied again chapter', - ]); - - /** @var Chapter $newChapter2 */ - $newChapter2 = Chapter::query()->where('name', '=', 'My copied again chapter')->first(); - $this->assertEquals($chapter->pages()->count(), $newChapter2->pages()->count()); - } public function test_sort_book_action_visible_if_permissions_allow() { diff --git a/tests/Entity/CopyTest.php b/tests/Entity/CopyTest.php new file mode 100644 index 000000000..d4b6d54cf --- /dev/null +++ b/tests/Entity/CopyTest.php @@ -0,0 +1,399 @@ +entities->book(); + $resp = $this->asEditor()->get($book->getUrl()); + + $this->withHtml($resp)->assertElementContains("a[href=\"{$book->getUrl('/copy')}\"]", 'Copy'); + } + + public function test_book_copy_view() + { + $book = $this->entities->book(); + $resp = $this->asEditor()->get($book->getUrl('/copy')); + + $resp->assertOk(); + $resp->assertSee('Copy Book'); + $this->withHtml($resp)->assertElementExists("input[name=\"name\"][value=\"{$book->name}\"]"); + } + + public function test_book_copy() + { + /** @var Book $book */ + $book = Book::query()->whereHas('chapters')->whereHas('pages')->first(); + $resp = $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); + + /** @var Book $copy */ + $copy = Book::query()->where('name', '=', 'My copy book')->first(); + + $resp->assertRedirect($copy->getUrl()); + $this->assertEquals($book->getDirectVisibleChildren()->count(), $copy->getDirectVisibleChildren()->count()); + + $this->get($copy->getUrl())->assertSee($book->description_html, false); + } + + public function test_book_copy_does_not_copy_non_visible_content() + { + /** @var Book $book */ + $book = Book::query()->whereHas('chapters')->whereHas('pages')->first(); + + // Hide child content + /** @var BookChild $page */ + foreach ($book->getDirectVisibleChildren() as $child) { + $this->permissions->setEntityPermissions($child, [], []); + } + + $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); + /** @var Book $copy */ + $copy = Book::query()->where('name', '=', 'My copy book')->first(); + + $this->assertEquals(0, $copy->getDirectVisibleChildren()->count()); + } + + public function test_book_copy_does_not_copy_pages_or_chapters_if_user_cant_create() + { + /** @var Book $book */ + $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first(); + $viewer = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($viewer, ['book-create-all']); + + $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']); + /** @var Book $copy */ + $copy = Book::query()->where('name', '=', 'My copy book')->first(); + + $this->assertEquals(0, $copy->pages()->count()); + $this->assertEquals(0, $copy->chapters()->count()); + } + + public function test_book_copy_clones_cover_image_if_existing() + { + $book = $this->entities->book(); + $bookRepo = $this->app->make(BookRepo::class); + $coverImageFile = $this->files->uploadedImage('cover.png'); + $bookRepo->updateCoverImage($book, $coverImageFile); + + $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book'])->assertRedirect(); + /** @var Book $copy */ + $copy = Book::query()->where('name', '=', 'My copy book')->first(); + + $this->assertNotNull($copy->coverInfo()->getImage()); + $this->assertNotEquals($book->coverInfo()->getImage()->id, $copy->coverInfo()->getImage()->id); + } + + public function test_book_copy_adds_book_to_shelves_if_edit_permissions_allows() + { + /** @var Bookshelf $shelfA */ + /** @var Bookshelf $shelfB */ + [$shelfA, $shelfB] = Bookshelf::query()->take(2)->get(); + $book = $this->entities->book(); + + $shelfA->appendBook($book); + $shelfB->appendBook($book); + + $viewer = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']); + $this->permissions->setEntityPermissions($shelfB); + + + $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); + /** @var Book $copy */ + $copy = Book::query()->where('name', '=', 'My copy book')->first(); + + $this->assertTrue($copy->shelves()->where('id', '=', $shelfA->id)->exists()); + $this->assertFalse($copy->shelves()->where('id', '=', $shelfB->id)->exists()); + } + + public function test_chapter_show_view_has_copy_button() + { + $chapter = $this->entities->chapter(); + + $resp = $this->asEditor()->get($chapter->getUrl()); + $this->withHtml($resp)->assertElementContains("a[href$=\"{$chapter->getUrl('/copy')}\"]", 'Copy'); + } + + public function test_chapter_copy_view() + { + $chapter = $this->entities->chapter(); + + $resp = $this->asEditor()->get($chapter->getUrl('/copy')); + $resp->assertOk(); + $resp->assertSee('Copy Chapter'); + $this->withHtml($resp)->assertElementExists("input[name=\"name\"][value=\"{$chapter->name}\"]"); + $this->withHtml($resp)->assertElementExists('input[name="entity_selection"]'); + } + + public function test_chapter_copy() + { + /** @var Chapter $chapter */ + $chapter = Chapter::query()->whereHas('pages')->first(); + /** @var Book $otherBook */ + $otherBook = Book::query()->where('id', '!=', $chapter->book_id)->first(); + + $resp = $this->asEditor()->post($chapter->getUrl('/copy'), [ + 'name' => 'My copied chapter', + 'entity_selection' => 'book:' . $otherBook->id, + ]); + + /** @var Chapter $newChapter */ + $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); + + $resp->assertRedirect($newChapter->getUrl()); + $this->assertEquals($otherBook->id, $newChapter->book_id); + $this->assertEquals($chapter->pages->count(), $newChapter->pages->count()); + } + + public function test_chapter_copy_does_not_copy_non_visible_pages() + { + $chapter = $this->entities->chapterHasPages(); + + // Hide pages to all non-admin roles + /** @var Page $page */ + foreach ($chapter->pages as $page) { + $this->permissions->setEntityPermissions($page, [], []); + } + + $this->asEditor()->post($chapter->getUrl('/copy'), [ + 'name' => 'My copied chapter', + ]); + + /** @var Chapter $newChapter */ + $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); + $this->assertEquals(0, $newChapter->pages()->count()); + } + + public function test_chapter_copy_does_not_copy_pages_if_user_cant_page_create() + { + $chapter = $this->entities->chapterHasPages(); + $viewer = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($viewer, ['chapter-create-all']); + + // Lacking permission results in no copied pages + $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [ + 'name' => 'My copied chapter', + ]); + + /** @var Chapter $newChapter */ + $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); + $this->assertEquals(0, $newChapter->pages()->count()); + + $this->permissions->grantUserRolePermissions($viewer, ['page-create-all']); + + // Having permission rules in copied pages + $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [ + 'name' => 'My copied again chapter', + ]); + + /** @var Chapter $newChapter2 */ + $newChapter2 = Chapter::query()->where('name', '=', 'My copied again chapter')->first(); + $this->assertEquals($chapter->pages()->count(), $newChapter2->pages()->count()); + } + + public function test_book_copy_updates_internal_references() + { + $book = $this->entities->bookHasChaptersAndPages(); + /** @var Chapter $chapter */ + $chapter = $book->chapters()->first(); + /** @var Page $page */ + $page = $chapter->pages()->first(); + $this->asEditor(); + $this->entities->updatePage($page, [ + 'name' => 'reference test page', + 'html' => '

This is a test book link

', + ]); + + // Quick pre-update to get stable slug + $this->put($book->getUrl(), ['name' => 'Internal ref test']); + $book->refresh(); + $page->refresh(); + + $html = '

This is a test page link

'; + $this->put($book->getUrl(), ['name' => 'Internal ref test', 'description_html' => $html]); + + $this->post($book->getUrl('/copy'), ['name' => 'My copied book']); + + $newBook = Book::query()->where('name', '=', 'My copied book')->first(); + $newPage = $newBook->pages()->where('name', '=', 'reference test page')->first(); + + $this->assertStringContainsString($newBook->getUrl(), $newPage->html); + $this->assertStringContainsString($newPage->getUrl(), $newBook->description_html); + + $this->assertStringNotContainsString($book->getUrl(), $newPage->html); + $this->assertStringNotContainsString($page->getUrl(), $newBook->description_html); + } + + public function test_chapter_copy_updates_internal_references() + { + $chapter = $this->entities->chapterHasPages(); + /** @var Page $page */ + $page = $chapter->pages()->first(); + $this->asEditor(); + $this->entities->updatePage($page, [ + 'name' => 'reference test page', + 'html' => '

This is a test chapter link

', + ]); + + // Quick pre-update to get stable slug + $this->put($chapter->getUrl(), ['name' => 'Internal ref test']); + $chapter->refresh(); + $page->refresh(); + + $html = '

This is a test page link

'; + $this->put($chapter->getUrl(), ['name' => 'Internal ref test', 'description_html' => $html]); + + $this->post($chapter->getUrl('/copy'), ['name' => 'My copied chapter']); + + $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); + $newPage = $newChapter->pages()->where('name', '=', 'reference test page')->first(); + + $this->assertStringContainsString($newChapter->getUrl() . '"', $newPage->html); + $this->assertStringContainsString($newPage->getUrl() . '"', $newChapter->description_html); + + $this->assertStringNotContainsString($chapter->getUrl() . '"', $newPage->html); + $this->assertStringNotContainsString($page->getUrl() . '"', $newChapter->description_html); + } + + public function test_chapter_copy_updates_internal_permalink_references_in_its_description() + { + $chapter = $this->entities->chapterHasPages(); + /** @var Page $page */ + $page = $chapter->pages()->first(); + + $this->asEditor()->put($chapter->getUrl(), [ + 'name' => 'Internal ref test', + 'description_html' => '

This is a test page link

', + ]); + $chapter->refresh(); + + $this->post($chapter->getUrl('/copy'), ['name' => 'My copied chapter']); + $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); + + $this->assertStringContainsString('/link/', $newChapter->description_html); + $this->assertStringNotContainsString($page->getPermalink() . '"', $newChapter->description_html); + } + + public function test_page_copy_updates_internal_self_references() + { + $page = $this->entities->page(); + $this->asEditor(); + + // Initial update to get stable slug + $this->entities->updatePage($page, ['name' => 'reference test page']); + + $page->refresh(); + $this->entities->updatePage($page, [ + 'name' => 'reference test page', + 'html' => '

This is a test page link

', + ]); + + $this->post($page->getUrl('/copy'), ['name' => 'My copied page']); + $newPage = Page::query()->where('name', '=', 'My copied page')->first(); + $this->assertNotNull($newPage); + + $this->assertStringContainsString($newPage->getUrl(), $newPage->html); + $this->assertStringNotContainsString($page->getUrl(), $newPage->html); + } + + public function test_page_copy() + { + $page = $this->entities->page(); + $page->html = '

This is some test content

'; + $page->save(); + + $currentBook = $page->book; + $newBook = Book::where('id', '!=', $currentBook->id)->first(); + + $resp = $this->asEditor()->get($page->getUrl('/copy')); + $resp->assertSee('Copy Page'); + + $movePageResp = $this->post($page->getUrl('/copy'), [ + 'entity_selection' => 'book:' . $newBook->id, + 'name' => 'My copied test page', + ]); + $pageCopy = Page::where('name', '=', 'My copied test page')->first(); + + $movePageResp->assertRedirect($pageCopy->getUrl()); + $this->assertTrue($pageCopy->book->id == $newBook->id, 'Page was copied to correct book'); + $this->assertStringContainsString('This is some test content', $pageCopy->html); + } + + public function test_page_copy_with_markdown_has_both_html_and_markdown() + { + $page = $this->entities->page(); + $page->html = '

This is some test content

'; + $page->markdown = '# This is some test content'; + $page->save(); + $newBook = Book::where('id', '!=', $page->book->id)->first(); + + $this->asEditor()->post($page->getUrl('/copy'), [ + 'entity_selection' => 'book:' . $newBook->id, + 'name' => 'My copied test page', + ]); + $pageCopy = Page::where('name', '=', 'My copied test page')->first(); + + $this->assertStringContainsString('This is some test content', $pageCopy->html); + $this->assertEquals('# This is some test content', $pageCopy->markdown); + } + + public function test_page_copy_with_no_destination() + { + $page = $this->entities->page(); + $currentBook = $page->book; + + $resp = $this->asEditor()->get($page->getUrl('/copy')); + $resp->assertSee('Copy Page'); + + $movePageResp = $this->post($page->getUrl('/copy'), [ + 'name' => 'My copied test page', + ]); + + $pageCopy = Page::where('name', '=', 'My copied test page')->first(); + + $movePageResp->assertRedirect($pageCopy->getUrl()); + $this->assertTrue($pageCopy->book->id == $currentBook->id, 'Page was copied to correct book'); + $this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance'); + } + + public function test_page_can_be_copied_without_edit_permission() + { + $page = $this->entities->page(); + $currentBook = $page->book; + $newBook = Book::where('id', '!=', $currentBook->id)->first(); + $viewer = $this->users->viewer(); + + $resp = $this->actingAs($viewer)->get($page->getUrl()); + $resp->assertDontSee($page->getUrl('/copy')); + + $newBook->owned_by = $viewer->id; + $newBook->save(); + $this->permissions->grantUserRolePermissions($viewer, ['page-create-own']); + $this->permissions->regenerateForEntity($newBook); + + $resp = $this->actingAs($viewer)->get($page->getUrl()); + $resp->assertSee($page->getUrl('/copy')); + + $movePageResp = $this->post($page->getUrl('/copy'), [ + 'entity_selection' => 'book:' . $newBook->id, + 'name' => 'My copied test page', + ]); + $movePageResp->assertRedirect(); + + $this->assertDatabaseHasEntityData('page', [ + 'name' => 'My copied test page', + 'created_by' => $viewer->id, + 'book_id' => $newBook->id, + ]); + } +} diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index b9e1294e0..1b2f3c9fe 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -178,119 +178,6 @@ class PageTest extends TestCase ]); } - public function test_page_copy() - { - $page = $this->entities->page(); - $page->html = '

This is some test content

'; - $page->save(); - - $currentBook = $page->book; - $newBook = Book::where('id', '!=', $currentBook->id)->first(); - - $resp = $this->asEditor()->get($page->getUrl('/copy')); - $resp->assertSee('Copy Page'); - - $movePageResp = $this->post($page->getUrl('/copy'), [ - 'entity_selection' => 'book:' . $newBook->id, - 'name' => 'My copied test page', - ]); - $pageCopy = Page::where('name', '=', 'My copied test page')->first(); - - $movePageResp->assertRedirect($pageCopy->getUrl()); - $this->assertTrue($pageCopy->book->id == $newBook->id, 'Page was copied to correct book'); - $this->assertStringContainsString('This is some test content', $pageCopy->html); - } - - public function test_page_copy_with_markdown_has_both_html_and_markdown() - { - $page = $this->entities->page(); - $page->html = '

This is some test content

'; - $page->markdown = '# This is some test content'; - $page->save(); - $newBook = Book::where('id', '!=', $page->book->id)->first(); - - $this->asEditor()->post($page->getUrl('/copy'), [ - 'entity_selection' => 'book:' . $newBook->id, - 'name' => 'My copied test page', - ]); - $pageCopy = Page::where('name', '=', 'My copied test page')->first(); - - $this->assertStringContainsString('This is some test content', $pageCopy->html); - $this->assertEquals('# This is some test content', $pageCopy->markdown); - } - - public function test_page_copy_with_no_destination() - { - $page = $this->entities->page(); - $currentBook = $page->book; - - $resp = $this->asEditor()->get($page->getUrl('/copy')); - $resp->assertSee('Copy Page'); - - $movePageResp = $this->post($page->getUrl('/copy'), [ - 'name' => 'My copied test page', - ]); - - $pageCopy = Page::where('name', '=', 'My copied test page')->first(); - - $movePageResp->assertRedirect($pageCopy->getUrl()); - $this->assertTrue($pageCopy->book->id == $currentBook->id, 'Page was copied to correct book'); - $this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance'); - } - - public function test_page_can_be_copied_without_edit_permission() - { - $page = $this->entities->page(); - $currentBook = $page->book; - $newBook = Book::where('id', '!=', $currentBook->id)->first(); - $viewer = $this->users->viewer(); - - $resp = $this->actingAs($viewer)->get($page->getUrl()); - $resp->assertDontSee($page->getUrl('/copy')); - - $newBook->owned_by = $viewer->id; - $newBook->save(); - $this->permissions->grantUserRolePermissions($viewer, ['page-create-own']); - $this->permissions->regenerateForEntity($newBook); - - $resp = $this->actingAs($viewer)->get($page->getUrl()); - $resp->assertSee($page->getUrl('/copy')); - - $movePageResp = $this->post($page->getUrl('/copy'), [ - 'entity_selection' => 'book:' . $newBook->id, - 'name' => 'My copied test page', - ]); - $movePageResp->assertRedirect(); - - $this->assertDatabaseHasEntityData('page', [ - 'name' => 'My copied test page', - 'created_by' => $viewer->id, - 'book_id' => $newBook->id, - ]); - } - - public function test_old_page_slugs_redirect_to_new_pages() - { - $page = $this->entities->page(); - - // Need to save twice since revisions are not generated in seeder. - $this->asAdmin()->put($page->getUrl(), [ - 'name' => 'super test', - 'html' => '

', - ]); - - $page->refresh(); - $pageUrl = $page->getUrl(); - - $this->put($pageUrl, [ - 'name' => 'super test page', - 'html' => '

', - ]); - - $this->get($pageUrl) - ->assertRedirect("/books/{$page->book->slug}/page/super-test-page"); - } - public function test_page_within_chapter_deletion_returns_to_chapter() { $chapter = $this->entities->chapter(); diff --git a/tests/Entity/SlugTest.php b/tests/Entity/SlugTest.php new file mode 100644 index 000000000..51cf34e5d --- /dev/null +++ b/tests/Entity/SlugTest.php @@ -0,0 +1,212 @@ +entities->newBook([ + 'name' => 'информация', + ]); + + $this->assertEquals('informaciia', $book->slug); + + $book = $this->entities->newBook([ + 'name' => '¿Qué?', + ]); + + $this->assertEquals('que', $book->slug); + } + + public function test_slug_format() + { + $book = $this->entities->newBook([ + 'name' => 'PartA / PartB / PartC', + ]); + + $this->assertEquals('parta-partb-partc', $book->slug); + } + + public function test_old_page_slugs_redirect_to_new_pages() + { + $page = $this->entities->page(); + $pageUrl = $page->getUrl(); + + $this->asAdmin()->put($pageUrl, [ + 'name' => 'super test page', + 'html' => '

', + ]); + + $this->get($pageUrl) + ->assertRedirect("/books/{$page->book->slug}/page/super-test-page"); + } + + public function test_old_shelf_slugs_redirect_to_new_shelf() + { + $shelf = $this->entities->shelf(); + $shelfUrl = $shelf->getUrl(); + + $this->asAdmin()->put($shelf->getUrl(), [ + 'name' => 'super test shelf', + ]); + + $this->get($shelfUrl) + ->assertRedirect("/shelves/super-test-shelf"); + } + + public function test_old_book_slugs_redirect_to_new_book() + { + $book = $this->entities->book(); + $bookUrl = $book->getUrl(); + + $this->asAdmin()->put($book->getUrl(), [ + 'name' => 'super test book', + ]); + + $this->get($bookUrl) + ->assertRedirect("/books/super-test-book"); + } + + public function test_old_chapter_slugs_redirect_to_new_chapter() + { + $chapter = $this->entities->chapter(); + $chapterUrl = $chapter->getUrl(); + + $this->asAdmin()->put($chapter->getUrl(), [ + 'name' => 'super test chapter', + ]); + + $this->get($chapterUrl) + ->assertRedirect("/books/{$chapter->book->slug}/chapter/super-test-chapter"); + } + + public function test_old_book_slugs_in_page_urls_redirect_to_current_page_url() + { + $page = $this->entities->page(); + $book = $page->book; + $pageUrl = $page->getUrl(); + + $this->asAdmin()->put($book->getUrl(), [ + 'name' => 'super test book', + ]); + + $this->get($pageUrl) + ->assertRedirect("/books/super-test-book/page/{$page->slug}"); + } + + public function test_old_book_slugs_in_chapter_urls_redirect_to_current_chapter_url() + { + $chapter = $this->entities->chapter(); + $book = $chapter->book; + $chapterUrl = $chapter->getUrl(); + + $this->asAdmin()->put($book->getUrl(), [ + 'name' => 'super test book', + ]); + + $this->get($chapterUrl) + ->assertRedirect("/books/super-test-book/chapter/{$chapter->slug}"); + } + + public function test_slug_lookup_controlled_by_permissions() + { + $editor = $this->users->editor(); + $pageA = $this->entities->page(); + $pageB = $this->entities->page(); + + SlugHistory::factory()->create(['sluggable_id' => $pageA->id, 'sluggable_type' => 'page', 'slug' => 'monkey', 'parent_slug' => 'animals', 'created_at' => now()]); + SlugHistory::factory()->create(['sluggable_id' => $pageB->id, 'sluggable_type' => 'page', 'slug' => 'monkey', 'parent_slug' => 'animals', 'created_at' => now()->subDay()]); + + // Defaults to latest where visible + $this->actingAs($editor)->get("/books/animals/page/monkey")->assertRedirect($pageA->getUrl()); + + $this->permissions->disableEntityInheritedPermissions($pageA); + + // Falls back to other entry where the latest is not visible + $this->actingAs($editor)->get("/books/animals/page/monkey")->assertRedirect($pageB->getUrl()); + + // Original still accessible where permissions allow + $this->asAdmin()->get("/books/animals/page/monkey")->assertRedirect($pageA->getUrl()); + } + + public function test_slugs_recorded_in_history_on_page_update() + { + $page = $this->entities->page(); + $this->asAdmin()->put($page->getUrl(), [ + 'name' => 'new slug', + 'html' => '

', + ]); + + $oldSlug = $page->slug; + $page->refresh(); + $this->assertNotEquals($oldSlug, $page->slug); + + $this->assertDatabaseHas('slug_history', [ + 'sluggable_id' => $page->id, + 'sluggable_type' => 'page', + 'slug' => $oldSlug, + 'parent_slug' => $page->book->slug, + ]); + } + + public function test_slugs_recorded_in_history_on_chapter_update() + { + $chapter = $this->entities->chapter(); + $this->asAdmin()->put($chapter->getUrl(), [ + 'name' => 'new slug', + ]); + + $oldSlug = $chapter->slug; + $chapter->refresh(); + $this->assertNotEquals($oldSlug, $chapter->slug); + + $this->assertDatabaseHas('slug_history', [ + 'sluggable_id' => $chapter->id, + 'sluggable_type' => 'chapter', + 'slug' => $oldSlug, + 'parent_slug' => $chapter->book->slug, + ]); + } + + public function test_slugs_recorded_in_history_on_book_update() + { + $book = $this->entities->book(); + $this->asAdmin()->put($book->getUrl(), [ + 'name' => 'new slug', + ]); + + $oldSlug = $book->slug; + $book->refresh(); + $this->assertNotEquals($oldSlug, $book->slug); + + $this->assertDatabaseHas('slug_history', [ + 'sluggable_id' => $book->id, + 'sluggable_type' => 'book', + 'slug' => $oldSlug, + 'parent_slug' => null, + ]); + } + + public function test_slugs_recorded_in_history_on_shelf_update() + { + $shelf = $this->entities->shelf(); + $this->asAdmin()->put($shelf->getUrl(), [ + 'name' => 'new slug', + ]); + + $oldSlug = $shelf->slug; + $shelf->refresh(); + $this->assertNotEquals($oldSlug, $shelf->slug); + + $this->assertDatabaseHas('slug_history', [ + 'sluggable_id' => $shelf->id, + 'sluggable_type' => 'bookshelf', + 'slug' => $oldSlug, + 'parent_slug' => null, + ]); + } +} diff --git a/tests/Search/EntitySearchTest.php b/tests/Search/EntitySearchTest.php index 8501b65c4..cb1149dd1 100644 --- a/tests/Search/EntitySearchTest.php +++ b/tests/Search/EntitySearchTest.php @@ -30,7 +30,15 @@ class EntitySearchTest extends TestCase public function test_search_shows_pagination() { $search = $this->asEditor()->get('/search?term=a'); - $this->withHtml($search)->assertLinkExists('/search?term=a&page=2', '2'); + $this->withHtml($search)->assertLinkExists(url('/search?term=a&page=2'), '2'); + } + + public function test_pagination_considers_sub_path_url_handling() + { + $this->runWithEnv(['APP_URL' => 'https://example.com/subpath'], function () { + $search = $this->asEditor()->get('https://example.com/search?term=a'); + $this->withHtml($search)->assertLinkExists('https://example.com/subpath/search?term=a&page=2', '2'); + }); } public function test_invalid_page_search() diff --git a/tests/Settings/PageListLimitsTest.php b/tests/Settings/PageListLimitsTest.php new file mode 100644 index 000000000..0d3dea3e1 --- /dev/null +++ b/tests/Settings/PageListLimitsTest.php @@ -0,0 +1,81 @@ +asAdmin()->post('/settings/sorting', [ + 'setting-lists-page-count-shelves' => '3', + 'setting-lists-page-count-books' => '6', + 'setting-lists-page-count-search' => '9', + ]); + $resp->assertRedirect('/settings/sorting'); + + $this->assertEquals(3, setting()->getInteger('lists-page-count-shelves', 18)); + $this->assertEquals(6, setting()->getInteger('lists-page-count-books', 18)); + $this->assertEquals(9, setting()->getInteger('lists-page-count-search', 18)); + + $resp = $this->get('/settings/sorting'); + $html = $this->withHtml($resp); + + $html->assertFieldHasValue('setting-lists-page-count-shelves', '3'); + $html->assertFieldHasValue('setting-lists-page-count-books', '6'); + $html->assertFieldHasValue('setting-lists-page-count-search', '9'); + } + + public function test_invalid_counts_will_use_default_when_fetched_as_an_integer() + { + $this->asAdmin()->post('/settings/sorting', [ + 'setting-lists-page-count-shelves' => 'cat', + ]); + + $this->assertEquals(18, setting()->getInteger('lists-page-count-shelves', 18)); + } + + public function test_shelf_count_is_used_on_shelves_view() + { + $resp = $this->asAdmin()->get('/shelves'); + $defaultCount = min(Bookshelf::query()->count(), 18); + $this->withHtml($resp)->assertElementCount('main [data-entity-type="bookshelf"]', $defaultCount); + + $this->post('/settings/sorting', [ + 'setting-lists-page-count-shelves' => '1', + ]); + + $resp = $this->get('/shelves'); + $this->withHtml($resp)->assertElementCount('main [data-entity-type="bookshelf"]', 1); + } + + public function test_book_count_is_used_on_books_view() + { + $resp = $this->asAdmin()->get('/books'); + $defaultCount = min(Book::query()->count(), 18); + $this->withHtml($resp)->assertElementCount('main [data-entity-type="book"]', $defaultCount); + + $this->post('/settings/sorting', [ + 'setting-lists-page-count-books' => '1', + ]); + + $resp = $this->get('/books'); + $this->withHtml($resp)->assertElementCount('main [data-entity-type="book"]', 1); + } + + public function test_search_count_is_used_on_search_view() + { + $resp = $this->asAdmin()->get('/search'); + $this->withHtml($resp)->assertElementCount('.entity-list [data-entity-id]', 18); + + $this->post('/settings/sorting', [ + 'setting-lists-page-count-search' => '1', + ]); + + $resp = $this->get('/search'); + $this->withHtml($resp)->assertElementCount('.entity-list [data-entity-id]', 1); + } +} diff --git a/tests/Settings/SettingsTest.php b/tests/Settings/SettingsTest.php index 9d45706e7..a3e65b483 100644 --- a/tests/Settings/SettingsTest.php +++ b/tests/Settings/SettingsTest.php @@ -101,4 +101,17 @@ class SettingsTest extends TestCase file_get_contents(public_path('favicon.ico')), ); } + + public function test_both_light_and_dark_colors_are_used_in_the_base_view() + { + // To allow for dynamic color changes on the front-end where desired. + $this->setSettings(['page-color' => 'superlightblue', 'page-color-dark' => 'superdarkblue']); + + $resp = $this->get('/login'); + + $resp->assertSee(':root {'); + $resp->assertSee('superlightblue'); + $resp->assertSee(':root.dark-mode {'); + $resp->assertSee('superdarkblue'); + } } diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 2aad158c9..1088e657e 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -697,15 +697,15 @@ class ImageTest extends TestCase $galleryFileSize = filesize($galleryThumbPath); // Basic scan of GIF content to check frame count - $originalFrameCount = count(explode("\x00\x21\xF9", file_get_contents($originalFile))); - $galleryFrameCount = count(explode("\x00\x21\xF9", file_get_contents($galleryThumbPath))); + $originalFrameCount = count(explode("\x00\x21\xF9", file_get_contents($originalFile))) - 1; + $galleryFrameCount = count(explode("\x00\x21\xF9", file_get_contents($galleryThumbPath))) - 1; $this->files->deleteAtRelativePath($relPath); $this->files->deleteAtRelativePath($galleryThumbRelPath); $this->assertNotEquals($originalFileSize, $galleryFileSize); - $this->assertEquals(3, $originalFrameCount); - $this->assertEquals(1, $galleryFrameCount); + $this->assertEquals(2, $originalFrameCount); + $this->assertLessThan(2, $galleryFrameCount); } protected function getTestProfileImage() diff --git a/tests/User/UserMyAccountTest.php b/tests/User/UserMyAccountTest.php index e1b40dadd..dbb53b061 100644 --- a/tests/User/UserMyAccountTest.php +++ b/tests/User/UserMyAccountTest.php @@ -329,11 +329,19 @@ class UserMyAccountTest extends TestCase $resp = $this->asEditor()->get('/my-account/notifications'); $resp->assertSee('Notify upon comments'); $resp->assertSee('Notify upon replies'); + $resp->assertSee('Notify when I\'m mentioned in a comment'); setting()->put('app-disable-comments', true); $resp = $this->get('/my-account/notifications'); $resp->assertDontSee('Notify upon comments'); $resp->assertDontSee('Notify upon replies'); + $resp->assertDontSee('Notify when I\'m mentioned in a comment'); + } + + public function test_notification_comment_mention_option_enabled_by_default() + { + $resp = $this->asEditor()->get('/my-account/notifications'); + $this->withHtml($resp)->assertElementExists('input[name="preferences[comment-mentions]"][value="true"]'); } } diff --git a/tests/User/UserSearchTest.php b/tests/User/UserSearchTest.php index 76efbf4af..25c7c6ee9 100644 --- a/tests/User/UserSearchTest.php +++ b/tests/User/UserSearchTest.php @@ -2,6 +2,7 @@ namespace Tests\User; +use BookStack\Permissions\Permission; use BookStack\Users\Models\User; use Tests\TestCase; @@ -62,4 +63,70 @@ class UserSearchTest extends TestCase $resp = $this->get('/search/users/select?search=a'); $this->assertPermissionError($resp); } + + public function test_mentions_search_matches_by_name() + { + $viewer = $this->users->viewer(); + $editor = $this->users->editor(); + + $resp = $this->actingAs($editor)->get('/search/users/mention?search=' . urlencode($viewer->name)); + + $resp->assertOk(); + $resp->assertSee($viewer->name); + $resp->assertDontSee($editor->name); + } + + public function test_mentions_search_does_not_match_by_email() + { + $viewer = $this->users->viewer(); + + $resp = $this->asEditor()->get('/search/users/mention?search=' . urlencode($viewer->email)); + + $resp->assertDontSee($viewer->name); + } + + public function test_mentions_search_requires_logged_in_user() + { + $this->setSettings(['app-public' => true]); + $guest = $this->users->guest(); + $this->permissions->grantUserRolePermissions($guest, [Permission::CommentCreateAll, Permission::CommentUpdateAll]); + + $resp = $this->get('/search/users/mention?search=a'); + $this->assertPermissionError($resp); + } + + public function test_mentions_search_requires_comment_create_or_update_permission() + { + $viewer = $this->users->viewer(); + $editor = $this->users->editor(); + + $resp = $this->actingAs($viewer)->get('/search/users/mention?search=' . urlencode($editor->name)); + $this->assertPermissionError($resp); + + $this->permissions->grantUserRolePermissions($viewer, [Permission::CommentCreateAll]); + + $resp = $this->actingAs($editor)->get('/search/users/mention?search=' . urlencode($viewer->name)); + $resp->assertOk(); + $resp->assertSee($viewer->name); + + $this->permissions->removeUserRolePermissions($viewer, [Permission::CommentCreateAll]); + $this->permissions->grantUserRolePermissions($viewer, [Permission::CommentUpdateAll]); + + $resp = $this->actingAs($editor)->get('/search/users/mention?search=' . urlencode($viewer->name)); + $resp->assertOk(); + $resp->assertSee($viewer->name); + } + + public function test_mentions_search_shows_first_by_name_without_search() + { + /** @var User $firstUser */ + $firstUser = User::query() + ->orderBy('name', 'asc') + ->first(); + + $resp = $this->asEditor()->get('/search/users/mention'); + + $resp->assertOk(); + $this->withHtml($resp)->assertElementContains('a[data-id]:first-child', $firstUser->name); + } }