From c13ce1883708c184d14b3be10734894ddf7c9e00 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 5 Feb 2025 16:52:20 +0000 Subject: [PATCH] Sorting: Added book autosort logic --- app/Entities/Repos/BaseRepo.php | 15 +++++ app/Entities/Repos/ChapterRepo.php | 6 ++ app/Entities/Repos/PageRepo.php | 6 ++ app/Sorting/BookSorter.php | 48 ++++++++++++++ app/Sorting/SortSetOperation.php | 9 +++ app/Sorting/SortSetOperationComparisons.php | 69 +++++++++++++++++++++ 6 files changed, 153 insertions(+) create mode 100644 app/Sorting/SortSetOperationComparisons.php diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index 033350743..151d5b055 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -4,6 +4,7 @@ namespace BookStack\Entities\Repos; use BookStack\Activity\TagRepo; use BookStack\Entities\Models\Book; +use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\HasCoverImage; @@ -12,6 +13,7 @@ use BookStack\Entities\Queries\PageQueries; use BookStack\Exceptions\ImageUploadException; use BookStack\References\ReferenceStore; use BookStack\References\ReferenceUpdater; +use BookStack\Sorting\BookSorter; use BookStack\Uploads\ImageRepo; use BookStack\Util\HtmlDescriptionFilter; use Illuminate\Http\UploadedFile; @@ -24,6 +26,7 @@ class BaseRepo protected ReferenceUpdater $referenceUpdater, protected ReferenceStore $referenceStore, protected PageQueries $pageQueries, + protected BookSorter $bookSorter, ) { } @@ -134,6 +137,18 @@ class BaseRepo $entity->save(); } + /** + * Sort the parent of the given entity, if any auto sort actions are set for it. + * Typical ran during create/update/insert events. + */ + public function sortParent(Entity $entity): void + { + if ($entity instanceof BookChild) { + $book = $entity->book; + $this->bookSorter->runBookAutoSort($book); + } + } + protected function updateDescription(Entity $entity, array $input): void { if (!in_array(HasHtmlDescription::class, class_uses($entity))) { diff --git a/app/Entities/Repos/ChapterRepo.php b/app/Entities/Repos/ChapterRepo.php index 17cbccd41..fdf2de4e2 100644 --- a/app/Entities/Repos/ChapterRepo.php +++ b/app/Entities/Repos/ChapterRepo.php @@ -34,6 +34,8 @@ class ChapterRepo $this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null)); Activity::add(ActivityType::CHAPTER_CREATE, $chapter); + $this->baseRepo->sortParent($chapter); + return $chapter; } @@ -50,6 +52,8 @@ class ChapterRepo Activity::add(ActivityType::CHAPTER_UPDATE, $chapter); + $this->baseRepo->sortParent($chapter); + return $chapter; } @@ -88,6 +92,8 @@ class ChapterRepo $chapter->rebuildPermissions(); Activity::add(ActivityType::CHAPTER_MOVE, $chapter); + $this->baseRepo->sortParent($chapter); + return $parent; } } diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index 68b1c398f..c3be6d826 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -83,6 +83,7 @@ class PageRepo $draft->refresh(); Activity::add(ActivityType::PAGE_CREATE, $draft); + $this->baseRepo->sortParent($draft); return $draft; } @@ -128,6 +129,7 @@ class PageRepo } Activity::add(ActivityType::PAGE_UPDATE, $page); + $this->baseRepo->sortParent($page); return $page; } @@ -243,6 +245,8 @@ class PageRepo Activity::add(ActivityType::PAGE_RESTORE, $page); Activity::add(ActivityType::REVISION_RESTORE, $revision); + $this->baseRepo->sortParent($page); + return $page; } @@ -272,6 +276,8 @@ class PageRepo Activity::add(ActivityType::PAGE_MOVE, $page); + $this->baseRepo->sortParent($page); + return $parent; } diff --git a/app/Sorting/BookSorter.php b/app/Sorting/BookSorter.php index 7268b3543..e89fdaccc 100644 --- a/app/Sorting/BookSorter.php +++ b/app/Sorting/BookSorter.php @@ -16,6 +16,54 @@ class BookSorter ) { } + /** + * Runs the auto-sort for a book if the book has a sort set applied to it. + * This does not consider permissions since the sort operations are centrally + * managed by admins so considered permitted if existing and assigned. + */ + public function runBookAutoSort(Book $book): void + { + $set = $book->sortSet; + if (!$set) { + return; + } + + $sortFunctions = array_map(function (SortSetOperation $op) { + return $op->getSortFunction(); + }, $set->getOperations()); + + $chapters = $book->chapters() + ->with('pages:id,name,priority,created_at,updated_at') + ->get(['id', 'name', 'priority', 'created_at', 'updated_at']); + + /** @var (Chapter|Book)[] $topItems */ + $topItems = [ + ...$book->directPages()->get(['id', 'name', 'priority', 'created_at', 'updated_at']), + ...$chapters, + ]; + + foreach ($sortFunctions as $sortFunction) { + usort($topItems, $sortFunction); + } + + foreach ($topItems as $index => $topItem) { + $topItem->priority = $index + 1; + $topItem->save(); + } + + foreach ($chapters as $chapter) { + $pages = $chapter->pages->all(); + foreach ($sortFunctions as $sortFunction) { + usort($pages, $sortFunction); + } + + foreach ($pages as $index => $page) { + $page->priority = $index + 1; + $page->save(); + } + } + } + /** * Sort the books content using the given sort map. diff --git a/app/Sorting/SortSetOperation.php b/app/Sorting/SortSetOperation.php index a6dd860f5..7fdd0b002 100644 --- a/app/Sorting/SortSetOperation.php +++ b/app/Sorting/SortSetOperation.php @@ -2,6 +2,9 @@ namespace BookStack\Sorting; +use Closure; +use Illuminate\Support\Str; + enum SortSetOperation: string { case NameAsc = 'name_asc'; @@ -33,6 +36,12 @@ enum SortSetOperation: string return trim($label); } + public function getSortFunction(): callable + { + $camelValue = Str::camel($this->value); + return SortSetOperationComparisons::$camelValue(...); + } + /** * @return SortSetOperation[] */ diff --git a/app/Sorting/SortSetOperationComparisons.php b/app/Sorting/SortSetOperationComparisons.php new file mode 100644 index 000000000..e1c3e625f --- /dev/null +++ b/app/Sorting/SortSetOperationComparisons.php @@ -0,0 +1,69 @@ +name <=> $b->name; + } + + public static function nameDesc(Entity $a, Entity $b): int + { + return $b->name <=> $a->name; + } + + public static function nameNumericAsc(Entity $a, Entity $b): int + { + $numRegex = '/^\d+(\.\d+)?/'; + $aMatches = []; + $bMatches = []; + preg_match($numRegex, $a, $aMatches); + preg_match($numRegex, $b, $bMatches); + return ($aMatches[0] ?? 0) <=> ($bMatches[0] ?? 0); + } + + public static function nameNumericDesc(Entity $a, Entity $b): int + { + return -(static::nameNumericAsc($a, $b)); + } + + public static function createdDateAsc(Entity $a, Entity $b): int + { + return $a->created_at->unix() <=> $b->created_at->unix(); + } + + public static function createdDateDesc(Entity $a, Entity $b): int + { + return $b->created_at->unix() <=> $a->created_at->unix(); + } + + public static function updatedDateAsc(Entity $a, Entity $b): int + { + return $a->updated_at->unix() <=> $b->updated_at->unix(); + } + + public static function updatedDateDesc(Entity $a, Entity $b): int + { + return $b->updated_at->unix() <=> $a->updated_at->unix(); + } + + public static function chaptersFirst(Entity $a, Entity $b): int + { + return ($b instanceof Chapter ? 1 : 0) - (($a instanceof Chapter) ? 1 : 0); + } + + public static function chaptersLast(Entity $a, Entity $b): int + { + return ($a instanceof Chapter ? 1 : 0) - (($b instanceof Chapter) ? 1 : 0); + } +}