Merge pull request #2748 from BookStackApp/favourite_system
Favourite System
This commit is contained in:
commit
dd6076049c
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,19 @@
|
||||||
<?php namespace BookStack\Actions;
|
<?php namespace BookStack\Actions;
|
||||||
|
|
||||||
|
use BookStack\Interfaces\Viewable;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class View
|
||||||
|
* Views are stored per-item per-person within the database.
|
||||||
|
* They can be used to find popular items or recently viewed items
|
||||||
|
* at a per-person level. They do not record every view instance as an
|
||||||
|
* activity. Only the latest and original view times could be recognised.
|
||||||
|
*
|
||||||
|
* @property int $views
|
||||||
|
* @property int $user_id
|
||||||
|
*/
|
||||||
class View extends Model
|
class View extends Model
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -9,10 +21,37 @@ class View extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all owning viewable models.
|
* Get all owning viewable models.
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
|
||||||
*/
|
*/
|
||||||
public function viewable()
|
public function viewable(): MorphTo
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the current user's view count for the given viewable model.
|
||||||
|
*/
|
||||||
|
public static function incrementFor(Viewable $viewable): int
|
||||||
|
{
|
||||||
|
$user = user();
|
||||||
|
if (is_null($user) || $user->isDefault()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var View $view */
|
||||||
|
$view = $viewable->views()->firstOrNew([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
], ['views' => 0]);
|
||||||
|
|
||||||
|
$view->forceFill(['views' => $view->views + 1])->save();
|
||||||
|
|
||||||
|
return $view->views;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all views from the system.
|
||||||
|
*/
|
||||||
|
public static function clearAll()
|
||||||
|
{
|
||||||
|
static::query()->truncate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
<?php namespace BookStack\Actions;
|
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
|
||||||
use BookStack\Entities\Models\Book;
|
|
||||||
use BookStack\Entities\Models\Entity;
|
|
||||||
use BookStack\Entities\EntityProvider;
|
|
||||||
use DB;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class ViewService
|
|
||||||
{
|
|
||||||
protected $view;
|
|
||||||
protected $permissionService;
|
|
||||||
protected $entityProvider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewService constructor.
|
|
||||||
* @param View $view
|
|
||||||
* @param PermissionService $permissionService
|
|
||||||
* @param EntityProvider $entityProvider
|
|
||||||
*/
|
|
||||||
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
|
|
||||||
{
|
|
||||||
$this->view = $view;
|
|
||||||
$this->permissionService = $permissionService;
|
|
||||||
$this->entityProvider = $entityProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a view to the given entity.
|
|
||||||
* @param \BookStack\Entities\Models\Entity $entity
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function add(Entity $entity)
|
|
||||||
{
|
|
||||||
$user = user();
|
|
||||||
if ($user === null || $user->isDefault()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
$view = $entity->views()->where('user_id', '=', $user->id)->first();
|
|
||||||
// Add view if model exists
|
|
||||||
if ($view) {
|
|
||||||
$view->increment('views');
|
|
||||||
return $view->views;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise create new view count
|
|
||||||
$entity->views()->save($this->view->newInstance([
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'views' => 1
|
|
||||||
]));
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the entities with the most views.
|
|
||||||
* @param int $count
|
|
||||||
* @param int $page
|
|
||||||
* @param string|array $filterModels
|
|
||||||
* @param string $action - used for permission checking
|
|
||||||
* @return Collection
|
|
||||||
*/
|
|
||||||
public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
|
|
||||||
{
|
|
||||||
$skipCount = $count * $page;
|
|
||||||
$query = $this->permissionService
|
|
||||||
->filterRestrictedEntityRelations($this->view->newQuery(), 'views', 'viewable_id', 'viewable_type', $action)
|
|
||||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
|
||||||
->groupBy('viewable_id', 'viewable_type')
|
|
||||||
->orderBy('view_count', 'desc');
|
|
||||||
|
|
||||||
if ($filterModels) {
|
|
||||||
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query->with('viewable')
|
|
||||||
->skip($skipCount)
|
|
||||||
->take($count)
|
|
||||||
->get()
|
|
||||||
->pluck('viewable')
|
|
||||||
->filter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all recently viewed entities for the current user.
|
|
||||||
*/
|
|
||||||
public function getUserRecentlyViewed(int $count = 10, int $page = 1)
|
|
||||||
{
|
|
||||||
$user = user();
|
|
||||||
if ($user === null || $user->isDefault()) {
|
|
||||||
return collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
$all = collect();
|
|
||||||
/** @var Entity $instance */
|
|
||||||
foreach ($this->entityProvider->all() as $name => $instance) {
|
|
||||||
$items = $instance::visible()->withLastView()
|
|
||||||
->having('last_viewed_at', '>', 0)
|
|
||||||
->orderBy('last_viewed_at', 'desc')
|
|
||||||
->skip($count * ($page - 1))
|
|
||||||
->take($count)
|
|
||||||
->get();
|
|
||||||
$all = $all->concat($items);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $all->sortByDesc('last_viewed_at')->slice(0, $count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset all view counts by deleting all views.
|
|
||||||
*/
|
|
||||||
public function resetAll()
|
|
||||||
{
|
|
||||||
$this->view->truncate();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -580,14 +580,15 @@ class PermissionService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter items that have entities set as a polymorphic relation.
|
* Filter items that have entities set as a polymorphic relation.
|
||||||
|
* @param Builder|\Illuminate\Database\Query\Builder $query
|
||||||
*/
|
*/
|
||||||
public function filterRestrictedEntityRelations(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view'): Builder
|
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
||||||
{
|
{
|
||||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||||
|
|
||||||
$q = $query->where(function ($query) use ($tableDetails, $action) {
|
$q = $query->where(function ($query) use ($tableDetails, $action) {
|
||||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
||||||
$permissionQuery->select('id')->from('joint_permissions')
|
$permissionQuery->select(['role_id'])->from('joint_permissions')
|
||||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||||
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
||||||
->where('action', '=', $action)
|
->where('action', '=', $action)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?php namespace BookStack\Auth;
|
<?php namespace BookStack\Auth;
|
||||||
|
|
||||||
|
use BookStack\Actions\Favourite;
|
||||||
use BookStack\Api\ApiToken;
|
use BookStack\Api\ApiToken;
|
||||||
use BookStack\Entities\Tools\SlugGenerator;
|
use BookStack\Entities\Tools\SlugGenerator;
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Interfaces\Loggable;
|
||||||
|
@ -240,6 +241,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
return $this->hasMany(ApiToken::class);
|
return $this->hasMany(ApiToken::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the favourite instances for this user.
|
||||||
|
*/
|
||||||
|
public function favourites(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Favourite::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last activity time for this user.
|
* Get the last activity time for this user.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -184,6 +184,7 @@ class UserRepo
|
||||||
{
|
{
|
||||||
$user->socialAccounts()->delete();
|
$user->socialAccounts()->delete();
|
||||||
$user->apiTokens()->delete();
|
$user->apiTokens()->delete();
|
||||||
|
$user->favourites()->delete();
|
||||||
$user->delete();
|
$user->delete();
|
||||||
|
|
||||||
// Delete user profile images
|
// Delete user profile images
|
||||||
|
|
|
@ -184,11 +184,9 @@ return [
|
||||||
|
|
||||||
// Custom BookStack
|
// Custom BookStack
|
||||||
'Activity' => BookStack\Facades\Activity::class,
|
'Activity' => BookStack\Facades\Activity::class,
|
||||||
'Views' => BookStack\Facades\Views::class,
|
|
||||||
'Images' => BookStack\Facades\Images::class,
|
'Images' => BookStack\Facades\Images::class,
|
||||||
'Permissions' => BookStack\Facades\Permissions::class,
|
'Permissions' => BookStack\Facades\Permissions::class,
|
||||||
'Theme' => BookStack\Facades\Theme::class,
|
'Theme' => BookStack\Facades\Theme::class,
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// Proxy configuration
|
// Proxy configuration
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
|
use BookStack\Actions\View;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class ClearViews extends Command
|
class ClearViews extends Command
|
||||||
|
@ -36,7 +37,7 @@ class ClearViews extends Command
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
\Views::resetAll();
|
View::clearAll();
|
||||||
$this->comment('Views cleared');
|
$this->comment('Views cleared');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use BookStack\Actions\Activity;
|
use BookStack\Actions\Activity;
|
||||||
use BookStack\Actions\Comment;
|
use BookStack\Actions\Comment;
|
||||||
|
use BookStack\Actions\Favourite;
|
||||||
use BookStack\Actions\Tag;
|
use BookStack\Actions\Tag;
|
||||||
use BookStack\Actions\View;
|
use BookStack\Actions\View;
|
||||||
use BookStack\Auth\Permissions\EntityPermission;
|
use BookStack\Auth\Permissions\EntityPermission;
|
||||||
|
@ -9,7 +10,9 @@ use BookStack\Auth\Permissions\JointPermission;
|
||||||
use BookStack\Entities\Tools\SearchIndex;
|
use BookStack\Entities\Tools\SearchIndex;
|
||||||
use BookStack\Entities\Tools\SlugGenerator;
|
use BookStack\Entities\Tools\SlugGenerator;
|
||||||
use BookStack\Facades\Permissions;
|
use BookStack\Facades\Permissions;
|
||||||
|
use BookStack\Interfaces\Favouritable;
|
||||||
use BookStack\Interfaces\Sluggable;
|
use BookStack\Interfaces\Sluggable;
|
||||||
|
use BookStack\Interfaces\Viewable;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
use BookStack\Traits\HasCreatorAndUpdater;
|
use BookStack\Traits\HasCreatorAndUpdater;
|
||||||
use BookStack\Traits\HasOwner;
|
use BookStack\Traits\HasOwner;
|
||||||
|
@ -38,7 +41,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
* @method static Builder withLastView()
|
* @method static Builder withLastView()
|
||||||
* @method static Builder withViewCount()
|
* @method static Builder withViewCount()
|
||||||
*/
|
*/
|
||||||
abstract class Entity extends Model implements Sluggable
|
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
use HasCreatorAndUpdater;
|
use HasCreatorAndUpdater;
|
||||||
|
@ -297,4 +300,22 @@ abstract class Entity extends Model implements Sluggable
|
||||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||||
return $this->slug;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php namespace BookStack\Entities\Queries;
|
||||||
|
|
||||||
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
|
use BookStack\Entities\EntityProvider;
|
||||||
|
|
||||||
|
abstract class EntityQuery
|
||||||
|
{
|
||||||
|
protected function permissionService(): PermissionService
|
||||||
|
{
|
||||||
|
return app()->make(PermissionService::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function entityProvider(): EntityProvider
|
||||||
|
{
|
||||||
|
return app()->make(EntityProvider::class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php namespace BookStack\Entities\Queries;
|
||||||
|
|
||||||
|
|
||||||
|
use BookStack\Actions\View;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class Popular extends EntityQuery
|
||||||
|
{
|
||||||
|
public function run(int $count, int $page, array $filterModels = null, string $action = 'view')
|
||||||
|
{
|
||||||
|
$query = $this->permissionService()
|
||||||
|
->filterRestrictedEntityRelations(View::query(), 'views', 'viewable_id', 'viewable_type', $action)
|
||||||
|
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||||
|
->groupBy('viewable_id', 'viewable_type')
|
||||||
|
->orderBy('view_count', 'desc');
|
||||||
|
|
||||||
|
if ($filterModels) {
|
||||||
|
$query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->with('viewable')
|
||||||
|
->skip($count * ($page - 1))
|
||||||
|
->take($count)
|
||||||
|
->get()
|
||||||
|
->pluck('viewable')
|
||||||
|
->filter();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php namespace BookStack\Entities\Queries;
|
||||||
|
|
||||||
|
use BookStack\Actions\View;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class RecentlyViewed extends EntityQuery
|
||||||
|
{
|
||||||
|
public function run(int $count, int $page): Collection
|
||||||
|
{
|
||||||
|
$user = user();
|
||||||
|
if ($user === null || $user->isDefault()) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->permissionService()->filterRestrictedEntityRelations(
|
||||||
|
View::query(),
|
||||||
|
'views',
|
||||||
|
'viewable_id',
|
||||||
|
'viewable_type',
|
||||||
|
'view'
|
||||||
|
)
|
||||||
|
->orderBy('views.updated_at', 'desc')
|
||||||
|
->where('user_id', '=', user()->id);
|
||||||
|
|
||||||
|
return $query->with('viewable')
|
||||||
|
->skip(($page - 1) * $count)
|
||||||
|
->take($count)
|
||||||
|
->get()
|
||||||
|
->pluck('viewable')
|
||||||
|
->filter();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php namespace BookStack\Entities\Queries;
|
||||||
|
|
||||||
|
use BookStack\Actions\Favourite;
|
||||||
|
use Illuminate\Database\Query\JoinClause;
|
||||||
|
|
||||||
|
class TopFavourites extends EntityQuery
|
||||||
|
{
|
||||||
|
public function run(int $count, int $skip = 0)
|
||||||
|
{
|
||||||
|
$user = user();
|
||||||
|
if (is_null($user) || $user->isDefault()) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $this->permissionService()
|
||||||
|
->filterRestrictedEntityRelations(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type', 'view')
|
||||||
|
->select('favourites.*')
|
||||||
|
->leftJoin('views', function (JoinClause $join) {
|
||||||
|
$join->on('favourites.favouritable_id', '=', 'views.viewable_id');
|
||||||
|
$join->on('favourites.favouritable_type', '=', 'views.viewable_type');
|
||||||
|
$join->where('views.user_id', '=', user()->id);
|
||||||
|
})
|
||||||
|
->orderBy('views.views', 'desc')
|
||||||
|
->where('favourites.user_id', '=', user()->id);
|
||||||
|
|
||||||
|
return $query->with('favouritable')
|
||||||
|
->skip($skip)
|
||||||
|
->take($count)
|
||||||
|
->get()
|
||||||
|
->pluck('favouritable')
|
||||||
|
->filter();
|
||||||
|
}
|
||||||
|
}
|
|
@ -317,6 +317,7 @@ class TrashCan
|
||||||
$entity->jointPermissions()->delete();
|
$entity->jointPermissions()->delete();
|
||||||
$entity->searchTerms()->delete();
|
$entity->searchTerms()->delete();
|
||||||
$entity->deletions()->delete();
|
$entity->deletions()->delete();
|
||||||
|
$entity->favourites()->delete();
|
||||||
|
|
||||||
if ($entity instanceof HasCoverImage && $entity->cover) {
|
if ($entity instanceof HasCoverImage && $entity->cover) {
|
||||||
$imageService = app()->make(ImageService::class);
|
$imageService = app()->make(ImageService::class);
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?php namespace BookStack\Facades;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Facade;
|
|
||||||
|
|
||||||
class Views extends Facade
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the registered name of the component.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected static function getFacadeAccessor()
|
|
||||||
{
|
|
||||||
return 'views';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Actions\ActivityType;
|
||||||
|
use BookStack\Actions\View;
|
||||||
use BookStack\Entities\Tools\BookContents;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Entities\Models\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||||
|
@ -112,7 +113,7 @@ class BookController extends Controller
|
||||||
$bookChildren = (new BookContents($book))->getTree(true);
|
$bookChildren = (new BookContents($book))->getTree(true);
|
||||||
$bookParentShelves = $book->shelves()->visible()->get();
|
$bookParentShelves = $book->shelves()->visible()->get();
|
||||||
|
|
||||||
Views::add($book);
|
View::incrementFor($book);
|
||||||
if ($request->has('shelf')) {
|
if ($request->has('shelf')) {
|
||||||
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
|
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
|
use BookStack\Actions\View;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||||
use BookStack\Entities\Tools\ShelfContext;
|
use BookStack\Entities\Tools\ShelfContext;
|
||||||
|
@ -109,7 +110,7 @@ class BookshelfController extends Controller
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
Views::add($shelf);
|
View::incrementFor($shelf);
|
||||||
$this->entityContextManager->setShelfContext($shelf->id);
|
$this->entityContextManager->setShelfContext($shelf->id);
|
||||||
$view = setting()->getForCurrentUser('bookshelf_view_type');
|
$view = setting()->getForCurrentUser('bookshelf_view_type');
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Actions\View;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Tools\BookContents;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Entities\Repos\ChapterRepo;
|
use BookStack\Entities\Repos\ChapterRepo;
|
||||||
|
@ -64,7 +65,7 @@ class ChapterController extends Controller
|
||||||
|
|
||||||
$sidebarTree = (new BookContents($chapter->book))->getTree();
|
$sidebarTree = (new BookContents($chapter->book))->getTree();
|
||||||
$pages = $chapter->getVisiblePages();
|
$pages = $chapter->getVisiblePages();
|
||||||
Views::add($chapter);
|
View::incrementFor($chapter);
|
||||||
|
|
||||||
$this->setPageTitle($chapter->getShortName());
|
$this->setPageTitle($chapter->getShortName());
|
||||||
return view('chapters.show', [
|
return view('chapters.show', [
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\Queries\TopFavourites;
|
||||||
|
use BookStack\Interfaces\Favouritable;
|
||||||
|
use BookStack\Model;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class FavouriteController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show a listing of all favourite items for the current user.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$viewCount = 20;
|
||||||
|
$page = intval($request->get('page', 1));
|
||||||
|
$favourites = (new TopFavourites)->run($viewCount + 1, (($page - 1) * $viewCount));
|
||||||
|
|
||||||
|
$hasMoreLink = ($favourites->count() > $viewCount) ? url("/favourites?page=" . ($page+1)) : null;
|
||||||
|
|
||||||
|
return view('common.detailed-listing-with-more', [
|
||||||
|
'title' => trans('entities.my_favourites'),
|
||||||
|
'entities' => $favourites->slice(0, $viewCount),
|
||||||
|
'hasMoreLink' => $hasMoreLink,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,12 @@
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Queries\RecentlyViewed;
|
||||||
|
use BookStack\Entities\Queries\TopFavourites;
|
||||||
use BookStack\Entities\Tools\PageContent;
|
use BookStack\Entities\Tools\PageContent;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Entities\Repos\BookRepo;
|
use BookStack\Entities\Repos\BookRepo;
|
||||||
use BookStack\Entities\Repos\BookshelfRepo;
|
use BookStack\Entities\Repos\BookshelfRepo;
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use Views;
|
use Views;
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
|
@ -32,12 +33,13 @@ class HomeController extends Controller
|
||||||
|
|
||||||
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
||||||
$recents = $this->isSignedIn() ?
|
$recents = $this->isSignedIn() ?
|
||||||
Views::getUserRecentlyViewed(12*$recentFactor, 1)
|
(new RecentlyViewed)->run(12*$recentFactor, 1)
|
||||||
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
|
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
|
||||||
|
$favourites = (new TopFavourites)->run(6);
|
||||||
$recentlyUpdatedPages = Page::visible()->with('book')
|
$recentlyUpdatedPages = Page::visible()->with('book')
|
||||||
->where('draft', false)
|
->where('draft', false)
|
||||||
->orderBy('updated_at', 'desc')
|
->orderBy('updated_at', 'desc')
|
||||||
->take(12)
|
->take($favourites->count() > 0 ? 6 : 12)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
|
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
|
||||||
|
@ -51,6 +53,7 @@ class HomeController extends Controller
|
||||||
'recents' => $recents,
|
'recents' => $recents,
|
||||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||||
'draftPages' => $draftPages,
|
'draftPages' => $draftPages,
|
||||||
|
'favourites' => $favourites,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add required list ordering & sorting for books & shelves views.
|
// Add required list ordering & sorting for books & shelves views.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Actions\View;
|
||||||
use BookStack\Entities\Tools\BookContents;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Entities\Tools\PageContent;
|
use BookStack\Entities\Tools\PageContent;
|
||||||
use BookStack\Entities\Tools\PageEditActivity;
|
use BookStack\Entities\Tools\PageEditActivity;
|
||||||
|
@ -141,7 +142,7 @@ class PageController extends Controller
|
||||||
$page->load(['comments.createdBy']);
|
$page->load(['comments.createdBy']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Views::add($page);
|
View::incrementFor($page);
|
||||||
$this->setPageTitle($page->getShortName());
|
$this->setPageTitle($page->getShortName());
|
||||||
return view('pages.show', [
|
return view('pages.show', [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
|
@ -337,9 +338,9 @@ class PageController extends Controller
|
||||||
->paginate(20)
|
->paginate(20)
|
||||||
->setPath(url('/pages/recently-updated'));
|
->setPath(url('/pages/recently-updated'));
|
||||||
|
|
||||||
return view('pages.detailed-listing', [
|
return view('common.detailed-listing-paginated', [
|
||||||
'title' => trans('entities.recently_updated_pages'),
|
'title' => trans('entities.recently_updated_pages'),
|
||||||
'pages' => $pages
|
'entities' => $pages
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\ViewService;
|
use BookStack\Entities\Queries\Popular;
|
||||||
use BookStack\Entities\Tools\SearchRunner;
|
use BookStack\Entities\Tools\SearchRunner;
|
||||||
use BookStack\Entities\Tools\ShelfContext;
|
use BookStack\Entities\Tools\ShelfContext;
|
||||||
use BookStack\Entities\Tools\SearchOptions;
|
use BookStack\Entities\Tools\SearchOptions;
|
||||||
|
@ -9,16 +9,13 @@ use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SearchController extends Controller
|
class SearchController extends Controller
|
||||||
{
|
{
|
||||||
protected $viewService;
|
|
||||||
protected $searchRunner;
|
protected $searchRunner;
|
||||||
protected $entityContextManager;
|
protected $entityContextManager;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ViewService $viewService,
|
|
||||||
SearchRunner $searchRunner,
|
SearchRunner $searchRunner,
|
||||||
ShelfContext $entityContextManager
|
ShelfContext $entityContextManager
|
||||||
) {
|
) {
|
||||||
$this->viewService = $viewService;
|
|
||||||
$this->searchRunner = $searchRunner;
|
$this->searchRunner = $searchRunner;
|
||||||
$this->entityContextManager = $entityContextManager;
|
$this->entityContextManager = $entityContextManager;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +79,7 @@ class SearchController extends Controller
|
||||||
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
|
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
|
||||||
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
|
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
|
||||||
} else {
|
} else {
|
||||||
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
|
$entities = (new Popular)->run(20, 0, $entityTypes, $permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('search.entity-ajax-list', ['entities' => $entities]);
|
return view('search.entity-ajax-list', ['entities' => $entities]);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php namespace BookStack\Interfaces;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
|
||||||
|
interface Viewable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all view instances for this viewable model.
|
||||||
|
*/
|
||||||
|
public function views(): MorphMany;
|
||||||
|
}
|
|
@ -3,7 +3,6 @@
|
||||||
namespace BookStack\Providers;
|
namespace BookStack\Providers;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityService;
|
use BookStack\Actions\ActivityService;
|
||||||
use BookStack\Actions\ViewService;
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Theming\ThemeService;
|
use BookStack\Theming\ThemeService;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
|
@ -32,10 +31,6 @@ class CustomFacadeProvider extends ServiceProvider
|
||||||
return $this->app->make(ActivityService::class);
|
return $this->app->make(ActivityService::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->singleton('views', function () {
|
|
||||||
return $this->app->make(ViewService::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->app->singleton('images', function () {
|
$this->app->singleton('images', function () {
|
||||||
return $this->app->make(ImageService::class);
|
return $this->app->make(ImageService::class);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<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"/>
|
<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>
|
</svg>
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 265 B |
|
@ -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>
|
After Width: | Height: | Size: 270 B |
|
@ -43,6 +43,10 @@ return [
|
||||||
'bookshelf_delete' => 'deleted bookshelf',
|
'bookshelf_delete' => 'deleted bookshelf',
|
||||||
'bookshelf_delete_notification' => 'Bookshelf Successfully Deleted',
|
'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
|
// Other
|
||||||
'commented_on' => 'commented on',
|
'commented_on' => 'commented on',
|
||||||
'permissions_update' => 'updated permissions',
|
'permissions_update' => 'updated permissions',
|
||||||
|
|
|
@ -40,6 +40,8 @@ return [
|
||||||
'remove' => 'Remove',
|
'remove' => 'Remove',
|
||||||
'add' => 'Add',
|
'add' => 'Add',
|
||||||
'fullscreen' => 'Fullscreen',
|
'fullscreen' => 'Fullscreen',
|
||||||
|
'favourite' => 'Favourite',
|
||||||
|
'unfavourite' => 'Unfavourite',
|
||||||
|
|
||||||
// Sort Options
|
// Sort Options
|
||||||
'sort_options' => 'Sort Options',
|
'sort_options' => 'Sort Options',
|
||||||
|
|
|
@ -27,6 +27,8 @@ return [
|
||||||
'images' => 'Images',
|
'images' => 'Images',
|
||||||
'my_recent_drafts' => 'My Recent Drafts',
|
'my_recent_drafts' => 'My Recent Drafts',
|
||||||
'my_recently_viewed' => 'My Recently Viewed',
|
'my_recently_viewed' => 'My Recently Viewed',
|
||||||
|
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
||||||
|
'my_favourites' => 'My Favourites',
|
||||||
'no_pages_viewed' => 'You have not viewed any pages',
|
'no_pages_viewed' => 'You have not viewed any pages',
|
||||||
'no_pages_recently_created' => 'No pages have been recently created',
|
'no_pages_recently_created' => 'No pages have been recently created',
|
||||||
'no_pages_recently_updated' => 'No pages have been recently updated',
|
'no_pages_recently_updated' => 'No pages have been recently updated',
|
||||||
|
|
|
@ -118,6 +118,9 @@
|
||||||
|
|
||||||
<hr class="primary-background">
|
<hr class="primary-background">
|
||||||
|
|
||||||
|
@if(signedInUser())
|
||||||
|
@include('partials.entity-favourite-action', ['entity' => $book])
|
||||||
|
@endif
|
||||||
@include('partials.entity-export-menu', ['entity' => $book])
|
@include('partials.entity-export-menu', ['entity' => $book])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -123,6 +123,9 @@
|
||||||
|
|
||||||
<hr class="primary-background"/>
|
<hr class="primary-background"/>
|
||||||
|
|
||||||
|
@if(signedInUser())
|
||||||
|
@include('partials.entity-favourite-action', ['entity' => $chapter])
|
||||||
|
@endif
|
||||||
@include('partials.entity-export-menu', ['entity' => $chapter])
|
@include('partials.entity-export-menu', ['entity' => $chapter])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
<h1 class="list-heading">{{ $title }}</h1>
|
<h1 class="list-heading">{{ $title }}</h1>
|
||||||
|
|
||||||
<div class="book-contents">
|
<div class="book-contents">
|
||||||
@include('partials.entity-list', ['entities' => $pages, 'style' => 'detailed'])
|
@include('partials.entity-list', ['entities' => $entities, 'style' => 'detailed'])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
{!! $pages->links() !!}
|
{!! $entities->links() !!}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,19 @@
|
||||||
|
@extends('simple-layout')
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<div class="container small pt-xl">
|
||||||
|
<main class="card content-wrap">
|
||||||
|
<h1 class="list-heading">{{ $title }}</h1>
|
||||||
|
|
||||||
|
<div class="book-contents">
|
||||||
|
@include('partials.entity-list', ['entities' => $entities, 'style' => 'detailed'])
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
@if($hasMoreLink)
|
||||||
|
<a href="{{ $hasMoreLink }}" class="button outline">{{ trans('common.more') }}</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
@stop
|
|
@ -61,6 +61,9 @@
|
||||||
<span class="name">{{ $currentUser->getShortName(9) }}</span> @icon('caret-down')
|
<span class="name">{{ $currentUser->getShortName(9) }}</span> @icon('caret-down')
|
||||||
</span>
|
</span>
|
||||||
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
|
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
|
||||||
|
<li>
|
||||||
|
<a href="{{ url('/favourites') }}">@icon('star'){{ trans('entities.my_favourites') }}</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ $currentUser->getProfileUrl() }}">@icon('user'){{ trans('common.view_profile') }}</a>
|
<a href="{{ $currentUser->getProfileUrl() }}">@icon('user'){{ trans('common.view_profile') }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -5,6 +5,20 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if(count($favourites) > 0)
|
||||||
|
<div id="top-favourites" class="card mb-xl">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<a href="{{ url('/favourites') }}" class="no-color">{{ trans('entities.my_most_viewed_favourites') }}</a>
|
||||||
|
</h3>
|
||||||
|
<div class="px-m">
|
||||||
|
@include('partials.entity-list', [
|
||||||
|
'entities' => $favourites,
|
||||||
|
'style' => 'compact',
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="mb-xl">
|
<div class="mb-xl">
|
||||||
<h5>{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h5>
|
<h5>{{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}</h5>
|
||||||
@include('partials.entity-list', [
|
@include('partials.entity-list', [
|
||||||
|
|
|
@ -42,6 +42,20 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@if(count($favourites) > 0)
|
||||||
|
<div id="top-favourites" class="card mb-xl">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<a href="{{ url('/favourites') }}" class="no-color">{{ trans('entities.my_most_viewed_favourites') }}</a>
|
||||||
|
</h3>
|
||||||
|
<div class="px-m">
|
||||||
|
@include('partials.entity-list', [
|
||||||
|
'entities' => $favourites,
|
||||||
|
'style' => 'compact',
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div id="recent-pages" class="card mb-xl">
|
<div id="recent-pages" class="card mb-xl">
|
||||||
<h3 class="card-title"><a class="no-color" href="{{ url("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h3>
|
<h3 class="card-title"><a class="no-color" href="{{ url("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h3>
|
||||||
<div id="recently-updated-pages" class="px-m">
|
<div id="recently-updated-pages" class="px-m">
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<div class="card mb-xl">
|
<div class="card mb-xl">
|
||||||
<h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
|
<h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
|
||||||
<div class="px-m">
|
<div class="px-m">
|
||||||
@include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['page']), 'style' => 'compact'])
|
@include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['page']), 'style' => 'compact'])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
<div class="card mb-xl">
|
<div class="card mb-xl">
|
||||||
<h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
|
<h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
|
||||||
<div class="px-m">
|
<div class="px-m">
|
||||||
@include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['book']), 'style' => 'compact'])
|
@include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['book']), 'style' => 'compact'])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
<div class="card mb-xl">
|
<div class="card mb-xl">
|
||||||
<h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
|
<h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
|
||||||
<div class="px-m">
|
<div class="px-m">
|
||||||
@include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['chapter']), 'style' => 'compact'])
|
@include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['chapter']), 'style' => 'compact'])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -150,7 +150,9 @@
|
||||||
|
|
||||||
<hr class="primary-background"/>
|
<hr class="primary-background"/>
|
||||||
|
|
||||||
{{--Export--}}
|
@if(signedInUser())
|
||||||
|
@include('partials.entity-favourite-action', ['entity' => $page])
|
||||||
|
@endif
|
||||||
@include('partials.entity-export-menu', ['entity' => $page])
|
@include('partials.entity-export-menu', ['entity' => $page])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -133,6 +133,11 @@
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if(signedInUser())
|
||||||
|
<hr class="primary-background">
|
||||||
|
@include('partials.entity-favourite-action', ['entity' => $shelf])
|
||||||
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@stop
|
@stop
|
||||||
|
|
|
@ -152,9 +152,15 @@ Route::group(['middleware' => 'auth'], function () {
|
||||||
// User Search
|
// User Search
|
||||||
Route::get('/search/users/select', 'UserSearchController@forSelect');
|
Route::get('/search/users/select', 'UserSearchController@forSelect');
|
||||||
|
|
||||||
|
// Template System
|
||||||
Route::get('/templates', 'PageTemplateController@list');
|
Route::get('/templates', 'PageTemplateController@list');
|
||||||
Route::get('/templates/{templateId}', 'PageTemplateController@get');
|
Route::get('/templates/{templateId}', 'PageTemplateController@get');
|
||||||
|
|
||||||
|
// Favourites
|
||||||
|
Route::get('/favourites', 'FavouriteController@index');
|
||||||
|
Route::post('/favourites/add', 'FavouriteController@add');
|
||||||
|
Route::post('/favourites/remove', 'FavouriteController@remove');
|
||||||
|
|
||||||
// Other Pages
|
// Other Pages
|
||||||
Route::get('/', 'HomeController@index');
|
Route::get('/', 'HomeController@index');
|
||||||
Route::get('/home', 'HomeController@index');
|
Route::get('/home', 'HomeController@index');
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use BookStack\Actions\Favourite;
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class FavouriteTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function test_page_add_favourite_flow()
|
||||||
|
{
|
||||||
|
$page = Page::query()->first();
|
||||||
|
$editor = $this->getEditor();
|
||||||
|
|
||||||
|
$resp = $this->actingAs($editor)->get($page->getUrl());
|
||||||
|
$resp->assertElementContains('button', 'Favourite');
|
||||||
|
$resp->assertElementExists('form[method="POST"][action$="/favourites/add"]');
|
||||||
|
|
||||||
|
$resp = $this->post('/favourites/add', [
|
||||||
|
'type' => get_class($page),
|
||||||
|
'id' => $page->id,
|
||||||
|
]);
|
||||||
|
$resp->assertRedirect($page->getUrl());
|
||||||
|
$resp->assertSessionHas('success', "\"{$page->name}\" has been added to your favourites");
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('favourites', [
|
||||||
|
'user_id' => $editor->id,
|
||||||
|
'favouritable_type' => $page->getMorphClass(),
|
||||||
|
'favouritable_id' => $page->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_page_remove_favourite_flow()
|
||||||
|
{
|
||||||
|
$page = Page::query()->first();
|
||||||
|
$editor = $this->getEditor();
|
||||||
|
Favourite::query()->forceCreate([
|
||||||
|
'user_id' => $editor->id,
|
||||||
|
'favouritable_id' => $page->id,
|
||||||
|
'favouritable_type' => $page->getMorphClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resp = $this->actingAs($editor)->get($page->getUrl());
|
||||||
|
$resp->assertElementContains('button', 'Unfavourite');
|
||||||
|
$resp->assertElementExists('form[method="POST"][action$="/favourites/remove"]');
|
||||||
|
|
||||||
|
$resp = $this->post('/favourites/remove', [
|
||||||
|
'type' => get_class($page),
|
||||||
|
'id' => $page->id,
|
||||||
|
]);
|
||||||
|
$resp->assertRedirect($page->getUrl());
|
||||||
|
$resp->assertSessionHas('success', "\"{$page->name}\" has been removed from your favourites");
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('favourites', [
|
||||||
|
'user_id' => $editor->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_book_chapter_shelf_pages_contain_favourite_button()
|
||||||
|
{
|
||||||
|
$entities = [
|
||||||
|
Bookshelf::query()->first(),
|
||||||
|
Book::query()->first(),
|
||||||
|
Chapter::query()->first(),
|
||||||
|
];
|
||||||
|
$this->actingAs($this->getEditor());
|
||||||
|
|
||||||
|
foreach ($entities as $entity) {
|
||||||
|
$resp = $this->get($entity->getUrl());
|
||||||
|
$resp->assertElementExists('form[method="POST"][action$="/favourites/add"]');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_header_contains_link_to_favourites_page_when_logged_in()
|
||||||
|
{
|
||||||
|
$this->setSettings(['app-public' => 'true']);
|
||||||
|
$this->get('/')->assertElementNotContains('header', 'My Favourites');
|
||||||
|
$this->actingAs($this->getViewer())->get('/')->assertElementContains('header a', 'My Favourites');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_favourites_shown_on_homepage()
|
||||||
|
{
|
||||||
|
$editor = $this->getEditor();
|
||||||
|
|
||||||
|
$resp = $this->actingAs($editor)->get('/');
|
||||||
|
$resp->assertElementNotExists('#top-favourites');
|
||||||
|
|
||||||
|
/** @var Page $page */
|
||||||
|
$page = Page::query()->first();
|
||||||
|
$page->favourites()->save((new Favourite)->forceFill(['user_id' => $editor->id]));
|
||||||
|
|
||||||
|
$resp = $this->get('/');
|
||||||
|
$resp->assertElementExists('#top-favourites');
|
||||||
|
$resp->assertElementContains('#top-favourites', $page->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_favourites_list_page_shows_favourites_and_has_working_pagination()
|
||||||
|
{
|
||||||
|
/** @var Page $page */
|
||||||
|
$page = Page::query()->first();
|
||||||
|
$editor = $this->getEditor();
|
||||||
|
|
||||||
|
$resp = $this->actingAs($editor)->get('/favourites');
|
||||||
|
$resp->assertDontSee($page->name);
|
||||||
|
|
||||||
|
$page->favourites()->save((new Favourite)->forceFill(['user_id' => $editor->id]));
|
||||||
|
|
||||||
|
$resp = $this->get('/favourites');
|
||||||
|
$resp->assertSee($page->name);
|
||||||
|
|
||||||
|
$resp = $this->get('/favourites?page=2');
|
||||||
|
$resp->assertDontSee($page->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue