diff --git a/app/Sorting/SortSetController.php b/app/Sorting/SortSetController.php
index b0ad2a7d7..7b1c0bc41 100644
--- a/app/Sorting/SortSetController.php
+++ b/app/Sorting/SortSetController.php
@@ -4,14 +4,13 @@ namespace BookStack\Sorting;
use BookStack\Activity\ActivityType;
use BookStack\Http\Controller;
-use BookStack\Http\Request;
+use Illuminate\Http\Request;
class SortSetController extends Controller
{
public function __construct()
{
$this->middleware('can:settings-manage');
- // TODO - Test
}
public function create()
diff --git a/database/factories/Entities/Models/BookFactory.php b/database/factories/Entities/Models/BookFactory.php
index 9cb8e971c..29403a294 100644
--- a/database/factories/Entities/Models/BookFactory.php
+++ b/database/factories/Entities/Models/BookFactory.php
@@ -26,7 +26,9 @@ class BookFactory extends Factory
'name' => $this->faker->sentence(),
'slug' => Str::random(10),
'description' => $description,
- 'description_html' => '
' . e($description) . '
'
+ 'description_html' => '' . e($description) . '
',
+ 'sort_set_id' => null,
+ 'default_template_id' => null,
];
}
}
diff --git a/lang/en/settings.php b/lang/en/settings.php
index 19ffd9240..344c186cb 100644
--- a/lang/en/settings.php
+++ b/lang/en/settings.php
@@ -80,6 +80,7 @@ return [
'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',
diff --git a/resources/views/settings/sort-sets/parts/sort-set-list-item.blade.php b/resources/views/settings/sort-sets/parts/sort-set-list-item.blade.php
index e977c286e..6c0b84047 100644
--- a/resources/views/settings/sort-sets/parts/sort-set-list-item.blade.php
+++ b/resources/views/settings/sort-sets/parts/sort-set-list-item.blade.php
@@ -6,7 +6,7 @@
{{ implode(', ', array_map(fn ($op) => $op->getLabel(), $set->getOperations())) }}
- @icon('book'){{ $set->books_count ?? 0 }}
\ No newline at end of file
diff --git a/tests/Sorting/SortSetTest.php b/tests/Sorting/SortSetTest.php
new file mode 100644
index 000000000..5f30034bd
--- /dev/null
+++ b/tests/Sorting/SortSetTest.php
@@ -0,0 +1,200 @@
+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}"],
+ ];
+
+ foreach ($actions as [$method, $path]) {
+ $resp = $this->call($method, $path);
+ $this->assertPermissionError($resp);
+ }
+
+ $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
+
+ foreach ($actions as [$method, $path]) {
+ $resp = $this->call($method, $path);
+ $this->assertNotPermissionError($resp);
+ }
+ }
+
+ public function test_create_flow()
+ {
+ $resp = $this->asAdmin()->get('/settings/sorting');
+ $this->withHtml($resp)->assertLinkExists(url('/settings/sorting/sets/new'));
+
+ $resp = $this->get('/settings/sorting/sets/new');
+ $this->withHtml($resp)->assertElementExists('form[action$="/settings/sorting/sets"] input[name="name"]');
+ $resp->assertSeeText('Name - Alphabetical (Asc)');
+
+ $details = ['name' => 'My new sort', 'sequence' => 'name_asc'];
+ $resp = $this->post('/settings/sorting/sets', $details);
+ $resp->assertRedirect('/settings/sorting');
+
+ $this->assertActivityExists(ActivityType::SORT_SET_CREATE);
+ $this->assertDatabaseHas('sort_sets', $details);
+ }
+
+ public function test_listing_in_settings()
+ {
+ $set = SortSet::factory()->create(['name' => 'My super sort set', 'sequence' => 'name_asc']);
+ $books = Book::query()->limit(5)->get();
+ foreach ($books as $book) {
+ $book->sort_set_id = $set->id;
+ $book->save();
+ }
+
+ $resp = $this->asAdmin()->get('/settings/sorting');
+ $resp->assertSeeText('My super sort set');
+ $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']);
+
+ $resp = $this->asAdmin()->get("/settings/sorting/sets/{$set->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->assertRedirect('/settings/sorting');
+ $this->assertActivityExists(ActivityType::SORT_SET_UPDATE);
+ $this->assertDatabaseHas('sort_sets', $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;
+ $book->save();
+ $chapter->priority = 10000;
+ $chapter->save();
+
+ $resp = $this->asAdmin()->put("/settings/sorting/sets/{$set->id}", ['name' => $set->name, 'sequence' => 'chapters_last']);
+ $resp->assertRedirect('/settings/sorting');
+
+ $chapter->refresh();
+ $this->assertNotEquals(10000, $chapter->priority);
+ }
+
+ public function test_delete_flow()
+ {
+ $set = SortSet::factory()->create();
+
+ $resp = $this->asAdmin()->get("/settings/sorting/sets/{$set->id}");
+ $resp->assertSeeText('Delete Sort Set');
+
+ $resp = $this->delete("settings/sorting/sets/{$set->id}");
+ $resp->assertRedirect('/settings/sorting');
+
+ $this->assertActivityExists(ActivityType::SORT_SET_DELETE);
+ $this->assertDatabaseMissing('sort_sets', ['id' => $set->id]);
+ }
+
+ public function test_delete_requires_confirmation_if_books_assigned()
+ {
+ $set = SortSet::factory()->create();
+ $books = Book::query()->limit(5)->get();
+ foreach ($books as $book) {
+ $book->sort_set_id = $set->id;
+ $book->save();
+ }
+
+ $resp = $this->asAdmin()->get("/settings/sorting/sets/{$set->id}");
+ $resp->assertSeeText('Delete Sort Set');
+
+ $resp = $this->delete("settings/sorting/sets/{$set->id}");
+ $resp->assertRedirect("/settings/sorting/sets/{$set->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 = $this->delete("settings/sorting/sets/{$set->id}", ['confirm' => 'true']);
+ $resp->assertRedirect('/settings/sorting');
+ $this->assertDatabaseMissing('sort_sets', ['id' => $set->id]);
+ $this->assertDatabaseMissing('books', ['sort_set_id' => $set->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;
+ $book->save();
+
+ $resp = $this->actingAsApiEditor()->post("/api/pages", [
+ 'book_id' => $book->id,
+ 'name' => '1111 page',
+ 'markdown' => 'Hi'
+ ]);
+ $resp->assertOk();
+
+ $this->assertDatabaseHas('pages', [
+ 'book_id' => $book->id,
+ 'name' => '1111 page',
+ 'priority' => $book->chapters()->count() + 1,
+ ]);
+ }
+
+ public function test_name_numeric_ordering()
+ {
+ $book = Book::factory()->create();
+ $set = SortSet::factory()->create(['sequence' => 'name_numeric_asc']);
+ $book->sort_set_id = $set->id;
+ $book->save();
+ $this->permissions->regenerateForEntity($book);
+
+ $namesToAdd = [
+ "1 - Pizza",
+ "2.0 - Tomato",
+ "2.5 - Beans",
+ "10 - Bread",
+ "20 - Milk",
+ ];
+
+ foreach ($namesToAdd as $name) {
+ $this->actingAsApiEditor()->post("/api/pages", [
+ 'book_id' => $book->id,
+ 'name' => $name,
+ 'markdown' => 'Hello'
+ ]);
+ }
+
+ foreach ($namesToAdd as $index => $name) {
+ $this->assertDatabaseHas('pages', [
+ 'book_id' => $book->id,
+ 'name' => $name,
+ 'priority' => $index + 1,
+ ]);
+ }
+ }
+}