Added bookshelves homepage options

- Updated homepage selection UI to be more scalable
- Cleaned homepage selection logic in code
- Added seed test data for bookshelves
- Added bookshelves to permission system
This commit is contained in:
Dan Brown 2018-09-20 15:27:30 +01:00
parent 47b08888ba
commit 81eb642f75
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
16 changed files with 192 additions and 61 deletions

View File

@ -67,6 +67,15 @@ class Book extends Entity
return $this->hasMany(Chapter::class); 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. * Get an excerpt of this book's description to the specified length or less.
* @param int $length * @param int $length

View File

@ -33,42 +33,42 @@ class HomeController extends Controller
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor); $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12); $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
$customHomepage = false; $homepageOption = setting('app-homepage-type', 'default');
$books = false; if (!in_array($homepageOption, $homepageOptions)) {
$booksViewType = false; $homepageOption = 'default';
// 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);
}
} }
$view = 'home'; $commonData = [
if ($bookHomepageSetting) {
$view = 'home-book';
} else if ($customHomepage) {
$view = 'home-custom';
}
return view('common/' . $view, [
'activity' => $activity, 'activity' => $activity,
'recents' => $recents, 'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages, 'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages, '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);
} }
/** /**

View File

@ -1250,14 +1250,14 @@ class EntityRepo
*/ */
public function destroyPage(Page $page) public function destroyPage(Page $page)
{ {
$this->destroyEntityCommonRelations($page);
// Check if set as custom homepage // Check if set as custom homepage
$customHome = setting('app-homepage', '0:'); $customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) { if (intval($page->id) === intval(explode(':', $customHome)[0])) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl()); throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
} }
$this->destroyEntityCommonRelations($page);
// Delete Attached Files // Delete Attached Files
$attachmentService = app(AttachmentService::class); $attachmentService = app(AttachmentService::class);
foreach ($page->attachments as $attachment) { foreach ($page->attachments as $attachment) {

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Services;
use BookStack\Book; use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Chapter; use BookStack\Chapter;
use BookStack\Entity; use BookStack\Entity;
use BookStack\EntityPermission; use BookStack\EntityPermission;
@ -25,6 +26,7 @@ class PermissionService
public $book; public $book;
public $chapter; public $chapter;
public $page; public $page;
public $bookshelf;
protected $db; protected $db;
@ -38,18 +40,23 @@ class PermissionService
* PermissionService constructor. * PermissionService constructor.
* @param JointPermission $jointPermission * @param JointPermission $jointPermission
* @param EntityPermission $entityPermission * @param EntityPermission $entityPermission
* @param Role $role
* @param Connection $db * @param Connection $db
* @param Bookshelf $bookshelf
* @param Book $book * @param Book $book
* @param Chapter $chapter * @param Chapter $chapter
* @param Page $page * @param Page $page
* @param Role $role
*/ */
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role) public function __construct(
JointPermission $jointPermission, EntityPermission $entityPermission, Role $role, Connection $db,
Bookshelf $bookshelf, Book $book, Chapter $chapter, Page $page
)
{ {
$this->db = $db; $this->db = $db;
$this->jointPermission = $jointPermission; $this->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission; $this->entityPermission = $entityPermission;
$this->role = $role; $this->role = $role;
$this->bookshelf = $bookshelf;
$this->book = $book; $this->book = $book;
$this->chapter = $chapter; $this->chapter = $chapter;
$this->page = $page; $this->page = $page;
@ -159,6 +166,12 @@ class PermissionService
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) { $this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $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 * Build joint permissions for an array of books
* @param Collection $books * @param Collection $books
@ -257,6 +284,12 @@ class PermissionService
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) { $this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $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);
});
} }
/** /**

View File

@ -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) { $factory->define(BookStack\Book::class, function ($faker) {
return [ return [
'name' => $faker->sentence, 'name' => $faker->sentence,

View File

@ -21,23 +21,29 @@ class DummyContentSeeder extends Seeder
$role = \BookStack\Role::getRole('viewer'); $role = \BookStack\Role::getRole('viewer');
$viewerUser->attachRole($role); $viewerUser->attachRole($role);
factory(\BookStack\Book::class, 5)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]) $byData = ['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]) factory(\BookStack\Book::class, 5)->create($byData)
->each(function($chapter) use ($editorUser, $book){ ->each(function($book) use ($editorUser, $byData) {
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'book_id' => $book->id]); $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); $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->chapters()->saveMany($chapters);
$book->pages()->saveMany($pages); $book->pages()->saveMany($pages);
}); });
$largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), '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(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]); $pages = factory(\BookStack\Page::class, 200)->make($byData);
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]); $chapters = factory(\BookStack\Chapter::class, 50)->make($byData);
$largeBook->pages()->saveMany($pages); $largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters); $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\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities(); app(\BookStack\Services\SearchService::class)->indexAllEntities();
} }

View File

@ -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;

View File

@ -19,6 +19,7 @@ let componentMapping = {
'toggle-switch': require('./toggle-switch'), 'toggle-switch': require('./toggle-switch'),
'page-display': require('./page-display'), 'page-display': require('./page-display'),
'shelf-sort': require('./shelf-sort'), 'shelf-sort': require('./shelf-sort'),
'homepage-control': require('./homepage-control'),
}; };
window.components = {}; window.components = {};

View File

@ -15,18 +15,20 @@ class PagePicker {
} }
setupListeners() { setupListeners() {
// Select click this.selectButton.addEventListener('click', this.showPopup.bind(this));
this.selectButton.addEventListener('click', event => { this.display.parentElement.addEventListener('click', this.showPopup.bind(this));
window.EntitySelectorPopup.show(entity => {
this.setValue(entity.id, entity.name);
});
});
this.resetButton.addEventListener('click', event => { this.resetButton.addEventListener('click', event => {
this.setValue('', ''); this.setValue('', '');
}); });
} }
showPopup() {
window.EntitySelectorPopup.show(entity => {
this.setValue(entity.id, entity.name);
});
}
setValue(value, name) { setValue(value, name) {
this.value = value; this.value = value;
this.input.value = value; this.input.value = value;

View File

@ -52,6 +52,7 @@ return [
'details' => 'Details', 'details' => 'Details',
'grid_view' => 'Grid View', 'grid_view' => 'Grid View',
'list_view' => 'List View', 'list_view' => 'List View',
'default' => 'Default',
/** /**
* Header * Header

View File

@ -223,6 +223,7 @@ return [
'message' => ':start :time. Take care not to overwrite each other\'s updates!', '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_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
'pages_specific' => 'Specific Page',
/** /**
* Editor sidebar * Editor sidebar

View File

@ -32,9 +32,8 @@ return [
'app_primary_color' => 'Application primary color', 'app_primary_color' => 'Application primary color',
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.', 'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
'app_homepage' => 'Application Homepage', '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_desc' => 'Select a view 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_select' => 'Select a page',
'app_homepage_books' => 'Or select the books page as your homepage. This will override any page selected as your homepage.',
'app_disable_comments' => 'Disable comments', 'app_disable_comments' => 'Disable comments',
'app_disable_comments_desc' => 'Disable comments across all pages in the application. Existing comments are not shown.', 'app_disable_comments_desc' => 'Disable comments across all pages in the application. Existing comments are not shown.',

View File

@ -0,0 +1,18 @@
@extends('sidebar-layout')
@section('toolbar')
<div class="col-sm-6 faded">
<div class="action-buttons text-left">
<a expand-toggle=".entity-list.compact .entity-item-snippet" class="text-primary text-button">@icon('expand-text'){{ trans('common.toggle_details') }}</a>
@include('shelves/view-toggle', ['shelvesViewType' => $shelvesViewType])
</div>
</div>
@stop
@section('sidebar')
@include('common/home-sidebar')
@stop
@section('body')
@include('shelves/list', ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType])
@stop

View File

@ -10,7 +10,7 @@
@section('body') @section('body')
<div class="container"> <div class="container" id="home-default">
<div class="row"> <div class="row">
<div class="col-sm-4"> <div class="col-sm-4">

View File

@ -76,12 +76,22 @@
<input type="text" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1"> <input type="text" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
<input type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light"> <input type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
</div> </div>
<div class="form-group" id="homepage-control"> <div homepage-control class="form-group" id="homepage-control">
<label for="setting-app-homepage">{{ trans('settings.app_homepage') }}</label> <label for="setting-app-homepage">{{ trans('settings.app_homepage') }}</label>
<p class="small">{{ trans('settings.app_homepage_desc') }}</p> <p class="small">{{ trans('settings.app_homepage_desc') }}</p>
@include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_default'), 'value' => setting('app-homepage')])
<p class="small">{{ trans('settings.app_homepage_books') }}</p> <select name="setting-app-homepage-type" id="setting-app-homepage-type">
@include('components.toggle-switch', ['name' => 'setting-app-book-homepage', 'value' => setting('app-book-homepage')]) <option @if(setting('app-homepage-type') === 'default') selected @endif value="default">{{ trans('common.default') }}</option>
<option @if(setting('app-homepage-type') === 'books') selected @endif value="books">{{ trans('entities.books') }}</option>
<option @if(setting('app-homepage-type') === 'bookshelves') selected @endif value="bookshelves">{{ trans('entities.shelves') }}</option>
<option @if(setting('app-homepage-type') === 'page') selected @endif value="page">{{ trans('entities.pages_specific') }}</option>
</select>
<br><br>
<div page-picker-container style="display: none;">
@include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')])
</div>
</div> </div>
</div> </div>

View File

@ -10,15 +10,17 @@ class HomepageTest extends TestCase
$homeVisit->assertSee('My Recently Viewed'); $homeVisit->assertSee('My Recently Viewed');
$homeVisit->assertSee('Recently Updated Pages'); $homeVisit->assertSee('Recently Updated Pages');
$homeVisit->assertSee('Recent Activity'); $homeVisit->assertSee('Recent Activity');
$homeVisit->assertSee('home-default');
} }
public function test_custom_homepage() public function test_custom_homepage()
{ {
$this->asEditor(); $this->asEditor();
$name = 'My custom homepage'; $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]); $customPage = $this->newPage(['name' => $name, 'html' => $content]);
$this->setSettings(['app-homepage' => $customPage->id]); $this->setSettings(['app-homepage' => $customPage->id]);
$this->setSettings(['app-homepage-type' => 'page']);
$homeVisit = $this->get('/'); $homeVisit = $this->get('/');
$homeVisit->assertSee($name); $homeVisit->assertSee($name);
@ -32,7 +34,7 @@ class HomepageTest extends TestCase
{ {
$this->asEditor(); $this->asEditor();
$name = 'My custom homepage'; $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]); $customPage = $this->newPage(['name' => $name, 'html' => $content]);
$this->setSettings(['app-homepage' => $customPage->id]); $this->setSettings(['app-homepage' => $customPage->id]);
@ -55,7 +57,7 @@ class HomepageTest extends TestCase
$editor = $this->getEditor(); $editor = $this->getEditor();
setting()->putUser($editor, 'books_view_type', 'grid'); setting()->putUser($editor, 'books_view_type', 'grid');
$this->setSettings(['app-book-homepage' => true]); $this->setSettings(['app-homepage-type' => 'books']);
$this->asEditor(); $this->asEditor();
$homeVisit = $this->get('/'); $homeVisit = $this->get('/');
@ -65,7 +67,26 @@ class HomepageTest extends TestCase
$homeVisit->assertSee('grid-card-footer'); $homeVisit->assertSee('grid-card-footer');
$homeVisit->assertSee('featured-image-container'); $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(); $this->test_default_homepage_visible();
} }
} }