mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-01-31 20:21:36 +01:00
Added uploaded to book/page filters & search in image manager
Also refactored tab styles which affected the settings area. Closes #41
This commit is contained in:
parent
8e614ecb6e
commit
cbff2c6035
@ -1,14 +1,9 @@
|
|||||||
<?php
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
|
||||||
|
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Repos\ImageRepo;
|
use BookStack\Repos\ImageRepo;
|
||||||
use Illuminate\Filesystem\Filesystem as File;
|
use Illuminate\Filesystem\Filesystem as File;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Intervention\Image\Facades\Image as ImageTool;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use BookStack\Image;
|
use BookStack\Image;
|
||||||
use BookStack\Repos\PageRepo;
|
use BookStack\Repos\PageRepo;
|
||||||
|
|
||||||
@ -44,6 +39,24 @@ class ImageController extends Controller
|
|||||||
return response()->json($imgData);
|
return response()->json($imgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search through images within a particular type.
|
||||||
|
* @param $type
|
||||||
|
* @param int $page
|
||||||
|
* @param Request $request
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function searchByType($type, $page = 0, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'term' => 'required|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$searchTerm = $request->get('term');
|
||||||
|
$imgData = $this->imageRepo->searchPaginatedByType($type, $page,24, $searchTerm);
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all images for a user.
|
* Get all images for a user.
|
||||||
* @param int $page
|
* @param int $page
|
||||||
@ -55,6 +68,27 @@ class ImageController extends Controller
|
|||||||
return response()->json($imgData);
|
return response()->json($imgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get gallery images with a specific filter such as book or page
|
||||||
|
* @param $filter
|
||||||
|
* @param int $page
|
||||||
|
* @param Request $request
|
||||||
|
*/
|
||||||
|
public function getGalleryFiltered($filter, $page = 0, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'page_id' => 'required|integer'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validFilters = collect(['page', 'book']);
|
||||||
|
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
|
||||||
|
|
||||||
|
$pageId = $request->get('page_id');
|
||||||
|
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
|
||||||
|
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles image uploads for use on pages.
|
* Handles image uploads for use on pages.
|
||||||
* @param string $type
|
* @param string $type
|
||||||
|
@ -75,6 +75,8 @@ Route::group(['middleware' => 'auth'], function () {
|
|||||||
Route::post('/{type}/upload', 'ImageController@uploadByType');
|
Route::post('/{type}/upload', 'ImageController@uploadByType');
|
||||||
Route::get('/{type}/all', 'ImageController@getAllByType');
|
Route::get('/{type}/all', 'ImageController@getAllByType');
|
||||||
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
|
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
|
||||||
|
Route::get('/{type}/search/{page}', 'ImageController@searchByType');
|
||||||
|
Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered');
|
||||||
Route::delete('/{imageId}', 'ImageController@destroy');
|
Route::delete('/{imageId}', 'ImageController@destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
|
|
||||||
use BookStack\Image;
|
use BookStack\Image;
|
||||||
|
use BookStack\Page;
|
||||||
use BookStack\Services\ImageService;
|
use BookStack\Services\ImageService;
|
||||||
use BookStack\Services\RestrictionService;
|
use BookStack\Services\RestrictionService;
|
||||||
use Setting;
|
use Setting;
|
||||||
@ -13,18 +14,21 @@ class ImageRepo
|
|||||||
protected $image;
|
protected $image;
|
||||||
protected $imageService;
|
protected $imageService;
|
||||||
protected $restictionService;
|
protected $restictionService;
|
||||||
|
protected $page;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageRepo constructor.
|
* ImageRepo constructor.
|
||||||
* @param Image $image
|
* @param Image $image
|
||||||
* @param ImageService $imageService
|
* @param ImageService $imageService
|
||||||
* @param RestrictionService $restrictionService
|
* @param RestrictionService $restrictionService
|
||||||
|
* @param Page $page
|
||||||
*/
|
*/
|
||||||
public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService)
|
public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page)
|
||||||
{
|
{
|
||||||
$this->image = $image;
|
$this->image = $image;
|
||||||
$this->imageService = $imageService;
|
$this->imageService = $imageService;
|
||||||
$this->restictionService = $restrictionService;
|
$this->restictionService = $restrictionService;
|
||||||
|
$this->page = $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -38,6 +42,31 @@ class ImageRepo
|
|||||||
return $this->image->findOrFail($id);
|
return $this->image->findOrFail($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a paginated query, returning in a standard format.
|
||||||
|
* Also runs the query through the restriction system.
|
||||||
|
* @param $query
|
||||||
|
* @param int $page
|
||||||
|
* @param int $pageSize
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function returnPaginated($query, $page = 0, $pageSize = 24)
|
||||||
|
{
|
||||||
|
$images = $this->restictionService->filterRelatedPages($query, 'images', 'uploaded_to');
|
||||||
|
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
|
||||||
|
$hasMore = count($images) > $pageSize;
|
||||||
|
|
||||||
|
$returnImages = $images->take(24);
|
||||||
|
$returnImages->each(function ($image) {
|
||||||
|
$this->loadThumbs($image);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
'images' => $returnImages,
|
||||||
|
'hasMore' => $hasMore
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a load images paginated, filtered by image type.
|
* Gets a load images paginated, filtered by image type.
|
||||||
* @param string $type
|
* @param string $type
|
||||||
@ -54,19 +83,46 @@ class ImageRepo
|
|||||||
$images = $images->where('created_by', '=', $userFilter);
|
$images = $images->where('created_by', '=', $userFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
$images = $this->restictionService->filterRelatedPages($images, 'images', 'uploaded_to');
|
return $this->returnPaginated($images, $page, $pageSize);
|
||||||
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
|
}
|
||||||
$hasMore = count($images) > $pageSize;
|
|
||||||
|
|
||||||
$returnImages = $images->take(24);
|
/**
|
||||||
$returnImages->each(function ($image) {
|
* Search for images by query, of a particular type.
|
||||||
$this->loadThumbs($image);
|
* @param string $type
|
||||||
});
|
* @param int $page
|
||||||
|
* @param int $pageSize
|
||||||
|
* @param string $searchTerm
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
|
||||||
|
{
|
||||||
|
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
|
||||||
|
return $this->returnPaginated($images, $page, $pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
/**
|
||||||
'images' => $returnImages,
|
* Get gallery images with a particular filter criteria such as
|
||||||
'hasMore' => $hasMore
|
* being within the current book or page.
|
||||||
];
|
* @param int $pagination
|
||||||
|
* @param int $pageSize
|
||||||
|
* @param $filter
|
||||||
|
* @param $pageId
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
|
||||||
|
{
|
||||||
|
$images = $this->image->where('type', '=', 'gallery');
|
||||||
|
|
||||||
|
$page = $this->page->findOrFail($pageId);
|
||||||
|
|
||||||
|
if ($filter === 'page') {
|
||||||
|
$images = $images->where('uploaded_to', '=', $page->id);
|
||||||
|
} elseif ($filter === 'book') {
|
||||||
|
$validPageIds = $page->book->pages->pluck('id')->toArray();
|
||||||
|
$images = $images->whereIn('uploaded_to', $validPageIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->returnPaginated($images, $pagination, $pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,20 +14,40 @@ module.exports = function (ngApp, events) {
|
|||||||
$scope.imageUpdateSuccess = false;
|
$scope.imageUpdateSuccess = false;
|
||||||
$scope.imageDeleteSuccess = false;
|
$scope.imageDeleteSuccess = false;
|
||||||
$scope.uploadedTo = $attrs.uploadedTo;
|
$scope.uploadedTo = $attrs.uploadedTo;
|
||||||
|
$scope.view = 'all';
|
||||||
|
|
||||||
|
$scope.searching = false;
|
||||||
|
$scope.searchTerm = '';
|
||||||
|
|
||||||
var page = 0;
|
var page = 0;
|
||||||
var previousClickTime = 0;
|
var previousClickTime = 0;
|
||||||
|
var previousClickImage = 0;
|
||||||
var dataLoaded = false;
|
var dataLoaded = false;
|
||||||
var callback = false;
|
var callback = false;
|
||||||
|
|
||||||
|
var preSearchImages = [];
|
||||||
|
var preSearchHasMore = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple returns the appropriate upload url depending on the image type set.
|
* Used by dropzone to get the endpoint to upload to.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
$scope.getUploadUrl = function () {
|
$scope.getUploadUrl = function () {
|
||||||
return '/images/' + $scope.imageType + '/upload';
|
return '/images/' + $scope.imageType + '/upload';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the current search operation.
|
||||||
|
*/
|
||||||
|
function cancelSearch() {
|
||||||
|
$scope.searching = false;
|
||||||
|
$scope.searchTerm = '';
|
||||||
|
$scope.images = preSearchImages;
|
||||||
|
$scope.hasMore = preSearchHasMore;
|
||||||
|
}
|
||||||
|
$scope.cancelSearch = cancelSearch;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs on image upload, Adds an image to local list of images
|
* Runs on image upload, Adds an image to local list of images
|
||||||
* and shows a success message to the user.
|
* and shows a success message to the user.
|
||||||
@ -59,7 +79,7 @@ module.exports = function (ngApp, events) {
|
|||||||
var currentTime = Date.now();
|
var currentTime = Date.now();
|
||||||
var timeDiff = currentTime - previousClickTime;
|
var timeDiff = currentTime - previousClickTime;
|
||||||
|
|
||||||
if (timeDiff < dblClickTime) {
|
if (timeDiff < dblClickTime && image.id === previousClickImage) {
|
||||||
// If double click
|
// If double click
|
||||||
callbackAndHide(image);
|
callbackAndHide(image);
|
||||||
} else {
|
} else {
|
||||||
@ -68,6 +88,7 @@ module.exports = function (ngApp, events) {
|
|||||||
$scope.dependantPages = false;
|
$scope.dependantPages = false;
|
||||||
}
|
}
|
||||||
previousClickTime = currentTime;
|
previousClickTime = currentTime;
|
||||||
|
previousClickImage = image.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,20 +131,69 @@ module.exports = function (ngApp, events) {
|
|||||||
$scope.showing = false;
|
$scope.showing = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var baseUrl = '/images/' + $scope.imageType + '/all/'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the list image data from the server.
|
* Fetch the list image data from the server.
|
||||||
*/
|
*/
|
||||||
function fetchData() {
|
function fetchData() {
|
||||||
var url = '/images/' + $scope.imageType + '/all/' + page;
|
var url = baseUrl + page + '?';
|
||||||
|
var components = {};
|
||||||
|
if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
|
||||||
|
if ($scope.searching) components['term'] = $scope.searchTerm;
|
||||||
|
|
||||||
|
|
||||||
|
var urlQueryString = Object.keys(components).map((key) => {
|
||||||
|
return key + '=' + encodeURIComponent(components[key]);
|
||||||
|
}).join('&');
|
||||||
|
url += urlQueryString;
|
||||||
|
|
||||||
$http.get(url).then((response) => {
|
$http.get(url).then((response) => {
|
||||||
$scope.images = $scope.images.concat(response.data.images);
|
$scope.images = $scope.images.concat(response.data.images);
|
||||||
$scope.hasMore = response.data.hasMore;
|
$scope.hasMore = response.data.hasMore;
|
||||||
page++;
|
page++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.fetchData = fetchData;
|
$scope.fetchData = fetchData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a search operation
|
||||||
|
* @param searchTerm
|
||||||
|
*/
|
||||||
|
$scope.searchImages = function() {
|
||||||
|
|
||||||
|
if ($scope.searchTerm === '') {
|
||||||
|
cancelSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scope.searching) {
|
||||||
|
preSearchImages = $scope.images;
|
||||||
|
preSearchHasMore = $scope.hasMore;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.searching = true;
|
||||||
|
$scope.images = [];
|
||||||
|
$scope.hasMore = false;
|
||||||
|
page = 0;
|
||||||
|
baseUrl = '/images/' + $scope.imageType + '/search/';
|
||||||
|
fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current image listing view.
|
||||||
|
* @param viewName
|
||||||
|
*/
|
||||||
|
$scope.setView = function(viewName) {
|
||||||
|
cancelSearch();
|
||||||
|
$scope.images = [];
|
||||||
|
$scope.hasMore = false;
|
||||||
|
page = 0;
|
||||||
|
$scope.view = viewName;
|
||||||
|
baseUrl = '/images/' + $scope.imageType + '/' + viewName + '/';
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the details of an image.
|
* Save the details of an image.
|
||||||
* @param event
|
* @param event
|
||||||
|
@ -189,12 +189,13 @@ form.search-box {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-nav {
|
.nav-tabs {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
a {
|
a, .tab-item {
|
||||||
padding: $-m;
|
padding: $-m;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
&.selected {
|
&.selected {
|
||||||
border-bottom: 2px solid $primary;
|
border-bottom: 2px solid $primary;
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,6 @@
|
|||||||
.image-manager-list {
|
.image-manager-list {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-manager-content {
|
.image-manager-content {
|
||||||
@ -128,6 +127,12 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.full-tab {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dropzone
|
// Dropzone
|
||||||
|
@ -177,3 +177,28 @@ $btt-size: 40px;
|
|||||||
top: -5px;
|
top: -5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contained-search-box {
|
||||||
|
display: flex;
|
||||||
|
input, button {
|
||||||
|
border-radius: 0;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
flex: 5;
|
||||||
|
&:focus, &:active {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
button i {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
button.cancel.active {
|
||||||
|
background-color: $negative;
|
||||||
|
color: #EEE;
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@
|
|||||||
.button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus {
|
.button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus {
|
||||||
background-color: {{ Setting::get('app-color') }};
|
background-color: {{ Setting::get('app-color') }};
|
||||||
}
|
}
|
||||||
.setting-nav a.selected {
|
.nav-tabs a.selected, .nav-tabs .tab-item.selected {
|
||||||
border-bottom-color: {{ Setting::get('app-color') }};
|
border-bottom-color: {{ Setting::get('app-color') }};
|
||||||
}
|
}
|
||||||
p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus {
|
p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus {
|
||||||
|
@ -3,6 +3,20 @@
|
|||||||
<div class="image-manager-body" ng-click="$event.stopPropagation()">
|
<div class="image-manager-body" ng-click="$event.stopPropagation()">
|
||||||
|
|
||||||
<div class="image-manager-content">
|
<div class="image-manager-content">
|
||||||
|
<div ng-if="imageType === 'gallery'" class="container">
|
||||||
|
<div class="image-manager-header row faded-small nav-tabs">
|
||||||
|
<div class="col-xs-4 tab-item" title="View all images" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> All</div>
|
||||||
|
<div class="col-xs-4 tab-item" title="View images uploaded to this book" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> Book</div>
|
||||||
|
<div class="col-xs-4 tab-item" title="View images uploaded to this page" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> Page</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ng-show="view === 'all'" >
|
||||||
|
<form ng-submit="searchImages()" class="contained-search-box">
|
||||||
|
<input type="text" placeholder="Search by image name" ng-model="searchTerm">
|
||||||
|
<button ng-class="{active: searching}" title="Clear Search" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
|
||||||
|
<button title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div class="image-manager-list">
|
<div class="image-manager-list">
|
||||||
<div ng-repeat="image in images">
|
<div ng-repeat="image in images">
|
||||||
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
|
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="faded-small toolbar">
|
<div class="faded-small toolbar">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12 setting-nav">
|
<div class="col-md-12 setting-nav nav-tabs">
|
||||||
<a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
|
<a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
|
||||||
<a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
|
<a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
|
||||||
<a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>
|
<a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user