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;
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;
protected ReferenceUpdater $referenceUpdater,
protected ReferenceStore $referenceStore,
protected PageQueries $pageQueries,
+ protected BookSorter $bookSorter,
) {
}
$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))) {
$this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
+ $this->baseRepo->sortParent($chapter);
+
return $chapter;
}
Activity::add(ActivityType::CHAPTER_UPDATE, $chapter);
+ $this->baseRepo->sortParent($chapter);
+
return $chapter;
}
$chapter->rebuildPermissions();
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
+ $this->baseRepo->sortParent($chapter);
+
return $parent;
}
}
$draft->refresh();
Activity::add(ActivityType::PAGE_CREATE, $draft);
+ $this->baseRepo->sortParent($draft);
return $draft;
}
}
Activity::add(ActivityType::PAGE_UPDATE, $page);
+ $this->baseRepo->sortParent($page);
return $page;
}
Activity::add(ActivityType::PAGE_RESTORE, $page);
Activity::add(ActivityType::REVISION_RESTORE, $revision);
+ $this->baseRepo->sortParent($page);
+
return $page;
}
Activity::add(ActivityType::PAGE_MOVE, $page);
+ $this->baseRepo->sortParent($page);
+
return $parent;
}
) {
}
+ /**
+ * 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.
namespace BookStack\Sorting;
+use Closure;
+use Illuminate\Support\Str;
+
enum SortSetOperation: string
{
case NameAsc = 'name_asc';
return trim($label);
}
+ public function getSortFunction(): callable
+ {
+ $camelValue = Str::camel($this->value);
+ return SortSetOperationComparisons::$camelValue(...);
+ }
+
/**
* @return SortSetOperation[]
*/
--- /dev/null
+<?php
+
+namespace BookStack\Sorting;
+
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+
+/**
+ * Sort comparison function for each of the possible SortSetOperation values.
+ * Method names should be camelCase names for the SortSetOperation enum value.
+ * TODO - Test to cover each SortSetOperation enum value is covered.
+ */
+class SortSetOperationComparisons
+{
+ public static function nameAsc(Entity $a, Entity $b): int
+ {
+ return $a->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);
+ }
+}