diff --git a/app/Book.php b/app/Book.php
index 51ea226b4..4e944ce10 100644
--- a/app/Book.php
+++ b/app/Book.php
@@ -48,14 +48,6 @@ class Book extends Entity
{
return $this->belongsTo(Image::class, 'image_id');
}
- /*
- * Get the edit url for this book.
- * @return string
- */
- public function getEditUrl()
- {
- return $this->getUrl() . '/edit';
- }
/**
* Get all pages within this book.
@@ -75,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/Bookshelf.php b/app/Bookshelf.php
new file mode 100644
index 000000000..ce2acbf0c
--- /dev/null
+++ b/app/Bookshelf.php
@@ -0,0 +1,84 @@
+belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
+ }
+
+ /**
+ * Get the url for this bookshelf.
+ * @param string|bool $path
+ * @return string
+ */
+ public function getUrl($path = false)
+ {
+ if ($path !== false) {
+ return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
+ }
+ return baseUrl('/shelves/' . urlencode($this->slug));
+ }
+
+ /**
+ * Returns BookShelf cover image, if cover does not exists return default cover image.
+ * @param int $width - Width of the image
+ * @param int $height - Height of the image
+ * @return string
+ */
+ public function getBookCover($width = 440, $height = 250)
+ {
+ $default = baseUrl('/book_default_cover.png');
+ if (!$this->image_id) {
+ return $default;
+ }
+
+ try {
+ $cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
+ } catch (\Exception $err) {
+ $cover = $default;
+ }
+ return $cover;
+ }
+
+ /**
+ * Get the cover image of the book
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function cover()
+ {
+ return $this->belongsTo(Image::class, 'image_id');
+ }
+
+ /**
+ * Get an excerpt of this book's description to the specified length or less.
+ * @param int $length
+ * @return string
+ */
+ public function getExcerpt($length = 100)
+ {
+ $description = $this->description;
+ return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
+ }
+
+ /**
+ * Return a generalised, common raw query that can be 'unioned' across entities.
+ * @return string
+ */
+ public function entityRawQuery()
+ {
+ return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+ }
+}
diff --git a/app/Entity.php b/app/Entity.php
index 5d4449f2b..fb1c6d48b 100644
--- a/app/Entity.php
+++ b/app/Entity.php
@@ -152,7 +152,7 @@ class Entity extends Ownable
*/
public static function getEntityInstance($type)
{
- $types = ['Page', 'Book', 'Chapter'];
+ $types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
$className = str_replace([' ', '-', '_'], '', ucwords($type));
if (!in_array($className, $types)) {
return null;
diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php
new file mode 100644
index 000000000..d1752d180
--- /dev/null
+++ b/app/Http/Controllers/BookshelfController.php
@@ -0,0 +1,244 @@
+entityRepo = $entityRepo;
+ $this->userRepo = $userRepo;
+ $this->exportService = $exportService;
+ parent::__construct();
+ }
+
+ /**
+ * Display a listing of the book.
+ * @return Response
+ */
+ public function index()
+ {
+ $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
+ $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
+ $popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
+ $new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
+ $shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
+
+ $this->setPageTitle(trans('entities.shelves'));
+ return view('shelves/index', [
+ 'shelves' => $shelves,
+ 'recents' => $recents,
+ 'popular' => $popular,
+ 'new' => $new,
+ 'shelvesViewType' => $shelvesViewType
+ ]);
+ }
+
+ /**
+ * Show the form for creating a new bookshelf.
+ * @return Response
+ */
+ public function create()
+ {
+ $this->checkPermission('bookshelf-create-all');
+ $books = $this->entityRepo->getAll('book', false, 'update');
+ $this->setPageTitle(trans('entities.shelves_create'));
+ return view('shelves/create', ['books' => $books]);
+ }
+
+ /**
+ * Store a newly created bookshelf in storage.
+ * @param Request $request
+ * @return Response
+ */
+ public function store(Request $request)
+ {
+ $this->checkPermission('bookshelf-create-all');
+ $this->validate($request, [
+ 'name' => 'required|string|max:255',
+ 'description' => 'string|max:1000',
+ ]);
+
+ $bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
+ $this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
+ Activity::add($bookshelf, 'bookshelf_create');
+
+ return redirect($bookshelf->getUrl());
+ }
+
+
+ /**
+ * Display the specified bookshelf.
+ * @param String $slug
+ * @return Response
+ * @throws \BookStack\Exceptions\NotFoundException
+ */
+ public function show(string $slug)
+ {
+ $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
+ $this->checkOwnablePermission('book-view', $bookshelf);
+
+ $books = $this->entityRepo->getBookshelfChildren($bookshelf);
+ Views::add($bookshelf);
+
+ $this->setPageTitle($bookshelf->getShortName());
+ return view('shelves/show', [
+ 'shelf' => $bookshelf,
+ 'books' => $books,
+ 'activity' => Activity::entityActivity($bookshelf, 20, 0)
+ ]);
+ }
+
+ /**
+ * Show the form for editing the specified bookshelf.
+ * @param $slug
+ * @return Response
+ * @throws \BookStack\Exceptions\NotFoundException
+ */
+ public function edit(string $slug)
+ {
+ $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
+ $this->checkOwnablePermission('bookshelf-update', $bookshelf);
+
+ $shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
+ $shelfBookIds = $shelfBooks->pluck('id');
+ $books = $this->entityRepo->getAll('book', false, 'update');
+ $books = $books->filter(function ($book) use ($shelfBookIds) {
+ return !$shelfBookIds->contains($book->id);
+ });
+
+ $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
+ return view('shelves/edit', [
+ 'shelf' => $bookshelf,
+ 'books' => $books,
+ 'shelfBooks' => $shelfBooks,
+ ]);
+ }
+
+
+ /**
+ * Update the specified bookshelf in storage.
+ * @param Request $request
+ * @param string $slug
+ * @return Response
+ * @throws \BookStack\Exceptions\NotFoundException
+ */
+ public function update(Request $request, string $slug)
+ {
+ $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
+ $this->checkOwnablePermission('bookshelf-update', $shelf);
+ $this->validate($request, [
+ 'name' => 'required|string|max:255',
+ 'description' => 'string|max:1000',
+ ]);
+
+ $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
+ $this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
+ Activity::add($shelf, 'bookshelf_update');
+
+ return redirect($shelf->getUrl());
+ }
+
+
+ /**
+ * Shows the page to confirm deletion
+ * @param $slug
+ * @return \Illuminate\View\View
+ * @throws \BookStack\Exceptions\NotFoundException
+ */
+ public function showDelete(string $slug)
+ {
+ $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
+ $this->checkOwnablePermission('bookshelf-delete', $bookshelf);
+
+ $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
+ return view('shelves/delete', ['shelf' => $bookshelf]);
+ }
+
+ /**
+ * Remove the specified bookshelf from storage.
+ * @param string $slug
+ * @return Response
+ * @throws \BookStack\Exceptions\NotFoundException
+ * @throws \Throwable
+ */
+ public function destroy(string $slug)
+ {
+ $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
+ $this->checkOwnablePermission('bookshelf-delete', $bookshelf);
+ Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
+ $this->entityRepo->destroyBookshelf($bookshelf);
+ return redirect('/shelves');
+ }
+
+ /**
+ * Show the Restrictions view.
+ * @param $slug
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * @throws \BookStack\Exceptions\NotFoundException
+ */
+ public function showRestrict(string $slug)
+ {
+ $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $this->checkOwnablePermission('restrictions-manage', $bookshelf);
+
+ $roles = $this->userRepo->getRestrictableRoles();
+ return view('shelves.restrictions', [
+ 'shelf' => $bookshelf,
+ 'roles' => $roles
+ ]);
+ }
+
+ /**
+ * Set the restrictions for this bookshelf.
+ * @param $slug
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * @throws \BookStack\Exceptions\NotFoundException
+ */
+ public function restrict(string $slug, Request $request)
+ {
+ $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $this->checkOwnablePermission('restrictions-manage', $bookshelf);
+
+ $this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
+ session()->flash('success', trans('entities.shelves_permissions_updated'));
+ return redirect($bookshelf->getUrl());
+ }
+
+ /**
+ * Copy the permissions of a bookshelf to the child books.
+ * @param string $slug
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * @throws \BookStack\Exceptions\NotFoundException
+ */
+ public function copyPermissions(string $slug)
+ {
+ $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $this->checkOwnablePermission('restrictions-manage', $bookshelf);
+
+ $updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
+ session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
+ return redirect($bookshelf->getUrl());
+ }
+
+}
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/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index d50baa86f..f6bd13e6f 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -252,7 +252,7 @@ class UserController extends Controller
return $this->currentUser->id == $id;
});
- $viewType = $request->get('book_view_type');
+ $viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
@@ -262,4 +262,27 @@ class UserController extends Controller
return redirect()->back(302, [], "/settings/users/$id");
}
+
+ /**
+ * Update the user's preferred shelf-list display setting.
+ * @param $id
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function switchShelfView($id, Request $request)
+ {
+ $this->checkPermissionOr('users-manage', function () use ($id) {
+ return $this->currentUser->id == $id;
+ });
+
+ $viewType = $request->get('view_type');
+ if (!in_array($viewType, ['grid', 'list'])) {
+ $viewType = 'list';
+ }
+
+ $user = $this->user->findOrFail($id);
+ setting()->putUser($user, 'bookshelves_view_type', $viewType);
+
+ return redirect()->back(302, [], "/settings/users/$id");
+ }
}
diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php
index bdd1e37b1..11f89fc34 100644
--- a/app/Repos/EntityRepo.php
+++ b/app/Repos/EntityRepo.php
@@ -1,6 +1,7 @@
bookshelf = $bookshelf;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
$this->pageRevision = $pageRevision;
$this->entities = [
+ 'bookshelf' => $this->bookshelf,
'page' => $this->page,
'chapter' => $this->chapter,
'book' => $this->book
@@ -331,6 +340,17 @@ class EntityRepo
->skip($count * $page)->take($count)->get();
}
+ /**
+ * Get the child items for a chapter sorted by priority but
+ * with draft items floated to the top.
+ * @param Bookshelf $bookshelf
+ * @return \Illuminate\Database\Eloquent\Collection|static[]
+ */
+ public function getBookshelfChildren(Bookshelf $bookshelf)
+ {
+ return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
+ }
+
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
@@ -533,6 +553,28 @@ class EntityRepo
return $entityModel;
}
+ /**
+ * Sync the books assigned to a shelf from a comma-separated list
+ * of book IDs.
+ * @param Bookshelf $shelf
+ * @param string $books
+ */
+ public function updateShelfBooks(Bookshelf $shelf, string $books)
+ {
+ $ids = explode(',', $books);
+
+ // Check books exist and match ordering
+ $bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
+ $syncData = [];
+ foreach ($ids as $index => $id) {
+ if ($bookIds->contains($id)) {
+ $syncData[$id] = ['order' => $index];
+ }
+ }
+
+ $shelf->books()->sync($syncData);
+ }
+
/**
* Change the book that an entity belongs to.
* @param string $type
@@ -1154,9 +1196,22 @@ class EntityRepo
$this->permissionService->buildJointPermissionsForEntity($book);
}
+ /**
+ * Destroy a bookshelf instance
+ * @param Bookshelf $shelf
+ * @throws \Throwable
+ */
+ public function destroyBookshelf(Bookshelf $shelf)
+ {
+ $this->destroyEntityCommonRelations($shelf);
+ $shelf->delete();
+ }
+
/**
* Destroy the provided book and all its child entities.
* @param Book $book
+ * @throws NotifyException
+ * @throws \Throwable
*/
public function destroyBook(Book $book)
{
@@ -1166,17 +1221,14 @@ class EntityRepo
foreach ($book->chapters as $chapter) {
$this->destroyChapter($chapter);
}
- \Activity::removeEntity($book);
- $book->views()->delete();
- $book->permissions()->delete();
- $this->permissionService->deleteJointPermissionsForEntity($book);
- $this->searchService->deleteEntityTerms($book);
+ $this->destroyEntityCommonRelations($book);
$book->delete();
}
/**
* Destroy a chapter and its relations.
* @param Chapter $chapter
+ * @throws \Throwable
*/
public function destroyChapter(Chapter $chapter)
{
@@ -1186,11 +1238,7 @@ class EntityRepo
$page->save();
}
}
- \Activity::removeEntity($chapter);
- $chapter->views()->delete();
- $chapter->permissions()->delete();
- $this->permissionService->deleteJointPermissionsForEntity($chapter);
- $this->searchService->deleteEntityTerms($chapter);
+ $this->destroyEntityCommonRelations($chapter);
$chapter->delete();
}
@@ -1198,23 +1246,18 @@ class EntityRepo
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
+ * @throws \Throwable
*/
public function destroyPage(Page $page)
{
- \Activity::removeEntity($page);
- $page->views()->delete();
- $page->tags()->delete();
- $page->revisions()->delete();
- $page->permissions()->delete();
- $this->permissionService->deleteJointPermissionsForEntity($page);
- $this->searchService->deleteEntityTerms($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) {
@@ -1223,4 +1266,46 @@ class EntityRepo
$page->delete();
}
+
+ /**
+ * Destroy or handle the common relations connected to an entity.
+ * @param Entity $entity
+ * @throws \Throwable
+ */
+ protected function destroyEntityCommonRelations(Entity $entity)
+ {
+ \Activity::removeEntity($entity);
+ $entity->views()->delete();
+ $entity->permissions()->delete();
+ $entity->tags()->delete();
+ $entity->comments()->delete();
+ $this->permissionService->deleteJointPermissionsForEntity($entity);
+ $this->searchService->deleteEntityTerms($entity);
+ }
+
+ /**
+ * Copy the permissions of a bookshelf to all child books.
+ * Returns the number of books that had permissions updated.
+ * @param Bookshelf $bookshelf
+ * @return int
+ * @throws \Throwable
+ */
+ public function copyBookshelfPermissions(Bookshelf $bookshelf)
+ {
+ $shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
+ $shelfBooks = $bookshelf->books()->get();
+ $updatedBookCount = 0;
+
+ foreach ($shelfBooks as $book) {
+ if (!userCan('restrictions-manage', $book)) continue;
+ $book->permissions()->delete();
+ $book->restricted = $bookshelf->restricted;
+ $book->permissions()->createMany($shelfPermissions);
+ $book->save();
+ $this->permissionService->buildJointPermissionsForEntity($book);
+ $updatedBookCount++;
+ }
+
+ return $updatedBookCount;
+ }
}
diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php
index 6f7ea1dc8..68c9270be 100644
--- a/app/Repos/PermissionsRepo.php
+++ b/app/Repos/PermissionsRepo.php
@@ -80,7 +80,7 @@ class PermissionsRepo
/**
* Updates an existing role.
- * Ensure Admin role always has all permissions.
+ * Ensure Admin role always have core permissions.
* @param $roleId
* @param $roleData
* @throws PermissionsException
@@ -90,13 +90,18 @@ class PermissionsRepo
$role = $this->role->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
- $this->assignRolePermissions($role, $permissions);
-
if ($role->system_name === 'admin') {
- $permissions = $this->permission->all()->pluck('id')->toArray();
- $role->permissions()->sync($permissions);
+ $permissions = array_merge($permissions, [
+ 'users-manage',
+ 'user-roles-manage',
+ 'restrictions-manage-all',
+ 'restrictions-manage-own',
+ 'settings-manage',
+ ]);
}
+ $this->assignRolePermissions($role, $permissions);
+
$role->fill($roleData);
$role->save();
$this->permissionService->buildJointPermissionForRole($role);
diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php
index 0dd316b34..dade68290 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;
- // TODO - Update so admin still goes through filters
}
/**
@@ -159,6 +165,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 +186,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
@@ -204,6 +230,7 @@ class PermissionService
/**
* Rebuild the entity jointPermissions for a particular entity.
* @param Entity $entity
+ * @throws \Throwable
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
@@ -214,7 +241,9 @@ class PermissionService
return;
}
- $entities[] = $entity->book;
+ if ($entity->book) {
+ $entities[] = $entity->book;
+ }
if ($entity->isA('page') && $entity->chapter_id) {
$entities[] = $entity->chapter;
@@ -226,13 +255,13 @@ class PermissionService
}
}
- $this->deleteManyJointPermissionsForEntities($entities);
$this->buildJointPermissionsForEntities(collect($entities));
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities
+ * @throws \Throwable
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
@@ -254,6 +283,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);
+ });
}
/**
@@ -412,7 +447,7 @@ class PermissionService
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
- if ($entity->isA('book')) {
+ if ($entity->isA('book') || $entity->isA('bookshelf')) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
}
@@ -484,11 +519,6 @@ class PermissionService
*/
public function checkOwnableUserAccess(Ownable $ownable, $permission)
{
- if ($this->isAdmin()) {
- $this->clean();
- return true;
- }
-
$explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id);
@@ -581,17 +611,16 @@ class PermissionService
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
- if (!$this->isAdmin()) {
- $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
- ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
- ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
- ->where(function ($query) {
- $query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
- $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
- });
+ // Add joint permission filter
+ $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
+ ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
+ ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
+ ->where(function ($query) {
+ $query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
+ $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
- $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
- }
+ });
+ $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
$this->clean();
@@ -619,11 +648,6 @@ class PermissionService
});
}
- if ($this->isAdmin()) {
- $this->clean();
- return $query;
- }
-
$this->currentAction = $action;
return $this->entityRestrictionQuery($query);
}
@@ -639,10 +663,6 @@ class PermissionService
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{
- if ($this->isAdmin()) {
- $this->clean();
- return $query;
- }
$this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
@@ -675,11 +695,6 @@ class PermissionService
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
{
- if ($this->isAdmin()) {
- $this->clean();
- return $query;
- }
-
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
@@ -704,19 +719,6 @@ class PermissionService
return $q;
}
- /**
- * Check if the current user is an admin.
- * @return bool
- */
- private function isAdmin()
- {
- if ($this->isAdminUser === null) {
- $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
- }
-
- return $this->isAdminUser;
- }
-
/**
* Get the current user
* @return User
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/migrations/2016_04_20_192649_create_joint_permissions_table.php b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php
index 4c1b43c4e..ce11f7b88 100644
--- a/database/migrations/2016_04_20_192649_create_joint_permissions_table.php
+++ b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php
@@ -74,10 +74,6 @@ class CreateJointPermissionsTable extends Migration
// Update admin role with system name
DB::table('roles')->where('name', '=', 'admin')->update(['system_name' => 'admin']);
-
- // Generate the new entity jointPermissions
- $restrictionService = app(\BookStack\Services\PermissionService::class);
- $restrictionService->buildJointPermissions();
}
/**
diff --git a/database/migrations/2018_08_04_115700_create_bookshelves_table.php b/database/migrations/2018_08_04_115700_create_bookshelves_table.php
new file mode 100644
index 000000000..e92b0edef
--- /dev/null
+++ b/database/migrations/2018_08_04_115700_create_bookshelves_table.php
@@ -0,0 +1,101 @@
+increments('id');
+ $table->string('name', 200);
+ $table->string('slug', 200);
+ $table->text('description');
+ $table->integer('created_by')->nullable()->default(null);
+ $table->integer('updated_by')->nullable()->default(null);
+ $table->boolean('restricted')->default(false);
+ $table->integer('image_id')->nullable()->default(null);
+ $table->timestamps();
+
+ $table->index('slug');
+ $table->index('created_by');
+ $table->index('updated_by');
+ $table->index('restricted');
+ });
+
+ Schema::create('bookshelves_books', function (Blueprint $table) {
+ $table->integer('bookshelf_id')->unsigned();
+ $table->integer('book_id')->unsigned();
+ $table->integer('order')->unsigned();
+
+ $table->foreign('bookshelf_id')->references('id')->on('bookshelves')
+ ->onUpdate('cascade')->onDelete('cascade');
+ $table->foreign('book_id')->references('id')->on('books')
+ ->onUpdate('cascade')->onDelete('cascade');
+
+ $table->primary(['bookshelf_id', 'book_id']);
+ });
+
+ // Copy existing role permissions from Books
+ $ops = ['View All', 'View Own', 'Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
+ foreach ($ops as $op) {
+ $dbOpName = strtolower(str_replace(' ', '-', $op));
+ $roleIdsWithBookPermission = DB::table('role_permissions')
+ ->leftJoin('permission_role', 'role_permissions.id', '=', 'permission_role.permission_id')
+ ->leftJoin('roles', 'roles.id', '=', 'permission_role.role_id')
+ ->where('role_permissions.name', '=', 'book-' . $dbOpName)->get(['roles.id'])->pluck('id');
+
+ $permId = DB::table('role_permissions')->insertGetId([
+ 'name' => 'bookshelf-' . $dbOpName,
+ 'display_name' => $op . ' ' . 'BookShelves',
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ ]);
+
+ $rowsToInsert = $roleIdsWithBookPermission->map(function($roleId) use ($permId) {
+ return [
+ 'role_id' => $roleId,
+ 'permission_id' => $permId
+ ];
+ })->toArray();
+
+ // Assign view permission to all current roles
+ DB::table('permission_role')->insert($rowsToInsert);
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ // Drop created permissions
+ $ops = ['bookshelf-create-all','bookshelf-create-own','bookshelf-delete-all','bookshelf-delete-own','bookshelf-update-all','bookshelf-update-own','bookshelf-view-all','bookshelf-view-own'];
+
+ $permissionIds = DB::table('role_permissions')->whereIn('name', $ops)
+ ->get(['id'])->pluck('id')->toArray();
+ DB::table('permission_role')->whereIn('permission_id', $permissionIds)->delete();
+ DB::table('role_permissions')->whereIn('id', $permissionIds)->delete();
+
+ // Drop shelves table
+ Schema::dropIfExists('bookshelves_books');
+ Schema::dropIfExists('bookshelves');
+
+ // Drop related polymorphic items
+ DB::table('activities')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('views')->where('viewable_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('tags')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('search_terms')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('comments')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
+ }
+}
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/package-lock.json b/package-lock.json
index ec4da5ce2..f8c43993b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5857,6 +5857,21 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
},
+ "jquery-sortable": {
+ "version": "0.9.13",
+ "resolved": "https://registry.npmjs.org/jquery-sortable/-/jquery-sortable-0.9.13.tgz",
+ "integrity": "sha1-HL+2VQE6B0c3BXHwbiL1JKAP+6I=",
+ "requires": {
+ "jquery": "^2.1.2"
+ },
+ "dependencies": {
+ "jquery": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
+ "integrity": "sha1-LInWiJterFIqfuoywUUhVZxsvwI="
+ }
+ }
+ },
"js-base64": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz",
diff --git a/package.json b/package.json
index 12d972cf9..58f2dad5e 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"codemirror": "^5.26.0",
"dropzone": "^5.4.0",
"jquery": "^3.3.1",
+ "jquery-sortable": "^0.9.13",
"markdown-it": "^8.3.1",
"markdown-it-task-lists": "^2.0.0",
"vue": "^2.2.6",
diff --git a/resources/assets/icons/bookshelf.svg b/resources/assets/icons/bookshelf.svg
new file mode 100644
index 000000000..03da68f96
--- /dev/null
+++ b/resources/assets/icons/bookshelf.svg
@@ -0,0 +1,2 @@
+
+
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 aa69f3265..768e0983f 100644
--- a/resources/assets/js/components/index.js
+++ b/resources/assets/js/components/index.js
@@ -18,6 +18,8 @@ let componentMapping = {
'collapsible': require('./collapsible'),
'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/assets/js/components/shelf-sort.js b/resources/assets/js/components/shelf-sort.js
new file mode 100644
index 000000000..59ac712a4
--- /dev/null
+++ b/resources/assets/js/components/shelf-sort.js
@@ -0,0 +1,71 @@
+
+class ShelfSort {
+
+ constructor(elem) {
+ this.elem = elem;
+ this.sortGroup = this.initSortable();
+ this.input = document.getElementById('books-input');
+ this.setupListeners();
+ }
+
+ initSortable() {
+ const sortable = require('jquery-sortable');
+ const placeHolderContent = this.getPlaceholderHTML();
+
+ return $('.scroll-box').sortable({
+ group: 'shelf-books',
+ exclude: '.instruction,.scroll-box-placeholder',
+ containerSelector: 'div.scroll-box',
+ itemSelector: '.scroll-box-item',
+ placeholder: placeHolderContent,
+ onDrop: this.onDrop.bind(this)
+ });
+ }
+
+ setupListeners() {
+ this.elem.addEventListener('click', event => {
+ const sortItem = event.target.closest('.scroll-box-item:not(.instruction)');
+ if (sortItem) {
+ event.preventDefault();
+ this.sortItemClick(sortItem);
+ }
+ });
+ }
+
+ /**
+ * Called when a sort item is clicked.
+ * @param {Element} sortItem
+ */
+ sortItemClick(sortItem) {
+ const lists = this.elem.querySelectorAll('.scroll-box');
+ const newList = Array.from(lists).filter(list => sortItem.parentElement !== list);
+ if (newList.length > 0) {
+ newList[0].appendChild(sortItem);
+ }
+ this.onChange();
+ }
+
+ onDrop($item, container, _super) {
+ this.onChange();
+ _super($item, container);
+ }
+
+ onChange() {
+ const data = this.sortGroup.sortable('serialize').get();
+ this.input.value = data[0].map(item => item.id).join(',');
+ const instruction = this.elem.querySelector('.scroll-box-item.instruction');
+ instruction.parentNode.insertBefore(instruction, instruction.parentNode.children[0]);
+ }
+
+ getPlaceholderHTML() {
+ const placeHolder = document.querySelector('.scroll-box-placeholder');
+ placeHolder.style.display = 'block';
+ const placeHolderContent = placeHolder.outerHTML;
+ placeHolder.style.display = 'none';
+ return placeHolderContent;
+ }
+
+
+}
+
+module.exports = ShelfSort;
\ No newline at end of file
diff --git a/resources/assets/sass/_grid.scss b/resources/assets/sass/_grid.scss
index 8f15153b5..0e1f85ce6 100644
--- a/resources/assets/sass/_grid.scss
+++ b/resources/assets/sass/_grid.scss
@@ -192,8 +192,26 @@ div[class^="col-"] img {
flex-direction: column;
border: 1px solid #ddd;
min-width: 100px;
+ h2 {
+ width: 100%;
+ font-size: 1.5em;
+ margin: 0 0 10px;
+ }
+ h2 a {
+ display: block;
+ width: 100%;
+ line-height: 1.2;
+ text-decoration: none;
+ }
+ p {
+ font-size: .85em;
+ margin: 0;
+ line-height: 1.6em;
+ }
.grid-card-content {
flex: 1;
+ border-top: 0;
+ border-bottom-width: 2px;
}
.grid-card-content, .grid-card-footer {
padding: $-l;
@@ -203,6 +221,23 @@ div[class^="col-"] img {
}
}
+.book-grid-item .grid-card-content h2 a {
+ color: $color-book;
+ fill: $color-book;
+}
+
+.bookshelf-grid-item .grid-card-content h2 a {
+ color: $color-bookshelf;
+ fill: $color-bookshelf;
+}
+
+.book-grid-item .grid-card-footer {
+ p.small {
+ font-size: .8em;
+ margin: 0;
+ }
+}
+
@include smaller-than($m) {
.grid.third {
grid-template-columns: 1fr 1fr;
diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss
index e8d131b52..0bf6be4c3 100644
--- a/resources/assets/sass/_lists.scss
+++ b/resources/assets/sass/_lists.scss
@@ -412,32 +412,3 @@ ul.pagination {
}
}
-.book-grid-item .grid-card-content {
- border-top: 0;
- border-bottom-width: 2px;
- h2 {
- width: 100%;
- font-size: 1.5em;
- margin: 0 0 10px;
- }
- h2 a {
- display: block;
- width: 100%;
- line-height: 1.2;
- color: #009688;;
- fill: #009688;;
- text-decoration: none;
- }
- p {
- font-size: .85em;
- margin: 0;
- line-height: 1.6em;
- }
-}
-
-.book-grid-item .grid-card-footer {
- p.small {
- font-size: .8em;
- margin: 0;
- }
-}
\ No newline at end of file
diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss
index da11846d8..63a91c968 100644
--- a/resources/assets/sass/_text.scss
+++ b/resources/assets/sass/_text.scss
@@ -281,6 +281,14 @@ p.secondary, p .secondary, span.secondary, .text-secondary {
}
}
+.text-bookshelf {
+ color: $color-bookshelf;
+ fill: $color-bookshelf;
+ &:hover {
+ color: $color-bookshelf;
+ fill: $color-bookshelf;
+ }
+}
.text-book {
color: $color-book;
fill: $color-book;
diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss
index e62d37efe..006d1b3f0 100644
--- a/resources/assets/sass/_variables.scss
+++ b/resources/assets/sass/_variables.scss
@@ -47,6 +47,7 @@ $warning: $secondary;
$primary-faded: rgba(21, 101, 192, 0.15);
// Item Colors
+$color-bookshelf: #af5a5a;
$color-book: #009688;
$color-chapter: #ef7c3c;
$color-page: $primary;
diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss
index 0b2dfbf75..ab5972cbd 100644
--- a/resources/assets/sass/styles.scss
+++ b/resources/assets/sass/styles.scss
@@ -206,6 +206,12 @@ $btt-size: 40px;
transition: all ease-in-out 120ms;
cursor: pointer;
}
+ &.compact {
+ font-size: 10px;
+ .entity-item-snippet {
+ display: none;
+ }
+ }
}
.entity-list-item.selected {
@@ -214,6 +220,20 @@ $btt-size: 40px;
}
}
+.scroll-box {
+ max-height: 250px;
+ overflow-y: scroll;
+ border: 1px solid #DDD;
+ border-radius: 3px;
+ .scroll-box-item {
+ padding: $-xs $-m;
+ border-bottom: 1px solid #DDD;
+ &:last-child {
+ border-bottom: 0;
+ }
+ }
+}
+
.center-box {
margin: $-xxl auto 0 auto;
width: 420px;
diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php
index 187fe1e53..153ae33f0 100644
--- a/resources/lang/en/activities.php
+++ b/resources/lang/en/activities.php
@@ -37,6 +37,14 @@ return [
'book_sort' => 'sorted book',
'book_sort_notification' => 'Book Successfully Re-sorted',
+ // Bookshelves
+ 'bookshelf_create' => 'created Bookshelf',
+ 'bookshelf_create_notification' => 'Bookshelf Successfully Created',
+ 'bookshelf_update' => 'updated bookshelf',
+ 'bookshelf_update_notification' => 'Bookshelf Successfully Updated',
+ 'bookshelf_delete' => 'deleted bookshelf',
+ 'bookshelf_delete_notification' => 'Bookshelf Successfully Deleted',
+
// Other
'commented_on' => 'commented on',
];
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 c99887401..4f110b724 100644
--- a/resources/lang/en/entities.php
+++ b/resources/lang/en/entities.php
@@ -64,6 +64,37 @@ return [
'search_set_date' => 'Set Date',
'search_update' => 'Update Search',
+ /**
+ * Shelves
+ */
+ 'shelves' => 'Shelves',
+ 'shelves_long' => 'Bookshelves',
+ 'shelves_empty' => 'No shelves have been created',
+ 'shelves_create' => 'Create New Shelf',
+ 'shelves_popular' => 'Popular Shelves',
+ 'shelves_new' => 'New Shelves',
+ 'shelves_popular_empty' => 'The most popular shelves will appear here.',
+ 'shelves_new_empty' => 'The most recently created shelves will appear here.',
+ 'shelves_save' => 'Save Shelf',
+ 'shelves_books' => 'Books on this shelf',
+ 'shelves_add_books' => 'Add books to this shelf',
+ 'shelves_drag_books' => 'Drag books here to add them to this shelf',
+ 'shelves_empty_contents' => 'This shelf has no books assigned to it',
+ 'shelves_edit_and_assign' => 'Edit shelf to assign books',
+ 'shelves_edit_named' => 'Edit Bookshelf :name',
+ 'shelves_edit' => 'Edit Bookshelf',
+ 'shelves_delete' => 'Delete Bookshelf',
+ 'shelves_delete_named' => 'Delete Bookshelf :name',
+ 'shelves_delete_explain' => "This will delete the bookshelf with the name ':name'. Contained books will not be deleted.",
+ 'shelves_delete_confirmation' => 'Are you sure you want to delete this bookshelf?',
+ 'shelves_permissions' => 'Bookshelf Permissions',
+ 'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
+ 'shelves_permissions_active' => 'Bookshelf Permissions Active',
+ 'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',
+ 'shelves_copy_permissions' => 'Copy Permissions',
+ 'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.',
+ 'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books',
+
/**
* Books
*/
@@ -199,6 +230,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
@@ -206,6 +238,7 @@ return [
'page_tags' => 'Page Tags',
'chapter_tags' => 'Chapter Tags',
'book_tags' => 'Book Tags',
+ 'shelf_tags' => 'Shelf Tags',
'tag' => 'Tag',
'tags' => 'Tags',
'tag_value' => 'Tag Value (Optional)',
diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php
index a86a1cdfc..fb09841cf 100644
--- a/resources/lang/en/errors.php
+++ b/resources/lang/en/errors.php
@@ -49,6 +49,7 @@ return [
// Entities
'entity_not_found' => 'Entity not found',
+ 'bookshelf_not_found' => 'Bookshelf not found',
'book_not_found' => 'Book not found',
'page_not_found' => 'Page not found',
'chapter_not_found' => 'Chapter not found',
diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php
index d6fbb6107..80ab77d19 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.',
@@ -91,6 +90,7 @@ return [
'role_manage_settings' => 'Manage app settings',
'role_asset' => 'Asset Permissions',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
+ 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
'role_all' => 'All',
'role_own' => 'Own',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php
index 8f6c2eb46..016f8e833 100644
--- a/resources/views/base.blade.php
+++ b/resources/views/base.blade.php
@@ -33,7 +33,7 @@