Dan Brown

Added uploaded to book/page filters & search in image manager

Also refactored tab styles which affected the settings area.

Closes #41
1 -<?php 1 +<?php namespace BookStack\Http\Controllers;
2 -
3 -namespace BookStack\Http\Controllers;
4 2
5 use BookStack\Exceptions\ImageUploadException; 3 use BookStack\Exceptions\ImageUploadException;
6 use BookStack\Repos\ImageRepo; 4 use BookStack\Repos\ImageRepo;
7 use Illuminate\Filesystem\Filesystem as File; 5 use Illuminate\Filesystem\Filesystem as File;
8 use Illuminate\Http\Request; 6 use Illuminate\Http\Request;
9 -use Illuminate\Support\Facades\Auth;
10 -use Intervention\Image\Facades\Image as ImageTool;
11 -use Illuminate\Support\Facades\DB;
12 use BookStack\Image; 7 use BookStack\Image;
13 use BookStack\Repos\PageRepo; 8 use BookStack\Repos\PageRepo;
14 9
...@@ -45,6 +40,24 @@ class ImageController extends Controller ...@@ -45,6 +40,24 @@ class ImageController extends Controller
45 } 40 }
46 41
47 /** 42 /**
43 + * Search through images within a particular type.
44 + * @param $type
45 + * @param int $page
46 + * @param Request $request
47 + * @return mixed
48 + */
49 + public function searchByType($type, $page = 0, Request $request)
50 + {
51 + $this->validate($request, [
52 + 'term' => 'required|string'
53 + ]);
54 +
55 + $searchTerm = $request->get('term');
56 + $imgData = $this->imageRepo->searchPaginatedByType($type, $page,24, $searchTerm);
57 + return response()->json($imgData);
58 + }
59 +
60 + /**
48 * Get all images for a user. 61 * Get all images for a user.
49 * @param int $page 62 * @param int $page
50 * @return \Illuminate\Http\JsonResponse 63 * @return \Illuminate\Http\JsonResponse
...@@ -56,6 +69,27 @@ class ImageController extends Controller ...@@ -56,6 +69,27 @@ class ImageController extends Controller
56 } 69 }
57 70
58 /** 71 /**
72 + * Get gallery images with a specific filter such as book or page
73 + * @param $filter
74 + * @param int $page
75 + * @param Request $request
76 + */
77 + public function getGalleryFiltered($filter, $page = 0, Request $request)
78 + {
79 + $this->validate($request, [
80 + 'page_id' => 'required|integer'
81 + ]);
82 +
83 + $validFilters = collect(['page', 'book']);
84 + if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
85 +
86 + $pageId = $request->get('page_id');
87 + $imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
88 +
89 + return response()->json($imgData);
90 + }
91 +
92 + /**
59 * Handles image uploads for use on pages. 93 * Handles image uploads for use on pages.
60 * @param string $type 94 * @param string $type
61 * @param Request $request 95 * @param Request $request
......
...@@ -75,6 +75,8 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -75,6 +75,8 @@ Route::group(['middleware' => 'auth'], function () {
75 Route::post('/{type}/upload', 'ImageController@uploadByType'); 75 Route::post('/{type}/upload', 'ImageController@uploadByType');
76 Route::get('/{type}/all', 'ImageController@getAllByType'); 76 Route::get('/{type}/all', 'ImageController@getAllByType');
77 Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); 77 Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
78 + Route::get('/{type}/search/{page}', 'ImageController@searchByType');
79 + Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered');
78 Route::delete('/{imageId}', 'ImageController@destroy'); 80 Route::delete('/{imageId}', 'ImageController@destroy');
79 }); 81 });
80 82
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 2
3 3
4 use BookStack\Image; 4 use BookStack\Image;
5 +use BookStack\Page;
5 use BookStack\Services\ImageService; 6 use BookStack\Services\ImageService;
6 use BookStack\Services\RestrictionService; 7 use BookStack\Services\RestrictionService;
7 use Setting; 8 use Setting;
...@@ -13,18 +14,21 @@ class ImageRepo ...@@ -13,18 +14,21 @@ class ImageRepo
13 protected $image; 14 protected $image;
14 protected $imageService; 15 protected $imageService;
15 protected $restictionService; 16 protected $restictionService;
17 + protected $page;
16 18
17 /** 19 /**
18 * ImageRepo constructor. 20 * ImageRepo constructor.
19 * @param Image $image 21 * @param Image $image
20 * @param ImageService $imageService 22 * @param ImageService $imageService
21 * @param RestrictionService $restrictionService 23 * @param RestrictionService $restrictionService
24 + * @param Page $page
22 */ 25 */
23 - public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService) 26 + public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page)
24 { 27 {
25 $this->image = $image; 28 $this->image = $image;
26 $this->imageService = $imageService; 29 $this->imageService = $imageService;
27 $this->restictionService = $restrictionService; 30 $this->restictionService = $restrictionService;
31 + $this->page = $page;
28 } 32 }
29 33
30 34
...@@ -39,22 +43,16 @@ class ImageRepo ...@@ -39,22 +43,16 @@ class ImageRepo
39 } 43 }
40 44
41 /** 45 /**
42 - * Gets a load images paginated, filtered by image type. 46 + * Execute a paginated query, returning in a standard format.
43 - * @param string $type 47 + * Also runs the query through the restriction system.
48 + * @param $query
44 * @param int $page 49 * @param int $page
45 * @param int $pageSize 50 * @param int $pageSize
46 - * @param bool|int $userFilter
47 * @return array 51 * @return array
48 */ 52 */
49 - public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false) 53 + private function returnPaginated($query, $page = 0, $pageSize = 24)
50 { 54 {
51 - $images = $this->image->where('type', '=', strtolower($type)); 55 + $images = $this->restictionService->filterRelatedPages($query, 'images', 'uploaded_to');
52 -
53 - if ($userFilter !== false) {
54 - $images = $images->where('created_by', '=', $userFilter);
55 - }
56 -
57 - $images = $this->restictionService->filterRelatedPages($images, 'images', 'uploaded_to');
58 $images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get(); 56 $images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
59 $hasMore = count($images) > $pageSize; 57 $hasMore = count($images) > $pageSize;
60 58
...@@ -70,6 +68,64 @@ class ImageRepo ...@@ -70,6 +68,64 @@ class ImageRepo
70 } 68 }
71 69
72 /** 70 /**
71 + * Gets a load images paginated, filtered by image type.
72 + * @param string $type
73 + * @param int $page
74 + * @param int $pageSize
75 + * @param bool|int $userFilter
76 + * @return array
77 + */
78 + public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
79 + {
80 + $images = $this->image->where('type', '=', strtolower($type));
81 +
82 + if ($userFilter !== false) {
83 + $images = $images->where('created_by', '=', $userFilter);
84 + }
85 +
86 + return $this->returnPaginated($images, $page, $pageSize);
87 + }
88 +
89 + /**
90 + * Search for images by query, of a particular type.
91 + * @param string $type
92 + * @param int $page
93 + * @param int $pageSize
94 + * @param string $searchTerm
95 + * @return array
96 + */
97 + public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
98 + {
99 + $images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
100 + return $this->returnPaginated($images, $page, $pageSize);
101 + }
102 +
103 + /**
104 + * Get gallery images with a particular filter criteria such as
105 + * being within the current book or page.
106 + * @param int $pagination
107 + * @param int $pageSize
108 + * @param $filter
109 + * @param $pageId
110 + * @return array
111 + */
112 + public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
113 + {
114 + $images = $this->image->where('type', '=', 'gallery');
115 +
116 + $page = $this->page->findOrFail($pageId);
117 +
118 + if ($filter === 'page') {
119 + $images = $images->where('uploaded_to', '=', $page->id);
120 + } elseif ($filter === 'book') {
121 + $validPageIds = $page->book->pages->pluck('id')->toArray();
122 + $images = $images->whereIn('uploaded_to', $validPageIds);
123 + }
124 +
125 + return $this->returnPaginated($images, $pagination, $pageSize);
126 + }
127 +
128 + /**
73 * Save a new image into storage and return the new image. 129 * Save a new image into storage and return the new image.
74 * @param UploadedFile $uploadFile 130 * @param UploadedFile $uploadFile
75 * @param string $type 131 * @param string $type
......
...@@ -14,14 +14,22 @@ module.exports = function (ngApp, events) { ...@@ -14,14 +14,22 @@ module.exports = function (ngApp, events) {
14 $scope.imageUpdateSuccess = false; 14 $scope.imageUpdateSuccess = false;
15 $scope.imageDeleteSuccess = false; 15 $scope.imageDeleteSuccess = false;
16 $scope.uploadedTo = $attrs.uploadedTo; 16 $scope.uploadedTo = $attrs.uploadedTo;
17 + $scope.view = 'all';
18 +
19 + $scope.searching = false;
20 + $scope.searchTerm = '';
17 21
18 var page = 0; 22 var page = 0;
19 var previousClickTime = 0; 23 var previousClickTime = 0;
24 + var previousClickImage = 0;
20 var dataLoaded = false; 25 var dataLoaded = false;
21 var callback = false; 26 var callback = false;
22 27
28 + var preSearchImages = [];
29 + var preSearchHasMore = false;
30 +
23 /** 31 /**
24 - * Simple returns the appropriate upload url depending on the image type set. 32 + * Used by dropzone to get the endpoint to upload to.
25 * @returns {string} 33 * @returns {string}
26 */ 34 */
27 $scope.getUploadUrl = function () { 35 $scope.getUploadUrl = function () {
...@@ -29,6 +37,18 @@ module.exports = function (ngApp, events) { ...@@ -29,6 +37,18 @@ module.exports = function (ngApp, events) {
29 }; 37 };
30 38
31 /** 39 /**
40 + * Cancel the current search operation.
41 + */
42 + function cancelSearch() {
43 + $scope.searching = false;
44 + $scope.searchTerm = '';
45 + $scope.images = preSearchImages;
46 + $scope.hasMore = preSearchHasMore;
47 + }
48 + $scope.cancelSearch = cancelSearch;
49 +
50 +
51 + /**
32 * Runs on image upload, Adds an image to local list of images 52 * Runs on image upload, Adds an image to local list of images
33 * and shows a success message to the user. 53 * and shows a success message to the user.
34 * @param file 54 * @param file
...@@ -59,7 +79,7 @@ module.exports = function (ngApp, events) { ...@@ -59,7 +79,7 @@ module.exports = function (ngApp, events) {
59 var currentTime = Date.now(); 79 var currentTime = Date.now();
60 var timeDiff = currentTime - previousClickTime; 80 var timeDiff = currentTime - previousClickTime;
61 81
62 - if (timeDiff < dblClickTime) { 82 + if (timeDiff < dblClickTime && image.id === previousClickImage) {
63 // If double click 83 // If double click
64 callbackAndHide(image); 84 callbackAndHide(image);
65 } else { 85 } else {
...@@ -68,6 +88,7 @@ module.exports = function (ngApp, events) { ...@@ -68,6 +88,7 @@ module.exports = function (ngApp, events) {
68 $scope.dependantPages = false; 88 $scope.dependantPages = false;
69 } 89 }
70 previousClickTime = currentTime; 90 previousClickTime = currentTime;
91 + previousClickImage = image.id;
71 }; 92 };
72 93
73 /** 94 /**
...@@ -110,21 +131,70 @@ module.exports = function (ngApp, events) { ...@@ -110,21 +131,70 @@ module.exports = function (ngApp, events) {
110 $scope.showing = false; 131 $scope.showing = false;
111 }; 132 };
112 133
134 + var baseUrl = '/images/' + $scope.imageType + '/all/'
135 +
113 /** 136 /**
114 * Fetch the list image data from the server. 137 * Fetch the list image data from the server.
115 */ 138 */
116 function fetchData() { 139 function fetchData() {
117 - var url = '/images/' + $scope.imageType + '/all/' + page; 140 + var url = baseUrl + page + '?';
141 + var components = {};
142 + if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
143 + if ($scope.searching) components['term'] = $scope.searchTerm;
144 +
145 +
146 + var urlQueryString = Object.keys(components).map((key) => {
147 + return key + '=' + encodeURIComponent(components[key]);
148 + }).join('&');
149 + url += urlQueryString;
150 +
118 $http.get(url).then((response) => { 151 $http.get(url).then((response) => {
119 $scope.images = $scope.images.concat(response.data.images); 152 $scope.images = $scope.images.concat(response.data.images);
120 $scope.hasMore = response.data.hasMore; 153 $scope.hasMore = response.data.hasMore;
121 page++; 154 page++;
122 }); 155 });
123 } 156 }
124 -
125 $scope.fetchData = fetchData; 157 $scope.fetchData = fetchData;
126 158
127 /** 159 /**
160 + * Start a search operation
161 + * @param searchTerm
162 + */
163 + $scope.searchImages = function() {
164 +
165 + if ($scope.searchTerm === '') {
166 + cancelSearch();
167 + return;
168 + }
169 +
170 + if (!$scope.searching) {
171 + preSearchImages = $scope.images;
172 + preSearchHasMore = $scope.hasMore;
173 + }
174 +
175 + $scope.searching = true;
176 + $scope.images = [];
177 + $scope.hasMore = false;
178 + page = 0;
179 + baseUrl = '/images/' + $scope.imageType + '/search/';
180 + fetchData();
181 + };
182 +
183 + /**
184 + * Set the current image listing view.
185 + * @param viewName
186 + */
187 + $scope.setView = function(viewName) {
188 + cancelSearch();
189 + $scope.images = [];
190 + $scope.hasMore = false;
191 + page = 0;
192 + $scope.view = viewName;
193 + baseUrl = '/images/' + $scope.imageType + '/' + viewName + '/';
194 + fetchData();
195 + }
196 +
197 + /**
128 * Save the details of an image. 198 * Save the details of an image.
129 * @param event 199 * @param event
130 */ 200 */
......
...@@ -189,12 +189,13 @@ form.search-box { ...@@ -189,12 +189,13 @@ form.search-box {
189 } 189 }
190 } 190 }
191 191
192 -.setting-nav { 192 +.nav-tabs {
193 text-align: center; 193 text-align: center;
194 - a { 194 + a, .tab-item {
195 padding: $-m; 195 padding: $-m;
196 display: inline-block; 196 display: inline-block;
197 color: #666; 197 color: #666;
198 + cursor: pointer;
198 &.selected { 199 &.selected {
199 border-bottom: 2px solid $primary; 200 border-bottom: 2px solid $primary;
200 } 201 }
......
...@@ -120,7 +120,6 @@ ...@@ -120,7 +120,6 @@
120 .image-manager-list { 120 .image-manager-list {
121 overflow-y: scroll; 121 overflow-y: scroll;
122 flex: 1; 122 flex: 1;
123 - border-top: 1px solid #ddd;
124 } 123 }
125 124
126 .image-manager-content { 125 .image-manager-content {
...@@ -128,6 +127,12 @@ ...@@ -128,6 +127,12 @@
128 flex-direction: column; 127 flex-direction: column;
129 height: 100%; 128 height: 100%;
130 flex: 1; 129 flex: 1;
130 + .container {
131 + width: 100%;
132 + }
133 + .full-tab {
134 + text-align: center;
135 + }
131 } 136 }
132 137
133 // Dropzone 138 // Dropzone
......
...@@ -177,3 +177,28 @@ $btt-size: 40px; ...@@ -177,3 +177,28 @@ $btt-size: 40px;
177 top: -5px; 177 top: -5px;
178 } 178 }
179 } 179 }
180 +
181 +.contained-search-box {
182 + display: flex;
183 + input, button {
184 + border-radius: 0;
185 + border: 1px solid #DDD;
186 + margin-left: -1px;
187 + }
188 + input {
189 + flex: 5;
190 + &:focus, &:active {
191 + outline: 0;
192 + }
193 + }
194 + button {
195 + width: 60px;
196 + }
197 + button i {
198 + padding: 0;
199 + }
200 + button.cancel.active {
201 + background-color: $negative;
202 + color: #EEE;
203 + }
204 +}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
12 .button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus { 12 .button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus {
13 background-color: {{ Setting::get('app-color') }}; 13 background-color: {{ Setting::get('app-color') }};
14 } 14 }
15 - .setting-nav a.selected { 15 + .nav-tabs a.selected, .nav-tabs .tab-item.selected {
16 border-bottom-color: {{ Setting::get('app-color') }}; 16 border-bottom-color: {{ Setting::get('app-color') }};
17 } 17 }
18 p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus { 18 p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus {
......
...@@ -3,6 +3,20 @@ ...@@ -3,6 +3,20 @@
3 <div class="image-manager-body" ng-click="$event.stopPropagation()"> 3 <div class="image-manager-body" ng-click="$event.stopPropagation()">
4 4
5 <div class="image-manager-content"> 5 <div class="image-manager-content">
6 + <div ng-if="imageType === 'gallery'" class="container">
7 + <div class="image-manager-header row faded-small nav-tabs">
8 + <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>
9 + <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>
10 + <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>
11 + </div>
12 + </div>
13 + <div ng-show="view === 'all'" >
14 + <form ng-submit="searchImages()" class="contained-search-box">
15 + <input type="text" placeholder="Search by image name" ng-model="searchTerm">
16 + <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>
17 + <button title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
18 + </form>
19 + </div>
6 <div class="image-manager-list"> 20 <div class="image-manager-list">
7 <div ng-repeat="image in images"> 21 <div ng-repeat="image in images">
8 <div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}" 22 <div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 <div class="faded-small toolbar"> 2 <div class="faded-small toolbar">
3 <div class="container"> 3 <div class="container">
4 <div class="row"> 4 <div class="row">
5 - <div class="col-md-12 setting-nav"> 5 + <div class="col-md-12 setting-nav nav-tabs">
6 <a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a> 6 <a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
7 <a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a> 7 <a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
8 <a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a> 8 <a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>
......