Sorting: Renamed sort set to sort rule

Renamed based on feedback from Tim and Script on Discord.
Also fixed flaky test
This commit is contained in:
Dan Brown 2025-02-11 14:36:25 +00:00
parent a208c46b62
commit b9306a9029
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
30 changed files with 232 additions and 224 deletions

View File

@ -71,9 +71,9 @@ class ActivityType
const IMPORT_RUN = 'import_run'; const IMPORT_RUN = 'import_run';
const IMPORT_DELETE = 'import_delete'; const IMPORT_DELETE = 'import_delete';
const SORT_SET_CREATE = 'sort_set_create'; const SORT_RULE_CREATE = 'sort_rule_create';
const SORT_SET_UPDATE = 'sort_set_update'; const SORT_RULE_UPDATE = 'sort_rule_update';
const SORT_SET_DELETE = 'sort_set_delete'; const SORT_RULE_DELETE = 'sort_rule_delete';
/** /**
* Get all the possible values. * Get all the possible values.

View File

@ -4,7 +4,7 @@ namespace BookStack\Console\Commands;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Sorting\BookSorter; use BookStack\Sorting\BookSorter;
use BookStack\Sorting\SortSet; use BookStack\Sorting\SortRule;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class AssignSortSetCommand extends Command class AssignSortSetCommand extends Command
@ -37,7 +37,7 @@ class AssignSortSetCommand extends Command
return $this->listSortSets(); return $this->listSortSets();
} }
$set = SortSet::query()->find($sortSetId); $set = SortRule::query()->find($sortSetId);
if ($this->option('all-books')) { if ($this->option('all-books')) {
$query = Book::query(); $query = Book::query();
} else if ($this->option('books-without-sort')) { } else if ($this->option('books-without-sort')) {
@ -87,7 +87,7 @@ class AssignSortSetCommand extends Command
protected function listSortSets(): int protected function listSortSets(): int
{ {
$sets = SortSet::query()->orderBy('id', 'asc')->get(); $sets = SortRule::query()->orderBy('id', 'asc')->get();
$this->error("Sort set ID required!"); $this->error("Sort set ID required!");
$this->warn("\nAvailable sort sets:"); $this->warn("\nAvailable sort sets:");
foreach ($sets as $set) { foreach ($sets as $set) {

View File

@ -2,7 +2,7 @@
namespace BookStack\Entities\Models; namespace BookStack\Entities\Models;
use BookStack\Sorting\SortSet; use BookStack\Sorting\SortRule;
use BookStack\Uploads\Image; use BookStack\Uploads\Image;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -17,14 +17,14 @@ use Illuminate\Support\Collection;
* @property string $description * @property string $description
* @property int $image_id * @property int $image_id
* @property ?int $default_template_id * @property ?int $default_template_id
* @property ?int $sort_set_id * @property ?int $sort_rule_id
* @property Image|null $cover * @property Image|null $cover
* @property \Illuminate\Database\Eloquent\Collection $chapters * @property \Illuminate\Database\Eloquent\Collection $chapters
* @property \Illuminate\Database\Eloquent\Collection $pages * @property \Illuminate\Database\Eloquent\Collection $pages
* @property \Illuminate\Database\Eloquent\Collection $directPages * @property \Illuminate\Database\Eloquent\Collection $directPages
* @property \Illuminate\Database\Eloquent\Collection $shelves * @property \Illuminate\Database\Eloquent\Collection $shelves
* @property ?Page $defaultTemplate * @property ?Page $defaultTemplate
* @property ?SortSet $sortSet * @property ?SortRule $sortRule
*/ */
class Book extends Entity implements HasCoverImage class Book extends Entity implements HasCoverImage
{ {
@ -88,9 +88,9 @@ class Book extends Entity implements HasCoverImage
/** /**
* Get the sort set assigned to this book, if existing. * Get the sort set assigned to this book, if existing.
*/ */
public function sortSet(): BelongsTo public function sortRule(): BelongsTo
{ {
return $this->belongsTo(SortSet::class); return $this->belongsTo(SortRule::class);
} }
/** /**

View File

@ -8,7 +8,7 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\TrashCan; use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Sorting\SortSet; use BookStack\Sorting\SortRule;
use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageRepo;
use Exception; use Exception;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
@ -35,8 +35,8 @@ class BookRepo
Activity::add(ActivityType::BOOK_CREATE, $book); Activity::add(ActivityType::BOOK_CREATE, $book);
$defaultBookSortSetting = intval(setting('sorting-book-default', '0')); $defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
if ($defaultBookSortSetting && SortSet::query()->find($defaultBookSortSetting)) { if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {
$book->sort_set_id = $defaultBookSortSetting; $book->sort_rule_id = $defaultBookSortSetting;
$book->save(); $book->save();
} }

View File

@ -69,10 +69,10 @@ class BookSortController extends Controller
if ($request->filled('auto-sort')) { if ($request->filled('auto-sort')) {
$sortSetId = intval($request->get('auto-sort')) ?: null; $sortSetId = intval($request->get('auto-sort')) ?: null;
if ($sortSetId && SortSet::query()->find($sortSetId) === null) { if ($sortSetId && SortRule::query()->find($sortSetId) === null) {
$sortSetId = null; $sortSetId = null;
} }
$book->sort_set_id = $sortSetId; $book->sort_rule_id = $sortSetId;
$book->save(); $book->save();
$sorter->runBookAutoSort($book); $sorter->runBookAutoSort($book);
if (!$loggedActivityForBook) { if (!$loggedActivityForBook) {

View File

@ -16,7 +16,7 @@ class BookSorter
) { ) {
} }
public function runBookAutoSortForAllWithSet(SortSet $set): void public function runBookAutoSortForAllWithSet(SortRule $set): void
{ {
$set->books()->chunk(50, function ($books) { $set->books()->chunk(50, function ($books) {
foreach ($books as $book) { foreach ($books as $book) {
@ -32,12 +32,12 @@ class BookSorter
*/ */
public function runBookAutoSort(Book $book): void public function runBookAutoSort(Book $book): void
{ {
$set = $book->sortSet; $set = $book->sortRule;
if (!$set) { if (!$set) {
return; return;
} }
$sortFunctions = array_map(function (SortSetOperation $op) { $sortFunctions = array_map(function (SortRuleOperation $op) {
return $op->getSortFunction(); return $op->getSortFunction();
}, $set->getOperations()); }, $set->getOperations());

View File

@ -17,24 +17,24 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property Carbon $created_at * @property Carbon $created_at
* @property Carbon $updated_at * @property Carbon $updated_at
*/ */
class SortSet extends Model implements Loggable class SortRule extends Model implements Loggable
{ {
use HasFactory; use HasFactory;
/** /**
* @return SortSetOperation[] * @return SortRuleOperation[]
*/ */
public function getOperations(): array public function getOperations(): array
{ {
return SortSetOperation::fromSequence($this->sequence); return SortRuleOperation::fromSequence($this->sequence);
} }
/** /**
* @param SortSetOperation[] $options * @param SortRuleOperation[] $options
*/ */
public function setOperations(array $options): void public function setOperations(array $options): void
{ {
$values = array_map(fn (SortSetOperation $opt) => $opt->value, $options); $values = array_map(fn (SortRuleOperation $opt) => $opt->value, $options);
$this->sequence = implode(',', $values); $this->sequence = implode(',', $values);
} }
@ -45,7 +45,7 @@ class SortSet extends Model implements Loggable
public function getUrl(): string public function getUrl(): string
{ {
return url("/settings/sorting/sets/{$this->id}"); return url("/settings/sorting/rules/{$this->id}");
} }
public function books(): HasMany public function books(): HasMany

View File

@ -6,7 +6,7 @@ use BookStack\Activity\ActivityType;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class SortSetController extends Controller class SortRuleController extends Controller
{ {
public function __construct() public function __construct()
{ {
@ -15,9 +15,9 @@ class SortSetController extends Controller
public function create() public function create()
{ {
$this->setPageTitle(trans('settings.sort_set_create')); $this->setPageTitle(trans('settings.sort_rule_create'));
return view('settings.sort-sets.create'); return view('settings.sort-rules.create');
} }
public function store(Request $request) public function store(Request $request)
@ -27,28 +27,28 @@ class SortSetController extends Controller
'sequence' => ['required', 'string', 'min:1'], 'sequence' => ['required', 'string', 'min:1'],
]); ]);
$operations = SortSetOperation::fromSequence($request->input('sequence')); $operations = SortRuleOperation::fromSequence($request->input('sequence'));
if (count($operations) === 0) { if (count($operations) === 0) {
return redirect()->withInput()->withErrors(['sequence' => 'No operations set.']); return redirect()->withInput()->withErrors(['sequence' => 'No operations set.']);
} }
$set = new SortSet(); $rule = new SortRule();
$set->name = $request->input('name'); $rule->name = $request->input('name');
$set->setOperations($operations); $rule->setOperations($operations);
$set->save(); $rule->save();
$this->logActivity(ActivityType::SORT_SET_CREATE, $set); $this->logActivity(ActivityType::SORT_RULE_CREATE, $rule);
return redirect('/settings/sorting'); return redirect('/settings/sorting');
} }
public function edit(string $id) public function edit(string $id)
{ {
$set = SortSet::query()->findOrFail($id); $rule = SortRule::query()->findOrFail($id);
$this->setPageTitle(trans('settings.sort_set_edit')); $this->setPageTitle(trans('settings.sort_rule_edit'));
return view('settings.sort-sets.edit', ['set' => $set]); return view('settings.sort-rules.edit', ['rule' => $rule]);
} }
public function update(string $id, Request $request, BookSorter $bookSorter) public function update(string $id, Request $request, BookSorter $bookSorter)
@ -58,21 +58,21 @@ class SortSetController extends Controller
'sequence' => ['required', 'string', 'min:1'], 'sequence' => ['required', 'string', 'min:1'],
]); ]);
$set = SortSet::query()->findOrFail($id); $rule = SortRule::query()->findOrFail($id);
$operations = SortSetOperation::fromSequence($request->input('sequence')); $operations = SortRuleOperation::fromSequence($request->input('sequence'));
if (count($operations) === 0) { if (count($operations) === 0) {
return redirect($set->getUrl())->withInput()->withErrors(['sequence' => 'No operations set.']); return redirect($rule->getUrl())->withInput()->withErrors(['sequence' => 'No operations set.']);
} }
$set->name = $request->input('name'); $rule->name = $request->input('name');
$set->setOperations($operations); $rule->setOperations($operations);
$changedSequence = $set->isDirty('sequence'); $changedSequence = $rule->isDirty('sequence');
$set->save(); $rule->save();
$this->logActivity(ActivityType::SORT_SET_UPDATE, $set); $this->logActivity(ActivityType::SORT_RULE_UPDATE, $rule);
if ($changedSequence) { if ($changedSequence) {
$bookSorter->runBookAutoSortForAllWithSet($set); $bookSorter->runBookAutoSortForAllWithSet($rule);
} }
return redirect('/settings/sorting'); return redirect('/settings/sorting');
@ -80,16 +80,16 @@ class SortSetController extends Controller
public function destroy(string $id, Request $request) public function destroy(string $id, Request $request)
{ {
$set = SortSet::query()->findOrFail($id); $rule = SortRule::query()->findOrFail($id);
$confirmed = $request->input('confirm') === 'true'; $confirmed = $request->input('confirm') === 'true';
$booksAssigned = $set->books()->count(); $booksAssigned = $rule->books()->count();
$warnings = []; $warnings = [];
if ($booksAssigned > 0) { if ($booksAssigned > 0) {
if ($confirmed) { if ($confirmed) {
$set->books()->update(['sort_set_id' => null]); $rule->books()->update(['sort_rule_id' => null]);
} else { } else {
$warnings[] = trans('settings.sort_set_delete_warn_books', ['count' => $booksAssigned]); $warnings[] = trans('settings.sort_rule_delete_warn_books', ['count' => $booksAssigned]);
} }
} }
@ -98,16 +98,16 @@ class SortSetController extends Controller
if ($confirmed) { if ($confirmed) {
setting()->remove('sorting-book-default'); setting()->remove('sorting-book-default');
} else { } else {
$warnings[] = trans('settings.sort_set_delete_warn_default'); $warnings[] = trans('settings.sort_rule_delete_warn_default');
} }
} }
if (count($warnings) > 0) { if (count($warnings) > 0) {
return redirect($set->getUrl() . '#delete')->withErrors(['delete' => $warnings]); return redirect($rule->getUrl() . '#delete')->withErrors(['delete' => $warnings]);
} }
$set->delete(); $rule->delete();
$this->logActivity(ActivityType::SORT_SET_DELETE, $set); $this->logActivity(ActivityType::SORT_RULE_DELETE, $rule);
return redirect('/settings/sorting'); return redirect('/settings/sorting');
} }

