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, + ]); + } + } +}