--- /dev/null
+<?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();
+ }
+}
<?php namespace BookStack\Actions;
use BookStack\Model;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
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();
}
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;
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;
* @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;
$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();
+ }
}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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
--- /dev/null
+<?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');
+ }
+}
<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
--- /dev/null
+<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
'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',
'remove' => 'Remove',
'add' => 'Add',
'fullscreen' => 'Fullscreen',
+ 'favourite' => 'Favourite',
+ 'unfavourite' => 'Unfavourite',
// Sort Options
'sort_options' => 'Sort Options',
<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>
--- /dev/null
+@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
// 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');