From 04197e393ac69934df85df76e5ba2c4361f5e1d4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 3 Oct 2020 18:44:12 +0100 Subject: [PATCH] Started work on the recycle bin interface --- .../{DeleteRecord.php => Deletion.php} | 10 +-- app/Entities/Entity.php | 4 +- app/Entities/EntityProvider.php | 1 + app/Entities/Managers/TrashCan.php | 66 ++++++++++++-- app/Http/Controllers/HomeController.php | 8 +- .../Controllers/MaintenanceController.php | 9 +- app/Http/Controllers/RecycleBinController.php | 35 ++++++++ ...0_09_27_210528_create_deletions_table.php} | 6 +- resources/lang/en/settings.php | 12 +++ resources/sass/styles.scss | 4 +- resources/views/partials/table-user.blade.php | 12 +++ resources/views/settings/audit.blade.php | 11 +-- .../views/settings/maintenance.blade.php | 18 ++++ .../views/settings/recycle-bin.blade.php | 90 +++++++++++++++++++ routes/web.php | 4 + 15 files changed, 259 insertions(+), 31 deletions(-) rename app/Entities/{DeleteRecord.php => Deletion.php} (80%) create mode 100644 app/Http/Controllers/RecycleBinController.php rename database/migrations/{2020_09_27_210528_create_delete_records_table.php => 2020_09_27_210528_create_deletions_table.php} (80%) create mode 100644 resources/views/partials/table-user.blade.php create mode 100644 resources/views/settings/recycle-bin.blade.php diff --git a/app/Entities/DeleteRecord.php b/app/Entities/Deletion.php similarity index 80% rename from app/Entities/DeleteRecord.php rename to app/Entities/Deletion.php index 84b37f5a3..576862caa 100644 --- a/app/Entities/DeleteRecord.php +++ b/app/Entities/Deletion.php @@ -5,7 +5,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; -class DeleteRecord extends Model +class Deletion extends Model { /** @@ -13,21 +13,21 @@ class DeleteRecord extends Model */ public function deletable(): MorphTo { - return $this->morphTo(); + return $this->morphTo('deletable')->withTrashed(); } /** * The the user that performed the deletion. */ - public function deletedBy(): BelongsTo + public function deleter(): BelongsTo { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class, 'deleted_by'); } /** * Create a new deletion record for the provided entity. */ - public static function createForEntity(Entity $entity): DeleteRecord + public static function createForEntity(Entity $entity): Deletion { $record = (new self())->forceFill([ 'deleted_by' => user()->id, diff --git a/app/Entities/Entity.php b/app/Entities/Entity.php index d1a8664e4..14328386c 100644 --- a/app/Entities/Entity.php +++ b/app/Entities/Entity.php @@ -204,9 +204,9 @@ class Entity extends Ownable /** * Get the related delete records for this entity. */ - public function deleteRecords(): MorphMany + public function deletions(): MorphMany { - return $this->morphMany(DeleteRecord::class, 'deletable'); + return $this->morphMany(Deletion::class, 'deletable'); } /** diff --git a/app/Entities/EntityProvider.php b/app/Entities/EntityProvider.php index 6bf923b31..d28afe6f2 100644 --- a/app/Entities/EntityProvider.php +++ b/app/Entities/EntityProvider.php @@ -57,6 +57,7 @@ class EntityProvider /** * Fetch all core entity types as an associated array * with their basic names as the keys. + * @return [string => Entity] */ public function all(): array { diff --git a/app/Entities/Managers/TrashCan.php b/app/Entities/Managers/TrashCan.php index 9a21f5e2c..280694906 100644 --- a/app/Entities/Managers/TrashCan.php +++ b/app/Entities/Managers/TrashCan.php @@ -3,8 +3,9 @@ use BookStack\Entities\Book; use BookStack\Entities\Bookshelf; use BookStack\Entities\Chapter; -use BookStack\Entities\DeleteRecord; +use BookStack\Entities\Deletion; use BookStack\Entities\Entity; +use BookStack\Entities\EntityProvider; use BookStack\Entities\HasCoverImage; use BookStack\Entities\Page; use BookStack\Exceptions\NotifyException; @@ -21,7 +22,7 @@ class TrashCan */ public function softDestroyShelf(Bookshelf $shelf) { - DeleteRecord::createForEntity($shelf); + Deletion::createForEntity($shelf); $shelf->delete(); } @@ -31,7 +32,7 @@ class TrashCan */ public function softDestroyBook(Book $book) { - DeleteRecord::createForEntity($book); + Deletion::createForEntity($book); foreach ($book->pages as $page) { $this->softDestroyPage($page, false); @@ -51,7 +52,7 @@ class TrashCan public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true) { if ($recordDelete) { - DeleteRecord::createForEntity($chapter); + Deletion::createForEntity($chapter); } if (count($chapter->pages) > 0) { @@ -70,7 +71,7 @@ class TrashCan public function softDestroyPage(Page $page, bool $recordDelete = true) { if ($recordDelete) { - DeleteRecord::createForEntity($page); + Deletion::createForEntity($page); } // Check if set as custom homepage & remove setting if not used or throw error if active @@ -151,6 +152,59 @@ class TrashCan $page->forceDelete(); } + /** + * Get the total counts of those that have been trashed + * but not yet fully deleted (In recycle bin). + */ + public function getTrashedCounts(): array + { + $provider = app(EntityProvider::class); + $counts = []; + + /** @var Entity $instance */ + foreach ($provider->all() as $key => $instance) { + $counts[$key] = $instance->newQuery()->onlyTrashed()->count(); + } + + return $counts; + } + + /** + * Destroy all items that have pending deletions. + */ + public function destroyFromAllDeletions() + { + $deletions = Deletion::all(); + foreach ($deletions as $deletion) { + // For each one we load in the relation since it may have already + // been deleted as part of another deletion in this loop. + $entity = $deletion->deletable()->first(); + if ($entity) { + $this->destroyEntity($deletion->deletable); + } + $deletion->delete(); + } + } + + /** + * Destroy the given entity. + */ + protected function destroyEntity(Entity $entity) + { + if ($entity->isA('page')) { + return $this->destroyPage($entity); + } + if ($entity->isA('chapter')) { + return $this->destroyChapter($entity); + } + if ($entity->isA('book')) { + return $this->destroyBook($entity); + } + if ($entity->isA('shelf')) { + return $this->destroyShelf($entity); + } + } + /** * Update entity relations to remove or update outstanding connections. */ @@ -163,7 +217,7 @@ class TrashCan $entity->comments()->delete(); $entity->jointPermissions()->delete(); $entity->searchTerms()->delete(); - $entity->deleteRecords()->delete(); + $entity->deletions()->delete(); if ($entity instanceof HasCoverImage && $entity->cover) { $imageService = app()->make(ImageService::class); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 3d68b8bcd..3b8b7c6e2 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -14,7 +14,6 @@ class HomeController extends Controller /** * Display the homepage. - * @return Response */ public function index() { @@ -22,9 +21,12 @@ class HomeController extends Controller $draftPages = []; if ($this->isSignedIn()) { - $draftPages = Page::visible()->where('draft', '=', true) + $draftPages = Page::visible() + ->where('draft', '=', true) ->where('created_by', '=', user()->id) - ->orderBy('updated_at', 'desc')->take(6)->get(); + ->orderBy('updated_at', 'desc') + ->take(6) + ->get(); } $recentFactor = count($draftPages) > 0 ? 0.5 : 1; diff --git a/app/Http/Controllers/MaintenanceController.php b/app/Http/Controllers/MaintenanceController.php index 664a896b2..0d6265f90 100644 --- a/app/Http/Controllers/MaintenanceController.php +++ b/app/Http/Controllers/MaintenanceController.php @@ -2,6 +2,7 @@ namespace BookStack\Http\Controllers; +use BookStack\Entities\Managers\TrashCan; use BookStack\Notifications\TestEmail; use BookStack\Uploads\ImageService; use Illuminate\Http\Request; @@ -19,7 +20,13 @@ class MaintenanceController extends Controller // Get application version $version = trim(file_get_contents(base_path('version'))); - return view('settings.maintenance', ['version' => $version]); + // Recycle bin details + $recycleStats = (new TrashCan())->getTrashedCounts(); + + return view('settings.maintenance', [ + 'version' => $version, + 'recycleStats' => $recycleStats, + ]); } /** diff --git a/app/Http/Controllers/RecycleBinController.php b/app/Http/Controllers/RecycleBinController.php new file mode 100644 index 000000000..b30eddf0c --- /dev/null +++ b/app/Http/Controllers/RecycleBinController.php @@ -0,0 +1,35 @@ +checkPermission('settings-manage'); + $this->checkPermission('restrictions-manage-all'); + + $deletions = Deletion::query()->with(['deletable', 'deleter'])->paginate(10); + + return view('settings.recycle-bin', [ + 'deletions' => $deletions, + ]); + } + + /** + * Empty out the recycle bin. + */ + public function empty() + { + $this->checkPermission('settings-manage'); + $this->checkPermission('restrictions-manage-all'); + + (new TrashCan())->destroyFromAllDeletions(); + return redirect('/settings/recycle-bin'); + } +} diff --git a/database/migrations/2020_09_27_210528_create_delete_records_table.php b/database/migrations/2020_09_27_210528_create_deletions_table.php similarity index 80% rename from database/migrations/2020_09_27_210528_create_delete_records_table.php rename to database/migrations/2020_09_27_210528_create_deletions_table.php index cdb18ced6..c38a9357f 100644 --- a/database/migrations/2020_09_27_210528_create_delete_records_table.php +++ b/database/migrations/2020_09_27_210528_create_deletions_table.php @@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreateDeleteRecordsTable extends Migration +class CreateDeletionsTable extends Migration { /** * Run the migrations. @@ -13,7 +13,7 @@ class CreateDeleteRecordsTable extends Migration */ public function up() { - Schema::create('delete_records', function (Blueprint $table) { + Schema::create('deletions', function (Blueprint $table) { $table->increments('id'); $table->integer('deleted_by'); $table->string('deletable_type', 100); @@ -33,6 +33,6 @@ class CreateDeleteRecordsTable extends Migration */ public function down() { - Schema::dropIfExists('delete_records'); + Schema::dropIfExists('deletions'); } } diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index e280396a2..66a1fe30c 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -80,6 +80,18 @@ return [ 'maint_send_test_email_mail_subject' => 'Test Email', 'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!', 'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.', + 'maint_recycle_bin_desc' => 'Items deleted remain in the recycle bin until it is emptied. Open the recycle bin to restore or permanently remove items.', + 'maint_recycle_bin_open' => 'Open Recycle Bin', + + // Recycle Bin + 'recycle_bin' => 'Recycle Bin', + 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', + 'recycle_bin_deleted_item' => 'Deleted Item', + 'recycle_bin_deleted_by' => 'Deleted By', + 'recycle_bin_deleted_at' => 'Deletion Time', + 'recycle_bin_contents_empty' => 'The recycle bin is currently empty', + 'recycle_bin_empty' => 'Empty Recycle Bin', + 'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?', // Audit Log 'audit' => 'Audit Log', diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index 376541b5d..78d94f977 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -290,12 +290,12 @@ $btt-size: 40px; } } -table a.audit-log-user { +table.table .table-user-item { display: grid; grid-template-columns: 42px 1fr; align-items: center; } -table a.icon-list-item { +table.table .table-entity-item { display: grid; grid-template-columns: 36px 1fr; align-items: center; diff --git a/resources/views/partials/table-user.blade.php b/resources/views/partials/table-user.blade.php new file mode 100644 index 000000000..a8f2777f0 --- /dev/null +++ b/resources/views/partials/table-user.blade.php @@ -0,0 +1,12 @@ +{{-- +$user - User mode to display, Can be null. +$user_id - Id of user to show. Must be provided. +--}} +@if($user) + +
{{ $user->name }}
+
{{ $user->name }}
+
+@else + [ID: {{ $user_id }}] {{ trans('common.deleted_user') }} +@endif \ No newline at end of file diff --git a/resources/views/settings/audit.blade.php b/resources/views/settings/audit.blade.php index 9b97f060d..47a2355d1 100644 --- a/resources/views/settings/audit.blade.php +++ b/resources/views/settings/audit.blade.php @@ -60,19 +60,12 @@ @foreach($activities as $activity) - @if($activity->user) - -
{{ $activity->user->name }}
-
{{ $activity->user->name }}
-
- @else - [ID: {{ $activity->user_id }}] {{ trans('common.deleted_user') }} - @endif + @include('partials.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id]) {{ $activity->key }} @if($activity->entity) - + @icon($activity->entity->getType())
{{ $activity->entity->name }} diff --git a/resources/views/settings/maintenance.blade.php b/resources/views/settings/maintenance.blade.php index 35686ca33..804112c91 100644 --- a/resources/views/settings/maintenance.blade.php +++ b/resources/views/settings/maintenance.blade.php @@ -50,5 +50,23 @@
+
+

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

+
+
+ @stop diff --git a/resources/views/settings/recycle-bin.blade.php b/resources/views/settings/recycle-bin.blade.php new file mode 100644 index 000000000..145eb5d3c --- /dev/null +++ b/resources/views/settings/recycle-bin.blade.php @@ -0,0 +1,90 @@ +@extends('simple-layout') + +@section('body') +
+ +
+
+ @include('settings.navbar', ['selected' => 'maintenance']) +
+
+ +
+

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

+ +
+
+

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

+
+
+ + +
+
+ + +
+ + {!! $deletions->links() !!} + + + + + + + + @if(count($deletions) === 0) + + + + @endif + @foreach($deletions as $deletion) + + + + + + @endforeach +
{{ trans('settings.recycle_bin_deleted_item') }}{{ trans('settings.recycle_bin_deleted_by') }}{{ trans('settings.recycle_bin_deleted_at') }}
+

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

+
+
+ @icon($deletion->deletable->getType()) +
+ {{ $deletion->deletable->name }} +
+
+ @if($deletion->deletable instanceof \BookStack\Entities\Book) +
+
+ @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }} +
+
+ @endif + @if($deletion->deletable instanceof \BookStack\Entities\Book || $deletion->deletable instanceof \BookStack\Entities\Chapter) +
+
+ @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }} +
+
+ @endif +
@include('partials.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by]){{ $deletion->created_at }}
+ + {!! $deletions->links() !!} + +
+ +
+@stop diff --git a/routes/web.php b/routes/web.php index acbcb4e8f..20f6639a5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -166,6 +166,10 @@ Route::group(['middleware' => 'auth'], function () { Route::delete('/maintenance/cleanup-images', 'MaintenanceController@cleanupImages'); Route::post('/maintenance/send-test-email', 'MaintenanceController@sendTestEmail'); + // Recycle Bin + Route::get('/recycle-bin', 'RecycleBinController@index'); + Route::post('/recycle-bin/empty', 'RecycleBinController@empty'); + // Audit Log Route::get('/audit', 'AuditLogController@index');