diff --git a/app/Book.php b/app/Book.php index effd6ca45..4e944ce10 100644 --- a/app/Book.php +++ b/app/Book.php @@ -67,6 +67,15 @@ class Book extends Entity return $this->hasMany(Chapter::class); } + /** + * Get the shelves this book is contained within. + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function shelves() + { + return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id'); + } + /** * Get an excerpt of this book's description to the specified length or less. * @param int $length diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 2077f6888..e47250318 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -33,42 +33,42 @@ class HomeController extends Controller $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor); $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12); - - $customHomepage = false; - $books = false; - $booksViewType = false; - - // Check book homepage - $bookHomepageSetting = setting('app-book-homepage'); - if ($bookHomepageSetting) { - $books = $this->entityRepo->getAllPaginated('book', 18); - $booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list')); - } else { - // Check custom homepage - $homepageSetting = setting('app-homepage'); - if ($homepageSetting) { - $id = intval(explode(':', $homepageSetting)[0]); - $customHomepage = $this->entityRepo->getById('page', $id, false, true); - $this->entityRepo->renderPage($customHomepage, true); - } + $homepageOptions = ['default', 'books', 'bookshelves', 'page']; + $homepageOption = setting('app-homepage-type', 'default'); + if (!in_array($homepageOption, $homepageOptions)) { + $homepageOption = 'default'; } - $view = 'home'; - if ($bookHomepageSetting) { - $view = 'home-book'; - } else if ($customHomepage) { - $view = 'home-custom'; - } - - return view('common/' . $view, [ + $commonData = [ 'activity' => $activity, 'recents' => $recents, 'recentlyUpdatedPages' => $recentlyUpdatedPages, 'draftPages' => $draftPages, - 'customHomepage' => $customHomepage, - 'books' => $books, - 'booksViewType' => $booksViewType - ]); + ]; + + if ($homepageOption === 'bookshelves') { + $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18); + $shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid')); + $data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]); + return view('common.home-shelves', $data); + } + + if ($homepageOption === 'books') { + $books = $this->entityRepo->getAllPaginated('book', 18); + $booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list')); + $data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]); + return view('common.home-book', $data); + } + + if ($homepageOption === 'page') { + $homepageSetting = setting('app-homepage', '0:'); + $id = intval(explode(':', $homepageSetting)[0]); + $customHomepage = $this->entityRepo->getById('page', $id, false, true); + $this->entityRepo->renderPage($customHomepage, true); + return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage])); + } + + return view('common.home', $commonData); } /** diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index ab4b7cc04..db9226411 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -1250,14 +1250,14 @@ class EntityRepo */ public function destroyPage(Page $page) { - $this->destroyEntityCommonRelations($page); - // Check if set as custom homepage $customHome = setting('app-homepage', '0:'); if (intval($page->id) === intval(explode(':', $customHome)[0])) { throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl()); } + $this->destroyEntityCommonRelations($page); + // Delete Attached Files $attachmentService = app(AttachmentService::class); foreach ($page->attachments as $attachment) { diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 428cb895f..13ec1d45b 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -1,6 +1,7 @@ db = $db; $this->jointPermission = $jointPermission; $this->entityPermission = $entityPermission; $this->role = $role; + $this->bookshelf = $bookshelf; $this->book = $book; $this->chapter = $chapter; $this->page = $page; @@ -159,6 +166,12 @@ class PermissionService $this->bookFetchQuery()->chunk(5, function ($books) use ($roles) { $this->buildJointPermissionsForBooks($books, $roles); }); + + // Chunk through all bookshelves + $this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by']) + ->chunk(50, function ($shelves) use ($roles) { + $this->buildJointPermissionsForShelves($shelves, $roles); + }); } /** @@ -174,6 +187,20 @@ class PermissionService }]); } + /** + * @param Collection $shelves + * @param array $roles + * @param bool $deleteOld + * @throws \Throwable + */ + protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false) + { + if ($deleteOld) { + $this->deleteManyJointPermissionsForEntities($shelves->all()); + } + $this->createManyJointPermissions($shelves, $roles); + } + /** * Build joint permissions for an array of books * @param Collection $books @@ -257,6 +284,12 @@ class PermissionService $this->bookFetchQuery()->chunk(20, function ($books) use ($roles) { $this->buildJointPermissionsForBooks($books, $roles); }); + + // Chunk through all bookshelves + $this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by']) + ->chunk(50, function ($shelves) use ($roles) { + $this->buildJointPermissionsForShelves($shelves, $roles); + }); } /** diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index c68f5c1e1..3d6ed1d63 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -21,6 +21,14 @@ $factory->define(BookStack\User::class, function ($faker) { ]; }); +$factory->define(BookStack\Bookshelf::class, function ($faker) { + return [ + 'name' => $faker->sentence, + 'slug' => str_random(10), + 'description' => $faker->paragraph + ]; +}); + $factory->define(BookStack\Book::class, function ($faker) { return [ 'name' => $faker->sentence, diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php index 41ac6650d..dcf589352 100644 --- a/database/seeds/DummyContentSeeder.php +++ b/database/seeds/DummyContentSeeder.php @@ -21,23 +21,29 @@ class DummyContentSeeder extends Seeder $role = \BookStack\Role::getRole('viewer'); $viewerUser->attachRole($role); - factory(\BookStack\Book::class, 5)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]) - ->each(function($book) use ($editorUser) { - $chapters = factory(\BookStack\Chapter::class, 3)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]) - ->each(function($chapter) use ($editorUser, $book){ - $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'book_id' => $book->id]); + $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]; + + factory(\BookStack\Book::class, 5)->create($byData) + ->each(function($book) use ($editorUser, $byData) { + $chapters = factory(\BookStack\Chapter::class, 3)->create($byData) + ->each(function($chapter) use ($editorUser, $book, $byData){ + $pages = factory(\BookStack\Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id])); $chapter->pages()->saveMany($pages); }); - $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]); + $pages = factory(\BookStack\Page::class, 3)->make($byData); $book->chapters()->saveMany($chapters); $book->pages()->saveMany($pages); }); - $largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]); - $pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]); - $chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]); + $largeBook = factory(\BookStack\Book::class)->create(array_merge($byData, ['name' => 'Large book' . str_random(10)])); + $pages = factory(\BookStack\Page::class, 200)->make($byData); + $chapters = factory(\BookStack\Chapter::class, 50)->make($byData); $largeBook->pages()->saveMany($pages); $largeBook->chapters()->saveMany($chapters); + + $shelves = factory(\BookStack\Bookshelf::class, 10)->create($byData); + $largeBook->shelves()->attach($shelves->pluck('id')); + app(\BookStack\Services\PermissionService::class)->buildJointPermissions(); app(\BookStack\Services\SearchService::class)->indexAllEntities(); } diff --git a/resources/assets/js/components/homepage-control.js b/resources/assets/js/components/homepage-control.js new file mode 100644 index 000000000..e1f66a592 --- /dev/null +++ b/resources/assets/js/components/homepage-control.js @@ -0,0 +1,22 @@ + +class HomepageControl { + + constructor(elem) { + this.elem = elem; + this.typeControl = elem.querySelector('[name="setting-app-homepage-type"]'); + this.pagePickerContainer = elem.querySelector('[page-picker-container]'); + + this.typeControl.addEventListener('change', this.controlPagePickerVisibility.bind(this)); + this.controlPagePickerVisibility(); + } + + controlPagePickerVisibility() { + const showPagePicker = this.typeControl.value === 'page'; + this.pagePickerContainer.style.display = (showPagePicker ? 'block' : 'none'); + } + + + +} + +module.exports = HomepageControl; \ No newline at end of file diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js index e1aef032c..768e0983f 100644 --- a/resources/assets/js/components/index.js +++ b/resources/assets/js/components/index.js @@ -19,6 +19,7 @@ let componentMapping = { 'toggle-switch': require('./toggle-switch'), 'page-display': require('./page-display'), 'shelf-sort': require('./shelf-sort'), + 'homepage-control': require('./homepage-control'), }; window.components = {}; diff --git a/resources/assets/js/components/page-picker.js b/resources/assets/js/components/page-picker.js index e697d5f68..5fd2920f4 100644 --- a/resources/assets/js/components/page-picker.js +++ b/resources/assets/js/components/page-picker.js @@ -15,18 +15,20 @@ class PagePicker { } setupListeners() { - // Select click - this.selectButton.addEventListener('click', event => { - window.EntitySelectorPopup.show(entity => { - this.setValue(entity.id, entity.name); - }); - }); + this.selectButton.addEventListener('click', this.showPopup.bind(this)); + this.display.parentElement.addEventListener('click', this.showPopup.bind(this)); this.resetButton.addEventListener('click', event => { this.setValue('', ''); }); } + showPopup() { + window.EntitySelectorPopup.show(entity => { + this.setValue(entity.id, entity.name); + }); + } + setValue(value, name) { this.value = value; this.input.value = value; diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index c2744d906..8e86129e2 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -52,6 +52,7 @@ return [ 'details' => 'Details', 'grid_view' => 'Grid View', 'list_view' => 'List View', + 'default' => 'Default', /** * Header diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 8d1363240..2228da2cd 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -223,6 +223,7 @@ return [ 'message' => ':start :time. Take care not to overwrite each other\'s updates!', ], 'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content', + 'pages_specific' => 'Specific Page', /** * Editor sidebar diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index d6fbb6107..e2c2ede2b 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -32,9 +32,8 @@ return [ 'app_primary_color' => 'Application primary color', 'app_primary_color_desc' => 'This should be a hex value.
Leave empty to reset to the default color.', 'app_homepage' => 'Application Homepage', - 'app_homepage_desc' => 'Select a page to show on the homepage instead of the default view. Page permissions are ignored for selected pages.', - 'app_homepage_default' => 'Default homepage view chosen', - 'app_homepage_books' => 'Or select the books page as your homepage. This will override any page selected as your homepage.', + 'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.', + 'app_homepage_select' => 'Select a page', 'app_disable_comments' => 'Disable comments', 'app_disable_comments_desc' => 'Disable comments across all pages in the application. Existing comments are not shown.', diff --git a/resources/views/common/home-shelves.blade.php b/resources/views/common/home-shelves.blade.php new file mode 100644 index 000000000..3ae055b33 --- /dev/null +++ b/resources/views/common/home-shelves.blade.php @@ -0,0 +1,18 @@ +@extends('sidebar-layout') + +@section('toolbar') +
+
+ @icon('expand-text'){{ trans('common.toggle_details') }} + @include('shelves/view-toggle', ['shelvesViewType' => $shelvesViewType]) +
+
+@stop + +@section('sidebar') + @include('common/home-sidebar') +@stop + +@section('body') + @include('shelves/list', ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]) +@stop \ No newline at end of file diff --git a/resources/views/common/home.blade.php b/resources/views/common/home.blade.php index bbddb072d..cc20fc68e 100644 --- a/resources/views/common/home.blade.php +++ b/resources/views/common/home.blade.php @@ -10,7 +10,7 @@ @section('body') -
+
diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php index 64017e6e0..3c563a61c 100644 --- a/resources/views/settings/index.blade.php +++ b/resources/views/settings/index.blade.php @@ -76,12 +76,22 @@
-
+

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

