use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
+/**
+ * @property Model deletable
+ */
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, '/'));
+ }
}
*/
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;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Models\Entity;
use BookStack\Entities\Tools\TrashCan;
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,
]);
}
'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.',
</tr>
@if(count($deletions) === 0)
<tr>
- <td colspan="4">
+ <td colspan="5">
<p class="text-muted"><em>{{ trans('settings.recycle_bin_contents_empty') }}</em></p>
</td>
</tr>
<div component="dropdown" class="dropdown-container">
<button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
<ul refs="dropdown@menu" class="dropdown-menu">
- <li><a class="block" href="{{ url('/settings/recycle-bin/'.$deletion->id.'/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
- <li><a class="block" href="{{ url('/settings/recycle-bin/'.$deletion->id.'/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
+ <li><a class="block" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
+ <li><a class="block" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
</ul>
</div>
</td>
<div class="card content-wrap auto-height">
<h2 class="list-heading">{{ trans('settings.recycle_bin_restore') }}</h2>
<p class="text-muted">{{ trans('settings.recycle_bin_restore_confirm') }}</p>
- <form action="{{ url('/settings/recycle-bin/' . $deletion->id . '/restore') }}" method="post">
+ <form action="{{ $deletion->getUrl('/restore') }}" method="post">
{!! csrf_field() !!}
<a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('common.cancel') }}</a>
<button type="submit" class="button">{{ trans('settings.recycle_bin_restore') }}</button>
@if($deletion->deletable instanceof \BookStack\Entities\Models\Entity)
<hr class="mt-m">
<h5>{{ trans('settings.recycle_bin_restore_list') }}</h5>
- @if($deletion->deletable->getParent() && $deletion->deletable->getParent()->trashed())
- <p class="text-neg">{{ trans('settings.recycle_bin_restore_deleted_parent') }}</p>
- @endif
+ <div class="flex-container-row mb-s items-center">
+ @if($deletion->deletable->getParent() && $deletion->deletable->getParent()->trashed())
+ <div class="text-neg flex">{{ trans('settings.recycle_bin_restore_deleted_parent') }}</div>
+ @endif
+ @if($parentDeletion)
+ <div class="flex fit-content ml-m">
+ <a class="button outline" href="{{ $parentDeletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore_parent') }}</a>
+ </div>
+ @endif
+ </div>
+
@include('settings.recycle-bin.deletable-entity-list', ['entity' => $deletion->deletable])
@endif
$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