3 namespace BookStack\Entities\Tools;
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\BookChild;
7 use BookStack\Entities\Models\Chapter;
8 use BookStack\Entities\Models\Entity;
9 use BookStack\Entities\Models\Page;
10 use BookStack\Exceptions\SortOperationException;
11 use Illuminate\Support\Collection;
21 * BookContents constructor.
23 public function __construct(Book $book)
29 * Get the current priority of the last item
30 * at the top-level of the book.
32 public function getLastPriority(): int
34 $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
35 ->where('draft', '=', false)
36 ->where('chapter_id', '=', 0)->max('priority');
37 $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
40 return max($maxChapter, $maxPage, 1);
44 * Get the contents as a sorted collection tree.
46 public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
48 $pages = $this->getPages($showDrafts, $renderPages);
49 $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
50 $all = collect()->concat($pages)->concat($chapters);
51 $chapterMap = $chapters->keyBy('id');
52 $lonePages = collect();
54 $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
55 $chapter = $chapterMap->get($chapter_id);
57 $chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
59 $lonePages = $lonePages->concat($pages);
63 $chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
64 $chapter->setAttribute('visible_pages', collect([]));
67 $all->each(function (Entity $entity) use ($renderPages) {
68 $entity->setRelation('book', $this->book);
70 if ($renderPages && $entity instanceof Page) {
71 $entity->html = (new PageContent($entity))->render();
75 return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
79 * Function for providing a sorting score for an entity in relation to the
80 * other items within the book.
82 protected function bookChildSortFunc(): callable
84 return function (Entity $entity) {
85 if (isset($entity['draft']) && $entity['draft']) {
89 return $entity['priority'] ?? 0;
94 * Get the visible pages within this book.
96 protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection
98 $query = Page::visible()
99 ->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes)
100 ->where('book_id', '=', $this->book->id);
103 $query->where('draft', '=', false);
106 return $query->get();
110 * Sort the books content using the given sort map.
111 * Returns a list of books that were involved in the operation.
113 * @throws SortOperationException
115 public function sortUsingMap(BookSortMap $sortMap): Collection
117 // Load models into map
118 $this->loadModelsIntoSortMap($sortMap);
119 $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
122 foreach ($sortMap->all() as $item) {
123 $this->applySortUpdates($item);
126 // Update permissions and activity.
127 $booksInvolved->each(function (Book $book) {
128 $book->rebuildPermissions();
131 return $booksInvolved;
135 * Using the given sort map item, detect changes for the related model
136 * and update it if required.
138 protected function applySortUpdates(BookSortMapItem $sortMapItem): void
140 $model = $sortMapItem->model;
145 $priorityChanged = $model->priority !== $sortMapItem->sort;
146 $bookChanged = $model->book_id !== $sortMapItem->parentBookId;
147 $chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
150 $model->changeBook($sortMapItem->parentBookId);
153 if ($chapterChanged) {
154 $model->chapter_id = intval($sortMapItem->parentChapterId);
158 if ($priorityChanged) {
159 $model->priority = $sortMapItem->sort;
165 * Load models from the database into the given sort map.
167 protected function loadModelsIntoSortMap(BookSortMap $sortMap): void
169 $collection = collect($sortMap->all());
171 $keyMap = $collection->keyBy(function (BookSortMapItem $sortMapItem) {
172 return $sortMapItem->type . ':' . $sortMapItem->id;
175 $pageIds = $collection->where('type', '=', 'page')->pluck('id');
176 $chapterIds = $collection->where('type', '=', 'chapter')->pluck('id');
178 $pages = Page::visible()->whereIn('id', $pageIds)->get();
179 $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
181 foreach ($pages as $page) {
182 /** @var BookSortMapItem $sortItem */
183 $sortItem = $keyMap->get('page:' . $page->id);
184 $sortItem->model = $page;
187 foreach ($chapters as $chapter) {
188 /** @var BookSortMapItem $sortItem */
189 $sortItem = $keyMap->get('chapter:' . $chapter->id);
190 $sortItem->model = $chapter;
195 * Get the books involved in a sort.
196 * The given sort map should have its models loaded first.
198 * @throws SortOperationException
200 protected function getBooksInvolvedInSort(BookSortMap $sortMap): Collection
202 $collection = collect($sortMap->all());
204 $bookIdsInvolved = array_unique(array_merge(
206 $collection->pluck('parentBookId')->values()->all(),
207 $collection->pluck('model.book_id')->values()->all(),
210 $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
212 if (count($books) !== count($bookIdsInvolved)) {
213 throw new SortOperationException('Could not find all books requested in sort operation');