mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-10-12 02:01:37 +00:00
Merge pull request 'Configurable block layouts' (#6179) from sidebar_management into development
Reviewed-on: https://codeberg.org/bookstack/bookstack/pulls/6179
This commit is contained in:
@@ -2,11 +2,8 @@
|
||||
|
||||
namespace BookStack\App;
|
||||
|
||||
use BookStack\Activity\ActivityQueries;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Queries\EntityQueries;
|
||||
use BookStack\Entities\Queries\QueryRecentlyViewed;
|
||||
use BookStack\Entities\Queries\QueryTopFavourites;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Http\Controller;
|
||||
use BookStack\Util\SimpleListOptions;
|
||||
@@ -24,49 +21,19 @@ class HomeController extends Controller
|
||||
*/
|
||||
public function index(
|
||||
Request $request,
|
||||
ActivityQueries $activities,
|
||||
QueryRecentlyViewed $recentlyViewed,
|
||||
QueryTopFavourites $topFavourites,
|
||||
) {
|
||||
$activity = $activities->latest(10);
|
||||
$draftPages = [];
|
||||
|
||||
if ($this->isSignedIn()) {
|
||||
$draftPages = $this->queries->pages->currentUserDraftsForList()
|
||||
->orderBy('updated_at', 'desc')
|
||||
->with('book')
|
||||
->take(6)
|
||||
->get();
|
||||
}
|
||||
|
||||
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
||||
$recents = $this->isSignedIn() ?
|
||||
$recentlyViewed->run(12 * $recentFactor, 1)
|
||||
: $this->queries->books->visibleForList()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
|
||||
$favourites = $topFavourites->run(6);
|
||||
$recentlyUpdatedPages = $this->queries->pages->visibleForList()
|
||||
->where('draft', false)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->take($favourites->count() > 0 ? 5 : 10)
|
||||
->get();
|
||||
|
||||
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
|
||||
$homepageOption = setting('app-homepage-type', 'default');
|
||||
if (!in_array($homepageOption, $homepageOptions)) {
|
||||
$homepageOption = 'default';
|
||||
$homepageType = setting('app-homepage-type');
|
||||
if (!in_array($homepageType, ['default', 'books', 'bookshelves', 'page'])) {
|
||||
$homepageType = 'default';
|
||||
}
|
||||
|
||||
$commonData = [
|
||||
'activity' => $activity,
|
||||
'recents' => $recents,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages,
|
||||
'favourites' => $favourites,
|
||||
'homeView' => $homepageType,
|
||||
];
|
||||
|
||||
// Add required list ordering & sorting for books & shelves views.
|
||||
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
|
||||
$key = $homepageOption;
|
||||
if ($homepageType === 'bookshelves' || $homepageType === 'books') {
|
||||
$key = $homepageType;
|
||||
$view = setting()->getForCurrentUser($key . '_view_type');
|
||||
$listOptions = SimpleListOptions::fromRequest($request, $key)->withSortOptions([
|
||||
'name' => trans('common.sort_name'),
|
||||
@@ -80,7 +47,7 @@ class HomeController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
if ($homepageOption === 'bookshelves') {
|
||||
if ($homepageType === 'bookshelves') {
|
||||
$shelves = $this->queries->shelves->visibleForListWithCover()
|
||||
->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
|
||||
->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000));
|
||||
@@ -89,7 +56,7 @@ class HomeController extends Controller
|
||||
return view('home.shelves', $data);
|
||||
}
|
||||
|
||||
if ($homepageOption === 'books') {
|
||||
if ($homepageType === 'books') {
|
||||
$books = $this->queries->books->visibleForListWithCover()
|
||||
->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
|
||||
->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000));
|
||||
@@ -98,7 +65,7 @@ class HomeController extends Controller
|
||||
return view('home.books', $data);
|
||||
}
|
||||
|
||||
if ($homepageOption === 'page') {
|
||||
if ($homepageType === 'page') {
|
||||
$homepageSetting = setting('app-homepage', '0:');
|
||||
$id = intval(explode(':', $homepageSetting)[0]);
|
||||
/** @var Page $customHomepage */
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
namespace BookStack\App\Providers;
|
||||
|
||||
use BookStack\Entities\BreadcrumbsViewComposer;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\Util\DateFormatter;
|
||||
use BookStack\View\ViewBlockManager;
|
||||
use BookStack\View\ViewBlockPreferences;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\View;
|
||||
@@ -18,6 +22,10 @@ class ViewTweaksServiceProvider extends ServiceProvider
|
||||
$app['config']->get('app.display_timezone'),
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->singleton(ViewBlockManager::class, function ($app) {
|
||||
return new ViewBlockManager(new ViewBlockPreferences());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +40,10 @@ class ViewTweaksServiceProvider extends ServiceProvider
|
||||
View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class);
|
||||
|
||||
// View Globals
|
||||
$viewBlockManager = $this->app->make(ViewBlockManager::class);
|
||||
View::share('dates', $this->app->make(DateFormatter::class));
|
||||
View::share('viewBlocks', $viewBlockManager);
|
||||
Theme::dispatch(ThemeEvents::VIEW_BLOCKS_REGISTER, $viewBlockManager);
|
||||
|
||||
// Custom blade view directives
|
||||
Blade::directive('icon', function ($expression) {
|
||||
|
||||
@@ -32,6 +32,7 @@ return [
|
||||
'page-draft-color-dark' => '#a66ce8',
|
||||
'app-custom-head' => false,
|
||||
'registration-enabled' => false,
|
||||
'app-homepage-type' => 'default',
|
||||
|
||||
// User-level default settings
|
||||
'user' => [
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace BookStack\Entities\Controllers;
|
||||
|
||||
use BookStack\Activity\ActivityQueries;
|
||||
use BookStack\Activity\ActivityType;
|
||||
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;
|
||||
@@ -19,7 +17,6 @@ use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Http\Controller;
|
||||
use BookStack\Permissions\Permission;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use BookStack\Util\DatabaseTransaction;
|
||||
use BookStack\Util\SimpleListOptions;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -34,7 +31,6 @@ class BookController extends Controller
|
||||
protected BookQueries $queries,
|
||||
protected EntityQueries $entityQueries,
|
||||
protected BookshelfQueries $shelfQueries,
|
||||
protected ReferenceFetcher $referenceFetcher,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -53,9 +49,6 @@ class BookController extends Controller
|
||||
$books = $this->queries->visibleForListWithCover()
|
||||
->orderBy($listOptions->getSort(), $listOptions->getOrder())
|
||||
->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();
|
||||
|
||||
$this->shelfContext->clearShelfContext();
|
||||
|
||||
@@ -63,9 +56,6 @@ class BookController extends Controller
|
||||
|
||||
return view('books.index', [
|
||||
'books' => $books,
|
||||
'recents' => $recents,
|
||||
'popular' => $popular,
|
||||
'new' => $new,
|
||||
'view' => $view,
|
||||
'listOptions' => $listOptions,
|
||||
]);
|
||||
@@ -127,7 +117,7 @@ class BookController extends Controller
|
||||
/**
|
||||
* Display the specified book.
|
||||
*/
|
||||
public function show(Request $request, ActivityQueries $activities, string $slug)
|
||||
public function show(Request $request, string $slug)
|
||||
{
|
||||
try {
|
||||
$book = $this->queries->findVisibleBySlugOrFail($slug);
|
||||
@@ -140,7 +130,6 @@ class BookController extends Controller
|
||||
}
|
||||
|
||||
$bookChildren = (new BookContents($book))->getTree(true);
|
||||
$bookParentShelves = $book->shelves()->scopes('visible')->get();
|
||||
|
||||
View::incrementFor($book);
|
||||
if ($request->has('shelf')) {
|
||||
@@ -153,10 +142,6 @@ class BookController extends Controller
|
||||
'book' => $book,
|
||||
'current' => $book,
|
||||
'bookChildren' => $bookChildren,
|
||||
'bookParentShelves' => $bookParentShelves,
|
||||
'watchOptions' => new UserEntityWatchOptions(user(), $book),
|
||||
'activity' => $activities->entityActivity($book, 20, 1),
|
||||
'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($book),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace BookStack\Entities\Controllers;
|
||||
|
||||
use BookStack\Activity\ActivityQueries;
|
||||
use BookStack\Activity\Models\View;
|
||||
use BookStack\Entities\Queries\BookQueries;
|
||||
use BookStack\Entities\Queries\BookshelfQueries;
|
||||
@@ -13,7 +12,6 @@ use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Http\Controller;
|
||||
use BookStack\Permissions\Permission;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use BookStack\Util\SimpleListOptions;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -27,7 +25,6 @@ class BookshelfController extends Controller
|
||||
protected EntityQueries $entityQueries,
|
||||
protected BookQueries $bookQueries,
|
||||
protected ShelfContext $shelfContext,
|
||||
protected ReferenceFetcher $referenceFetcher,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -46,21 +43,12 @@ class BookshelfController extends Controller
|
||||
$shelves = $this->queries->visibleForListWithCover()
|
||||
->orderBy($listOptions->getSort(), $listOptions->getOrder())
|
||||
->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()
|
||||
->orderBy('created_at', 'desc')
|
||||
->take(4)
|
||||
->get();
|
||||
|
||||
$this->shelfContext->clearShelfContext();
|
||||
$this->setPageTitle(trans('entities.shelves'));
|
||||
|
||||
return view('shelves.index', [
|
||||
'shelves' => $shelves,
|
||||
'recents' => $recents,
|
||||
'popular' => $popular,
|
||||
'new' => $new,
|
||||
'view' => $view,
|
||||
'listOptions' => $listOptions,
|
||||
]);
|
||||
@@ -105,7 +93,7 @@ class BookshelfController extends Controller
|
||||
*
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function show(Request $request, ActivityQueries $activities, string $slug)
|
||||
public function show(Request $request, string $slug)
|
||||
{
|
||||
try {
|
||||
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
|
||||
@@ -144,9 +132,7 @@ class BookshelfController extends Controller
|
||||
'shelf' => $shelf,
|
||||
'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
|
||||
'view' => $view,
|
||||
'activity' => $activities->entityActivity($shelf, 20, 1),
|
||||
'listOptions' => $listOptions,
|
||||
'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($shelf),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace BookStack\Entities\Controllers;
|
||||
|
||||
use BookStack\Activity\Models\View;
|
||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Queries\ChapterQueries;
|
||||
use BookStack\Entities\Queries\EntityQueries;
|
||||
@@ -18,7 +17,6 @@ use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Http\Controller;
|
||||
use BookStack\Permissions\Permission;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use BookStack\Util\DatabaseTransaction;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -30,7 +28,6 @@ class ChapterController extends Controller
|
||||
protected ChapterRepo $chapterRepo,
|
||||
protected ChapterQueries $queries,
|
||||
protected EntityQueries $entityQueries,
|
||||
protected ReferenceFetcher $referenceFetcher,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -87,10 +84,10 @@ class ChapterController extends Controller
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
|
||||
$sidebarTree = (new BookContents($chapter->book))->getTree();
|
||||
$pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();
|
||||
|
||||
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
|
||||
$bookTree = (new BookContents($chapter->book))->getTree();
|
||||
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $bookTree);
|
||||
View::incrementFor($chapter);
|
||||
|
||||
$this->setPageTitle($chapter->getShortName());
|
||||
@@ -99,12 +96,10 @@ class ChapterController extends Controller
|
||||
'book' => $chapter->book,
|
||||
'chapter' => $chapter,
|
||||
'current' => $chapter,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'watchOptions' => new UserEntityWatchOptions(user(), $chapter),
|
||||
'pages' => $pages,
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($chapter),
|
||||
'bookTree' => $bookTree,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace BookStack\Entities\Controllers;
|
||||
|
||||
use BookStack\Activity\Models\View;
|
||||
use BookStack\Activity\Tools\CommentTree;
|
||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
@@ -21,7 +20,6 @@ use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Http\Controller;
|
||||
use BookStack\Permissions\Permission;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use BookStack\Util\HtmlContentFilter;
|
||||
use BookStack\Util\HtmlContentFilterConfig;
|
||||
use Exception;
|
||||
@@ -36,7 +34,6 @@ class PageController extends Controller
|
||||
protected PageRepo $pageRepo,
|
||||
protected PageQueries $queries,
|
||||
protected EntityQueries $entityQueries,
|
||||
protected ReferenceFetcher $referenceFetcher
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -154,11 +151,10 @@ class PageController extends Controller
|
||||
|
||||
$pageContent = (new PageContent($page));
|
||||
$page->html = $pageContent->render();
|
||||
$pageNav = $pageContent->getNavigation($page->html);
|
||||
|
||||
$sidebarTree = (new BookContents($page->book))->getTree();
|
||||
$bookTree = (new BookContents($page->book))->getTree();
|
||||
$commentTree = (new CommentTree($page));
|
||||
$nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
|
||||
$nextPreviousLocator = new NextPreviousContentLocator($page, $bookTree);
|
||||
|
||||
View::incrementFor($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
@@ -167,13 +163,10 @@ class PageController extends Controller
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'current' => $page,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'bookTree' => $bookTree,
|
||||
'commentTree' => $commentTree,
|
||||
'pageNav' => $pageNav,
|
||||
'watchOptions' => new UserEntityWatchOptions(user(), $page),
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($page),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ class Kernel extends HttpKernel
|
||||
\BookStack\Http\Middleware\CheckEmailConfirmed::class,
|
||||
\BookStack\Http\Middleware\RunThemeActions::class,
|
||||
\BookStack\Http\Middleware\Localization::class,
|
||||
\BookStack\Http\Middleware\ClearPerRequestCaches::class,
|
||||
],
|
||||
'api' => [
|
||||
\BookStack\Http\Middleware\ThrottleApiRequests::class,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use BookStack\View\ViewBlockManager;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Custom middleware to clear any local caches in the app which are created on a
|
||||
* per-request basis. While this can be somewhat redundant in the normal PHP request
|
||||
* lifecycle (since in memory caches are cleared on each request), this can be useful
|
||||
* in testing and to prepare for future long-serving PHP runtimes.
|
||||
*/
|
||||
class ClearPerRequestCaches
|
||||
{
|
||||
public function __construct(
|
||||
protected ViewBlockManager $viewBlockManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$this->clearCaches();
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
protected function clearCaches(): void
|
||||
{
|
||||
$this->viewBlockManager->clearLocalCache();
|
||||
}
|
||||
}
|
||||
@@ -264,6 +264,14 @@ class SettingService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user-specific setting from the database, for the current access user.
|
||||
*/
|
||||
public function removeForCurrentUser(string $key): void
|
||||
{
|
||||
$this->remove($this->userKey(user()->id, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete settings for a given user id.
|
||||
*/
|
||||
|
||||
@@ -180,6 +180,15 @@ class ThemeEvents
|
||||
*/
|
||||
const THEME_REGISTER_VIEWS = 'theme_register_views';
|
||||
|
||||
/**
|
||||
* View blocks register event.
|
||||
* Runs once a ViewBlockManager instance is available so that custom blocks can be registered
|
||||
* for use within user-configurable layouts in the system.
|
||||
*
|
||||
* @param \BookStack\View\ViewBlockManager $manager
|
||||
*/
|
||||
const VIEW_BLOCKS_REGISTER = 'view_blocks_register';
|
||||
|
||||
/**
|
||||
* Web before middleware action.
|
||||
* Runs before the request is handled but after all other middleware apart from those
|
||||
|
||||
@@ -10,6 +10,7 @@ use BookStack\Settings\UserNotificationPreferences;
|
||||
use BookStack\Settings\UserShortcutMap;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use BookStack\Users\UserRepo;
|
||||
use BookStack\View\ViewBlockManager;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
@@ -159,6 +160,37 @@ class UserAccountController extends Controller
|
||||
return redirect('/my-account/notifications');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view for the "Interface Preferences" user account area.
|
||||
*/
|
||||
public function showInterface(ViewBlockManager $viewBlockManager)
|
||||
{
|
||||
$this->setPageTitle(trans('preferences.interface'));
|
||||
|
||||
return view('users.account.interface', [
|
||||
'category' => 'interface',
|
||||
'namedLocations' => $viewBlockManager->getNamedLocations(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the submission of the interface preferences form.
|
||||
*/
|
||||
public function updateInterface(Request $request)
|
||||
{
|
||||
$this->preventAccessInDemoMode();
|
||||
|
||||
$user = user();
|
||||
$validated = $this->validate($request, [
|
||||
'language' => ['string', 'max:15', 'alpha_dash'],
|
||||
'display_mode' => ['string', 'max:15', 'alpha_dash'],
|
||||
]);
|
||||
|
||||
$this->userRepo->update($user, $validated, userCan(Permission::UsersManage));
|
||||
|
||||
return redirect('/my-account/interface');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view for the "Access & Security" account options.
|
||||
*/
|
||||
|
||||
@@ -122,7 +122,7 @@ class UserRepo
|
||||
/**
|
||||
* Update the given user with the given data, but do not create an activity.
|
||||
*
|
||||
* @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array<int>, language: ?string} $data
|
||||
* @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array<int>, language: ?string, display_mode: ?string} $data
|
||||
*
|
||||
* @throws UserUpdateException
|
||||
*/
|
||||
@@ -153,6 +153,11 @@ class UserRepo
|
||||
setting()->putUser($user, 'language', $data['language']);
|
||||
}
|
||||
|
||||
if (!empty($data['display_mode'])) {
|
||||
$value = $data['display_mode'] === 'dark' ? 'true' : 'false';
|
||||
setting()->putUser($user, 'dark-mode-enabled', $value);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View;
|
||||
|
||||
use BookStack\Http\Controller;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LayoutController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected ViewBlockManager $viewBlocks,
|
||||
protected ViewBlockPreferences $viewBlockPreferences,
|
||||
) {
|
||||
$this->middleware(function (Request $request, Closure $next) {
|
||||
$this->preventGuestAccess();
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing the layout for a specific location.
|
||||
*/
|
||||
public function edit(string $location)
|
||||
{
|
||||
$namedLocations = $this->viewBlocks->getNamedLocations();
|
||||
$locationName = $namedLocations[$location] ?? $location;
|
||||
$blocks = $this->viewBlocks->getForLocationForCurrentUser($location);
|
||||
|
||||
$this->setPageTitle(trans('preferences.layout_edit'));
|
||||
|
||||
return view('settings.layouts.edit', [
|
||||
'location' => $location,
|
||||
'locationName' => $locationName,
|
||||
'namedLocations' => $namedLocations,
|
||||
'blocks' => $blocks,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the layout for a specific location for the current user.
|
||||
*/
|
||||
public function update(string $location, Request $request)
|
||||
{
|
||||
$data = $this->validate($request, [
|
||||
'layout' => ['required', 'string', 'json'],
|
||||
]);
|
||||
|
||||
$layoutData = json_decode($data['layout'], true, 5);
|
||||
if (is_array($layoutData)) {
|
||||
$this->viewBlocks->updatePreferencesFromIdPositionMap($location, $layoutData);
|
||||
}
|
||||
|
||||
$this->showSuccessNotification(trans('preferences.layout_update_success'));
|
||||
|
||||
return redirect("/layouts/{$location}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the layout for a specific location, for the current user,
|
||||
* back to system defaults by removing any user-specific preferences.
|
||||
*/
|
||||
public function reset(string $location)
|
||||
{
|
||||
$this->viewBlockPreferences->clearForLocation($location);
|
||||
|
||||
$this->showSuccessNotification(trans('preferences.layout_reset_success'));
|
||||
|
||||
return redirect("/layouts/{$location}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View;
|
||||
|
||||
abstract class ViewBlock implements ViewBlockInterface
|
||||
{
|
||||
protected static string $id;
|
||||
protected static string $view;
|
||||
protected static string $labelTranslationKey;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getId(): string
|
||||
{
|
||||
return static::$id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getLabel(): string
|
||||
{
|
||||
return trans(static::$labelTranslationKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getView(array $viewData): string
|
||||
{
|
||||
return static::$view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View;
|
||||
|
||||
class ViewBlockDefaults
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<string, class-string<ViewBlockInterface>[]>>
|
||||
*/
|
||||
protected static array $defaults = [
|
||||
'home-default' => [
|
||||
'left' => [
|
||||
ViewBlocks\HomeRecentDrafts::class,
|
||||
ViewBlocks\HomeRecentlyViewedOrRecentBooks::class,
|
||||
],
|
||||
'center' => [
|
||||
ViewBlocks\HomeTopFavourites::class,
|
||||
ViewBlocks\HomeRecentlyUpdatedPages::class,
|
||||
],
|
||||
'right' => [
|
||||
ViewBlocks\HomeRecentActivity::class,
|
||||
],
|
||||
],
|
||||
'home-non-default' => [
|
||||
'left' => [
|
||||
ViewBlocks\HomeRecentDrafts::class,
|
||||
ViewBlocks\HomeTopFavourites::class,
|
||||
ViewBlocks\HomeRecentlyViewedOrRecentBooks::class,
|
||||
ViewBlocks\HomeRecentlyUpdatedPages::class,
|
||||
ViewBlocks\HomeRecentActivity::class,
|
||||
],
|
||||
'right' => [
|
||||
ViewBlocks\HomeActions::class,
|
||||
],
|
||||
],
|
||||
'shelves-index' => [
|
||||
'left' => [
|
||||
ViewBlocks\ShelvesIndexRecents::class,
|
||||
ViewBlocks\ShelvesIndexPopular::class,
|
||||
ViewBlocks\ShelvesIndexNew::class,
|
||||
],
|
||||
'right' => [
|
||||
ViewBlocks\ShelvesIndexActions::class,
|
||||
],
|
||||
],
|
||||
'shelves-show' => [
|
||||
'left' => [
|
||||
ViewBlocks\ShelvesShowTags::class,
|
||||
ViewBlocks\ShelvesShowDetails::class,
|
||||
ViewBlocks\ShelvesShowActivity::class,
|
||||
],
|
||||
'right' => [
|
||||
ViewBlocks\ShelvesShowActions::class,
|
||||
],
|
||||
],
|
||||
'books-index' => [
|
||||
'left' => [
|
||||
ViewBlocks\BooksIndexRecents::class,
|
||||
ViewBlocks\BooksIndexPopular::class,
|
||||
ViewBlocks\BooksIndexNew::class,
|
||||
],
|
||||
'right' => [
|
||||
ViewBlocks\BooksIndexActions::class,
|
||||
],
|
||||
],
|
||||
'books-show' => [
|
||||
'left' => [
|
||||
ViewBlocks\BooksShowSearchForm::class,
|
||||
ViewBlocks\BooksShowTags::class,
|
||||
ViewBlocks\BooksShowShelves::class,
|
||||
ViewBlocks\BooksShowActivity::class,
|
||||
],
|
||||
'right' => [
|
||||
ViewBlocks\BooksShowDetails::class,
|
||||
ViewBlocks\BooksShowActions::class,
|
||||
],
|
||||
],
|
||||
'chapters-show' => [
|
||||
'left' => [
|
||||
ViewBlocks\ChaptersShowSearchForm::class,
|
||||
ViewBlocks\ChaptersShowTags::class,
|
||||
ViewBlocks\ChaptersShowBookTree::class,
|
||||
],
|
||||
'right' => [
|
||||
ViewBlocks\ChaptersShowDetails::class,
|
||||
ViewBlocks\ChaptersShowActions::class,
|
||||
],
|
||||
],
|
||||
'pages-show' => [
|
||||
'left' => [
|
||||
ViewBlocks\PagesShowTags::class,
|
||||
ViewBlocks\PagesShowAttachments::class,
|
||||
ViewBlocks\PagesShowPageNav::class,
|
||||
ViewBlocks\PagesShowBookTree::class,
|
||||
],
|
||||
'right' => [
|
||||
ViewBlocks\PagesShowDetails::class,
|
||||
ViewBlocks\PagesShowActions::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the default view blocks for the given location.
|
||||
*/
|
||||
public static function getForLocation(string $location): array
|
||||
{
|
||||
return self::$defaults[$location] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locations for all default blocks.
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getLocations(): array
|
||||
{
|
||||
return array_keys(self::$defaults);
|
||||
}
|
||||
|
||||
public static function getLocationLabels(): array
|
||||
{
|
||||
return [
|
||||
'home-default' => trans('common.homepage'),
|
||||
'home-non-default' => trans('common.homepage'),
|
||||
'shelves-index' => trans('entities.shelves'),
|
||||
'shelves-show' => trans('entities.shelf'),
|
||||
'books-index' => trans('entities.books'),
|
||||
'books-show' => trans('entities.book'),
|
||||
'chapters-show' => trans('entities.chapter'),
|
||||
'pages-show' => trans('entities.page'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View;
|
||||
|
||||
interface ViewBlockInterface
|
||||
{
|
||||
/**
|
||||
* Get the unique ID for this block.
|
||||
*/
|
||||
public static function getId(): string;
|
||||
|
||||
/**
|
||||
* Get the label for this block.
|
||||
*/
|
||||
public static function getLabel(): string;
|
||||
|
||||
/**
|
||||
* Get the view to render for this block.
|
||||
* Is provided with the existing parent view data.
|
||||
*/
|
||||
public function getView(array $viewData): string;
|
||||
|
||||
/**
|
||||
* Specify the data to pass to the view on render.
|
||||
* Is provided with the existing parent view data and the original request.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function withData(array $viewData): array;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
|
||||
class ViewBlockManager
|
||||
{
|
||||
public function __construct(
|
||||
protected ViewBlockPreferences $preferences,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, class-string<ViewBlockInterface>[]>>
|
||||
*/
|
||||
protected array $blocksByLocationAndPosition = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, class-string<ViewBlockInterface>[]>>
|
||||
*/
|
||||
protected array $locationBlockCache = [];
|
||||
|
||||
/**
|
||||
* Register a block type to be displayed at the given location and position.
|
||||
* @param class-string<ViewBlockInterface> $blockClass
|
||||
*/
|
||||
public function register(string $location, string $defaultPosition, string $blockClass): void
|
||||
{
|
||||
if (!isset($this->blocksByLocationAndPosition[$location])) {
|
||||
$this->blocksByLocationAndPosition[$location] = [];
|
||||
}
|
||||
|
||||
if (!isset($this->blocksByLocationAndPosition[$location][$defaultPosition])) {
|
||||
$this->blocksByLocationAndPosition[$location][$defaultPosition] = [];
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
if (!is_a($blockClass, ViewBlockInterface::class, true)) {
|
||||
throw new \InvalidArgumentException('When registering a view block, the block class must implement ViewBlockInterface');
|
||||
}
|
||||
|
||||
$this->blocksByLocationAndPosition[$location][$defaultPosition][] = $blockClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all blocks registered for a given location and position, considering the
|
||||
* preferences for the current user.
|
||||
* @return ViewBlockInterface[]
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function getInstancesForLocationAndPositionForCurrentUser(string $location, string $position): array
|
||||
{
|
||||
$key = $location;
|
||||
if (isset($this->locationBlockCache[$key])) {
|
||||
$blocks = $this->locationBlockCache[$key][$position] ?? [];
|
||||
return $this->blocksToInstances($blocks);
|
||||
}
|
||||
|
||||
$forLocation = $this->getForLocationForCurrentUser($location);
|
||||
$this->locationBlockCache[$key] = $forLocation;
|
||||
|
||||
$blocks = $forLocation[$position] ?? [];
|
||||
return $this->blocksToInstances($blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create instances of the given block classes.
|
||||
* @param class-string<ViewBlockInterface>[] $blocks
|
||||
* @return ViewBlockInterface[]
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
protected function blocksToInstances(array $blocks): array
|
||||
{
|
||||
return array_map(fn (string $blockClass) => app()->make($blockClass), $blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all blocks registered for a given location, as sets of arrays
|
||||
* keyed by position.
|
||||
* @return array<string, class-string<ViewBlockInterface>[]>
|
||||
*/
|
||||
protected function getForLocation(string $location): array
|
||||
{
|
||||
$defaults = ViewBlockDefaults::getForLocation($location);
|
||||
$registered = $this->blocksByLocationAndPosition[$location] ?? [];
|
||||
return array_merge_recursive($defaults, $registered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all blocks registered for a given location, as sets of arrays
|
||||
* keyed by position, for the current user.
|
||||
* Same as above but with user-specific preferences applied.
|
||||
* @return array<string, list<class-string<ViewBlockInterface>>>
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function getForLocationForCurrentUser(string $location): array
|
||||
{
|
||||
$forLocation = $this->getForLocation($location);
|
||||
$userBlocksByPosition = $this->preferences->getIdByPositionMap($location);
|
||||
if (empty($userBlocksByPosition)) {
|
||||
return $forLocation;
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$blocksById = $this->blocksByPositionToIdMap($forLocation);
|
||||
$idPositionMap = $this->blocksByPositionToIdPositionMap($forLocation);
|
||||
$locations = array_keys($forLocation);
|
||||
$locations[] = 'unused';
|
||||
|
||||
// Add based on user preferences
|
||||
foreach ($locations as $position) {
|
||||
$userBlockIds = $userBlocksByPosition[$position] ?? [];
|
||||
$results[$position] = [];
|
||||
foreach ($userBlockIds as $blockId) {
|
||||
$block = $blocksById[$blockId] ?? null;
|
||||
if ($block) {
|
||||
$results[$position][] = $block;
|
||||
unset($blocksById[$blockId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining blocks based on their default locations
|
||||
foreach ($blocksById as $block) {
|
||||
$position = $idPositionMap[$block::getId()] ?? 'unused';
|
||||
$results[$position][] = $block;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of all locations where blocks are registered.
|
||||
* Returns an array where the keys are location strings, and the
|
||||
* values are translated labels for that location.
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getNamedLocations(): array
|
||||
{
|
||||
$labels = ViewBlockDefaults::getLocationLabels();
|
||||
$defaults = ViewBlockDefaults::getLocations();
|
||||
$registered = array_keys($this->blocksByLocationAndPosition);
|
||||
$merged = array_unique(array_merge($defaults, $registered));
|
||||
|
||||
$results = [];
|
||||
foreach ($merged as $location) {
|
||||
$results[$location] = $labels[$location] ?? $location;
|
||||
}
|
||||
|
||||
$usingDefaultHome = setting('app-homepage-type') === 'default';
|
||||
$toIgnore = $usingDefaultHome ? 'home-non-default' : 'home-default';
|
||||
unset($results[$toIgnore]);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update user preferences for a given location to match the given layout data map.
|
||||
* @param array<string, string[]> $layoutData
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function updatePreferencesFromIdPositionMap(string $location, array $layoutData): void
|
||||
{
|
||||
$this->preferences->storeByIdPositionMap(
|
||||
$location,
|
||||
$layoutData,
|
||||
$this->getForLocation($location),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the local user-specific cache of blocks.
|
||||
* The cache only needs to exist for the current request time since its purpose is to
|
||||
* avoid duplicate loading across views.
|
||||
*/
|
||||
public function clearLocalCache(): void
|
||||
{
|
||||
$this->locationBlockCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a blocksByPosition array into a map of block IDs to blocks.
|
||||
* @param array<string, class-string<ViewBlockInterface>[]> $blocksByPosition
|
||||
* @return array<string, class-string<ViewBlockInterface>>
|
||||
*/
|
||||
protected function blocksByPositionToIdMap(array $blocksByPosition): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($blocksByPosition as $position => $blocks) {
|
||||
foreach ($blocks as $block) {
|
||||
$map[$block::getId()] = $block;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a blocksByPosition array into a map of block IDs to their positions.
|
||||
* @param array<string, class-string<ViewBlockInterface>[]> $blocksByPosition
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function blocksByPositionToIdPositionMap(array $blocksByPosition): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($blocksByPosition as $position => $blocks) {
|
||||
foreach ($blocks as $block) {
|
||||
$map[$block::getId()] = $position;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View;
|
||||
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
|
||||
class ViewBlockPreferences
|
||||
{
|
||||
/**
|
||||
* From the request data of the layout editor, validate the request data to ensure the blocks
|
||||
* and locations are valid and then store the layout data.
|
||||
* The request data is expected to be in the format of:
|
||||
* [
|
||||
* 'position-1' => ['block-id-1', 'block-id-2'],
|
||||
* 'position-2' => ['block-id-3'],
|
||||
* ]
|
||||
* @param array<string, string[]> $layoutData
|
||||
* @param array<string, class-string<ViewBlockInterface>[]> $validBlocksByPosition
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function storeByIdPositionMap(
|
||||
string $location,
|
||||
array $layoutData,
|
||||
array $validBlocksByPosition,
|
||||
): void {
|
||||
$validIds = $this->extractValidBlockIds($validBlocksByPosition);
|
||||
$validPositions = array_keys($validBlocksByPosition);
|
||||
$validPositions[] = 'unused';
|
||||
|
||||
// Ignore updates for invalid/unknown locations
|
||||
if (empty($validBlocksByPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var array<string, string[]> $validatedLayoutData */
|
||||
$validatedLayoutData = [];
|
||||
|
||||
foreach ($layoutData as $position => $blockIds) {
|
||||
// @phpstan-ignore-next-line
|
||||
if (!in_array($position, $validPositions) || !is_array($blockIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$validatedLayoutData[$position] = array_filter($blockIds, function ($id) use ($validIds) {
|
||||
return is_string($id) && in_array($id, $validIds);
|
||||
});
|
||||
}
|
||||
|
||||
$settingKey = $this->getSettingKey($location);
|
||||
setting()->putForCurrentUser($settingKey, json_encode($validatedLayoutData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the view block preferences for a given location for the current user.
|
||||
*/
|
||||
public function clearForLocation(string $location): void
|
||||
{
|
||||
$settingKey = $this->getSettingKey($location);
|
||||
setting()->removeForCurrentUser($settingKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the layout data for a given location.
|
||||
* Provides arrays of block ids keyed by position.
|
||||
* @return array<string, string[]>
|
||||
*/
|
||||
public function getIdByPositionMap(string $location): array
|
||||
{
|
||||
$settingKey = $this->getSettingKey($location);
|
||||
$layoutData = setting()->getForCurrentUser($settingKey, '{}');
|
||||
return json_decode($layoutData, true) ?? [];
|
||||
}
|
||||
|
||||
protected function getSettingKey(string $location): string
|
||||
{
|
||||
return 'view-layout#' . $location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, class-string<ViewBlockInterface>[]> $blocksByPosition
|
||||
* @return string[]
|
||||
*/
|
||||
protected function extractValidBlockIds(array $blocksByPosition): array
|
||||
{
|
||||
$ids = [];
|
||||
|
||||
foreach ($blocksByPosition as $blocks) {
|
||||
foreach ($blocks as $block) {
|
||||
$ids[] = $block::getId();
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($ids);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksIndexActions extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-index-actions';
|
||||
protected static string $view = 'books.parts.index-sidebar-section-actions';
|
||||
protected static string $labelTranslationKey = 'common.actions';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [
|
||||
'view' => $viewData['view'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\BookQueries;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksIndexNew extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-index-new';
|
||||
protected static string $view = 'books.parts.index-sidebar-section-new';
|
||||
protected static string $labelTranslationKey = 'entities.books_new';
|
||||
|
||||
public function __construct(
|
||||
protected BookQueries $queries,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$new = $this->queries->visibleForList()
|
||||
->orderBy('created_at', 'desc')
|
||||
->take(4)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'new' => $new,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\BookQueries;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksIndexPopular extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-index-popular';
|
||||
protected static string $view = 'books.parts.index-sidebar-section-popular';
|
||||
protected static string $labelTranslationKey = 'entities.books_popular';
|
||||
|
||||
public function __construct(
|
||||
protected BookQueries $queries,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [
|
||||
'popular' => $this->queries->popularForList()->take(4)->get(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\BookQueries;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksIndexRecents extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-index-recents';
|
||||
protected static string $view = 'books.parts.index-sidebar-section-recents';
|
||||
protected static string $labelTranslationKey = 'entities.recently_viewed';
|
||||
|
||||
public function __construct(
|
||||
protected BookQueries $queries,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$recents = null;
|
||||
if (!user()->isGuest()) {
|
||||
$recents = $this->queries->recentlyViewedForCurrentUser()->take(4)->get();
|
||||
}
|
||||
|
||||
return [
|
||||
'recents' => $recents,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksShowActions extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-show-actions';
|
||||
protected static string $view = 'books.parts.show-sidebar-section-actions';
|
||||
protected static string $labelTranslationKey = 'common.actions';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = $viewData['book'];
|
||||
|
||||
return [
|
||||
'book' => $book,
|
||||
'watchOptions' => new UserEntityWatchOptions(user(), $book),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Activity\ActivityQueries;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksShowActivity extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-show-activity';
|
||||
protected static string $view = 'books.parts.show-sidebar-section-activity';
|
||||
protected static string $labelTranslationKey = 'entities.recent_activity';
|
||||
|
||||
public function __construct(
|
||||
protected ActivityQueries $activityQueries,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = $viewData['book'];
|
||||
|
||||
return [
|
||||
'activity' => $this->activityQueries->entityActivity($book, 20, 1),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksShowDetails extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-show-details';
|
||||
protected static string $view = 'books.parts.show-sidebar-section-details';
|
||||
protected static string $labelTranslationKey = 'common.details';
|
||||
|
||||
public function __construct(
|
||||
protected ReferenceFetcher $referenceFetcher,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = $viewData['book'];
|
||||
$referenceCount = $this->referenceFetcher->getReferenceCountToEntity($book);
|
||||
|
||||
return [
|
||||
'book' => $book,
|
||||
'watchOptions' => new UserEntityWatchOptions(user(), $book),
|
||||
'referenceCount' => $referenceCount,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksShowSearchForm extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-show-search-form';
|
||||
protected static string $view = 'entities.search-form';
|
||||
protected static string $labelTranslationKey = 'common.search';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [
|
||||
'label' => trans('entities.books_search_this'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksShowShelves extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-show-shelves';
|
||||
protected static string $view = 'books.parts.show-sidebar-section-shelves';
|
||||
protected static string $labelTranslationKey = 'entities.shelves';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = $viewData['book'];
|
||||
$shelves = $book->shelves()->scopes('visible')->get();
|
||||
|
||||
return [
|
||||
'shelves' => $shelves,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksShowTags extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_books-show-tags';
|
||||
protected static string $view = 'books.parts.show-sidebar-section-tags';
|
||||
protected static string $labelTranslationKey = 'entities.tags';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [
|
||||
'book' => $viewData['book'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ChaptersShowActions extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_chapters-show-actions';
|
||||
protected static string $view = 'chapters.parts.show-sidebar-section-actions';
|
||||
protected static string $labelTranslationKey = 'common.actions';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Chapter $chapter */
|
||||
$chapter = $viewData['chapter'];
|
||||
|
||||
return [
|
||||
'chapter' => $chapter,
|
||||
'watchOptions' => new UserEntityWatchOptions(user(), $chapter),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ChaptersShowBookTree extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_chapters-show-book-tree';
|
||||
protected static string $view = 'entities.book-tree';
|
||||
protected static string $labelTranslationKey = 'entities.books_navigation';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = $viewData['book'];
|
||||
|
||||
return [
|
||||
'book' => $book,
|
||||
'bookTree' => $viewData['bookTree'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ChaptersShowDetails extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_chapters-show-details';
|
||||
protected static string $view = 'chapters.parts.show-sidebar-section-details';
|
||||
protected static string $labelTranslationKey = 'common.details';
|
||||
|
||||
public function __construct(
|
||||
protected ReferenceFetcher $referenceFetcher,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = $viewData['book'];
|
||||
/** @var Chapter $chapter */
|
||||
$chapter = $viewData['chapter'];
|
||||
|
||||
$referenceCount = $this->referenceFetcher->getReferenceCountToEntity($chapter);
|
||||
|
||||
return [
|
||||
'chapter' => $chapter,
|
||||
'book' => $book,
|
||||
'watchOptions' => new UserEntityWatchOptions(user(), $chapter),
|
||||
'referenceCount' => $referenceCount,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ChaptersShowSearchForm extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_chapters-show-search-form';
|
||||
protected static string $view = 'entities.search-form';
|
||||
protected static string $labelTranslationKey = 'common.search';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [
|
||||
'label' => trans('entities.chapters_search_this'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ChaptersShowTags extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_chapters-show-tags';
|
||||
protected static string $view = 'chapters.parts.show-sidebar-section-tags';
|
||||
protected static string $labelTranslationKey = 'entities.tags';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Chapter $chapter */
|
||||
$chapter = $viewData['chapter'];
|
||||
|
||||
return [
|
||||
'chapter' => $chapter,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\View\ViewBlock;
|
||||
|
||||
class HomeActions extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_home-actions';
|
||||
protected static string $view = 'home.parts.configured-section-actions';
|
||||
protected static string $labelTranslationKey = 'common.actions';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [
|
||||
'view' => $viewData['view'] ?? '',
|
||||
'homeView' => $viewData['homeView'] ?? 'default',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Activity\ActivityQueries;
|
||||
use BookStack\View\ViewBlockInterface;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HomeRecentActivity implements ViewBlockInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected ActivityQueries $activityQueries
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getId(): string
|
||||
{
|
||||
return 'builtin_home-recent-activity';
|
||||
}
|
||||
|
||||
public static function getLabel(): string
|
||||
{
|
||||
return trans('entities.recent_activity');
|
||||
}
|
||||
|
||||
public function getView(array $viewData): string
|
||||
{
|
||||
if ($viewData['homeView'] === 'default') {
|
||||
return 'home.parts.default-card-recent-activity';
|
||||
}
|
||||
|
||||
return 'home.parts.configured-section-recent-activity';
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$activity = $this->activityQueries->latest(10);
|
||||
return [
|
||||
'activity' => $activity,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\PageQueries;
|
||||
use BookStack\View\ViewBlockInterface;
|
||||
|
||||
class HomeRecentDrafts implements ViewBlockInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected PageQueries $pageQueries
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getId(): string
|
||||
{
|
||||
return 'builtin_home-recent-drafts';
|
||||
}
|
||||
|
||||
public static function getLabel(): string
|
||||
{
|
||||
return trans('entities.my_recent_drafts');
|
||||
}
|
||||
|
||||
public function getView(array $viewData): string
|
||||
{
|
||||
if ($viewData['homeView'] === 'default') {
|
||||
return 'home.parts.default-card-recent-drafts';
|
||||
}
|
||||
|
||||
return 'home.parts.configured-section-recent-drafts';
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$draftPages = [];
|
||||
|
||||
if (!user()->isGuest()) {
|
||||
$draftPages = $this->pageQueries->currentUserDraftsForList()
|
||||
->orderBy('updated_at', 'desc')
|
||||
->with('book')
|
||||
->take(6)
|
||||
->get();
|
||||
}
|
||||
|
||||
return [
|
||||
'draftPages' => $draftPages,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\PageQueries;
|
||||
use BookStack\View\ViewBlockInterface;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HomeRecentlyUpdatedPages implements ViewBlockInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected PageQueries $queries
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getId(): string
|
||||
{
|
||||
return 'builtin_home-recently-updated-pages';
|
||||
}
|
||||
|
||||
public static function getLabel(): string
|
||||
{
|
||||
return trans('entities.recently_updated_pages');
|
||||
}
|
||||
|
||||
public function getView(array $viewData): string
|
||||
{
|
||||
if ($viewData['homeView'] === 'default') {
|
||||
return 'home.parts.default-card-recently-updated-pages';
|
||||
}
|
||||
|
||||
return 'home.parts.configured-section-recently-updated-pages';
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$recentlyUpdatedPages = $this->queries->visibleForList()
|
||||
->where('draft', false)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->take(8)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\EntityQueries;
|
||||
use BookStack\Entities\Queries\QueryRecentlyViewed;
|
||||
use BookStack\Entities\Queries\QueryTopFavourites;
|
||||
use BookStack\View\ViewBlockInterface;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HomeRecentlyViewedOrRecentBooks implements ViewBlockInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected EntityQueries $queries,
|
||||
protected QueryRecentlyViewed $recentlyViewed,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getId(): string
|
||||
{
|
||||
return 'builtin_home-recently-viewed-or-recent-books';
|
||||
}
|
||||
|
||||
public static function getLabel(): string
|
||||
{
|
||||
$key = user()->isGuest() ? 'books_recent' : 'my_recently_viewed';
|
||||
return trans("entities.{$key}");
|
||||
}
|
||||
|
||||
public function getView(array $viewData): string
|
||||
{
|
||||
if ($viewData['homeView'] === 'default') {
|
||||
return 'home.parts.default-card-recently-viewed-or-recent-books';
|
||||
}
|
||||
|
||||
return 'home.parts.configured-section-recently-viewed-or-recent-books';
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
if (user()->isGuest()) {
|
||||
$recents = $this->queries->books->visibleForList()
|
||||
->orderBy('created_at', 'desc')
|
||||
->take(10)
|
||||
->get();
|
||||
} else {
|
||||
$recents = $this->recentlyViewed->run(10, 1);
|
||||
}
|
||||
|
||||
return [
|
||||
'recents' => $recents
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\QueryTopFavourites;
|
||||
use BookStack\View\ViewBlockInterface;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HomeTopFavourites implements ViewBlockInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected QueryTopFavourites $topFavourites
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getId(): string
|
||||
{
|
||||
return 'builtin_home-top-favourites';
|
||||
}
|
||||
|
||||
public static function getLabel(): string
|
||||
{
|
||||
return trans('entities.my_most_viewed_favourites');
|
||||
}
|
||||
|
||||
public function getView(array $viewData): string
|
||||
{
|
||||
if ($viewData['homeView'] === 'default') {
|
||||
return 'home.parts.default-card-top-favourites';
|
||||
}
|
||||
|
||||
return 'home.parts.configured-section-top-favourites';
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$favourites = $this->topFavourites->run(6);
|
||||
return [
|
||||
'favourites' => $favourites
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PagesShowActions extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_pages-show-actions';
|
||||
protected static string $view = 'pages.parts.show-sidebar-section-actions';
|
||||
protected static string $labelTranslationKey = 'common.actions';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Page $page */
|
||||
$page = $viewData['page'];
|
||||
|
||||
return [
|
||||
'page' => $page,
|
||||
'watchOptions' => new UserEntityWatchOptions(user(), $page),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PagesShowAttachments extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_pages-show-attachments';
|
||||
protected static string $view = 'pages.parts.show-sidebar-section-attachments';
|
||||
protected static string $labelTranslationKey = 'entities.pages_attachments';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Page $page */
|
||||
$page = $viewData['page'];
|
||||
|
||||
return [
|
||||
'page' => $page,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PagesShowBookTree extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_pages-show-book-tree';
|
||||
protected static string $view = 'entities.book-tree';
|
||||
protected static string $labelTranslationKey = 'entities.books_navigation';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = $viewData['book'];
|
||||
|
||||
return [
|
||||
'book' => $book,
|
||||
'bookTree' => $viewData['bookTree'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PagesShowDetails extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_pages-show-details';
|
||||
protected static string $view = 'pages.parts.show-sidebar-section-details';
|
||||
protected static string $labelTranslationKey = 'common.details';
|
||||
|
||||
public function __construct(
|
||||
protected ReferenceFetcher $referenceFetcher,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Book $book */
|
||||
$book = $viewData['book'];
|
||||
/** @var Page $page */
|
||||
$page = $viewData['page'];
|
||||
|
||||
$referenceCount = $this->referenceFetcher->getReferenceCountToEntity($page);
|
||||
|
||||
return [
|
||||
'page' => $page,
|
||||
'book' => $book,
|
||||
'watchOptions' => new UserEntityWatchOptions(user(), $page),
|
||||
'referenceCount' => $referenceCount,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PagesShowPageNav extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_pages-show-page-nav';
|
||||
protected static string $view = 'pages.parts.show-sidebar-section-page-nav';
|
||||
protected static string $labelTranslationKey = 'entities.pages_navigation';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Page $page */
|
||||
$page = $viewData['page'];
|
||||
|
||||
$pageContent = new PageContent($page);
|
||||
$pageNav = $pageContent->getNavigation($page->html);
|
||||
|
||||
return [
|
||||
'pageNav' => $pageNav,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PagesShowTags extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_pages-show-tags';
|
||||
protected static string $view = 'pages.parts.show-sidebar-section-tags';
|
||||
protected static string $labelTranslationKey = 'entities.tags';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Page $page */
|
||||
$page = $viewData['page'];
|
||||
|
||||
return [
|
||||
'page' => $page,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShelvesIndexActions extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_shelves-index-actions';
|
||||
protected static string $view = 'shelves.parts.index-sidebar-section-actions';
|
||||
protected static string $labelTranslationKey = 'common.actions';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [
|
||||
'view' => $viewData['view'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\BookshelfQueries;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShelvesIndexNew extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_shelves-index-new';
|
||||
protected static string $view = 'shelves.parts.index-sidebar-section-new';
|
||||
protected static string $labelTranslationKey = 'entities.shelves_new';
|
||||
|
||||
public function __construct(
|
||||
protected BookshelfQueries $queries,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$new = $this->queries->visibleForList()
|
||||
->orderBy('created_at', 'desc')
|
||||
->take(4)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'new' => $new,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\BookshelfQueries;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShelvesIndexPopular extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_shelves-index-popular';
|
||||
protected static string $view = 'shelves.parts.index-sidebar-section-popular';
|
||||
protected static string $labelTranslationKey = 'entities.shelves_popular';
|
||||
|
||||
public function __construct(
|
||||
protected BookshelfQueries $queries,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [
|
||||
'popular' => $this->queries->popularForList()->take(4)->get(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Queries\BookshelfQueries;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShelvesIndexRecents extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_shelves-index-recents';
|
||||
protected static string $view = 'shelves.parts.index-sidebar-section-recents';
|
||||
protected static string $labelTranslationKey = 'entities.recently_viewed';
|
||||
|
||||
public function __construct(
|
||||
protected BookshelfQueries $queries,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$recents = null;
|
||||
if (!user()->isGuest()) {
|
||||
$recents = $this->queries->recentlyViewedForCurrentUser()->take(4)->get();
|
||||
}
|
||||
|
||||
return [
|
||||
'recents' => $recents,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShelvesShowActions extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_shelves-show-actions';
|
||||
protected static string $view = 'shelves.parts.show-sidebar-section-actions';
|
||||
protected static string $labelTranslationKey = 'common.actions';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Bookshelf $shelf */
|
||||
$shelf = $viewData['shelf'];
|
||||
|
||||
return [
|
||||
'shelf' => $shelf,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Activity\ActivityQueries;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShelvesShowActivity extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_shelves-show-activity';
|
||||
protected static string $view = 'shelves.parts.show-sidebar-section-activity';
|
||||
protected static string $labelTranslationKey = 'entities.recent_activity';
|
||||
|
||||
public function __construct(
|
||||
protected ActivityQueries $activityQueries,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Bookshelf $shelf */
|
||||
$shelf = $viewData['shelf'];
|
||||
|
||||
return [
|
||||
'activity' => $this->activityQueries->entityActivity($shelf, 20, 1),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShelvesShowDetails extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_shelves-show-details';
|
||||
protected static string $view = 'shelves.parts.show-sidebar-section-details';
|
||||
protected static string $labelTranslationKey = 'common.details';
|
||||
|
||||
public function __construct(
|
||||
protected ReferenceFetcher $referenceFetcher,
|
||||
) {
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
/** @var Bookshelf $shelf */
|
||||
$shelf = $viewData['shelf'];
|
||||
$referenceCount = $this->referenceFetcher->getReferenceCountToEntity($shelf);
|
||||
|
||||
return [
|
||||
'shelf' => $shelf,
|
||||
'referenceCount' => $referenceCount,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\View\ViewBlocks;
|
||||
|
||||
use BookStack\View\ViewBlock;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShelvesShowTags extends ViewBlock
|
||||
{
|
||||
protected static string $id = 'builtin_shelves-show-tags';
|
||||
protected static string $view = 'shelves.parts.show-sidebar-section-tags';
|
||||
protected static string $labelTranslationKey = 'entities.tags';
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
return [
|
||||
'shelf' => $viewData['shelf'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -193,4 +193,105 @@ Theme::addSocialDriver('reddit', [
|
||||
$driver->with(['prompt' => 'select_account']);
|
||||
$driver->scopes(['open_id']);
|
||||
});
|
||||
```
|
||||
```
|
||||
|
||||
## Custom View Layout Block Example
|
||||
|
||||
By listening to the `ThemeEvents::VIEW_BLOCKS_REGISTER` event, it's possible to register
|
||||
custom view blocks which can be shown in a layout in the application. View blocks are typically the sections shown in the sidebar,
|
||||
or the cards shown on the home view.
|
||||
|
||||
As an example of using this system, we'll define a custom view block which will show on the default home view to state how many
|
||||
books there are in the system.
|
||||
To start, we'll need to create a custom class to represent our view block. This must implement `\BookStack\View\ViewBlockInterface`:
|
||||
|
||||
```php
|
||||
use BookStack\Entities\Queries\BookQueries;
|
||||
use BookStack\View\ViewBlockInterface;
|
||||
use BookStack\View\ViewBlockManager;
|
||||
|
||||
class BookTotalBlock implements ViewBlockInterface {
|
||||
|
||||
public function __construct(
|
||||
protected BookQueries $bookQueries
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getId(): string
|
||||
{
|
||||
return 'custom_book_total_block';
|
||||
}
|
||||
|
||||
public static function getLabel(): string
|
||||
{
|
||||
return 'Total books displays';
|
||||
}
|
||||
|
||||
public function getView(array $viewData): string
|
||||
{
|
||||
return 'blocks.total-blocks';
|
||||
}
|
||||
|
||||
public function withData(array $viewData): array
|
||||
{
|
||||
$totalBooks = $this->bookQueries->visibleForList()->count();
|
||||
return [
|
||||
'totalBooks' => $totalBooks,
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The interface has a few required methods:
|
||||
|
||||
- The `getId` method must provide a unique per-block-type string ID.
|
||||
- The `getLabel` method must provide a general string label for the block.
|
||||
- The `getView` method provides a string path to a view file (Can be one custom registered).
|
||||
- This is provided the available view data at time of render, so it can be dynamic based on context.
|
||||
- The `withData` method is called when block is being rendered, and it should return an array of data which will be merged with existing view data.
|
||||
- This is also provided available view data, for use as context.
|
||||
|
||||
In this example, we're also making use of the `BookQueries` internal BookStack class.
|
||||
You're able to inject any other dependent classes/services via the constructor like this, and BookStack will attempt to auto-resolve them.
|
||||
|
||||
We can then register this block class using the logical theme system like so:
|
||||
|
||||
```php
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\View\ViewBlockManager;
|
||||
|
||||
Theme::listen(ThemeEvents::VIEW_BLOCKS_REGISTER, function (ViewBlockManager $manager) {
|
||||
$manager->register(
|
||||
'home-default', // The location/layout where this block will be displayed
|
||||
'right', // The default position for the block within the location
|
||||
BookTotalBlock::class // The block class to register
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
The above registration code would typically be within your `functions.php` theme file.
|
||||
You could also define the above block class in the same file or separate it into its own file
|
||||
and include it from the `functions.php` via a `require_once()` call.
|
||||
|
||||
Lastly, we'll need to create the view for the registered block.
|
||||
In our example we return `blocks.total-blocks` from the `getView` method, so our view needs to be located at
|
||||
`blocks/total-blocks.blade.php` from a view providing directory.
|
||||
In a theme folder, we can just create it at `blocks/total-blocks.blade.php` within the folder path.
|
||||
If we were building in a module, we'd need to create this at `views/blocks/total-blocks.blade.php` within our module folder.
|
||||
For our example, we'll use this content to make use of the data we're passing to the view:
|
||||
|
||||
```html
|
||||
<div class="card mb-xl">
|
||||
<h3 class="card-title">Total Books</h3>
|
||||
<div class="px-m pb-xs">
|
||||
<p>
|
||||
There are currently {{ $totalBooks }} books in the system!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
This will then show up on the default home grid view, in the right column.
|
||||
The exact position within that location may change depending on user preferences, but the block will be limited
|
||||
to the location provided during registration unless it has also been registered for other locations.
|
||||
|
||||
@@ -84,6 +84,8 @@ return [
|
||||
'status_inactive' => 'Inactive',
|
||||
'never' => 'Never',
|
||||
'none' => 'None',
|
||||
'move_left' => 'Move Left',
|
||||
'move_right' => 'Move Right',
|
||||
|
||||
// Header
|
||||
'homepage' => 'Homepage',
|
||||
|
||||
@@ -19,6 +19,27 @@ return [
|
||||
'shortcuts_update_success' => 'Shortcut preferences have been updated!',
|
||||
'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',
|
||||
|
||||
'interface' => 'Interface Preferences',
|
||||
'interface_desc' => 'Here you can find options to customize the appearance of the application user interface.',
|
||||
'interface_display_mode' => 'Display Mode',
|
||||
'interface_display_mode_desc' => 'Choose whether the application should show in dark or light mode. This can also be toggled from the home view, or via the profile dropdown in the header bar.',
|
||||
'layouts' => 'UI Layout Preferences',
|
||||
'layouts_desc' => 'Customize the layout of sections shown in the user interface for a range of views.',
|
||||
'layout_edit' => 'Edit Layout',
|
||||
'layout_edit_desc' => 'Drag and drop sections, or use the action menu found on each, to reconfigure which sections show within this layout in the interface, and where they are displayed.',
|
||||
'layout_edit_column_hint' => 'When viewed on smaller screen sizes, right column sections will be stacked on top of left column sections.',
|
||||
'layout_edit_save' => 'Save Layout',
|
||||
'layout_edit_layouts' => 'Layouts',
|
||||
'layout_edit_back_to_preferences' => 'Back to Preferences',
|
||||
'layout_edit_left' => 'Left',
|
||||
'layout_edit_right' => 'Right',
|
||||
'layout_edit_center' => 'Center',
|
||||
'layout_edit_unused' => 'Unused',
|
||||
'layout_edit_empty' => 'No sections to display',
|
||||
'layout_edit_reset_to_defaults' => 'Reset to Default',
|
||||
'layout_update_success' => 'Layout preferences have been updated!',
|
||||
'layout_reset_success' => 'Layout preferences have been reset!',
|
||||
|
||||
'notifications' => 'Notification Preferences',
|
||||
'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',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14.59 6 16 7.41 11.42 12 16 16.59 14.59 18l-6-6z"/></svg>
|
||||
|
After Width: | Height: | Size: 127 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M440-120H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h240v720Zm-80-80v-560H200v560h160Zm160-320v-320h240q33 0 56.5 23.5T840-760v240H520Zm80-80h160v-160H600v160Zm-80 480v-320h320v240q0 33-23.5 56.5T760-120H520Zm80-80h160v-160H600v160ZM360-480Zm240-120Zm0 240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 379 B |
@@ -12,7 +12,7 @@ export class Dropdown extends Component {
|
||||
this.container = this.$el;
|
||||
this.menu = this.$refs.menu;
|
||||
this.toggle = this.$refs.toggle;
|
||||
this.moveMenu = this.$opts.moveMenu;
|
||||
this.fixedPositionMenu = this.$opts.fixedPositionMenu === 'true';
|
||||
this.bubbleEscapes = this.$opts.bubbleEscapes === 'true';
|
||||
|
||||
this.direction = (document.dir === 'rtl') ? 'right' : 'left';
|
||||
@@ -33,13 +33,13 @@ export class Dropdown extends Component {
|
||||
const menuOriginalRect = this.menu.getBoundingClientRect();
|
||||
let heightOffset = 0;
|
||||
const toggleHeight = this.toggle.getBoundingClientRect().height;
|
||||
const containerBounds = findClosestScrollContainer(this.menu).getBoundingClientRect();
|
||||
const containerEl = this.fixedPositionMenu ? this.body : findClosestScrollContainer(this.menu);
|
||||
const containerBounds = containerEl.getBoundingClientRect();
|
||||
const dropUpwards = menuOriginalRect.bottom > containerBounds.bottom;
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
|
||||
// If enabled, Move to body to prevent being trapped within scrollable sections
|
||||
if (this.moveMenu) {
|
||||
this.body.appendChild(this.menu);
|
||||
if (this.fixedPositionMenu) {
|
||||
this.menu.style.position = 'fixed';
|
||||
this.menu.style.width = `${menuOriginalRect.width}px`;
|
||||
this.menu.style.left = `${menuOriginalRect.left}px`;
|
||||
@@ -99,12 +99,11 @@ export class Dropdown extends Component {
|
||||
this.menu.style.bottom = '';
|
||||
this.menu.style.maxHeight = '';
|
||||
|
||||
if (this.moveMenu) {
|
||||
if (this.fixedPositionMenu) {
|
||||
this.menu.style.position = '';
|
||||
this.menu.style[this.direction] = '';
|
||||
this.menu.style.width = '';
|
||||
this.menu.style.left = '';
|
||||
this.container.appendChild(this.menu);
|
||||
}
|
||||
|
||||
this.showing = false;
|
||||
@@ -125,10 +124,6 @@ export class Dropdown extends Component {
|
||||
this.hide();
|
||||
});
|
||||
|
||||
if (this.moveMenu) {
|
||||
keyboardNavHandler.shareHandlingToEl(this.menu);
|
||||
}
|
||||
|
||||
// Hide menu on option click
|
||||
this.container.addEventListener('click', event => {
|
||||
const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
|
||||
|
||||
@@ -30,6 +30,7 @@ export {GlobalSearch} from './global-search';
|
||||
export {HeaderMobileToggle} from './header-mobile-toggle';
|
||||
export {ImageManager} from './image-manager';
|
||||
export {ImagePicker} from './image-picker';
|
||||
export {LayoutEditor} from './layout-editor';
|
||||
export {ListSortControl} from './list-sort-control';
|
||||
export {LoadingButton} from './loading-button';
|
||||
export {MarkdownEditor} from './markdown-editor';
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import Sortable from "sortablejs";
|
||||
import {Component} from "./component";
|
||||
import {buildListActions, sortActionClickListener} from "../services/multi-lists";
|
||||
|
||||
|
||||
export class LayoutEditor extends Component {
|
||||
protected input!: HTMLInputElement;
|
||||
protected columns!: HTMLElement[];
|
||||
protected actionMenus!: HTMLElement[];
|
||||
|
||||
setup(): void {
|
||||
this.input = this.$refs.input as HTMLInputElement;
|
||||
this.columns = this.$manyRefs.column || [];
|
||||
this.actionMenus = this.$manyRefs.actionMenu || [];
|
||||
|
||||
this.initSortable();
|
||||
|
||||
const unusedColumn = this.columns.find(column => column.dataset.column === 'unused') as HTMLElement;
|
||||
const listActions = buildListActions(...this.columns);
|
||||
listActions.add = this.addBlockToLeastPopulated.bind(this);
|
||||
listActions.remove = function (item: HTMLElement) {
|
||||
unusedColumn.appendChild(item);
|
||||
};
|
||||
const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this));
|
||||
for (const actionMenu of this.actionMenus) {
|
||||
actionMenu.addEventListener('click', sortActionListener);
|
||||
}
|
||||
}
|
||||
|
||||
protected initSortable(): void {
|
||||
const sortAction = this.onChange.bind(this);
|
||||
|
||||
for (const column of this.columns) {
|
||||
new Sortable(column, {
|
||||
group: 'layout-editor-blocks',
|
||||
ghostClass: 'primary-background-light',
|
||||
handle: '.handle',
|
||||
animation: 150,
|
||||
onSort: sortAction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected onChange(): void {
|
||||
const configured: Record<string, string[]> = {};
|
||||
for (const column of this.columns) {
|
||||
const location = column.dataset.column || '';
|
||||
if (!location) continue;
|
||||
configured[location] = [];
|
||||
|
||||
const blockNodes = column.children;
|
||||
for (let i = 0; i < blockNodes.length; i++) {
|
||||
const blockNode = blockNodes[i] as HTMLElement;
|
||||
const blockId = blockNode.dataset.blockId || '';
|
||||
if (!blockId) continue;
|
||||
configured[location].push(blockId);
|
||||
}
|
||||
}
|
||||
|
||||
this.input.value = JSON.stringify(configured, null, 2);
|
||||
}
|
||||
|
||||
protected addBlockToLeastPopulated(item: HTMLElement): void {
|
||||
let populationCount = 100;
|
||||
let leastPopulated: null|HTMLElement = null
|
||||
for (const column of this.columns) {
|
||||
const blockCount = column.children.length;
|
||||
if (column.dataset.column === 'unused') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blockCount < populationCount) {
|
||||
leastPopulated = column;
|
||||
populationCount = blockCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (leastPopulated) {
|
||||
leastPopulated.appendChild(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Sortable from 'sortablejs';
|
||||
import {Component} from './component';
|
||||
import {buildListActions, sortActionClickListener} from '../services/dual-lists.ts';
|
||||
import {buildListActions, sortActionClickListener} from '../services/multi-lists.ts';
|
||||
|
||||
export class ShelfSort extends Component {
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Component} from "./component.js";
|
||||
import Sortable from "sortablejs";
|
||||
import {buildListActions, sortActionClickListener} from "../services/dual-lists";
|
||||
import {buildListActions, sortActionClickListener} from "../services/multi-lists";
|
||||
|
||||
|
||||
export class SortRuleManager extends Component {
|
||||
|
||||
@@ -259,7 +259,7 @@ export function hashElement(element: HTMLElement): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest scroll container parent for the given element
|
||||
* Find the closest scroll container parent for the given element,
|
||||
* otherwise will default to the body element.
|
||||
*/
|
||||
export function findClosestScrollContainer(start: HTMLElement): HTMLElement {
|
||||
|
||||
@@ -87,7 +87,9 @@ export class KeyboardNavigationHandler {
|
||||
const focusable: HTMLElement[] = [];
|
||||
const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])';
|
||||
for (const container of this.containers) {
|
||||
const toAdd = [...container.querySelectorAll(selector)].filter(e => isHTMLElement(e));
|
||||
const toAdd = [...container.querySelectorAll(selector)].filter(e => {
|
||||
return isHTMLElement(e) && e.checkVisibility();
|
||||
}) as HTMLElement[];
|
||||
focusable.push(...toAdd);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/**
|
||||
* Service for helping manage common dual-list scenarios.
|
||||
* (Shelf book manager, sort set manager).
|
||||
* (Shelf book manager, sort set manager, layout-editor).
|
||||
*/
|
||||
|
||||
type ListActionsSet = Record<string, ((item: HTMLElement) => void)>;
|
||||
|
||||
export function buildListActions(
|
||||
availableList: HTMLElement,
|
||||
configuredList: HTMLElement,
|
||||
...lists: HTMLElement[]
|
||||
): ListActionsSet {
|
||||
return {
|
||||
move_up(item) {
|
||||
@@ -22,11 +21,29 @@ export function buildListActions(
|
||||
const newIndex = Math.min(index + 2, list.children.length);
|
||||
list.insertBefore(item, list.children[newIndex] || null);
|
||||
},
|
||||
move_right(item) {
|
||||
const list = item.parentNode as HTMLElement;
|
||||
const listIndex = lists.indexOf(list);
|
||||
const targetListIndex = Math.min(listIndex + 1, lists.length - 1);
|
||||
lists[targetListIndex].appendChild(item);
|
||||
},
|
||||
move_left(item) {
|
||||
const list = item.parentNode as HTMLElement;
|
||||
const listIndex = lists.indexOf(list);
|
||||
const targetListIndex = Math.max(listIndex - 1, 0);
|
||||
lists[targetListIndex].appendChild(item);
|
||||
},
|
||||
remove(item) {
|
||||
availableList.appendChild(item);
|
||||
const otherList = lists.find(list => list !== item.parentNode);
|
||||
if (otherList) {
|
||||
otherList.appendChild(item);
|
||||
}
|
||||
},
|
||||
add(item) {
|
||||
configuredList.appendChild(item);
|
||||
const otherList = lists.find(list => list !== item.parentNode);
|
||||
if (otherList) {
|
||||
otherList.appendChild(item);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1246,11 +1246,19 @@ input.scroll-box-search, .scroll-box-header-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scroll-box > li.empty-state {
|
||||
.scroll-box.layout-editor-column-left [data-action="move_left"],
|
||||
.scroll-box.layout-editor-column-right [data-action="move_right"],
|
||||
.scroll-box.layout-editor-column-unused [data-action="move_right"],
|
||||
.scroll-box.layout-editor-column-unused [data-action="move_left"],
|
||||
.scroll-box.layout-editor-column-unused [data-action="remove"],
|
||||
.scroll-box.layout-editor-column-left [data-action="add"],
|
||||
.scroll-box.layout-editor-column-right [data-action="add"],
|
||||
.scroll-box.layout-editor-column-center [data-action="add"],
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
.scroll-box > li.empty-state:last-child {
|
||||
display: list-item;
|
||||
.scroll-box:has(> li:not(.empty-state)) .empty-state {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details.section-expander summary {
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
@stop
|
||||
|
||||
@section('left')
|
||||
@include('books.parts.index-sidebar-section-recents', ['recents' => $recents])
|
||||
@include('books.parts.index-sidebar-section-popular', ['popular' => $popular])
|
||||
@include('books.parts.index-sidebar-section-new', ['new' => $new])
|
||||
@include('common.view-blocks', ['location' => 'books-index', 'position' => 'left'])
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
@include('books.parts.index-sidebar-section-actions', ['view' => $view])
|
||||
@include('common.view-blocks', ['location' => 'books-index', 'position' => 'right'])
|
||||
@stop
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="mb-xl">
|
||||
<h5>{{ trans('common.details') }}</h5>
|
||||
<div class="blended-links">
|
||||
@include('entities.meta', ['entity' => $book, 'watchOptions' => $watchOptions])
|
||||
@include('entities.meta', ['entity' => $book, 'watchOptions' => $watchOptions, 'referenceCount' => $referenceCount])
|
||||
@if($book->hasPermissions())
|
||||
<div class="active-restriction">
|
||||
@if(userCan(\BookStack\Permissions\Permission::RestrictionsManage, $book))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@if(count($bookParentShelves) > 0)
|
||||
@if(count($shelves) > 0)
|
||||
<div class="actions mb-xl">
|
||||
<h5>{{ trans('entities.shelves') }}</h5>
|
||||
@include('entities.list', ['entities' => $bookParentShelves, 'style' => 'compact'])
|
||||
@include('entities.list', ['entities' => $shelves, 'style' => 'compact'])
|
||||
</div>
|
||||
@endif
|
||||
@@ -66,15 +66,10 @@
|
||||
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
@include('books.parts.show-sidebar-section-details', ['book' => $book, 'watchOptions' => $watchOptions])
|
||||
@include('books.parts.show-sidebar-section-actions', ['book' => $book, 'watchOptions' => $watchOptions])
|
||||
@stop
|
||||
|
||||
@section('left')
|
||||
@include('entities.search-form', ['label' => trans('entities.books_search_this')])
|
||||
@include('books.parts.show-sidebar-section-tags', ['book' => $book])
|
||||
@include('books.parts.show-sidebar-section-shelves', ['bookParentShelves' => $bookParentShelves])
|
||||
@include('books.parts.show-sidebar-section-activity', ['activity' => $activity])
|
||||
@include('common.view-blocks', ['location' => 'books-show', 'position' => 'left'])
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
@include('common.view-blocks', ['location' => 'books-show', 'position' => 'right'])
|
||||
@stop
|
||||
|
||||
@@ -62,15 +62,10 @@
|
||||
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
@include('chapters.parts.show-sidebar-section-details', ['chapter' => $chapter, 'book' => $book, 'watchOptions' => $watchOptions])
|
||||
@include('chapters.parts.show-sidebar-section-actions', ['chapter' => $chapter, 'watchOptions' => $watchOptions])
|
||||
@stop
|
||||
|
||||
@section('left')
|
||||
@include('entities.search-form', ['label' => trans('entities.chapters_search_this')])
|
||||
@include('chapters.parts.show-sidebar-section-tags', ['chapter' => $chapter])
|
||||
@include('entities.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
|
||||
@include('common.view-blocks', ['location' => 'chapters-show', 'position' => 'left'])
|
||||
@stop
|
||||
|
||||
|
||||
@section('right')
|
||||
@include('common.view-blocks', ['location' => 'chapters-show', 'position' => 'right'])
|
||||
@stop
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{{--
|
||||
$location - string
|
||||
$position - string
|
||||
--}}
|
||||
@php
|
||||
$sectionVars = get_defined_vars();
|
||||
@endphp
|
||||
@foreach($viewBlocks->getInstancesForLocationAndPositionForCurrentUser($location, $position) as $block)
|
||||
@include($block->getView($sectionVars), $block->withData($sectionVars))
|
||||
@endforeach
|
||||
@@ -11,7 +11,7 @@
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@foreach($sidebarTree as $bookChild)
|
||||
@foreach($bookTree as $bookChild)
|
||||
<li class="list-item-{{ $bookChild->getType() }} {{ $bookChild->getType() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
|
||||
@include('entities.list-item-basic', ['entity' => $bookChild, 'classes' => $current->matches($bookChild)? 'selected' : ''])
|
||||
|
||||
|
||||
@@ -5,26 +5,9 @@
|
||||
@stop
|
||||
|
||||
@section('left')
|
||||
@include('home.parts.sidebar')
|
||||
@include('common.view-blocks', ['location' => 'home-non-default', 'position' => 'left'])
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
<div class="actions mb-xl">
|
||||
<h5>{{ trans('common.actions') }}</h5>
|
||||
<div class="icon-list text-link">
|
||||
@if(userCan(\BookStack\Permissions\Permission::BookCreateAll))
|
||||
<a href="{{ url("/create-book") }}" class="icon-list-item">
|
||||
<span>@icon('add')</span>
|
||||
<span>{{ trans('entities.books_create') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
@include('entities.view-toggle', ['view' => $view, 'type' => 'books'])
|
||||
<a href="{{ url('/tags') }}" class="icon-list-item">
|
||||
<span>@icon('tag')</span>
|
||||
<span>{{ trans('entities.tags_view_tags') }}</span>
|
||||
</a>
|
||||
@include('home.parts.expand-toggle', ['classes' => 'text-link', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
|
||||
@include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-link'])
|
||||
</div>
|
||||
</div>
|
||||
@include('common.view-blocks', ['location' => 'home-non-default', 'position' => 'right'])
|
||||
@stop
|
||||
|
||||
@@ -16,65 +16,16 @@
|
||||
<div class="container" id="home-default">
|
||||
<div class="grid third gap-x-xxl no-row-gap">
|
||||
<div>
|
||||
@if(count($draftPages) > 0)
|
||||
<div id="recent-drafts" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.my_recent_drafts') }}</h3>
|
||||
<div class="px-m">
|
||||
@include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div id="{{ auth()->check() ? 'recently-viewed' : 'recent-books' }}" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h3>
|
||||
<div class="px-m">
|
||||
@include('entities.list', [
|
||||
'entities' => $recents,
|
||||
'style' => 'compact',
|
||||
'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
@include('common.view-blocks', ['location' => 'home-default', 'position' => 'left'])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@if(count($favourites) > 0)
|
||||
<div id="top-favourites" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.my_most_viewed_favourites') }}</h3>
|
||||
<div class="px-m">
|
||||
@include('entities.list', [
|
||||
'entities' => $favourites,
|
||||
'style' => 'compact',
|
||||
])
|
||||
</div>
|
||||
<a href="{{ url('/favourites') }}" class="card-footer-link">{{ trans('common.view_all') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div id="recent-pages" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.recently_updated_pages') }}</h3>
|
||||
<div id="recently-updated-pages" class="px-m">
|
||||
@include('entities.list', [
|
||||
'entities' => $recentlyUpdatedPages,
|
||||
'style' => 'compact',
|
||||
'emptyText' => trans('entities.no_pages_recently_updated'),
|
||||
])
|
||||
</div>
|
||||
@if(count($recentlyUpdatedPages) > 0)
|
||||
<a href="{{ url("/pages/recently-updated") }}" class="card-footer-link">{{ trans('common.view_all') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
@include('common.view-blocks', ['location' => 'home-default', 'position' => 'center'])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div id="recent-activity" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.recent_activity') }}</h3>
|
||||
<div class="px-m">
|
||||
@include('common.activity-list', ['activity' => $activity])
|
||||
</div>
|
||||
</div>
|
||||
@include('common.view-blocks', ['location' => 'home-default', 'position' => 'right'])
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{{--
|
||||
$homeView - string - The type of home view being used
|
||||
$view - string - Configured view type from parent (for books/shelves list/grid view)
|
||||
--}}
|
||||
<div class="actions mb-xl">
|
||||
<h5>{{ trans('common.actions') }}</h5>
|
||||
<div class="icon-list text-link">
|
||||
@if($homeView === 'books')
|
||||
@if(userCan(\BookStack\Permissions\Permission::BookCreateAll))
|
||||
<a href="{{ url("/create-book") }}" class="icon-list-item">
|
||||
<span>@icon('add')</span>
|
||||
<span>{{ trans('entities.books_create') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
@include('entities.view-toggle', ['view' => $view, 'type' => 'books'])
|
||||
<a href="{{ url('/tags') }}" class="icon-list-item">
|
||||
<span>@icon('tag')</span>
|
||||
<span>{{ trans('entities.tags_view_tags') }}</span>
|
||||
</a>
|
||||
@elseif ($homeView === 'bookshelves')
|
||||
@if(userCan(\BookStack\Permissions\Permission::BookshelfCreateAll))
|
||||
<a href="{{ url("/create-shelf") }}" class="icon-list-item">
|
||||
<span>@icon('add')</span>
|
||||
<span>{{ trans('entities.shelves_new_action') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
@include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelves'])
|
||||
<a href="{{ url('/tags') }}" class="icon-list-item">
|
||||
<span>@icon('tag')</span>
|
||||
<span>{{ trans('entities.tags_view_tags') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
@include('home.parts.expand-toggle', ['classes' => 'text-link', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
|
||||
@include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-link'])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
{{--
|
||||
$activity - array
|
||||
--}}
|
||||
<div id="recent-activity" class="mb-xl">
|
||||
<h5>{{ trans('entities.recent_activity') }}</h5>
|
||||
@include('common.activity-list', ['activity' => $activity])
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
{{--
|
||||
$draftPages - array
|
||||
--}}
|
||||
@if(count($draftPages) > 0)
|
||||
<div id="recent-drafts" class="mb-xl">
|
||||
<h5>{{ trans('entities.my_recent_drafts') }}</h5>
|
||||
@include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,14 @@
|
||||
{{--
|
||||
$recentlyUpdatedPages - array
|
||||
--}}
|
||||
<div class="mb-xl">
|
||||
<h5>{{ trans('entities.recently_updated_pages') }}</h5>
|
||||
<div id="recently-updated-pages">
|
||||
@include('entities.list', [
|
||||
'entities' => $recentlyUpdatedPages,
|
||||
'style' => 'compact',
|
||||
'emptyText' => trans('entities.no_pages_recently_updated')
|
||||
])
|
||||
</div>
|
||||
<a href="{{ url('/pages/recently-updated') }}" class="text-muted block py-xs">{{ trans('common.view_all') }}</a>
|
||||
</div>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{{--
|
||||
$recents - array
|
||||
--}}
|
||||
<div class="mb-xl">
|
||||
<h5>{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h5>
|
||||
@include('entities.list', [
|
||||
'entities' => $recents,
|
||||
'style' => 'compact',
|
||||
'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
|
||||
])
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
{{--
|
||||
$favourites - array
|
||||
--}}
|
||||
@if(count($favourites) > 0)
|
||||
<div id="top-favourites" class="mb-xl">
|
||||
<h5>{{ trans('entities.my_most_viewed_favourites') }}</h5>
|
||||
@include('entities.list', [
|
||||
'entities' => $favourites,
|
||||
'style' => 'compact',
|
||||
])
|
||||
<a href="{{ url('/favourites') }}" class="text-muted block py-xs">{{ trans('common.view_all') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,9 @@
|
||||
{{--
|
||||
$activity - array
|
||||
--}}
|
||||
<div id="recent-activity" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.recent_activity') }}</h3>
|
||||
<div class="px-m">
|
||||
@include('common.activity-list', ['activity' => $activity])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
{{--
|
||||
$draftPages - array
|
||||
--}}
|
||||
@if(count($draftPages) > 0)
|
||||
<div id="recent-drafts" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.my_recent_drafts') }}</h3>
|
||||
<div class="px-m">
|
||||
@include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,16 @@
|
||||
{{--
|
||||
$recentlyUpdatedPages - array
|
||||
--}}
|
||||
<div id="recent-pages" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.recently_updated_pages') }}</h3>
|
||||
<div id="recently-updated-pages" class="px-m">
|
||||
@include('entities.list', [
|
||||
'entities' => $recentlyUpdatedPages,
|
||||
'style' => 'compact',
|
||||
'emptyText' => trans('entities.no_pages_recently_updated'),
|
||||
])
|
||||
</div>
|
||||
@if(count($recentlyUpdatedPages) > 0)
|
||||
<a href="{{ url("/pages/recently-updated") }}" class="card-footer-link">{{ trans('common.view_all') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
{{--
|
||||
$recents - array
|
||||
--}}
|
||||
|
||||
<div id="{{ auth()->check() ? 'recently-viewed' : 'recent-books' }}" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h3>
|
||||
<div class="px-m">
|
||||
@include('entities.list', [
|
||||
'entities' => $recents,
|
||||
'style' => 'compact',
|
||||
'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
{{--
|
||||
$favourites - array
|
||||
--}}
|
||||
@if(count($favourites) > 0)
|
||||
<div id="top-favourites" class="card mb-xl">
|
||||
<h3 class="card-title">{{ trans('entities.my_most_viewed_favourites') }}</h3>
|
||||
<div class="px-m">
|
||||
@include('entities.list', [
|
||||
'entities' => $favourites,
|
||||
'style' => 'compact',
|
||||
])
|
||||
</div>
|
||||
<a href="{{ url('/favourites') }}" class="card-footer-link">{{ trans('common.view_all') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
@@ -1,43 +0,0 @@
|
||||
@if(count($draftPages) > 0)
|
||||
<div id="recent-drafts" class="mb-xl">
|
||||
<h5>{{ trans('entities.my_recent_drafts') }}</h5>
|
||||
@include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(count($favourites) > 0)
|
||||
<div id="top-favourites" class="mb-xl">
|
||||
<h5>{{ trans('entities.my_most_viewed_favourites') }}</h5>
|
||||
@include('entities.list', [
|
||||
'entities' => $favourites,
|
||||
'style' => 'compact',
|
||||
])
|
||||
<a href="{{ url('/favourites') }}" class="text-muted block py-xs">{{ trans('common.view_all') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mb-xl">
|
||||
<h5>{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h5>
|
||||
@include('entities.list', [
|
||||
'entities' => $recents,
|
||||
'style' => 'compact',
|
||||
'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
|
||||
])
|
||||
</div>
|
||||
|
||||
<div class="mb-xl">
|
||||
<h5>{{ trans('entities.recently_updated_pages') }}</h5>
|
||||
<div id="recently-updated-pages">
|
||||
@include('entities.list', [
|
||||
'entities' => $recentlyUpdatedPages,
|
||||
'style' => 'compact',
|
||||
'emptyText' => trans('entities.no_pages_recently_updated')
|
||||
])
|
||||
</div>
|
||||
<a href="{{ url('/pages/recently-updated') }}" class="text-muted block py-xs">{{ trans('common.view_all') }}</a>
|
||||
</div>
|
||||
|
||||
<div id="recent-activity" class="mb-xl">
|
||||
<h5>{{ trans('entities.recent_activity') }}</h5>
|
||||
@include('common.activity-list', ['activity' => $activity])
|
||||
</div>
|
||||
@@ -5,26 +5,9 @@
|
||||
@stop
|
||||
|
||||
@section('left')
|
||||
@include('home.parts.sidebar')
|
||||
@include('common.view-blocks', ['location' => 'home-non-default', 'position' => 'left'])
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
<div class="actions mb-xl">
|
||||
<h5>{{ trans('common.actions') }}</h5>
|
||||
<div class="icon-list text-link">
|
||||
@if(userCan(\BookStack\Permissions\Permission::BookshelfCreateAll))
|
||||
<a href="{{ url("/create-shelf") }}" class="icon-list-item">
|
||||
<span>@icon('add')</span>
|
||||
<span>{{ trans('entities.shelves_new_action') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
@include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelves'])
|
||||
<a href="{{ url('/tags') }}" class="icon-list-item">
|
||||
<span>@icon('tag')</span>
|
||||
<span>{{ trans('entities.tags_view_tags') }}</span>
|
||||
</a>
|
||||
@include('home.parts.expand-toggle', ['classes' => 'text-link', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
|
||||
@include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-link'])
|
||||
</div>
|
||||
</div>
|
||||
@include('common.view-blocks', ['location' => 'home-non-default', 'position' => 'right'])
|
||||
@stop
|
||||
|
||||
@@ -13,15 +13,9 @@
|
||||
@stop
|
||||
|
||||
@section('left')
|
||||
@include('home.parts.sidebar')
|
||||
@include('common.view-blocks', ['location' => 'home-non-default', 'position' => 'left'])
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
<div class="actions mb-xl">
|
||||
<h5>{{ trans('common.actions') }}</h5>
|
||||
<div class="icon-list text-link">
|
||||
@include('home.parts.expand-toggle', ['classes' => 'text-link', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
|
||||
@include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-link'])
|
||||
</div>
|
||||
</div>
|
||||
@include('common.view-blocks', ['location' => 'home-non-default', 'position' => 'right'])
|
||||
@stop
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="text-center">
|
||||
<div component="dropdown"
|
||||
option:dropdown:move-menu="true"
|
||||
option:dropdown:fixed-position-menu="true"
|
||||
class="dropdown-container draft-display text {{ $draftsEnabled ? '' : 'hidden' }}">
|
||||
<div class="flex-container-row items-center justify-center">
|
||||
<button type="button"
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
<div class="flex-container-row justify-flex-end gap-x-m items-center">
|
||||
<div component="dropdown"
|
||||
option:dropdown:move-menu="true"
|
||||
option:dropdown:fixed-position-menu="true"
|
||||
class="dropdown-container">
|
||||
<button refs="dropdown@toggle" type="button" aria-haspopup="true" aria-expanded="false" class="icon-list-item text-link">
|
||||
<span>@icon('edit')</span>
|
||||
|
||||
@@ -36,13 +36,9 @@
|
||||
@stop
|
||||
|
||||
@section('left')
|
||||
@include('pages.parts.show-sidebar-section-tags', ['page' => $page])
|
||||
@include('pages.parts.show-sidebar-section-attachments', ['page' => $page])
|
||||
@include('pages.parts.show-sidebar-section-page-nav', ['pageNav' => $pageNav])
|
||||
@include('entities.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
|
||||
@include('common.view-blocks', ['location' => 'pages-show', 'position' => 'left'])
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
@include('pages.parts.show-sidebar-section-details', ['page' => $page, 'watchOptions' => $watchOptions, 'book' => $book])
|
||||
@include('pages.parts.show-sidebar-section-actions', ['page' => $page, 'watchOptions' => $watchOptions])
|
||||
@include('common.view-blocks', ['location' => 'pages-show', 'position' => 'right'])
|
||||
@stop
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
@extends('layouts.simple')
|
||||
|
||||
@section('body')
|
||||
|
||||
<div class="container medium my-xl">
|
||||
|
||||
<div class="grid gap-xxl right-focus">
|
||||
|
||||
<div>
|
||||
<div class="mb-l">
|
||||
<a href="{{ url('/my-account/interface') }}" class="text-link">@icon('back') {{ trans('preferences.layout_edit_back_to_preferences') }}</a>
|
||||
</div>
|
||||
<h5>{{ trans('preferences.layout_edit_layouts') }}</h5>
|
||||
<nav class="active-link-list in-sidebar">
|
||||
@foreach($namedLocations as $locationOptKey => $locationOptName)
|
||||
<a href="{{ url('/layouts/' . $locationOptKey) }}" class="{{ $locationOptKey === $location ? 'active' : '' }}">{{ $locationOptName }}</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card content-wrap auto-height">
|
||||
<div class="flex-container-row justify-space-between items-center">
|
||||
<h1 class="list-heading">{{ trans('preferences.layout_edit') }}</h1>
|
||||
<span class="text-muted text-bigger">{{ $locationName }}</span>
|
||||
</div>
|
||||
|
||||
<p class="small text-muted">{{ trans('preferences.layout_edit_desc') }}</p>
|
||||
@if(!isset($blocks['center']))
|
||||
<p class="small text-muted">
|
||||
{{ trans('preferences.layout_edit_column_hint') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<form component="layout-editor" action="{{ url('layouts/' . $location) }}" method="POST">
|
||||
{{ method_field('PUT') }}
|
||||
{{ csrf_field() }}
|
||||
|
||||
<input refs="layout-editor@input" type="hidden" name="layout" value="">
|
||||
|
||||
<div class="flex-container-row gap-m mb-m wrap">
|
||||
@include('settings.layouts.parts.block-column', ['columnBlocks' => $blocks['left'] ?? [], 'label' => trans('preferences.layout_edit_left'), 'id' => 'left'])
|
||||
@if(isset($blocks['center']))
|
||||
@include('settings.layouts.parts.block-column', ['columnBlocks' => $blocks['center'] ?? [], 'label' => trans('preferences.layout_edit_center'), 'id' => 'center'])
|
||||
@endif
|
||||
@include('settings.layouts.parts.block-column', ['columnBlocks' => $blocks['right'] ?? [], 'label' => trans('preferences.layout_edit_right'), 'id' => 'right'])
|
||||
</div>
|
||||
|
||||
<div class="mb-l">
|
||||
@include('settings.layouts.parts.block-column', ['columnBlocks' => $blocks['unused'] ?? [], 'label' => trans('preferences.layout_edit_unused'), 'id' => 'unused'])
|
||||
</div>
|
||||
|
||||
<div class="form-group text-right">
|
||||
<a href="{{ url('/my-account/interface') }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<button type="submit" form="layout-reset-form" class="button outline">{{ trans('preferences.layout_edit_reset_to_defaults') }}</button>
|
||||
<button type="submit" class="button">{{ trans('preferences.layout_edit_save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form id="layout-reset-form" action="{{ url('/layouts/' . $location . '/reset') }}" method="POST">
|
||||
{{ csrf_field() }}
|
||||
{{ method_field('PUT') }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@stop
|
||||
@@ -0,0 +1,17 @@
|
||||
{{--
|
||||
$columnBlocks - array - Blocks to list
|
||||
$label - string - Section title
|
||||
$id - string - identifier for location/column
|
||||
--}}
|
||||
<div class="form-group flex min-width-m">
|
||||
<label id="layout-blocks-{{ $id }}">{{ $label }}</label>
|
||||
<ul refs="layout-editor@column"
|
||||
data-column="{{ $id }}"
|
||||
aria-labelledby="layout-blocks-{{ $id }}"
|
||||
class="scroll-box layout-editor-column-{{ $id }}">
|
||||
<li class="text-muted empty-state px-m py-s italic text-small">{{ trans('preferences.layout_edit_empty') }}</li>
|
||||
@foreach($columnBlocks as $block)
|
||||
@include('settings.layouts.parts.block', ['block' => $block])
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
@php /** @var $block class-string<\BookStack\View\ViewBlockInterface> */ @endphp
|
||||
<li data-block-id="{{ $block::getId() }}"
|
||||
class="scroll-box-item items-center">
|
||||
<div class="handle px-s">@icon('grip')</div>
|
||||
<div class="text-small">{{ $block::getLabel() }}</div>
|
||||
<div component="dropdown"
|
||||
option:dropdown:fixed-position-menu="true"
|
||||
class="dropdown-container ml-auto">
|
||||
<button type="button"
|
||||
refs="dropdown@toggle"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded="false"
|
||||
class="icon-button p-xxs">@icon('more')</button>
|
||||
<ul refs="dropdown@menu layout-editor@action-menu" role="menu" aria-label="{{ trans('common.actions') }}" class="dropdown-menu">
|
||||
<li><button type="button" data-action="move_left" class="icon-item">@icon('chevron-left') {{ trans('common.move_left') }}</button></li>
|
||||
<li><button type="button" data-action="move_up" class="icon-item">@icon('chevron-up') {{ trans('entities.books_sort_move_up') }}</button></li>
|
||||
<li><button type="button" data-action="move_down" class="icon-item">@icon('chevron-down') {{ trans('entities.books_sort_move_down') }}</button></li>
|
||||
<li><button type="button" data-action="move_right" class="icon-item">@icon('chevron-right') {{ trans('common.move_right') }}</button></li>
|
||||
<li><button type="button" data-action="add" class="icon-item">@icon('add-small') {{ trans('common.add') }}</button></li>
|
||||
<li><button type="button" data-action="remove" class="icon-item">@icon('remove') {{ trans('common.remove') }}</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -4,12 +4,10 @@
|
||||
@include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view, 'listOptions' => $listOptions])
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
@include('shelves.parts.index-sidebar-section-actions', ['view' => $view])
|
||||
@section('left')
|
||||
@include('common.view-blocks', ['location' => 'shelves-index', 'position' => 'left'])
|
||||
@stop
|
||||
|
||||
@section('left')
|
||||
@include('shelves.parts.index-sidebar-section-recents', ['recents' => $recents])
|
||||
@include('shelves.parts.index-sidebar-section-popular', ['popular' => $popular])
|
||||
@include('shelves.parts.index-sidebar-section-new', ['new' => $new])
|
||||
@stop
|
||||
@section('right')
|
||||
@include('common.view-blocks', ['location' => 'shelves-index', 'position' => 'right'])
|
||||
@stop
|
||||
|
||||
@@ -69,15 +69,9 @@
|
||||
@stop
|
||||
|
||||
@section('left')
|
||||
@include('shelves.parts.show-sidebar-section-tags', ['shelf' => $shelf])
|
||||
@include('shelves.parts.show-sidebar-section-details', ['shelf' => $shelf])
|
||||
@include('shelves.parts.show-sidebar-section-activity', ['activity' => $activity])
|
||||
@include('common.view-blocks', ['location' => 'shelves-show', 'position' => 'left'])
|
||||
@stop
|
||||
|
||||
@section('right')
|
||||
@include('shelves.parts.show-sidebar-section-actions', ['shelf' => $shelf, 'view' => $view])
|
||||
@include('common.view-blocks', ['location' => 'shelves-show', 'position' => 'right'])
|
||||
@stop
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
@extends('users.account.layout')
|
||||
|
||||
@section('main')
|
||||
<section class="card content-wrap auto-height">
|
||||
<form action="{{ url('/my-account/interface') }}" method="post">
|
||||
{{ method_field('put') }}
|
||||
{{ csrf_field() }}
|
||||
|
||||
<h1 class="list-heading">{{ trans('preferences.interface') }}</h1>
|
||||
<p class="text-small text-muted mb-none">{{ trans('preferences.interface_desc') }}</p>
|
||||
|
||||
<div class="setting-list">
|
||||
@include('users.parts.language-option-row', ['value' => old('language') ?? user()->getLocale()->appLocale()])
|
||||
@include('users.account.parts.display-mode-option-row')
|
||||
</div>
|
||||
|
||||
<div class="form-group text-right">
|
||||
<button class="button">{{ trans('common.save') }}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('preferences.layouts') }}</h2>
|
||||
<p class="text-small text-muted">{{ trans('preferences.layouts_desc') }}</p>
|
||||
|
||||
<div class="item-list mb-m">
|
||||
@foreach($namedLocations as $locationKey => $locationName)
|
||||
<div class="flex-container-row justify-space-between item-list-row items-center wrap px-xs">
|
||||
<div class="py-xs px-s min-width-m">
|
||||
<a href="{{ url('/layouts/' . $locationKey) }}">{{ $locationName }}</a>
|
||||
</div>
|
||||
<div class="py-xs min-width-m text-m-right px-m">
|
||||
<a class="button outline small" href="{{ url('/layouts/' . $locationKey) }}">{{ trans('common.configure') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="card content-wrap auto-height">
|
||||
<div class="flex-container-row gap-l items-center wrap">
|
||||
<div class="flex">
|
||||
<h2 class="list-heading">{{ trans('preferences.shortcuts_interface') }}</h2>
|
||||
<p class="text-small text-muted">{{ trans('preferences.shortcuts_overview_desc') }}</p>
|
||||
</div>
|
||||
<div class="text-m-right">
|
||||
<a class="button outline" href="{{ url('/my-account/shortcuts') }}">{{ trans('common.open') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@stop
|
||||
@@ -11,6 +11,7 @@
|
||||
<nav class="active-link-list in-sidebar">
|
||||
<a href="{{ url('/my-account/profile') }}" class="{{ $category === 'profile' ? 'active' : '' }}">@icon('user') {{ trans('preferences.profile') }}</a>
|
||||
<a href="{{ url('/my-account/auth') }}" class="{{ $category === 'auth' ? 'active' : '' }}">@icon('security') {{ trans('preferences.auth') }}</a>
|
||||
<a href="{{ url('/my-account/interface') }}" class="{{ $category === 'interface' ? 'active' : '' }}">@icon('interface') {{ trans('preferences.interface') }}</a>
|
||||
<a href="{{ url('/my-account/shortcuts') }}" class="{{ $category === 'shortcuts' ? 'active' : '' }}">@icon('shortcuts') {{ trans('preferences.shortcuts_interface') }}</a>
|
||||
@if(userCan(\BookStack\Permissions\Permission::ReceiveNotifications))
|
||||
<a href="{{ url('/my-account/notifications') }}" class="{{ $category === 'notifications' ? 'active' : '' }}">@icon('notifications') {{ trans('preferences.notifications') }}</a>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user