diff --git a/app/Entities/Models/Deletion.php b/app/Entities/Models/Deletion.php index 1be0ba4c6..2295a5e21 100644 --- a/app/Entities/Models/Deletion.php +++ b/app/Entities/Models/Deletion.php @@ -7,6 +7,9 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; +/** + * @property Model deletable + */ class Deletion extends Model implements Loggable { @@ -45,4 +48,12 @@ class Deletion extends Model implements Loggable $deletable = $this->deletable()->first(); return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}"; } + + /** + * Get a URL for this specific deletion. + */ + public function getUrl($path): string + { + return url("/settings/recycle-bin/{$this->id}/" . ltrim($path, '/')); + } } diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 561876769..caa25d678 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -266,10 +266,10 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable */ public function getParent(): ?Entity { - if ($this->isA('page')) { + if ($this instanceof Page) { return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first(); } - if ($this->isA('chapter')) { + if ($this instanceof Chapter) { return $this->book()->withTrashed()->first(); } return null; diff --git a/app/Http/Controllers/RecycleBinController.php b/app/Http/Controllers/RecycleBinController.php index a644a2889..312646008 100644 --- a/app/Http/Controllers/RecycleBinController.php +++ b/app/Http/Controllers/RecycleBinController.php @@ -2,6 +2,7 @@ use BookStack\Actions\ActivityType; use BookStack\Entities\Models\Deletion; +use BookStack\Entities\Models\Entity; use BookStack\Entities\Tools\TrashCan; class RecycleBinController extends Controller @@ -44,8 +45,23 @@ class RecycleBinController extends Controller /** @var Deletion $deletion */ $deletion = Deletion::query()->findOrFail($id); + // Walk the parent chain to find any cascading parent deletions + $currentDeletable = $deletion->deletable; + $searching = true; + while ($searching && $currentDeletable instanceof Entity) { + $parent = $currentDeletable->getParent(); + if ($parent && $parent->trashed()) { + $currentDeletable = $parent; + } else { + $searching = false; + } + } + /** @var ?Deletion $parentDeletion */ + $parentDeletion = ($currentDeletable === $deletion->deletable) ? null : $currentDeletable->deletions()->first(); + return view('settings.recycle-bin.restore', [ 'deletion' => $deletion, + 'parentDeletion' => $parentDeletion, ]); } diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index b04ea01cd..789ef9d1b 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -105,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', diff --git a/resources/views/settings/recycle-bin/index.blade.php b/resources/views/settings/recycle-bin/index.blade.php index 54420d0a2..cf9808d95 100644 --- a/resources/views/settings/recycle-bin/index.blade.php +++ b/resources/views/settings/recycle-bin/index.blade.php @@ -47,7 +47,7 @@ @if(count($deletions) === 0) - +

{{ trans('settings.recycle_bin_contents_empty') }}

@@ -95,8 +95,8 @@ diff --git a/resources/views/settings/recycle-bin/restore.blade.php b/resources/views/settings/recycle-bin/restore.blade.php index c888aa8e5..8decd13f6 100644 --- a/resources/views/settings/recycle-bin/restore.blade.php +++ b/resources/views/settings/recycle-bin/restore.blade.php @@ -10,7 +10,7 @@

{{ trans('settings.recycle_bin_restore') }}

{{ trans('settings.recycle_bin_restore_confirm') }}

-
+ {!! csrf_field() !!} {{ trans('common.cancel') }} @@ -19,9 +19,17 @@ @if($deletion->deletable instanceof \BookStack\Entities\Models\Entity)
{{ trans('settings.recycle_bin_restore_list') }}
- @if($deletion->deletable->getParent() && $deletion->deletable->getParent()->trashed()) -

{{ trans('settings.recycle_bin_restore_deleted_parent') }}

- @endif +
+ @if($deletion->deletable->getParent() && $deletion->deletable->getParent()->trashed()) +
{{ trans('settings.recycle_bin_restore_deleted_parent') }}
+ @endif + @if($parentDeletion) + + @endif +
+ @include('settings.recycle-bin.deletable-entity-list', ['entity' => $deletion->deletable]) @endif diff --git a/tests/RecycleBinTest.php b/tests/RecycleBinTest.php index 55a9571de..1cfcc0bce 100644 --- a/tests/RecycleBinTest.php +++ b/tests/RecycleBinTest.php @@ -247,4 +247,22 @@ class RecycleBinTest extends TestCase $chapter->refresh(); $this->assertNull($chapter->deleted_at); } + + public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted() + { + /** @var Book $book */ + $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); + $chapter = $book->chapters->first(); + /** @var Page $page */ + $page = $chapter->pages->first(); + $this->asEditor()->delete($page->getUrl()); + $this->asEditor()->delete($book->getUrl()); + + $bookDeletion = $book->deletions()->first(); + $pageDeletion = $page->deletions()->first(); + + $pageRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$pageDeletion->id}/restore"); + $pageRestoreView->assertSee('The parent of this item has also been deleted.'); + $pageRestoreView->assertElementContains('a[href$="/settings/recycle-bin/' . $bookDeletion->id. '/restore"]', 'Restore Parent'); + } } \ No newline at end of file