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