From bf8e7f3393d48e6300c4d8775daeb40d55ea2017 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Sun, 16 May 2021 00:29:56 +0100 Subject: [PATCH] Started addition of favourite system --- app/Actions/Favourite.php | 17 +++++ app/Actions/View.php | 4 +- app/Entities/Models/Entity.php | 22 +++++- app/Http/Controllers/FavouriteController.php | 76 +++++++++++++++++++ app/Interfaces/Favouritable.php | 11 +++ ...1_05_15_173110_create_favourites_table.php | 36 +++++++++ resources/icons/star-circle.svg | 1 - resources/icons/star-outline.svg | 1 + resources/lang/en/activities.php | 4 + resources/lang/en/common.php | 2 + resources/views/pages/show.blade.php | 3 + .../entity-favourite-action.blade.php | 12 +++ routes/web.php | 5 ++ 13 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 app/Actions/Favourite.php create mode 100644 app/Http/Controllers/FavouriteController.php create mode 100644 app/Interfaces/Favouritable.php create mode 100644 database/migrations/2021_05_15_173110_create_favourites_table.php create mode 100644 resources/icons/star-outline.svg create mode 100644 resources/views/partials/entity-favourite-action.blade.php diff --git a/app/Actions/Favourite.php b/app/Actions/Favourite.php new file mode 100644 index 000000000..107a76578 --- /dev/null +++ b/app/Actions/Favourite.php @@ -0,0 +1,17 @@ +<?php namespace BookStack\Actions; + +use BookStack\Model; +use Illuminate\Database\Eloquent\Relations\MorphTo; + +class Favourite extends Model +{ + protected $fillable = ['user_id']; + + /** + * Get the related model that can be favourited. + */ + public function favouritable(): MorphTo + { + return $this->morphTo(); + } +} diff --git a/app/Actions/View.php b/app/Actions/View.php index e9841293b..c5ec6a38d 100644 --- a/app/Actions/View.php +++ b/app/Actions/View.php @@ -1,6 +1,7 @@ <?php namespace BookStack\Actions; use BookStack\Model; +use Illuminate\Database\Eloquent\Relations\MorphTo; class View extends Model { @@ -9,9 +10,8 @@ class View extends Model /** * Get all owning viewable models. - * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ - public function viewable() + public function viewable(): MorphTo { return $this->morphTo(); } diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index d4b477304..be63cec2b 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -2,6 +2,7 @@ use BookStack\Actions\Activity; use BookStack\Actions\Comment; +use BookStack\Actions\Favourite; use BookStack\Actions\Tag; use BookStack\Actions\View; use BookStack\Auth\Permissions\EntityPermission; @@ -9,6 +10,7 @@ use BookStack\Auth\Permissions\JointPermission; use BookStack\Entities\Tools\SearchIndex; use BookStack\Entities\Tools\SlugGenerator; use BookStack\Facades\Permissions; +use BookStack\Interfaces\Favouritable; use BookStack\Interfaces\Sluggable; use BookStack\Model; use BookStack\Traits\HasCreatorAndUpdater; @@ -38,7 +40,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @method static Builder withLastView() * @method static Builder withViewCount() */ -abstract class Entity extends Model implements Sluggable +abstract class Entity extends Model implements Sluggable, Favouritable { use SoftDeletes; use HasCreatorAndUpdater; @@ -297,4 +299,22 @@ abstract class Entity extends Model implements Sluggable $this->slug = app(SlugGenerator::class)->generate($this); return $this->slug; } + + /** + * @inheritdoc + */ + public function favourites(): MorphMany + { + return $this->morphMany(Favourite::class, 'favouritable'); + } + + /** + * Check if the entity is a favourite of the current user. + */ + public function isFavourite(): bool + { + return $this->favourites() + ->where('user_id', '=', user()->id) + ->exists(); + } } diff --git a/app/Http/Controllers/FavouriteController.php b/app/Http/Controllers/FavouriteController.php new file mode 100644 index 000000000..8a26eac8e --- /dev/null +++ b/app/Http/Controllers/FavouriteController.php @@ -0,0 +1,76 @@ +<?php + +namespace BookStack\Http\Controllers; + +use BookStack\Entities\Models\Entity; +use BookStack\Interfaces\Favouritable; +use BookStack\Model; +use Illuminate\Http\Request; + +class FavouriteController extends Controller +{ + /** + * Add a new item as a favourite. + */ + public function add(Request $request) + { + $favouritable = $this->getValidatedModelFromRequest($request); + $favouritable->favourites()->firstOrCreate([ + 'user_id' => user()->id, + ]); + + $this->showSuccessNotification(trans('activities.favourite_add_notification', [ + 'name' => $favouritable->name, + ])); + return redirect()->back(); + } + + /** + * Remove an item as a favourite. + */ + public function remove(Request $request) + { + $favouritable = $this->getValidatedModelFromRequest($request); + $favouritable->favourites()->where([ + 'user_id' => user()->id, + ])->delete(); + + $this->showSuccessNotification(trans('activities.favourite_remove_notification', [ + 'name' => $favouritable->name, + ])); + return redirect()->back(); + } + + /** + * @throws \Illuminate\Validation\ValidationException + * @throws \Exception + */ + protected function getValidatedModelFromRequest(Request $request): Favouritable + { + $modelInfo = $this->validate($request, [ + 'type' => 'required|string', + 'id' => 'required|integer', + ]); + + if (!class_exists($modelInfo['type'])) { + throw new \Exception('Model not found'); + } + + /** @var Model $model */ + $model = new $modelInfo['type']; + if (! $model instanceof Favouritable) { + throw new \Exception('Model not favouritable'); + } + + $modelInstance = $model->newQuery() + ->where('id', '=', $modelInfo['id']) + ->first(['id', 'name']); + + $inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance)); + if (is_null($modelInstance) || $inaccessibleEntity) { + throw new \Exception('Model instance not found'); + } + + return $modelInstance; + } +} diff --git a/app/Interfaces/Favouritable.php b/app/Interfaces/Favouritable.php new file mode 100644 index 000000000..dd335feed --- /dev/null +++ b/app/Interfaces/Favouritable.php @@ -0,0 +1,11 @@ +<?php namespace BookStack\Interfaces; + +use Illuminate\Database\Eloquent\Relations\MorphMany; + +interface Favouritable +{ + /** + * Get the related favourite instances. + */ + public function favourites(): MorphMany; +} \ No newline at end of file diff --git a/database/migrations/2021_05_15_173110_create_favourites_table.php b/database/migrations/2021_05_15_173110_create_favourites_table.php new file mode 100644 index 000000000..783bf5825 --- /dev/null +++ b/database/migrations/2021_05_15_173110_create_favourites_table.php @@ -0,0 +1,36 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +class CreateFavouritesTable extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::create('favourites', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id')->index(); + $table->integer('favouritable_id'); + $table->string('favouritable_type', 100); + $table->timestamps(); + + $table->index(['favouritable_id', 'favouritable_type'], 'favouritable_index'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('favourites'); + } +} diff --git a/resources/icons/star-circle.svg b/resources/icons/star-circle.svg index 0e3f16879..a7667e48f 100644 --- a/resources/icons/star-circle.svg +++ b/resources/icons/star-circle.svg @@ -1,4 +1,3 @@ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> - <path d="M0 0h24v24H0z" fill="none"/> <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm4.24 16L12 15.45 7.77 18l1.12-4.81-3.73-3.23 4.92-.42L12 5l1.92 4.53 4.92.42-3.73 3.23L16.23 18z"/> </svg> \ No newline at end of file diff --git a/resources/icons/star-outline.svg b/resources/icons/star-outline.svg new file mode 100644 index 000000000..4e83ab42b --- /dev/null +++ b/resources/icons/star-outline.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></svg> \ No newline at end of file diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php index fe937b061..5917de2cf 100644 --- a/resources/lang/en/activities.php +++ b/resources/lang/en/activities.php @@ -43,6 +43,10 @@ return [ 'bookshelf_delete' => 'deleted bookshelf', 'bookshelf_delete_notification' => 'Bookshelf Successfully Deleted', + // Favourites + 'favourite_add_notification' => '":name" has been added to your favourites', + 'favourite_remove_notification' => '":name" has been removed from your favourites', + // Other 'commented_on' => 'commented on', 'permissions_update' => 'updated permissions', diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index 855c1c807..e198878ad 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -40,6 +40,8 @@ return [ 'remove' => 'Remove', 'add' => 'Add', 'fullscreen' => 'Fullscreen', + 'favourite' => 'Favourite', + 'unfavourite' => 'Unfavourite', // Sort Options 'sort_options' => 'Sort Options', diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 13125464a..73a107cf7 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -151,6 +151,9 @@ <hr class="primary-background"/> {{--Export--}} + @if(signedInUser()) + @include('partials.entity-favourite-action', ['entity' => $page, 'alreadyFavourite' => $page->isFavourite()]) + @endif @include('partials.entity-export-menu', ['entity' => $page]) </div> diff --git a/resources/views/partials/entity-favourite-action.blade.php b/resources/views/partials/entity-favourite-action.blade.php new file mode 100644 index 000000000..49ba6aa5d --- /dev/null +++ b/resources/views/partials/entity-favourite-action.blade.php @@ -0,0 +1,12 @@ +@php + $isFavourite = $entity->isFavourite(); +@endphp +<form action="{{ url('/favourites/' . ($isFavourite ? 'remove' : 'add')) }}" method="POST"> + {{ csrf_field() }} + <input type="hidden" name="type" value="{{ get_class($entity) }}"> + <input type="hidden" name="id" value="{{ $entity->id }}"> + <button type="submit" class="icon-list-item text-primary"> + <span>@icon($isFavourite ? 'star' : 'star-outline')</span> + <span>{{ $isFavourite ? trans('common.unfavourite') : trans('common.favourite') }}</span> + </button> +</form> \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 730c795f8..69823b4ba 100644 --- a/routes/web.php +++ b/routes/web.php @@ -152,9 +152,14 @@ Route::group(['middleware' => 'auth'], function () { // User Search Route::get('/search/users/select', 'UserSearchController@forSelect'); + // Template System Route::get('/templates', 'PageTemplateController@list'); Route::get('/templates/{templateId}', 'PageTemplateController@get'); + // Favourites + Route::post('/favourites/add', 'FavouriteController@add'); + Route::post('/favourites/remove', 'FavouriteController@remove'); + // Other Pages Route::get('/', 'HomeController@index'); Route::get('/home', 'HomeController@index');