0
0
Fork 0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-03-14 20:32:49 +00:00

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_DELETE = 'import_delete';
const SORT_SET_CREATE = 'sort_set_create';
const SORT_SET_UPDATE = 'sort_set_update';
const SORT_SET_DELETE = 'sort_set_delete';
const SORT_RULE_CREATE = 'sort_rule_create';
const SORT_RULE_UPDATE = 'sort_rule_update';
const SORT_RULE_DELETE = 'sort_rule_delete';
/**
* Get all the possible values.

View file

@ -4,7 +4,7 @@ namespace BookStack\Console\Commands;
use BookStack\Entities\Models\Book;
use BookStack\Sorting\BookSorter;
use BookStack\Sorting\SortSet;
use BookStack\Sorting\SortRule;
use Illuminate\Console\Command;
class AssignSortSetCommand extends Command
@ -37,7 +37,7 @@ class AssignSortSetCommand extends Command
return $this->listSortSets();
}
$set = SortSet::query()->find($sortSetId);
$set = SortRule::query()->find($sortSetId);
if ($this->option('all-books')) {
$query = Book::query();
} else if ($this->option('books-without-sort')) {
@ -87,7 +87,7 @@ class AssignSortSetCommand extends Command
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->warn("\nAvailable sort sets:");
foreach ($sets as $set) {

View file

@ -2,7 +2,7 @@
namespace BookStack\Entities\Models;
use BookStack\Sorting\SortSet;
use BookStack\Sorting\SortRule;
use BookStack\Uploads\Image;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -17,14 +17,14 @@ use Illuminate\Support\Collection;
* @property string $description
* @property int $image_id
* @property ?int $default_template_id
* @property ?int $sort_set_id
* @property ?int $sort_rule_id
* @property Image|null $cover
* @property \Illuminate\Database\Eloquent\Collection $chapters
* @property \Illuminate\Database\Eloquent\Collection $pages
* @property \Illuminate\Database\Eloquent\Collection $directPages
* @property \Illuminate\Database\Eloquent\Collection $shelves
* @property ?Page $defaultTemplate
* @property ?SortSet $sortSet
* @property ?SortRule $sortRule
*/
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.
*/
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\Exceptions\ImageUploadException;
use BookStack\Facades\Activity;
use BookStack\Sorting\SortSet;
use BookStack\Sorting\SortRule;
use BookStack\Uploads\ImageRepo;
use Exception;
use Illuminate\Http\UploadedFile;
@ -35,8 +35,8 @@ class BookRepo
Activity::add(ActivityType::BOOK_CREATE, $book);
$defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
if ($defaultBookSortSetting && SortSet::query()->find($defaultBookSortSetting)) {
$book->sort_set_id = $defaultBookSortSetting;
if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {
$book->sort_rule_id = $defaultBookSortSetting;
$book->save();
}

View file

@ -69,10 +69,10 @@ class BookSortController extends Controller
if ($request->filled('auto-sort')) {
$sortSetId = intval($request->get('auto-sort')) ?: null;
if ($sortSetId && SortSet::query()->find($sortSetId) === null) {
if ($sortSetId && SortRule::query()->find($sortSetId) === null) {
$sortSetId = null;
}
$book->sort_set_id = $sortSetId;
$book->sort_rule_id = $sortSetId;
$book->save();
$sorter->runBookAutoSort($book);
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) {
foreach ($books as $book) {
@ -32,12 +32,12 @@ class BookSorter
*/
public function runBookAutoSort(Book $book): void
{
$set = $book->sortSet;
$set = $book->sortRule;
if (!$set) {
return;
}
$sortFunctions = array_map(function (SortSetOperation $op) {
$sortFunctions = array_map(function (SortRuleOperation $op) {
return $op->getSortFunction();
}, $set->getOperations());

View file

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

View file

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

View file

@ -5,7 +5,7 @@ namespace BookStack\Sorting;
use Closure;
use Illuminate\Support\Str;
enum SortSetOperation: string
enum SortRuleOperation: string
{
case NameAsc = 'name_asc';
case NameDesc = 'name_desc';
@ -26,13 +26,13 @@ enum SortSetOperation: string
$label = '';
if (str_ends_with($key, '_asc')) {
$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')) {
$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);
}
@ -43,12 +43,12 @@ enum SortSetOperation: string
}
/**
* @return SortSetOperation[]
* @return SortRuleOperation[]
*/
public static function allExcluding(array $operations): array
{
$all = SortSetOperation::cases();
$filtered = array_filter($all, function (SortSetOperation $operation) use ($operations) {
$all = SortRuleOperation::cases();
$filtered = array_filter($all, function (SortRuleOperation $operation) use ($operations) {
return !in_array($operation, $operations);
});
return array_values($filtered);
@ -57,12 +57,12 @@ enum SortSetOperation: string
/**
* Create a set of operations from a string sequence representation.
* (values seperated by commas).
* @return SortSetOperation[]
* @return SortRuleOperation[]
*/
public static function fromSequence(string $sequence): array
{
$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);
}
}

View file

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

View file

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

View file

@ -11,7 +11,7 @@ return new class extends Migration
*/
public function up(): void
{
Schema::create('sort_sets', function (Blueprint $table) {
Schema::create('sort_rules', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->text('sequence');
@ -24,6 +24,6 @@ return new class extends Migration
*/
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
{
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
{
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_navigation' => 'Book Navigation',
'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_active' => 'Auto Sort Active: :sortName',
'books_sort_named' => 'Sort Book :bookName',

View file

@ -77,32 +77,32 @@ return [
// Sorting Settings
'sorting' => 'Sorting',
'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_sets' => 'Sort Sets',
'sorting_sets_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_set_create' => 'Create Sort Set',
'sort_set_edit' => 'Edit Sort Set',
'sort_set_delete' => 'Delete Sort Set',
'sort_set_delete_desc' => 'Remove this sort set 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_set_delete_warn_default' => 'This sort set is currently used as the default for books. Are you sure you want to delete this?',
'sort_set_details' => 'Sort Set Details',
'sort_set_details_desc' => 'Set a name for this sort set, which will appear in lists when users are selecting a sort.',
'sort_set_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_set_available_operations' => 'Available Operations',
'sort_set_available_operations_empty' => 'No operations remaining',
'sort_set_configured_operations' => 'Configured Operations',
'sort_set_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
'sort_set_op_asc' => '(Asc)',
'sort_set_op_desc' => '(Desc)',
'sort_set_op_name' => 'Name - Alphabetical',
'sort_set_op_name_numeric' => 'Name - Numeric',
'sort_set_op_created_date' => 'Created Date',
'sort_set_op_updated_date' => 'Updated Date',
'sort_set_op_chapters_first' => 'Chapters First',
'sort_set_op_chapters_last' => 'Chapters Last',
'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_rules' => 'Sort Rules',
'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',
'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',
'sort_rule_create' => 'Create Sort Rule',
'sort_rule_edit' => 'Edit Sort Rule',
'sort_rule_delete' => 'Delete Sort Rule',
'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',
'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). 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_rule_details' => 'Sort Rule Details',
'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',
'sort_rule_operations' => 'Sort Operations',
'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_rule_available_operations' => 'Available Operations',
'sort_rule_available_operations_empty' => 'No operations remaining',
'sort_rule_configured_operations' => 'Configured Operations',
'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
'sort_rule_op_asc' => '(Asc)',
'sort_rule_op_desc' => '(Desc)',
'sort_rule_op_name' => 'Name - Alphabetical',
'sort_rule_op_name_numeric' => 'Name - Numeric',
'sort_rule_op_created_date' => 'Created Date',
'sort_rule_op_updated_date' => 'Updated Date',
'sort_rule_op_chapters_first' => 'Chapters First',
'sort_rule_op_chapters_last' => 'Chapters Last',
// Maintenance settings
'maint' => 'Maintenance',

View file

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

View file

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

View file

@ -9,18 +9,23 @@
<span>{{ $book->name }}</span>
</div>
<div class="flex-container-row items-center text-book">
@if($book->sortSet)
<span title="{{ trans('entities.books_sort_auto_sort_active', ['sortName' => $book->sortSet->name]) }}">@icon('auto-sort')</span>
@if($book->sortRule)
<span title="{{ trans('entities.books_sort_auto_sort_active', ['sortName' => $book->sortRule->name]) }}">@icon('auto-sort')</span>
@endif
</div>
</h5>
</summary>
<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="created" class="button outline small">{{ trans('entities.books_sort_created') }}</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>
<button type="button" data-sort="name"
class="button outline small">{{ trans('entities.books_sort_name') }}</button>
<button type="button" data-sort="created"
class="button outline small">{{ trans('entities.books_sort_created') }}</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>
<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>
<div class="min-width-s">
@php
$autoSortVal = intval(old('auto-sort') ?? $book->sort_set_id ?? 0);
$autoSortVal = intval(old('auto-sort') ?? $book->sort_rule_id ?? 0);
@endphp
<label for="auto-sort">{{ trans('entities.books_sort_auto_sort') }}</label>
<select id="auto-sort"
name="auto-sort"
form="sort-form"
class="{{ $errors->has('auto-sort') ? 'neg' : '' }}">
<option value="0" @if($autoSortVal === 0) selected @endif>-- {{ trans('common.none') }} --</option>
@foreach(\BookStack\Sorting\SortSet::allByName() as $set)
<option value="{{$set->id}}"
@if($autoSortVal === $set->id) selected @endif
<option value="0" @if($autoSortVal === 0) selected @endif>-- {{ trans('common.none') }}
--
</option>
@foreach(\BookStack\Sorting\SortRule::allByName() as $rule)
<option value="{{$rule->id}}"
@if($autoSortVal === $rule->id) selected @endif
>
{{ $set->name }}
{{ $rule->name }}
</option>
@endforeach
</select>

View file

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

View file

@ -7,11 +7,11 @@
@include('settings.parts.navbar', ['selected' => 'settings'])
<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() }}
@include('settings.sort-sets.parts.form', ['model' => null])
@include('settings.sort-rules.parts.form', ['model' => null])
<div class="form-group text-right">
<a href="{{ url("/settings/sorting") }}" class="button outline">{{ trans('common.cancel') }}</a>

View file

@ -7,13 +7,13 @@
@include('settings.parts.navbar', ['selected' => 'settings'])
<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') }}
{{ csrf_field() }}
@include('settings.sort-sets.parts.form', ['model' => $set])
@include('settings.sort-rules.parts.form', ['model' => $rule])
<div class="form-group text-right">
<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 class="flex-container-row items-center gap-l">
<div class="mb-m">
<h2 class="list-heading">{{ trans('settings.sort_set_delete') }}</h2>
<p class="text-muted mb-xs">{{ trans('settings.sort_set_delete_desc') }}</p>
<h2 class="list-heading">{{ trans('settings.sort_rule_delete') }}</h2>
<p class="text-muted mb-xs">{{ trans('settings.sort_rule_delete_desc') }}</p>
@if($errors->has('delete'))
@foreach($errors->get('delete') as $error)
<p class="text-neg mb-xs">{{ $error }}</p>
@ -34,7 +34,7 @@
@endif
</div>
<div class="flex">
<form action="{{ $set->getUrl() }}" method="POST">
<form action="{{ $rule->getUrl() }}" method="POST">
{{ method_field('DELETE') }}
{{ csrf_field() }}

View file

@ -1,8 +1,8 @@
<div class="setting-list">
<div class="grid half">
<div>
<label class="setting-list-label">{{ trans('settings.sort_set_details') }}</label>
<p class="text-muted text-small">{{ trans('settings.sort_set_details_desc') }}</p>
<label class="setting-list-label">{{ trans('settings.sort_rule_details') }}</label>
<p class="text-muted text-small">{{ trans('settings.sort_rule_details_desc') }}</p>
</div>
<div>
<div class="form-group">
@ -12,42 +12,42 @@
</div>
</div>
<div component="sort-set-manager">
<label class="setting-list-label">{{ trans('settings.sort_set_operations') }}</label>
<p class="text-muted text-small">{{ trans('settings.sort_set_operations_desc') }}</p>
<div component="sort-rule-manager">
<label class="setting-list-label">{{ trans('settings.sort_rule_operations') }}</label>
<p class="text-muted text-small">{{ trans('settings.sort_rule_operations_desc') }}</p>
@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 ?? '' }}">
@php
$configuredOps = old('sequence') ? \BookStack\Sorting\SortSetOperation::fromSequence(old('sequence')) : ($model?->getOperations() ?? []);
$configuredOps = old('sequence') ? \BookStack\Sorting\SortRuleOperation::fromSequence(old('sequence')) : ($model?->getOperations() ?? []);
@endphp
<div class="grid half">
<div class="form-group">
<label for="books"
id="sort-set-configured-operations">{{ trans('settings.sort_set_configured_operations') }}</label>
<ul refs="sort-set-manager@configured-operations-list"
aria-labelledby="sort-set-configured-operations"
id="sort-rule-configured-operations">{{ trans('settings.sort_rule_configured_operations') }}</label>
<ul refs="sort-rule-manager@configured-operations-list"
aria-labelledby="sort-rule-configured-operations"
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)
@include('settings.sort-sets.parts.operation', ['operation' => $operation])
@include('settings.sort-rules.parts.operation', ['operation' => $operation])
@endforeach
</ul>
</div>
<div class="form-group">
<label for="books"
id="sort-set-available-operations">{{ trans('settings.sort_set_available_operations') }}</label>
<ul refs="sort-set-manager@available-operations-list"
aria-labelledby="sort-set-available-operations"
id="sort-rule-available-operations">{{ trans('settings.sort_rule_available_operations') }}</label>
<ul refs="sort-rule-manager@available-operations-list"
aria-labelledby="sort-rule-available-operations"
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>
@foreach(\BookStack\Sorting\SortSetOperation::allExcluding($configuredOps) as $operation)
@include('settings.sort-sets.parts.operation', ['operation' => $operation])
<li class="text-muted empty-state px-m py-s italic text-small">{{ trans('settings.sort_rule_available_operations_empty') }}</li>
@foreach(\BookStack\Sorting\SortRuleOperation::allExcluding($configuredOps) as $operation)
@include('settings.sort-rules.parts.operation', ['operation' => $operation])
@endforeach
</ul>
</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="py-xs flex">
<a href="{{ $set->getUrl() }}">{{ $set->name }}</a>
<a href="{{ $rule->getUrl() }}">{{ $rule->name }}</a>
</div>
<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>
<span title="{{ trans_choice('settings.sort_set_assigned_to_x_books', $set->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>
<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>{{ $rule->books_count ?? 0 }}</span>
</div>
</div>

View file

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

View file

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

View file

@ -300,7 +300,7 @@ class PageTest extends TestCase
]);
$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()

View file

@ -5,7 +5,7 @@ namespace Sorting;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Sorting\SortSet;
use BookStack\Sorting\SortRule;
use Tests\TestCase;
class BookSortTest extends TestCase
@ -223,13 +223,13 @@ class BookSortTest extends TestCase
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();
$resp = $this->asAdmin()->get($book->getUrl('/sort-item'));
$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();
$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()
{
$sort = SortSet::factory()->create();
$sort = SortRule::factory()->create();
$book = $this->entities->book();
$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()
{
$sort = SortSet::factory()->create();
$sort = SortRule::factory()->create();
$book = $this->entities->book();
$bookPage = $book->pages()->first();
$bookPage->priority = 10000;
@ -261,7 +261,7 @@ class BookSortTest extends TestCase
$book->refresh();
$bookPage->refresh();
$this->assertEquals($sort->id, $book->sort_set_id);
$this->assertEquals($sort->id, $book->sort_rule_id);
$this->assertNotEquals(10000, $bookPage->priority);
$resp = $this->get($book->getUrl('/sort'));

View file

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