Started work on the recycle bin interface
This commit is contained in:
parent
691027a522
commit
04197e393a
15 changed files with 259 additions and 31 deletions
|
@ -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,
|
|
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
35
app/Http/Controllers/RecycleBinController.php
Normal file
35
app/Http/Controllers/RecycleBinController.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Entities\Deletion;
|
||||
use BookStack\Entities\Managers\TrashCan;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RecycleBinController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the top-level listing for the recycle bin.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
12
resources/views/partials/table-user.blade.php
Normal file
12
resources/views/partials/table-user.blade.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{--
|
||||
$user - User mode to display, Can be null.
|
||||
$user_id - Id of user to show. Must be provided.
|
||||
--}}
|
||||
@if($user)
|
||||
<a href="{{ $user->getEditUrl() }}" class="table-user-item">
|
||||
<div><img class="avatar block" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></div>
|
||||
<div>{{ $user->name }}</div>
|
||||
</a>
|
||||
@else
|
||||
[ID: {{ $user_id }}] {{ trans('common.deleted_user') }}
|
||||
@endif
|
|
@ -60,19 +60,12 @@
|
|||
@foreach($activities as $activity)
|
||||
<tr>
|
||||
<td>
|
||||
@if($activity->user)
|
||||
<a href="{{ $activity->user->getEditUrl() }}" class="audit-log-user">
|
||||
<div><img class="avatar block" src="{{ $activity->user->getAvatar(40)}}" alt="{{ $activity->user->name }}"></div>
|
||||
<div>{{ $activity->user->name }}</div>
|
||||
</a>
|
||||
@else
|
||||
[ID: {{ $activity->user_id }}] {{ trans('common.deleted_user') }}
|
||||
@endif
|
||||
@include('partials.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id])
|
||||
</td>
|
||||
<td>{{ $activity->key }}</td>
|
||||
<td>
|
||||
@if($activity->entity)
|
||||
<a href="{{ $activity->entity->getUrl() }}" class="icon-list-item">
|
||||
<a href="{{ $activity->entity->getUrl() }}" class="table-entity-item">
|
||||
<span role="presentation" class="icon text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
|
||||
<div class="text-{{ $activity->entity->getType() }}">
|
||||
{{ $activity->entity->name }}
|
||||
|
|
|
@ -50,5 +50,23 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card content-wrap auto-height pb-xl">
|
||||
<h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
|
||||
<div class="grid half gap-xl">
|
||||
<div>
|
||||
<p class="small text-muted">{{ trans('settings.maint_recycle_bin_desc') }}</p>
|
||||
<div class="grid half no-gap">
|
||||
<p class="mb-xs text-bookshelf">@icon('bookshelf'){{ trans('entities.shelves') }}: {{ $recycleStats['bookshelf'] }}</p>
|
||||
<p class="mb-xs text-book">@icon('book'){{ trans('entities.books') }}: {{ $recycleStats['book'] }}</p>
|
||||
<p class="mb-xs text-chapter">@icon('chapter'){{ trans('entities.chapters') }}: {{ $recycleStats['chapter'] }}</p>
|
||||
<p class="mb-xs text-page">@icon('page'){{ trans('entities.pages') }}: {{ $recycleStats['page'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('settings.maint_recycle_bin_open') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@stop
|
||||
|
|
90
resources/views/settings/recycle-bin.blade.php
Normal file
90
resources/views/settings/recycle-bin.blade.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
@extends('simple-layout')
|
||||
|
||||
@section('body')
|
||||
<div class="container">
|
||||
|
||||
<div class="grid left-focus v-center no-row-gap">
|
||||
<div class="py-m">
|
||||
@include('settings.navbar', ['selected' => 'maintenance'])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
|
||||
|
||||
<div class="grid half left-focus">
|
||||
<div>
|
||||
<p class="text-muted">{{ trans('settings.recycle_bin_desc') }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div component="dropdown" class="dropdown-container">
|
||||
<button refs="dropdown@toggle"
|
||||
type="button"
|
||||
class="button outline">{{ trans('settings.recycle_bin_empty') }} </button>
|
||||
<div refs="dropdown@menu" class="dropdown-menu">
|
||||
<p class="text-neg small px-m mb-xs">{{ trans('settings.recycle_bin_empty_confirm') }}</p>
|
||||
|
||||
<form action="{{ url('/settings/recycle-bin/empty') }}" method="POST">
|
||||
{!! csrf_field() !!}
|
||||
<button type="submit" class="text-primary small delete">{{ trans('common.confirm') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr class="mt-l mb-s">
|
||||
|
||||
{!! $deletions->links() !!}
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{{ trans('settings.recycle_bin_deleted_item') }}</th>
|
||||
<th>{{ trans('settings.recycle_bin_deleted_by') }}</th>
|
||||
<th>{{ trans('settings.recycle_bin_deleted_at') }}</th>
|
||||
</tr>
|
||||
@if(count($deletions) === 0)
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<p class="text-muted"><em>{{ trans('settings.recycle_bin_contents_empty') }}</em></p>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@foreach($deletions as $deletion)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="table-entity-item mb-m">
|
||||
<span role="presentation" class="icon text-{{$deletion->deletable->getType()}}">@icon($deletion->deletable->getType())</span>
|
||||
<div class="text-{{ $deletion->deletable->getType() }}">
|
||||
{{ $deletion->deletable->name }}
|
||||
</div>
|
||||
</div>
|
||||
@if($deletion->deletable instanceof \BookStack\Entities\Book)
|
||||
<div class="pl-xl block inline">
|
||||
<div class="text-chapter">
|
||||
@icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if($deletion->deletable instanceof \BookStack\Entities\Book || $deletion->deletable instanceof \BookStack\Entities\Chapter)
|
||||
<div class="pl-xl block inline">
|
||||
<div class="text-page">
|
||||
@icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>@include('partials.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])</td>
|
||||
<td>{{ $deletion->created_at }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
|
||||
{!! $deletions->links() !!}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@stop
|
|
@ -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');
|
||||
|
||||
|
|
Loading…
Reference in a new issue