diff --git a/app/Http/Controllers/ReferenceController.php b/app/Http/Controllers/ReferenceController.php
index bed2c5f30..3af4feb06 100644
--- a/app/Http/Controllers/ReferenceController.php
+++ b/app/Http/Controllers/ReferenceController.php
@@ -3,7 +3,12 @@
namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\PermissionApplicator;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class ReferenceController extends Controller
@@ -23,8 +28,64 @@ class ReferenceController extends Controller
{
/** @var Page $page */
$page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->firstOrFail();
+ $references = $this->getEntityReferences($page);
- $baseQuery = $page->referencesTo()
+ return view('pages.references', [
+ 'page' => $page,
+ 'references' => $references,
+ ]);
+ }
+
+ /**
+ * Display the references to a given chapter.
+ */
+ public function chapter(string $bookSlug, string $chapterSlug)
+ {
+ /** @var Chapter $chapter */
+ $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
+ $references = $this->getEntityReferences($chapter);
+
+ return view('chapters.references', [
+ 'chapter' => $chapter,
+ 'references' => $references,
+ ]);
+ }
+
+ /**
+ * Display the references to a given book.
+ */
+ public function book(string $slug)
+ {
+ $book = Book::visible()->where('slug', '=', $slug)->firstOrFail();
+ $references = $this->getEntityReferences($book);
+
+ return view('books.references', [
+ 'book' => $book,
+ 'references' => $references,
+ ]);
+ }
+
+ /**
+ * Display the references to a given shelf.
+ */
+ public function shelf(string $slug)
+ {
+ $shelf = Bookshelf::visible()->where('slug', '=', $slug)->firstOrFail();
+ $references = $this->getEntityReferences($shelf);
+
+ return view('shelves.references', [
+ 'shelf' => $shelf,
+ 'references' => $references,
+ ]);
+ }
+
+ /**
+ * Query the references for the given entities.
+ * Loads the commonly required relations while taking permissions into account.
+ */
+ protected function getEntityReferences(Entity $entity): Collection
+ {
+ $baseQuery = $entity->referencesTo()
->where('from_type', '=', (new Page())->getMorphClass())
->with([
'from' => fn(Relation $query) => $query->select(Page::$listAttributes),
@@ -39,9 +100,6 @@ class ReferenceController extends Controller
'from_type'
)->get();
- return view('pages.references', [
- 'page' => $page,
- 'references' => $references,
- ]);
+ return $references;
}
}
diff --git a/resources/views/books/references.blade.php b/resources/views/books/references.blade.php
new file mode 100644
index 000000000..2468ed111
--- /dev/null
+++ b/resources/views/books/references.blade.php
@@ -0,0 +1,20 @@
+@extends('layouts.simple')
+
+@section('body')
+
+
+
+
+ @include('entities.breadcrumbs', ['crumbs' => [
+ $book,
+ $book->getUrl('/references') => [
+ 'text' => trans('entities.references'),
+ 'icon' => 'reference',
+ ]
+ ]])
+
+
+ @include('entities.references', ['references' => $references])
+
+
+@stop
diff --git a/resources/views/chapters/references.blade.php b/resources/views/chapters/references.blade.php
new file mode 100644
index 000000000..7241c2b55
--- /dev/null
+++ b/resources/views/chapters/references.blade.php
@@ -0,0 +1,21 @@
+@extends('layouts.simple')
+
+@section('body')
+
+
+
+
+ @include('entities.breadcrumbs', ['crumbs' => [
+ $chapter->book,
+ $chapter,
+ $chapter->getUrl('/references') => [
+ 'text' => trans('entities.references'),
+ 'icon' => 'reference',
+ ]
+ ]])
+
+
+ @include('entities.references', ['references' => $references])
+
+
+@stop
diff --git a/resources/views/entities/references.blade.php b/resources/views/entities/references.blade.php
new file mode 100644
index 000000000..db9e167aa
--- /dev/null
+++ b/resources/views/entities/references.blade.php
@@ -0,0 +1,13 @@
+
+ {{ trans('entities.references') }}
+ {{ trans('entities.references_to_desc') }}
+
+ @if(count($references) > 0)
+
+ @include('entities.list', ['entities' => $references->pluck('from'), 'showPath' => true])
+
+ @else
+ {{ trans('entities.references_none') }}
+ @endif
+
+
\ No newline at end of file
diff --git a/resources/views/pages/references.blade.php b/resources/views/pages/references.blade.php
index 3f35a1629..42ae7076f 100644
--- a/resources/views/pages/references.blade.php
+++ b/resources/views/pages/references.blade.php
@@ -16,19 +16,7 @@
]])
-
- {{ trans('entities.references') }}
- {{ trans('entities.references_to_desc') }}
-
- @if(count($references) > 0)
-
- @include('entities.list', ['entities' => $references->pluck('from'), 'showPath' => true])
-
- @else
- {{ trans('entities.references_none') }}
- @endif
-
-
+ @include('entities.references', ['references' => $references])
@stop
diff --git a/resources/views/shelves/references.blade.php b/resources/views/shelves/references.blade.php
new file mode 100644
index 000000000..7336c07af
--- /dev/null
+++ b/resources/views/shelves/references.blade.php
@@ -0,0 +1,20 @@
+@extends('layouts.simple')
+
+@section('body')
+
+
+
+
+ @include('entities.breadcrumbs', ['crumbs' => [
+ $shelf,
+ $shelf->getUrl('/references') => [
+ 'text' => trans('entities.references'),
+ 'icon' => 'reference',
+ ]
+ ]])
+
+
+ @include('entities.references', ['references' => $references])
+
+
+@stop
diff --git a/routes/web.php b/routes/web.php
index a16960283..dc46821cb 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -64,6 +64,7 @@ Route::middleware('auth')->group(function () {
Route::get('/shelves/{slug}/permissions', [BookshelfController::class, 'showPermissions']);
Route::put('/shelves/{slug}/permissions', [BookshelfController::class, 'permissions']);
Route::post('/shelves/{slug}/copy-permissions', [BookshelfController::class, 'copyPermissions']);
+ Route::get('/shelves/{slug}/references', [ReferenceController::class, 'shelf']);
// Book Creation
Route::get('/shelves/{shelfSlug}/create-book', [BookController::class, 'create']);
@@ -86,6 +87,7 @@ Route::middleware('auth')->group(function () {
Route::post('/books/{bookSlug}/convert-to-shelf', [BookController::class, 'convertToShelf']);
Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']);
Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']);
+ Route::get('/books/{slug}/references', [ReferenceController::class, 'book']);
Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']);
Route::get('/books/{bookSlug}/export/pdf', [BookExportController::class, 'pdf']);
Route::get('/books/{bookSlug}/export/markdown', [BookExportController::class, 'markdown']);
@@ -142,6 +144,7 @@ Route::middleware('auth')->group(function () {
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/markdown', [ChapterExportController::class, 'markdown']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/plaintext', [ChapterExportController::class, 'plainText']);
Route::put('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'permissions']);
+ Route::get('/books/{bookSlug}/chapter/{chapterSlug}/references', [ReferenceController::class, 'chapter']);
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/delete', [ChapterController::class, 'showDelete']);
Route::delete('/books/{bookSlug}/chapter/{chapterSlug}', [ChapterController::class, 'destroy']);
diff --git a/tests/References/ReferencesTest.php b/tests/References/ReferencesTest.php
index 1285f5916..20829b6b4 100644
--- a/tests/References/ReferencesTest.php
+++ b/tests/References/ReferencesTest.php
@@ -54,6 +54,46 @@ class ReferencesTest extends TestCase
$this->assertDatabaseMissing('references', ['to_id' => $pageA->id, 'to_type' => $pageA->getMorphClass()]);
}
+ public function test_references_to_visible_on_references_page()
+ {
+ $entities = $this->getEachEntityType();
+ $this->asEditor();
+ foreach ($entities as $entity) {
+ $this->createReference($entities['page'], $entity);
+ }
+
+ foreach ($entities as $entity) {
+ $resp = $this->get($entity->getUrl('/references'));
+ $resp->assertSee('References');
+ $resp->assertSee($entities['page']->name);
+ $resp->assertDontSee('There are no tracked references');
+ }
+ }
+
+ public function test_reference_not_visible_if_view_permission_does_not_permit()
+ {
+ /** @var Page $page */
+ /** @var Page $pageB */
+ $page = Page::query()->first();
+ $pageB = Page::query()->where('id', '!=', $page->id)->first();
+ $this->createReference($pageB, $page);
+
+ $this->setEntityRestrictions($pageB);
+
+ $this->asEditor()->get($page->getUrl('/references'))->assertDontSee($pageB->name);
+ $this->asAdmin()->get($page->getUrl('/references'))->assertSee($pageB->name);
+ }
+
+ public function test_reference_page_shows_empty_state_with_no_references()
+ {
+ /** @var Page $page */
+ $page = Page::query()->first();
+
+ $this->asEditor()
+ ->get($page->getUrl('/references'))
+ ->assertSee('There are no tracked references');
+ }
+
protected function createReference(Model $from, Model $to)
{
(new Reference())->forceFill([
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 92ae33a4e..3ca7638c8 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -430,7 +430,7 @@ abstract class TestCase extends BaseTestCase
}
/**
- * @return Entity[]
+ * @return array{page: Page, chapter: Chapter, book: Book, bookshelf: Bookshelf}
*/
protected function getEachEntityType(): array
{