View File

@ -5,7 +5,7 @@ namespace BookStack\Sorting;
use Closure; use Closure;
use Illuminate\Support\Str; use Illuminate\Support\Str;
enum SortSetOperation: string enum SortRuleOperation: string
{ {
case NameAsc = 'name_asc'; case NameAsc = 'name_asc';
case NameDesc = 'name_desc'; case NameDesc = 'name_desc';
@ -26,13 +26,13 @@ enum SortSetOperation: string
$label = ''; $label = '';
if (str_ends_with($key, '_asc')) { if (str_ends_with($key, '_asc')) {
$key = substr($key, 0, -4); $key = substr($key, 0, -4);
$label = trans('settings.sort_set_op_asc'); $label = trans('settings.sort_rule_op_asc');
} elseif (str_ends_with($key, '_desc')) { } elseif (str_ends_with($key, '_desc')) {
$key = substr($key, 0, -5); $key = substr($key, 0, -5);
$label = trans('settings.sort_set_op_desc'); $label = trans('settings.sort_rule_op_desc');
} }
$label = trans('settings.sort_set_op_' . $key) . ' ' . $label; $label = trans('settings.sort_rule_op_' . $key) . ' ' . $label;
return trim($label); return trim($label);
} }
@ -43,12 +43,12 @@ enum SortSetOperation: string
} }
/** /**
* @return SortSetOperation[] * @return SortRuleOperation[]
*/ */
public static function allExcluding(array $operations): array public static function allExcluding(array $operations): array
{ {
$all = SortSetOperation::cases(); $all = SortRuleOperation::cases();
$filtered = array_filter($all, function (SortSetOperation $operation) use ($operations) { $filtered = array_filter($all, function (SortRuleOperation $operation) use ($operations) {
return !in_array($operation, $operations); return !in_array($operation, $operations);
}); });
return array_values($filtered); return array_values($filtered);
@ -57,12 +57,12 @@ enum SortSetOperation: string
/** /**
* Create a set of operations from a string sequence representation. * Create a set of operations from a string sequence representation.
* (values seperated by commas). * (values seperated by commas).
* @return SortSetOperation[] * @return SortRuleOperation[]
*/ */
public static function fromSequence(string $sequence): array public static function fromSequence(string $sequence): array
{ {
$strOptions = explode(',', $sequence); $strOptions = explode(',', $sequence);
$options = array_map(fn ($val) => SortSetOperation::tryFrom($val), $strOptions); $options = array_map(fn ($val) => SortRuleOperation::tryFrom($val), $strOptions);
return array_filter($options); return array_filter($options);
} }
} }