- @include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_default'), 'value' => setting('app-homepage')]) -

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

- @include('components.toggle-switch', ['name' => 'setting-app-book-homepage', 'value' => setting('app-book-homepage')]) + + + +

+ +
+ @include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')]) +
diff --git a/tests/HomepageTest.php b/tests/HomepageTest.php index 29e0985c3..86cae7893 100644 --- a/tests/HomepageTest.php +++ b/tests/HomepageTest.php @@ -10,15 +10,17 @@ class HomepageTest extends TestCase $homeVisit->assertSee('My Recently Viewed'); $homeVisit->assertSee('Recently Updated Pages'); $homeVisit->assertSee('Recent Activity'); + $homeVisit->assertSee('home-default'); } public function test_custom_homepage() { $this->asEditor(); $name = 'My custom homepage'; - $content = 'This is the body content of my custom homepage.'; + $content = str_repeat('This is the body content of my custom homepage.', 20); $customPage = $this->newPage(['name' => $name, 'html' => $content]); $this->setSettings(['app-homepage' => $customPage->id]); + $this->setSettings(['app-homepage-type' => 'page']); $homeVisit = $this->get('/'); $homeVisit->assertSee($name); @@ -32,7 +34,7 @@ class HomepageTest extends TestCase { $this->asEditor(); $name = 'My custom homepage'; - $content = 'This is the body content of my custom homepage.'; + $content = str_repeat('This is the body content of my custom homepage.', 20); $customPage = $this->newPage(['name' => $name, 'html' => $content]); $this->setSettings(['app-homepage' => $customPage->id]); @@ -55,7 +57,7 @@ class HomepageTest extends TestCase $editor = $this->getEditor(); setting()->putUser($editor, 'books_view_type', 'grid'); - $this->setSettings(['app-book-homepage' => true]); + $this->setSettings(['app-homepage-type' => 'books']); $this->asEditor(); $homeVisit = $this->get('/'); @@ -65,7 +67,26 @@ class HomepageTest extends TestCase $homeVisit->assertSee('grid-card-footer'); $homeVisit->assertSee('featured-image-container'); - $this->setSettings(['app-book-homepage' => false]); + $this->setSettings(['app-homepage-type' => false]); + $this->test_default_homepage_visible(); + } + + public function test_set_bookshelves_homepage() + { + $editor = $this->getEditor(); + setting()->putUser($editor, 'bookshelves_view_type', 'grid'); + + $this->setSettings(['app-homepage-type' => 'bookshelves']); + + $this->asEditor(); + $homeVisit = $this->get('/'); + $homeVisit->assertSee('Shelves'); + $homeVisit->assertSee('bookshelf-grid-item grid-card'); + $homeVisit->assertSee('grid-card-content'); + $homeVisit->assertSee('grid-card-footer'); + $homeVisit->assertSee('featured-image-container'); + + $this->setSettings(['app-homepage-type' => false]); $this->test_default_homepage_visible(); } }