View File

@ -27,7 +27,7 @@ class BookFactory extends Factory
'slug' => Str::random(10), 'slug' => Str::random(10),
'description' => $description, 'description' => $description,
'description_html' => '<p>' . e($description) . '</p>', 'description_html' => '<p>' . e($description) . '</p>',
'sort_set_id' => null, 'sort_rule_id' => null,
'default_template_id' => null, 'default_template_id' => null,
]; ];
} }

View File

@ -2,25 +2,25 @@
namespace Database\Factories\Sorting; namespace Database\Factories\Sorting;
use BookStack\Sorting\SortSet; use BookStack\Sorting\SortRule;
use BookStack\Sorting\SortSetOperation; use BookStack\Sorting\SortRuleOperation;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
class SortSetFactory extends Factory class SortRuleFactory extends Factory
{ {
/** /**
* The name of the factory's corresponding model. * The name of the factory's corresponding model.
* *
* @var string * @var string
*/ */
protected $model = SortSet::class; protected $model = SortRule::class;
/** /**
* Define the model's default state. * Define the model's default state.
*/ */
public function definition(): array public function definition(): array
{ {
$cases = SortSetOperation::cases(); $cases = SortRuleOperation::cases();
$op = $cases[array_rand($cases)]; $op = $cases[array_rand($cases)];
return [ return [
'name' => $op->name . ' Sort', 'name' => $op->name . ' Sort',

View File

@ -11,7 +11,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('sort_sets', function (Blueprint $table) { Schema::create('sort_rules', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->string('name'); $table->string('name');
$table->text('sequence'); $table->text('sequence');
@ -24,6 +24,6 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
Schema::dropIfExists('sort_sets'); Schema::dropIfExists('sort_rules');
} }
}; };

View File

@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::table('books', function (Blueprint $table) { Schema::table('books', function (Blueprint $table) {
$table->unsignedInteger('sort_set_id')->nullable()->default(null); $table->unsignedInteger('sort_rule_id')->nullable()->default(null);
}); });
} }
@ -22,7 +22,7 @@ return new class extends Migration
public function down(): void public function down(): void
{ {
Schema::table('books', function (Blueprint $table) { Schema::table('books', function (Blueprint $table) {
$table->dropColumn('sort_set_id'); $table->dropColumn('sort_rule_id');
}); });
} }
}; };

View File

@ -166,7 +166,7 @@ return [
'books_search_this' => 'Search this book', 'books_search_this' => 'Search this book',
'books_navigation' => 'Book Navigation', 'books_navigation' => 'Book Navigation',
'books_sort' => 'Sort Book Contents', 'books_sort' => 'Sort Book Contents',
'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort option can be set to automatically sort this book\'s contents upon changes.', 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.',
'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort' => 'Auto Sort Option',
'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',
'books_sort_named' => 'Sort Book :bookName', 'books_sort_named' => 'Sort Book :bookName',

View File

@ -77,32 +77,32 @@ return [
// Sorting Settings // Sorting Settings
'sorting' => 'Sorting', 'sorting' => 'Sorting',
'sorting_book_default' => 'Default Book Sort', 'sorting_book_default' => 'Default Book Sort',
'sorting_book_default_desc' => 'Select the default sort set to apply to new books. This won\'t affect existing books, and can be overridden per-book.', 'sorting_book_default_desc' => 'Select the default sort role to apply to new books. This won\'t affect existing books, and can be overridden per-book.',
'sorting_sets' => 'Sort Sets', 'sorting_rules' => 'Sort Rules',
'sorting_sets_desc' => 'These are predefined sorting operations which can be applied to content in the system.', 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',
'sort_set_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books', 'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',
'sort_set_create' => 'Create Sort Set', 'sort_rule_create' => 'Create Sort Rule',
'sort_set_edit' => 'Edit Sort Set', 'sort_rule_edit' => 'Edit Sort Rule',
'sort_set_delete' => 'Delete Sort Set', 'sort_rule_delete' => 'Delete Sort Rule',
'sort_set_delete_desc' => 'Remove this sort set from the system. Books using this sort will revert to manual sorting.', 'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',
'sort_set_delete_warn_books' => 'This sort set is currently used on :count book(s). Are you sure you want to delete this?', 'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',
'sort_set_delete_warn_default' => 'This sort set is currently used as the default for books. Are you sure you want to delete this?', 'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',
'sort_set_details' => 'Sort Set Details', 'sort_rule_details' => 'Sort Rule Details',
'sort_set_details_desc' => 'Set a name for this sort set, which will appear in lists when users are selecting a sort.', 'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',
'sort_set_operations' => 'Sort Operations', 'sort_rule_operations' => 'Sort Operations',
'sort_set_operations_desc' => 'Configure the sort actions to be performed in this set by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom.', 'sort_rule_operations_desc' => 'Configure the sort actions to be performed in this set by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom.',
'sort_set_available_operations' => 'Available Operations', 'sort_rule_available_operations' => 'Available Operations',
'sort_set_available_operations_empty' => 'No operations remaining', 'sort_rule_available_operations_empty' => 'No operations remaining',
'sort_set_configured_operations' => 'Configured Operations', 'sort_rule_configured_operations' => 'Configured Operations',
'sort_set_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list', 'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
'sort_set_op_asc' => '(Asc)', 'sort_rule_op_asc' => '(Asc)',
'sort_set_op_desc' => '(Desc)', 'sort_rule_op_desc' => '(Desc)',
'sort_set_op_name' => 'Name - Alphabetical', 'sort_rule_op_name' => 'Name - Alphabetical',
'sort_set_op_name_numeric' => 'Name - Numeric', 'sort_rule_op_name_numeric' => 'Name - Numeric',
'sort_set_op_created_date' => 'Created Date', 'sort_rule_op_created_date' => 'Created Date',
'sort_set_op_updated_date' => 'Updated Date', 'sort_rule_op_updated_date' => 'Updated Date',
'sort_set_op_chapters_first' => 'Chapters First', 'sort_rule_op_chapters_first' => 'Chapters First',
'sort_set_op_chapters_last' => 'Chapters Last', 'sort_rule_op_chapters_last' => 'Chapters Last',
// Maintenance settings // Maintenance settings
'maint' => 'Maintenance', 'maint' => 'Maintenance',

View File

@ -50,7 +50,7 @@ export {ShelfSort} from './shelf-sort';
export {Shortcuts} from './shortcuts'; export {Shortcuts} from './shortcuts';
export {ShortcutInput} from './shortcut-input'; export {ShortcutInput} from './shortcut-input';
export {SortableList} from './sortable-list'; export {SortableList} from './sortable-list';
export {SortSetManager} from './sort-set-manager' export {SortRuleManager} from './sort-rule-manager'
export {SubmitOnChange} from './submit-on-change'; export {SubmitOnChange} from './submit-on-change';
export {Tabs} from './tabs'; export {Tabs} from './tabs';
export {TagManager} from './tag-manager'; export {TagManager} from './tag-manager';

View File

@ -3,7 +3,7 @@ import Sortable from "sortablejs";
import {buildListActions, sortActionClickListener} from "../services/dual-lists"; import {buildListActions, sortActionClickListener} from "../services/dual-lists";
export class SortSetManager extends Component { export class SortRuleManager extends Component {
protected input!: HTMLInputElement; protected input!: HTMLInputElement;
protected configuredList!: HTMLElement; protected configuredList!: HTMLElement;
@ -25,7 +25,7 @@ export class SortSetManager extends Component {
const scrollBoxes = [this.configuredList, this.availableList]; const scrollBoxes = [this.configuredList, this.availableList];
for (const scrollBox of scrollBoxes) { for (const scrollBox of scrollBoxes) {
new Sortable(scrollBox, { new Sortable(scrollBox, {
group: 'sort-set-operations', group: 'sort-rule-operations',
ghostClass: 'primary-background-light', ghostClass: 'primary-background-light',
handle: '.handle', handle: '.handle',
animation: 150, animation: 150,

View File

@ -9,18 +9,23 @@
<span>{{ $book->name }}</span> <span>{{ $book->name }}</span>
</div> </div>
<div class="flex-container-row items-center text-book"> <div class="flex-container-row items-center text-book">
@if($book->sortSet) @if($book->sortRule)
<span title="{{ trans('entities.books_sort_auto_sort_active', ['sortName' => $book->sortSet->name]) }}">@icon('auto-sort')</span> <span title="{{ trans('entities.books_sort_auto_sort_active', ['sortName' => $book->sortRule->name]) }}">@icon('auto-sort')</span>
@endif @endif
</div> </div>
</h5> </h5>
</summary> </summary>
<div class="sort-box-options pb-sm"> <div class="sort-box-options pb-sm">
<button type="button" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</button> <button type="button" data-sort="name"
<button type="button" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</button> class="button outline small">{{ trans('entities.books_sort_name') }}</button>
<button type="button" data-sort="updated" class="button outline small">{{ trans('entities.books_sort_updated') }}</button> <button type="button" data-sort="created"
<button type="button" data-sort="chaptersFirst" class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</button> class="button outline small">{{ trans('entities.books_sort_created') }}</button>
<button type="button" data-sort="chaptersLast" class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</button> <button type="button" data-sort="updated"
class="button outline small">{{ trans('entities.books_sort_updated') }}</button>
<button type="button" data-sort="chaptersFirst"
class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</button>
<button type="button" data-sort="chaptersLast"
class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</button>
</div> </div>
<ul class="sortable-page-list sort-list"> <ul class="sortable-page-list sort-list">

View File

@ -23,19 +23,21 @@
<p class="text-muted flex min-width-s mb-none">{{ trans('entities.books_sort_desc') }}</p> <p class="text-muted flex min-width-s mb-none">{{ trans('entities.books_sort_desc') }}</p>
<div class="min-width-s"> <div class="min-width-s">
@php @php
$autoSortVal = intval(old('auto-sort') ?? $book->sort_set_id ?? 0); $autoSortVal = intval(old('auto-sort') ?? $book->sort_rule_id ?? 0);
@endphp @endphp
<label for="auto-sort">{{ trans('entities.books_sort_auto_sort') }}</label> <label for="auto-sort">{{ trans('entities.books_sort_auto_sort') }}</label>
<select id="auto-sort" <select id="auto-sort"
name="auto-sort" name="auto-sort"
form="sort-form" form="sort-form"
class="{{ $errors->has('auto-sort') ? 'neg' : '' }}"> class="{{ $errors->has('auto-sort') ? 'neg' : '' }}">
<option value="0" @if($autoSortVal === 0) selected @endif>-- {{ trans('common.none') }} --</option> <option value="0" @if($autoSortVal === 0) selected @endif>-- {{ trans('common.none') }}
@foreach(\BookStack\Sorting\SortSet::allByName() as $set) --
<option value="{{$set->id}}" </option>
@if($autoSortVal === $set->id) selected @endif @foreach(\BookStack\Sorting\SortRule::allByName() as $rule)
<option value="{{$rule->id}}"
@if($autoSortVal === $rule->id) selected @endif
> >
{{ $set->name }} {{ $rule->name }}
</option> </option>
@endforeach @endforeach
</select> </select>

View File

@ -1,7 +1,7 @@
@extends('settings.layout') @extends('settings.layout')
@php @php
$sortSets = \BookStack\Sorting\SortSet::allByName(); $sortRules = \BookStack\Sorting\SortRule::allByName();
@endphp @endphp
@section('card') @section('card')
@ -23,7 +23,7 @@
<option value="0" @if(intval(setting('sorting-book-default', '0')) === 0) selected @endif> <option value="0" @if(intval(setting('sorting-book-default', '0')) === 0) selected @endif>
-- {{ trans('common.none') }} -- -- {{ trans('common.none') }} --
</option> </option>
@foreach($sortSets as $set) @foreach($sortRules as $set)
<option value="{{$set->id}}" <option value="{{$set->id}}"
@if(intval(setting('sorting-book-default', '0')) === $set->id) selected @endif @if(intval(setting('sorting-book-default', '0')) === $set->id) selected @endif
> >
@ -46,20 +46,21 @@
<div class="card content-wrap auto-height"> <div class="card content-wrap auto-height">
<div class="flex-container-row items-center gap-m"> <div class="flex-container-row items-center gap-m">
<div class="flex"> <div class="flex">
<h2 class="list-heading">{{ trans('settings.sorting_sets') }}</h2> <h2 class="list-heading">{{ trans('settings.sorting_rules') }}</h2>
<p class="text-muted">{{ trans('settings.sorting_sets_desc') }}</p> <p class="text-muted">{{ trans('settings.sorting_rules_desc') }}</p>
</div> </div>
<div> <div>
<a href="{{ url('/settings/sorting/sets/new') }}" class="button outline">{{ trans('settings.sort_set_create') }}</a> <a href="{{ url('/settings/sorting/rules/new') }}"
class="button outline">{{ trans('settings.sort_rule_create') }}</a>
</div> </div>
</div> </div>
@if(empty($sortSets)) @if(empty($sortRules))
<p class="italic text-muted">{{ trans('common.no_items') }}</p> <p class="italic text-muted">{{ trans('common.no_items') }}</p>
@else @else
<div class="item-list"> <div class="item-list">
@foreach($sortSets as $set) @foreach($sortRules as $rule)
@include('settings.sort-sets.parts.sort-set-list-item', ['set' => $set]) @include('settings.sort-rules.parts.sort-rule-list-item', ['rule' => $rule])
@endforeach @endforeach
</div> </div>
@endif @endif

View File

@ -7,11 +7,11 @@
@include('settings.parts.navbar', ['selected' => 'settings']) @include('settings.parts.navbar', ['selected' => 'settings'])
<div class="card content-wrap auto-height"> <div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('settings.sort_set_create') }}</h1> <h1 class="list-heading">{{ trans('settings.sort_rule_create') }}</h1>
<form action="{{ url("/settings/sorting/sets") }}" method="POST"> <form action="{{ url("/settings/sorting/rules") }}" method="POST">
{{ csrf_field() }} {{ csrf_field() }}
@include('settings.sort-sets.parts.form', ['model' => null]) @include('settings.sort-rules.parts.form', ['model' => null])
<div class="form-group text-right"> <div class="form-group text-right">
<a href="{{ url("/settings/sorting") }}" class="button outline">{{ trans('common.cancel') }}</a> <a href="{{ url("/settings/sorting") }}" class="button outline">{{ trans('common.cancel') }}</a>

View File

@ -7,13 +7,13 @@
@include('settings.parts.navbar', ['selected' => 'settings']) @include('settings.parts.navbar', ['selected' => 'settings'])
<div class="card content-wrap auto-height"> <div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('settings.sort_set_edit') }}</h1> <h1 class="list-heading">{{ trans('settings.sort_rule_edit') }}</h1>
<form action="{{ $set->getUrl() }}" method="POST"> <form action="{{ $rule->getUrl() }}" method="POST">
{{ method_field('PUT') }} {{ method_field('PUT') }}
{{ csrf_field() }} {{ csrf_field() }}
@include('settings.sort-sets.parts.form', ['model' => $set]) @include('settings.sort-rules.parts.form', ['model' => $rule])
<div class="form-group text-right"> <div class="form-group text-right">
<a href="{{ url("/settings/sorting") }}" class="button outline">{{ trans('common.cancel') }}</a> <a href="{{ url("/settings/sorting") }}" class="button outline">{{ trans('common.cancel') }}</a>
@ -25,8 +25,8 @@
<div id="delete" class="card content-wrap auto-height"> <div id="delete" class="card content-wrap auto-height">
<div class="flex-container-row items-center gap-l"> <div class="flex-container-row items-center gap-l">
<div class="mb-m"> <div class="mb-m">
<h2 class="list-heading">{{ trans('settings.sort_set_delete') }}</h2> <h2 class="list-heading">{{ trans('settings.sort_rule_delete') }}</h2>
<p class="text-muted mb-xs">{{ trans('settings.sort_set_delete_desc') }}</p> <p class="text-muted mb-xs">{{ trans('settings.sort_rule_delete_desc') }}</p>
@if($errors->has('delete')) @if($errors->has('delete'))
@foreach($errors->get('delete') as $error) @foreach($errors->get('delete') as $error)
<p class="text-neg mb-xs">{{ $error }}</p> <p class="text-neg mb-xs">{{ $error }}</p>
@ -34,7 +34,7 @@
@endif @endif
</div> </div>
<div class="flex"> <div class="flex">
<form action="{{ $set->getUrl() }}" method="POST"> <form action="{{ $rule->getUrl() }}" method="POST">
{{ method_field('DELETE') }} {{ method_field('DELETE') }}
{{ csrf_field() }} {{ csrf_field() }}

View File

@ -1,8 +1,8 @@
<div class="setting-list"> <div class="setting-list">
<div class="grid half"> <div class="grid half">
<div> <div>
<label class="setting-list-label">{{ trans('settings.sort_set_details') }}</label> <label class="setting-list-label">{{ trans('settings.sort_rule_details') }}</label>
<p class="text-muted text-small">{{ trans('settings.sort_set_details_desc') }}</p> <p class="text-muted text-small">{{ trans('settings.sort_rule_details_desc') }}</p>
</div> </div>
<div> <div>
<div class="form-group"> <div class="form-group">
@ -12,42 +12,42 @@
</div> </div>
</div> </div>
<div component="sort-set-manager"> <div component="sort-rule-manager">
<label class="setting-list-label">{{ trans('settings.sort_set_operations') }}</label> <label class="setting-list-label">{{ trans('settings.sort_rule_operations') }}</label>
<p class="text-muted text-small">{{ trans('settings.sort_set_operations_desc') }}</p> <p class="text-muted text-small">{{ trans('settings.sort_rule_operations_desc') }}</p>
@include('form.errors', ['name' => 'sequence']) @include('form.errors', ['name' => 'sequence'])
<input refs="sort-set-manager@input" type="hidden" name="sequence" <input refs="sort-rule-manager@input" type="hidden" name="sequence"
value="{{ old('sequence') ?? $model?->sequence ?? '' }}"> value="{{ old('sequence') ?? $model?->sequence ?? '' }}">
@php @php
$configuredOps = old('sequence') ? \BookStack\Sorting\SortSetOperation::fromSequence(old('sequence')) : ($model?->getOperations() ?? []); $configuredOps = old('sequence') ? \BookStack\Sorting\SortRuleOperation::fromSequence(old('sequence')) : ($model?->getOperations() ?? []);
@endphp @endphp
<div class="grid half"> <div class="grid half">
<div class="form-group"> <div class="form-group">
<label for="books" <label for="books"
id="sort-set-configured-operations">{{ trans('settings.sort_set_configured_operations') }}</label> id="sort-rule-configured-operations">{{ trans('settings.sort_rule_configured_operations') }}</label>
<ul refs="sort-set-manager@configured-operations-list" <ul refs="sort-rule-manager@configured-operations-list"
aria-labelledby="sort-set-configured-operations" aria-labelledby="sort-rule-configured-operations"
class="scroll-box configured-option-list"> class="scroll-box configured-option-list">
<li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_set_configured_operations_empty') }}</li> <li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_rule_configured_operations_empty') }}</li>
@foreach($configuredOps as $operation) @foreach($configuredOps as $operation)
@include('settings.sort-sets.parts.operation', ['operation' => $operation]) @include('settings.sort-rules.parts.operation', ['operation' => $operation])
@endforeach @endforeach
</ul> </ul>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="books" <label for="books"
id="sort-set-available-operations">{{ trans('settings.sort_set_available_operations') }}</label> id="sort-rule-available-operations">{{ trans('settings.sort_rule_available_operations') }}</label>
<ul refs="sort-set-manager@available-operations-list" <ul refs="sort-rule-manager@available-operations-list"
aria-labelledby="sort-set-available-operations" aria-labelledby="sort-rule-available-operations"
class="scroll-box available-option-list"> class="scroll-box available-option-list">
<li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_set_available_operations_empty') }}</li> <li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_rule_available_operations_empty') }}</li>
@foreach(\BookStack\Sorting\SortSetOperation::allExcluding($configuredOps) as $operation) @foreach(\BookStack\Sorting\SortRuleOperation::allExcluding($configuredOps) as $operation)
@include('settings.sort-sets.parts.operation', ['operation' => $operation]) @include('settings.sort-rules.parts.operation', ['operation' => $operation])
@endforeach @endforeach
</ul> </ul>
</div> </div>

View File

@ -1,12 +1,12 @@
<div class="item-list-row flex-container-row py-xs px-m gap-m items-center"> <div class="item-list-row flex-container-row py-xs px-m gap-m items-center">
<div class="py-xs flex"> <div class="py-xs flex">
<a href="{{ $set->getUrl() }}">{{ $set->name }}</a> <a href="{{ $rule->getUrl() }}">{{ $rule->name }}</a>
</div> </div>
<div class="px-m text-small text-muted ml-auto"> <div class="px-m text-small text-muted ml-auto">
{{ implode(', ', array_map(fn ($op) => $op->getLabel(), $set->getOperations())) }} {{ implode(', ', array_map(fn ($op) => $op->getLabel(), $rule->getOperations())) }}
</div> </div>
<div> <div>
<span title="{{ trans_choice('settings.sort_set_assigned_to_x_books', $set->books_count ?? 0) }}" <span title="{{ trans_choice('settings.sort_rule_assigned_to_x_books', $rule->books_count ?? 0) }}"
class="flex fill-area min-width-xxs bold text-right text-book"><span class="opacity-60">@icon('book')</span>{{ $set->books_count ?? 0 }}</span> class="flex fill-area min-width-xxs bold text-right text-book"><span class="opacity-60">@icon('book')</span>{{ $rule->books_count ?? 0 }}</span>
</div> </div>
</div> </div>

View File

@ -295,12 +295,12 @@ Route::middleware('auth')->group(function () {
Route::get('/settings/webhooks/{id}/delete', [ActivityControllers\WebhookController::class, 'delete']); Route::get('/settings/webhooks/{id}/delete', [ActivityControllers\WebhookController::class, 'delete']);
Route::delete('/settings/webhooks/{id}', [ActivityControllers\WebhookController::class, 'destroy']); Route::delete('/settings/webhooks/{id}', [ActivityControllers\WebhookController::class, 'destroy']);
// Sort Sets // Sort Rules
Route::get('/settings/sorting/sets/new', [SortingControllers\SortSetController::class, 'create']); Route::get('/settings/sorting/rules/new', [SortingControllers\SortRuleController::class, 'create']);
Route::post('/settings/sorting/sets', [SortingControllers\SortSetController::class, 'store']); Route::post('/settings/sorting/rules', [SortingControllers\SortRuleController::class, 'store']);
Route::get('/settings/sorting/sets/{id}', [SortingControllers\SortSetController::class, 'edit']); Route::get('/settings/sorting/rules/{id}', [SortingControllers\SortRuleController::class, 'edit']);
Route::put('/settings/sorting/sets/{id}', [SortingControllers\SortSetController::class, 'update']); Route::put('/settings/sorting/rules/{id}', [SortingControllers\SortRuleController::class, 'update']);
Route::delete('/settings/sorting/sets/{id}', [SortingControllers\SortSetController::class, 'destroy']); Route::delete('/settings/sorting/rules/{id}', [SortingControllers\SortRuleController::class, 'destroy']);
// Settings // Settings
Route::get('/settings', [SettingControllers\SettingController::class, 'index'])->name('settings'); Route::get('/settings', [SettingControllers\SettingController::class, 'index'])->name('settings');

View File

@ -3,14 +3,14 @@
namespace Commands; namespace Commands;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Sorting\SortSet; use BookStack\Sorting\SortRule;
use Tests\TestCase; use Tests\TestCase;
class AssignSortSetCommandTest extends TestCase class AssignSortSetCommandTest extends TestCase
{ {
public function test_no_given_sort_set_lists_options() public function test_no_given_sort_set_lists_options()
{ {
$sortSets = SortSet::factory()->createMany(10); $sortSets = SortRule::factory()->createMany(10);
$commandRun = $this->artisan('bookstack:assign-sort-set') $commandRun = $this->artisan('bookstack:assign-sort-set')
->expectsOutputToContain('Sort set ID required!') ->expectsOutputToContain('Sort set ID required!')
@ -37,7 +37,7 @@ class AssignSortSetCommandTest extends TestCase
public function test_confirmation_required() public function test_confirmation_required()
{ {
$sortSet = SortSet::factory()->create(); $sortSet = SortRule::factory()->create();
$this->artisan("bookstack:assign-sort-set {$sortSet->id} --all-books") $this->artisan("bookstack:assign-sort-set {$sortSet->id} --all-books")
->expectsConfirmation('Are you sure you want to continue?', 'no') ->expectsConfirmation('Are you sure you want to continue?', 'no')
@ -49,7 +49,7 @@ class AssignSortSetCommandTest extends TestCase
public function test_assign_to_all_books() public function test_assign_to_all_books()
{ {
$sortSet = SortSet::factory()->create(); $sortSet = SortRule::factory()->create();
$booksWithoutSort = Book::query()->whereNull('sort_set_id')->count(); $booksWithoutSort = Book::query()->whereNull('sort_set_id')->count();
$this->assertGreaterThan(0, $booksWithoutSort); $this->assertGreaterThan(0, $booksWithoutSort);
@ -67,9 +67,9 @@ class AssignSortSetCommandTest extends TestCase
{ {
$totalBooks = Book::query()->count(); $totalBooks = Book::query()->count();
$book = $this->entities->book(); $book = $this->entities->book();
$sortSetA = SortSet::factory()->create(); $sortSetA = SortRule::factory()->create();
$sortSetB = SortSet::factory()->create(); $sortSetB = SortRule::factory()->create();
$book->sort_set_id = $sortSetA->id; $book->sort_rule_id = $sortSetA->id;
$book->save(); $book->save();
$booksWithoutSort = Book::query()->whereNull('sort_set_id')->count(); $booksWithoutSort = Book::query()->whereNull('sort_set_id')->count();
@ -88,9 +88,9 @@ class AssignSortSetCommandTest extends TestCase
public function test_assign_to_all_books_with_sort() public function test_assign_to_all_books_with_sort()
{ {
$book = $this->entities->book(); $book = $this->entities->book();
$sortSetA = SortSet::factory()->create(); $sortSetA = SortRule::factory()->create();
$sortSetB = SortSet::factory()->create(); $sortSetB = SortRule::factory()->create();
$book->sort_set_id = $sortSetA->id; $book->sort_rule_id = $sortSetA->id;
$book->save(); $book->save();
$this->artisan("bookstack:assign-sort-set {$sortSetB->id} --books-with-sort={$sortSetA->id}") $this->artisan("bookstack:assign-sort-set {$sortSetB->id} --books-with-sort={$sortSetA->id}")
@ -99,7 +99,7 @@ class AssignSortSetCommandTest extends TestCase
->assertExitCode(0); ->assertExitCode(0);
$book->refresh(); $book->refresh();
$this->assertEquals($sortSetB->id, $book->sort_set_id); $this->assertEquals($sortSetB->id, $book->sort_rule_id);
$this->assertEquals(1, $sortSetB->books()->count()); $this->assertEquals(1, $sortSetB->books()->count());
} }

View File

@ -300,7 +300,7 @@ class PageTest extends TestCase
]); ]);
$resp = $this->asAdmin()->get('/pages/recently-updated'); $resp = $this->asAdmin()->get('/pages/recently-updated');
$this->withHtml($resp)->assertElementContains('.entity-list .page:nth-child(1)', 'Updated 0 seconds ago by ' . $user->name); $this->withHtml($resp)->assertElementContains('.entity-list .page:nth-child(1) small', 'by ' . $user->name);
} }
public function test_recently_updated_pages_view_shows_parent_chain() public function test_recently_updated_pages_view_shows_parent_chain()

View File

@ -5,7 +5,7 @@ namespace Sorting;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Repos\PageRepo;
use BookStack\Sorting\SortSet; use BookStack\Sorting\SortRule;
use Tests\TestCase; use Tests\TestCase;
class BookSortTest extends TestCase class BookSortTest extends TestCase
@ -223,13 +223,13 @@ class BookSortTest extends TestCase
public function test_book_sort_item_shows_auto_sort_status() public function test_book_sort_item_shows_auto_sort_status()
{ {
$sort = SortSet::factory()->create(['name' => 'My sort']); $sort = SortRule::factory()->create(['name' => 'My sort']);
$book = $this->entities->book(); $book = $this->entities->book();
$resp = $this->asAdmin()->get($book->getUrl('/sort-item')); $resp = $this->asAdmin()->get($book->getUrl('/sort-item'));
$this->withHtml($resp)->assertElementNotExists("span[title='Auto Sort Active: My sort']"); $this->withHtml($resp)->assertElementNotExists("span[title='Auto Sort Active: My sort']");
$book->sort_set_id = $sort->id; $book->sort_rule_id = $sort->id;
$book->save(); $book->save();
$resp = $this->asAdmin()->get($book->getUrl('/sort-item')); $resp = $this->asAdmin()->get($book->getUrl('/sort-item'));
@ -238,7 +238,7 @@ class BookSortTest extends TestCase
public function test_auto_sort_options_shown_on_sort_page() public function test_auto_sort_options_shown_on_sort_page()
{ {
$sort = SortSet::factory()->create(); $sort = SortRule::factory()->create();
$book = $this->entities->book(); $book = $this->entities->book();
$resp = $this->asAdmin()->get($book->getUrl('/sort')); $resp = $this->asAdmin()->get($book->getUrl('/sort'));
@ -247,7 +247,7 @@ class BookSortTest extends TestCase
public function test_auto_sort_option_submit_saves_to_book() public function test_auto_sort_option_submit_saves_to_book()
{ {
$sort = SortSet::factory()->create(); $sort = SortRule::factory()->create();
$book = $this->entities->book(); $book = $this->entities->book();
$bookPage = $book->pages()->first(); $bookPage = $book->pages()->first();
$bookPage->priority = 10000; $bookPage->priority = 10000;
@ -261,7 +261,7 @@ class BookSortTest extends TestCase
$book->refresh(); $book->refresh();
$bookPage->refresh(); $bookPage->refresh();
$this->assertEquals($sort->id, $book->sort_set_id); $this->assertEquals($sort->id, $book->sort_rule_id);
$this->assertNotEquals(10000, $bookPage->priority); $this->assertNotEquals(10000, $bookPage->priority);
$resp = $this->get($book->getUrl('/sort')); $resp = $this->get($book->getUrl('/sort'));

View File

@ -4,26 +4,26 @@ namespace Sorting;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Sorting\SortSet; use BookStack\Sorting\SortRule;
use Tests\Api\TestsApi; use Tests\Api\TestsApi;
use Tests\TestCase; use Tests\TestCase;
class SortSetTest extends TestCase class SortRuleTest extends TestCase
{ {
use TestsApi; use TestsApi;
public function test_manage_settings_permission_required() public function test_manage_settings_permission_required()
{ {
$set = SortSet::factory()->create(); $rule = SortRule::factory()->create();
$user = $this->users->viewer(); $user = $this->users->viewer();
$this->actingAs($user); $this->actingAs($user);
$actions = [ $actions = [
['GET', '/settings/sorting'], ['GET', '/settings/sorting'],
['POST', '/settings/sorting/sets'], ['POST', '/settings/sorting/rules'],
['GET', "/settings/sorting/sets/{$set->id}"], ['GET', "/settings/sorting/rules/{$rule->id}"],
['PUT', "/settings/sorting/sets/{$set->id}"], ['PUT', "/settings/sorting/rules/{$rule->id}"],
['DELETE', "/settings/sorting/sets/{$set->id}"], ['DELETE', "/settings/sorting/rules/{$rule->id}"],
]; ];
foreach ($actions as [$method, $path]) { foreach ($actions as [$method, $path]) {
@ -42,63 +42,63 @@ class SortSetTest extends TestCase
public function test_create_flow() public function test_create_flow()
{ {
$resp = $this->asAdmin()->get('/settings/sorting'); $resp = $this->asAdmin()->get('/settings/sorting');
$this->withHtml($resp)->assertLinkExists(url('/settings/sorting/sets/new')); $this->withHtml($resp)->assertLinkExists(url('/settings/sorting/rules/new'));
$resp = $this->get('/settings/sorting/sets/new'); $resp = $this->get('/settings/sorting/rules/new');
$this->withHtml($resp)->assertElementExists('form[action$="/settings/sorting/sets"] input[name="name"]'); $this->withHtml($resp)->assertElementExists('form[action$="/settings/sorting/rules"] input[name="name"]');
$resp->assertSeeText('Name - Alphabetical (Asc)'); $resp->assertSeeText('Name - Alphabetical (Asc)');
$details = ['name' => 'My new sort', 'sequence' => 'name_asc']; $details = ['name' => 'My new sort', 'sequence' => 'name_asc'];
$resp = $this->post('/settings/sorting/sets', $details); $resp = $this->post('/settings/sorting/rules', $details);
$resp->assertRedirect('/settings/sorting'); $resp->assertRedirect('/settings/sorting');
$this->assertActivityExists(ActivityType::SORT_SET_CREATE); $this->assertActivityExists(ActivityType::SORT_RULE_CREATE);
$this->assertDatabaseHas('sort_sets', $details); $this->assertDatabaseHas('sort_rules', $details);
} }
public function test_listing_in_settings() public function test_listing_in_settings()
{ {
$set = SortSet::factory()->create(['name' => 'My super sort set', 'sequence' => 'name_asc']); $rule = SortRule::factory()->create(['name' => 'My super sort rule', 'sequence' => 'name_asc']);
$books = Book::query()->limit(5)->get(); $books = Book::query()->limit(5)->get();
foreach ($books as $book) { foreach ($books as $book) {
$book->sort_set_id = $set->id; $book->sort_rule_id = $rule->id;
$book->save(); $book->save();
} }
$resp = $this->asAdmin()->get('/settings/sorting'); $resp = $this->asAdmin()->get('/settings/sorting');
$resp->assertSeeText('My super sort set'); $resp->assertSeeText('My super sort rule');
$resp->assertSeeText('Name - Alphabetical (Asc)'); $resp->assertSeeText('Name - Alphabetical (Asc)');
$this->withHtml($resp)->assertElementContains('.item-list-row [title="Assigned to 5 Books"]', '5'); $this->withHtml($resp)->assertElementContains('.item-list-row [title="Assigned to 5 Books"]', '5');
} }
public function test_update_flow() public function test_update_flow()
{ {
$set = SortSet::factory()->create(['name' => 'My sort set to update', 'sequence' => 'name_asc']); $rule = SortRule::factory()->create(['name' => 'My sort rule to update', 'sequence' => 'name_asc']);
$resp = $this->asAdmin()->get("/settings/sorting/sets/{$set->id}"); $resp = $this->asAdmin()->get("/settings/sorting/rules/{$rule->id}");
$respHtml = $this->withHtml($resp); $respHtml = $this->withHtml($resp);
$respHtml->assertElementContains('.configured-option-list', 'Name - Alphabetical (Asc)'); $respHtml->assertElementContains('.configured-option-list', 'Name - Alphabetical (Asc)');
$respHtml->assertElementNotContains('.available-option-list', 'Name - Alphabetical (Asc)'); $respHtml->assertElementNotContains('.available-option-list', 'Name - Alphabetical (Asc)');
$updateData = ['name' => 'My updated sort', 'sequence' => 'name_desc,chapters_last']; $updateData = ['name' => 'My updated sort', 'sequence' => 'name_desc,chapters_last'];
$resp = $this->put("/settings/sorting/sets/{$set->id}", $updateData); $resp = $this->put("/settings/sorting/rules/{$rule->id}", $updateData);
$resp->assertRedirect('/settings/sorting'); $resp->assertRedirect('/settings/sorting');
$this->assertActivityExists(ActivityType::SORT_SET_UPDATE); $this->assertActivityExists(ActivityType::SORT_RULE_UPDATE);
$this->assertDatabaseHas('sort_sets', $updateData); $this->assertDatabaseHas('sort_rules', $updateData);
} }
public function test_update_triggers_resort_on_assigned_books() public function test_update_triggers_resort_on_assigned_books()
{ {
$book = $this->entities->bookHasChaptersAndPages(); $book = $this->entities->bookHasChaptersAndPages();
$chapter = $book->chapters()->first(); $chapter = $book->chapters()->first();
$set = SortSet::factory()->create(['name' => 'My sort set to update', 'sequence' => 'name_asc']); $rule = SortRule::factory()->create(['name' => 'My sort rule to update', 'sequence' => 'name_asc']);
$book->sort_set_id = $set->id; $book->sort_rule_id = $rule->id;
$book->save(); $book->save();
$chapter->priority = 10000; $chapter->priority = 10000;
$chapter->save(); $chapter->save();
$resp = $this->asAdmin()->put("/settings/sorting/sets/{$set->id}", ['name' => $set->name, 'sequence' => 'chapters_last']); $resp = $this->asAdmin()->put("/settings/sorting/rules/{$rule->id}", ['name' => $rule->name, 'sequence' => 'chapters_last']);
$resp->assertRedirect('/settings/sorting'); $resp->assertRedirect('/settings/sorting');
$chapter->refresh(); $chapter->refresh();
@ -107,48 +107,48 @@ class SortSetTest extends TestCase
public function test_delete_flow() public function test_delete_flow()
{ {
$set = SortSet::factory()->create(); $rule = SortRule::factory()->create();
$resp = $this->asAdmin()->get("/settings/sorting/sets/{$set->id}"); $resp = $this->asAdmin()->get("/settings/sorting/rules/{$rule->id}");
$resp->assertSeeText('Delete Sort Set'); $resp->assertSeeText('Delete Sort Rule');
$resp = $this->delete("settings/sorting/sets/{$set->id}"); $resp = $this->delete("settings/sorting/rules/{$rule->id}");
$resp->assertRedirect('/settings/sorting'); $resp->assertRedirect('/settings/sorting');
$this->assertActivityExists(ActivityType::SORT_SET_DELETE); $this->assertActivityExists(ActivityType::SORT_RULE_DELETE);
$this->assertDatabaseMissing('sort_sets', ['id' => $set->id]); $this->assertDatabaseMissing('sort_rules', ['id' => $rule->id]);
} }
public function test_delete_requires_confirmation_if_books_assigned() public function test_delete_requires_confirmation_if_books_assigned()
{ {
$set = SortSet::factory()->create(); $rule = SortRule::factory()->create();
$books = Book::query()->limit(5)->get(); $books = Book::query()->limit(5)->get();
foreach ($books as $book) { foreach ($books as $book) {
$book->sort_set_id = $set->id; $book->sort_rule_id = $rule->id;
$book->save(); $book->save();
} }
$resp = $this->asAdmin()->get("/settings/sorting/sets/{$set->id}"); $resp = $this->asAdmin()->get("/settings/sorting/rules/{$rule->id}");
$resp->assertSeeText('Delete Sort Set'); $resp->assertSeeText('Delete Sort Rule');
$resp = $this->delete("settings/sorting/sets/{$set->id}"); $resp = $this->delete("settings/sorting/rules/{$rule->id}");
$resp->assertRedirect("/settings/sorting/sets/{$set->id}#delete"); $resp->assertRedirect("/settings/sorting/rules/{$rule->id}#delete");
$resp = $this->followRedirects($resp); $resp = $this->followRedirects($resp);
$resp->assertSeeText('This sort set is currently used on 5 book(s). Are you sure you want to delete this?'); $resp->assertSeeText('This sort rule is currently used on 5 book(s). Are you sure you want to delete this?');
$this->assertDatabaseHas('sort_sets', ['id' => $set->id]); $this->assertDatabaseHas('sort_rules', ['id' => $rule->id]);
$resp = $this->delete("settings/sorting/sets/{$set->id}", ['confirm' => 'true']); $resp = $this->delete("settings/sorting/rules/{$rule->id}", ['confirm' => 'true']);
$resp->assertRedirect('/settings/sorting'); $resp->assertRedirect('/settings/sorting');
$this->assertDatabaseMissing('sort_sets', ['id' => $set->id]); $this->assertDatabaseMissing('sort_rules', ['id' => $rule->id]);
$this->assertDatabaseMissing('books', ['sort_set_id' => $set->id]); $this->assertDatabaseMissing('books', ['sort_rule_id' => $rule->id]);
} }
public function test_page_create_triggers_book_sort() public function test_page_create_triggers_book_sort()
{ {
$book = $this->entities->bookHasChaptersAndPages(); $book = $this->entities->bookHasChaptersAndPages();
$set = SortSet::factory()->create(['sequence' => 'name_asc,chapters_first']); $rule = SortRule::factory()->create(['sequence' => 'name_asc,chapters_first']);
$book->sort_set_id = $set->id; $book->sort_rule_id = $rule->id;
$book->save(); $book->save();
$resp = $this->actingAsApiEditor()->post("/api/pages", [ $resp = $this->actingAsApiEditor()->post("/api/pages", [
@ -168,8 +168,8 @@ class SortSetTest extends TestCase
public function test_name_numeric_ordering() public function test_name_numeric_ordering()
{ {
$book = Book::factory()->create(); $book = Book::factory()->create();
$set = SortSet::factory()->create(['sequence' => 'name_numeric_asc']); $rule = SortRule::factory()->create(['sequence' => 'name_numeric_asc']);
$book->sort_set_id = $set->id; $book->sort_rule_id = $rule->id;
$book->save(); $book->save();
$this->permissions->regenerateForEntity($book); $this->permissions->regenerateForEntity($book);