Showing
33 changed files
with
345 additions
and
107 deletions
| ... | @@ -7,7 +7,6 @@ Homestead.yaml | ... | @@ -7,7 +7,6 @@ Homestead.yaml |
| 7 | /public/plugins | 7 | /public/plugins |
| 8 | /public/css/*.map | 8 | /public/css/*.map |
| 9 | /public/js/*.map | 9 | /public/js/*.map |
| 10 | -/public/uploads | ||
| 11 | /public/bower | 10 | /public/bower |
| 12 | /storage/images | 11 | /storage/images |
| 13 | _ide_helper.php | 12 | _ide_helper.php | ... | ... |
| 1 | <?php namespace BookStack\Exceptions; | 1 | <?php namespace BookStack\Exceptions; |
| 2 | 2 | ||
| 3 | 3 | ||
| 4 | -class ConfirmationEmailException extends NotifyException | ||
| 5 | -{ | ||
| 6 | - | ||
| 7 | -} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 4 | +class ConfirmationEmailException extends NotifyException {} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -5,6 +5,7 @@ namespace BookStack\Exceptions; | ... | @@ -5,6 +5,7 @@ namespace BookStack\Exceptions; |
| 5 | use Exception; | 5 | use Exception; |
| 6 | use Illuminate\Contracts\Validation\ValidationException; | 6 | use Illuminate\Contracts\Validation\ValidationException; |
| 7 | use Illuminate\Database\Eloquent\ModelNotFoundException; | 7 | use Illuminate\Database\Eloquent\ModelNotFoundException; |
| 8 | +use PhpSpec\Exception\Example\ErrorException; | ||
| 8 | use Symfony\Component\HttpKernel\Exception\HttpException; | 9 | use Symfony\Component\HttpKernel\Exception\HttpException; |
| 9 | use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; | 10 | use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; |
| 10 | use Illuminate\Auth\Access\AuthorizationException; | 11 | use Illuminate\Auth\Access\AuthorizationException; |
| ... | @@ -38,17 +39,26 @@ class Handler extends ExceptionHandler | ... | @@ -38,17 +39,26 @@ class Handler extends ExceptionHandler |
| 38 | /** | 39 | /** |
| 39 | * Render an exception into an HTTP response. | 40 | * Render an exception into an HTTP response. |
| 40 | * | 41 | * |
| 41 | - * @param \Illuminate\Http\Request $request | 42 | + * @param \Illuminate\Http\Request $request |
| 42 | - * @param \Exception $e | 43 | + * @param \Exception $e |
| 43 | * @return \Illuminate\Http\Response | 44 | * @return \Illuminate\Http\Response |
| 44 | */ | 45 | */ |
| 45 | public function render($request, Exception $e) | 46 | public function render($request, Exception $e) |
| 46 | { | 47 | { |
| 47 | - if($e instanceof NotifyException) { | 48 | + // Handle notify exceptions which will redirect to the |
| 49 | + // specified location then show a notification message. | ||
| 50 | + if ($e instanceof NotifyException) { | ||
| 48 | \Session::flash('error', $e->message); | 51 | \Session::flash('error', $e->message); |
| 49 | return response()->redirectTo($e->redirectLocation); | 52 | return response()->redirectTo($e->redirectLocation); |
| 50 | } | 53 | } |
| 51 | 54 | ||
| 55 | + // Handle pretty exceptions which will show a friendly application-fitting page | ||
| 56 | + // Which will include the basic message to point the user roughly to the cause. | ||
| 57 | + if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) { | ||
| 58 | + $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage(); | ||
| 59 | + return response()->view('errors/500', ['message' => $message], 500); | ||
| 60 | + } | ||
| 61 | + | ||
| 52 | return parent::render($request, $e); | 62 | return parent::render($request, $e); |
| 53 | } | 63 | } |
| 54 | } | 64 | } | ... | ... |
| 1 | <?php namespace BookStack\Exceptions; | 1 | <?php namespace BookStack\Exceptions; |
| 2 | 2 | ||
| 3 | - | ||
| 4 | -use Exception; | ||
| 5 | - | ||
| 6 | -class ImageUploadException extends Exception {} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 3 | +class ImageUploadException extends PrettyException {} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | <?php namespace BookStack\Exceptions; | 1 | <?php namespace BookStack\Exceptions; |
| 2 | 2 | ||
| 3 | - | ||
| 4 | -use Exception; | ||
| 5 | - | ||
| 6 | -class LdapException extends Exception | ||
| 7 | -{ | ||
| 8 | - | ||
| 9 | -} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 3 | +class LdapException extends PrettyException {} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
app/Exceptions/PrettyException.php
0 → 100644
| 1 | <?php namespace BookStack\Exceptions; | 1 | <?php namespace BookStack\Exceptions; |
| 2 | 2 | ||
| 3 | 3 | ||
| 4 | -class SocialDriverNotConfigured extends \Exception | ||
| 5 | -{ | ||
| 6 | -} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 4 | +class SocialDriverNotConfigured extends PrettyException {} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | <?php namespace BookStack\Exceptions; | 1 | <?php namespace BookStack\Exceptions; |
| 2 | 2 | ||
| 3 | 3 | ||
| 4 | -class SocialSignInException extends NotifyException | ||
| 5 | -{ | ||
| 6 | - | ||
| 7 | -} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 4 | +class SocialSignInException extends NotifyException {} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| 1 | <?php namespace BookStack\Exceptions; | 1 | <?php namespace BookStack\Exceptions; |
| 2 | 2 | ||
| 3 | 3 | ||
| 4 | -class UserRegistrationException extends NotifyException | ||
| 5 | -{ | ||
| 6 | - | ||
| 7 | -} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 4 | +class UserRegistrationException extends NotifyException {} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -130,8 +130,8 @@ class UserController extends Controller | ... | @@ -130,8 +130,8 @@ class UserController extends Controller |
| 130 | }); | 130 | }); |
| 131 | 131 | ||
| 132 | $this->validate($request, [ | 132 | $this->validate($request, [ |
| 133 | - 'name' => 'required', | 133 | + 'name' => 'min:2', |
| 134 | - 'email' => 'required|email|unique:users,email,' . $id, | 134 | + 'email' => 'min:2|email|unique:users,email,' . $id, |
| 135 | 'password' => 'min:5|required_with:password_confirm', | 135 | 'password' => 'min:5|required_with:password_confirm', |
| 136 | 'password-confirm' => 'same:password|required_with:password', | 136 | 'password-confirm' => 'same:password|required_with:password', |
| 137 | 'role' => 'exists:roles,id' | 137 | 'role' => 'exists:roles,id' | ... | ... |
| ... | @@ -4,6 +4,7 @@ use BookStack\Exceptions\ImageUploadException; | ... | @@ -4,6 +4,7 @@ use BookStack\Exceptions\ImageUploadException; |
| 4 | use BookStack\Image; | 4 | use BookStack\Image; |
| 5 | use BookStack\User; | 5 | use BookStack\User; |
| 6 | use Exception; | 6 | use Exception; |
| 7 | +use Intervention\Image\Exception\NotSupportedException; | ||
| 7 | use Intervention\Image\ImageManager; | 8 | use Intervention\Image\ImageManager; |
| 8 | use Illuminate\Contracts\Filesystem\Factory as FileSystem; | 9 | use Illuminate\Contracts\Filesystem\Factory as FileSystem; |
| 9 | use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; | 10 | use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; |
| ... | @@ -119,10 +120,12 @@ class ImageService | ... | @@ -119,10 +120,12 @@ class ImageService |
| 119 | * Checks the cache then storage to avoid creating / accessing the filesystem on every check. | 120 | * Checks the cache then storage to avoid creating / accessing the filesystem on every check. |
| 120 | * | 121 | * |
| 121 | * @param Image $image | 122 | * @param Image $image |
| 122 | - * @param int $width | 123 | + * @param int $width |
| 123 | - * @param int $height | 124 | + * @param int $height |
| 124 | - * @param bool $keepRatio | 125 | + * @param bool $keepRatio |
| 125 | * @return string | 126 | * @return string |
| 127 | + * @throws Exception | ||
| 128 | + * @throws ImageUploadException | ||
| 126 | */ | 129 | */ |
| 127 | public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) | 130 | public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) |
| 128 | { | 131 | { |
| ... | @@ -139,8 +142,16 @@ class ImageService | ... | @@ -139,8 +142,16 @@ class ImageService |
| 139 | return $this->getPublicUrl($thumbFilePath); | 142 | return $this->getPublicUrl($thumbFilePath); |
| 140 | } | 143 | } |
| 141 | 144 | ||
| 142 | - // Otherwise create the thumbnail | 145 | + try { |
| 143 | - $thumb = $this->imageTool->make($storage->get($image->path)); | 146 | + $thumb = $this->imageTool->make($storage->get($image->path)); |
| 147 | + } catch (Exception $e) { | ||
| 148 | + if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { | ||
| 149 | + throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.'); | ||
| 150 | + } else { | ||
| 151 | + throw $e; | ||
| 152 | + } | ||
| 153 | + } | ||
| 154 | + | ||
| 144 | if ($keepRatio) { | 155 | if ($keepRatio) { |
| 145 | $thumb->resize($width, null, function ($constraint) { | 156 | $thumb->resize($width, null, function ($constraint) { |
| 146 | $constraint->aspectRatio(); | 157 | $constraint->aspectRatio(); | ... | ... |
| ... | @@ -46,7 +46,7 @@ class LdapService | ... | @@ -46,7 +46,7 @@ class LdapService |
| 46 | 46 | ||
| 47 | $user = $users[0]; | 47 | $user = $users[0]; |
| 48 | return [ | 48 | return [ |
| 49 | - 'uid' => $user['uid'][0], | 49 | + 'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'], |
| 50 | 'name' => $user['cn'][0], | 50 | 'name' => $user['cn'][0], |
| 51 | 'dn' => $user['dn'], | 51 | 'dn' => $user['dn'], |
| 52 | 'email' => (isset($user['mail'])) ? $user['mail'][0] : null | 52 | 'email' => (isset($user['mail'])) ? $user['mail'][0] : null | ... | ... |
| ... | @@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration | ... | @@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration |
| 28 | $table->dropColumn('external_auth_id'); | 28 | $table->dropColumn('external_auth_id'); |
| 29 | }); | 29 | }); |
| 30 | } | 30 | } |
| 31 | -} | 31 | +} |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
public/uploads/.gitignore
0 → 100644
| ... | @@ -17,19 +17,13 @@ A platform to create documentation/wiki content. General information about BookS | ... | @@ -17,19 +17,13 @@ A platform to create documentation/wiki content. General information about BookS |
| 17 | 17 | ||
| 18 | ## Requirements | 18 | ## Requirements |
| 19 | 19 | ||
| 20 | -BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing. | 20 | +BookStack has similar requirements to Laravel: |
| 21 | 21 | ||
| 22 | * PHP >= 5.5.9, Will need to be usable from the command line. | 22 | * PHP >= 5.5.9, Will need to be usable from the command line. |
| 23 | -* OpenSSL PHP Extension | 23 | +* PHP Extensions: `OpenSSL`, `PDO`, `MBstring`, `Tokenizer`, `GD` |
| 24 | -* PDO PHP Extension | ||
| 25 | -* MBstring PHP Extension | ||
| 26 | -* Tokenizer PHP Extension | ||
| 27 | * MySQL >= 5.6 | 24 | * MySQL >= 5.6 |
| 28 | * Git (Not strictly required but helps manage updates) | 25 | * Git (Not strictly required but helps manage updates) |
| 29 | * [Composer](https://getcomposer.org/) | 26 | * [Composer](https://getcomposer.org/) |
| 30 | -* [Node.js](https://nodejs.org/en/) **Development Only** | ||
| 31 | -* [Gulp](http://gulpjs.com/) **Development Only** | ||
| 32 | - | ||
| 33 | 27 | ||
| 34 | ## Installation | 28 | ## Installation |
| 35 | 29 | ||
| ... | @@ -144,7 +138,14 @@ A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user ui | ... | @@ -144,7 +138,14 @@ A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user ui |
| 144 | 138 | ||
| 145 | You may find that you cannot log in with your initial Admin account after changing the `AUTH_METHOD` to `ldap`. To get around this set the `AUTH_METHOD` to `standard`, login with your admin account then change it back to `ldap`. You get then edit your profile and add your LDAP uid under the 'External Authentication ID' field. You will then be able to login in with that ID. | 139 | You may find that you cannot log in with your initial Admin account after changing the `AUTH_METHOD` to `ldap`. To get around this set the `AUTH_METHOD` to `standard`, login with your admin account then change it back to `ldap`. You get then edit your profile and add your LDAP uid under the 'External Authentication ID' field. You will then be able to login in with that ID. |
| 146 | 140 | ||
| 147 | -## Testing | 141 | +## Development & Testing |
| 142 | + | ||
| 143 | +All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements: | ||
| 144 | + | ||
| 145 | +* [Node.js](https://nodejs.org/en/) **Development Only** | ||
| 146 | +* [Gulp](http://gulpjs.com/) **Development Only** | ||
| 147 | + | ||
| 148 | +SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp. | ||
| 148 | 149 | ||
| 149 | BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing. | 150 | BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing. |
| 150 | 151 | ... | ... |
| 1 | "use strict"; | 1 | "use strict"; |
| 2 | 2 | ||
| 3 | -module.exports = function (ngApp) { | 3 | +module.exports = function (ngApp, events) { |
| 4 | 4 | ||
| 5 | ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', | 5 | ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', |
| 6 | function ($scope, $attrs, $http, $timeout, imageManagerService) { | 6 | function ($scope, $attrs, $http, $timeout, imageManagerService) { |
| ... | @@ -17,21 +17,40 @@ module.exports = function (ngApp) { | ... | @@ -17,21 +17,40 @@ module.exports = function (ngApp) { |
| 17 | var dataLoaded = false; | 17 | var dataLoaded = false; |
| 18 | var callback = false; | 18 | var callback = false; |
| 19 | 19 | ||
| 20 | + /** | ||
| 21 | + * Simple returns the appropriate upload url depending on the image type set. | ||
| 22 | + * @returns {string} | ||
| 23 | + */ | ||
| 20 | $scope.getUploadUrl = function () { | 24 | $scope.getUploadUrl = function () { |
| 21 | return '/images/' + $scope.imageType + '/upload'; | 25 | return '/images/' + $scope.imageType + '/upload'; |
| 22 | }; | 26 | }; |
| 23 | 27 | ||
| 28 | + /** | ||
| 29 | + * Runs on image upload, Adds an image to local list of images | ||
| 30 | + * and shows a success message to the user. | ||
| 31 | + * @param file | ||
| 32 | + * @param data | ||
| 33 | + */ | ||
| 24 | $scope.uploadSuccess = function (file, data) { | 34 | $scope.uploadSuccess = function (file, data) { |
| 25 | $scope.$apply(() => { | 35 | $scope.$apply(() => { |
| 26 | $scope.images.unshift(data); | 36 | $scope.images.unshift(data); |
| 27 | }); | 37 | }); |
| 38 | + events.emit('success', 'Image uploaded'); | ||
| 28 | }; | 39 | }; |
| 29 | 40 | ||
| 41 | + /** | ||
| 42 | + * Runs the callback and hides the image manager. | ||
| 43 | + * @param returnData | ||
| 44 | + */ | ||
| 30 | function callbackAndHide(returnData) { | 45 | function callbackAndHide(returnData) { |
| 31 | if (callback) callback(returnData); | 46 | if (callback) callback(returnData); |
| 32 | $scope.showing = false; | 47 | $scope.showing = false; |
| 33 | } | 48 | } |
| 34 | 49 | ||
| 50 | + /** | ||
| 51 | + * Image select action. Checks if a double-click was fired. | ||
| 52 | + * @param image | ||
| 53 | + */ | ||
| 35 | $scope.imageSelect = function (image) { | 54 | $scope.imageSelect = function (image) { |
| 36 | var dblClickTime = 300; | 55 | var dblClickTime = 300; |
| 37 | var currentTime = Date.now(); | 56 | var currentTime = Date.now(); |
| ... | @@ -48,10 +67,19 @@ module.exports = function (ngApp) { | ... | @@ -48,10 +67,19 @@ module.exports = function (ngApp) { |
| 48 | previousClickTime = currentTime; | 67 | previousClickTime = currentTime; |
| 49 | }; | 68 | }; |
| 50 | 69 | ||
| 70 | + /** | ||
| 71 | + * Action that runs when the 'Select image' button is clicked. | ||
| 72 | + * Runs the callback and hides the image manager. | ||
| 73 | + */ | ||
| 51 | $scope.selectButtonClick = function () { | 74 | $scope.selectButtonClick = function () { |
| 52 | callbackAndHide($scope.selectedImage); | 75 | callbackAndHide($scope.selectedImage); |
| 53 | }; | 76 | }; |
| 54 | 77 | ||
| 78 | + /** | ||
| 79 | + * Show the image manager. | ||
| 80 | + * Takes a callback to execute later on. | ||
| 81 | + * @param doneCallback | ||
| 82 | + */ | ||
| 55 | function show(doneCallback) { | 83 | function show(doneCallback) { |
| 56 | callback = doneCallback; | 84 | callback = doneCallback; |
| 57 | $scope.showing = true; | 85 | $scope.showing = true; |
| ... | @@ -62,6 +90,8 @@ module.exports = function (ngApp) { | ... | @@ -62,6 +90,8 @@ module.exports = function (ngApp) { |
| 62 | } | 90 | } |
| 63 | } | 91 | } |
| 64 | 92 | ||
| 93 | + // Connects up the image manger so it can be used externally | ||
| 94 | + // such as from TinyMCE. | ||
| 65 | imageManagerService.show = show; | 95 | imageManagerService.show = show; |
| 66 | imageManagerService.showExternal = function (doneCallback) { | 96 | imageManagerService.showExternal = function (doneCallback) { |
| 67 | $scope.$apply(() => { | 97 | $scope.$apply(() => { |
| ... | @@ -70,10 +100,16 @@ module.exports = function (ngApp) { | ... | @@ -70,10 +100,16 @@ module.exports = function (ngApp) { |
| 70 | }; | 100 | }; |
| 71 | window.ImageManager = imageManagerService; | 101 | window.ImageManager = imageManagerService; |
| 72 | 102 | ||
| 103 | + /** | ||
| 104 | + * Hide the image manager | ||
| 105 | + */ | ||
| 73 | $scope.hide = function () { | 106 | $scope.hide = function () { |
| 74 | $scope.showing = false; | 107 | $scope.showing = false; |
| 75 | }; | 108 | }; |
| 76 | 109 | ||
| 110 | + /** | ||
| 111 | + * Fetch the list image data from the server. | ||
| 112 | + */ | ||
| 77 | function fetchData() { | 113 | function fetchData() { |
| 78 | var url = '/images/' + $scope.imageType + '/all/' + page; | 114 | var url = '/images/' + $scope.imageType + '/all/' + page; |
| 79 | $http.get(url).then((response) => { | 115 | $http.get(url).then((response) => { |
| ... | @@ -82,28 +118,33 @@ module.exports = function (ngApp) { | ... | @@ -82,28 +118,33 @@ module.exports = function (ngApp) { |
| 82 | page++; | 118 | page++; |
| 83 | }); | 119 | }); |
| 84 | } | 120 | } |
| 121 | + $scope.fetchData = fetchData; | ||
| 85 | 122 | ||
| 123 | + /** | ||
| 124 | + * Save the details of an image. | ||
| 125 | + * @param event | ||
| 126 | + */ | ||
| 86 | $scope.saveImageDetails = function (event) { | 127 | $scope.saveImageDetails = function (event) { |
| 87 | event.preventDefault(); | 128 | event.preventDefault(); |
| 88 | var url = '/images/update/' + $scope.selectedImage.id; | 129 | var url = '/images/update/' + $scope.selectedImage.id; |
| 89 | $http.put(url, this.selectedImage).then((response) => { | 130 | $http.put(url, this.selectedImage).then((response) => { |
| 90 | - $scope.imageUpdateSuccess = true; | 131 | + events.emit('success', 'Image details updated'); |
| 91 | - $timeout(() => { | ||
| 92 | - $scope.imageUpdateSuccess = false; | ||
| 93 | - }, 3000); | ||
| 94 | }, (response) => { | 132 | }, (response) => { |
| 95 | var errors = response.data; | 133 | var errors = response.data; |
| 96 | var message = ''; | 134 | var message = ''; |
| 97 | Object.keys(errors).forEach((key) => { | 135 | Object.keys(errors).forEach((key) => { |
| 98 | message += errors[key].join('\n'); | 136 | message += errors[key].join('\n'); |
| 99 | }); | 137 | }); |
| 100 | - $scope.imageUpdateFailure = message; | 138 | + events.emit('error', message); |
| 101 | - $timeout(() => { | ||
| 102 | - $scope.imageUpdateFailure = false; | ||
| 103 | - }, 5000); | ||
| 104 | }); | 139 | }); |
| 105 | }; | 140 | }; |
| 106 | 141 | ||
| 142 | + /** | ||
| 143 | + * Delete an image from system and notify of success. | ||
| 144 | + * Checks if it should force delete when an image | ||
| 145 | + * has dependant pages. | ||
| 146 | + * @param event | ||
| 147 | + */ | ||
| 107 | $scope.deleteImage = function (event) { | 148 | $scope.deleteImage = function (event) { |
| 108 | event.preventDefault(); | 149 | event.preventDefault(); |
| 109 | var force = $scope.dependantPages !== false; | 150 | var force = $scope.dependantPages !== false; |
| ... | @@ -112,10 +153,7 @@ module.exports = function (ngApp) { | ... | @@ -112,10 +153,7 @@ module.exports = function (ngApp) { |
| 112 | $http.delete(url).then((response) => { | 153 | $http.delete(url).then((response) => { |
| 113 | $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); | 154 | $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); |
| 114 | $scope.selectedImage = false; | 155 | $scope.selectedImage = false; |
| 115 | - $scope.imageDeleteSuccess = true; | 156 | + events.emit('success', 'Image successfully deleted'); |
| 116 | - $timeout(() => { | ||
| 117 | - $scope.imageDeleteSuccess = false; | ||
| 118 | - }, 3000); | ||
| 119 | }, (response) => { | 157 | }, (response) => { |
| 120 | // Pages failure | 158 | // Pages failure |
| 121 | if (response.status === 400) { | 159 | if (response.status === 400) { |
| ... | @@ -124,6 +162,15 @@ module.exports = function (ngApp) { | ... | @@ -124,6 +162,15 @@ module.exports = function (ngApp) { |
| 124 | }); | 162 | }); |
| 125 | }; | 163 | }; |
| 126 | 164 | ||
| 165 | + /** | ||
| 166 | + * Simple date creator used to properly format dates. | ||
| 167 | + * @param stringDate | ||
| 168 | + * @returns {Date} | ||
| 169 | + */ | ||
| 170 | + $scope.getDate = function(stringDate) { | ||
| 171 | + return new Date(stringDate); | ||
| 172 | + }; | ||
| 173 | + | ||
| 127 | }]); | 174 | }]); |
| 128 | 175 | ||
| 129 | 176 | ... | ... |
| ... | @@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html'); | ... | @@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html'); |
| 5 | var imagePickerTemplate = require('./components/image-picker.html'); | 5 | var imagePickerTemplate = require('./components/image-picker.html'); |
| 6 | var dropZoneTemplate = require('./components/drop-zone.html'); | 6 | var dropZoneTemplate = require('./components/drop-zone.html'); |
| 7 | 7 | ||
| 8 | -module.exports = function (ngApp) { | 8 | +module.exports = function (ngApp, events) { |
| 9 | 9 | ||
| 10 | /** | 10 | /** |
| 11 | * Toggle Switches | 11 | * Toggle Switches | ... | ... |
| 1 | - | 1 | +"use strict"; |
| 2 | 2 | ||
| 3 | // AngularJS - Create application and load components | 3 | // AngularJS - Create application and load components |
| 4 | var angular = require('angular'); | 4 | var angular = require('angular'); |
| ... | @@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate'); | ... | @@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate'); |
| 7 | var ngSanitize = require('angular-sanitize'); | 7 | var ngSanitize = require('angular-sanitize'); |
| 8 | 8 | ||
| 9 | var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']); | 9 | var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']); |
| 10 | -var services = require('./services')(ngApp); | 10 | + |
| 11 | -var directives = require('./directives')(ngApp); | 11 | + |
| 12 | -var controllers = require('./controllers')(ngApp); | 12 | +// Global Event System |
| 13 | +var Events = { | ||
| 14 | + listeners: {}, | ||
| 15 | + emit: function (eventName, eventData) { | ||
| 16 | + if (typeof this.listeners[eventName] === 'undefined') return this; | ||
| 17 | + var eventsToStart = this.listeners[eventName]; | ||
| 18 | + for (let i = 0; i < eventsToStart.length; i++) { | ||
| 19 | + var event = eventsToStart[i]; | ||
| 20 | + event(eventData); | ||
| 21 | + } | ||
| 22 | + return this; | ||
| 23 | + }, | ||
| 24 | + listen: function (eventName, callback) { | ||
| 25 | + if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = []; | ||
| 26 | + this.listeners[eventName].push(callback); | ||
| 27 | + return this; | ||
| 28 | + } | ||
| 29 | +}; | ||
| 30 | +window.Events = Events; | ||
| 31 | + | ||
| 32 | +var services = require('./services')(ngApp, Events); | ||
| 33 | +var directives = require('./directives')(ngApp, Events); | ||
| 34 | +var controllers = require('./controllers')(ngApp, Events); | ||
| 13 | 35 | ||
| 14 | //Global jQuery Config & Extensions | 36 | //Global jQuery Config & Extensions |
| 15 | 37 | ||
| ... | @@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) { | ... | @@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) { |
| 32 | // Global jQuery Elements | 54 | // Global jQuery Elements |
| 33 | $(function () { | 55 | $(function () { |
| 34 | 56 | ||
| 57 | + | ||
| 58 | + var notifications = $('.notification'); | ||
| 59 | + var successNotification = notifications.filter('.pos'); | ||
| 60 | + var errorNotification = notifications.filter('.neg'); | ||
| 61 | + // Notification Events | ||
| 62 | + window.Events.listen('success', function (text) { | ||
| 63 | + successNotification.hide(); | ||
| 64 | + successNotification.find('span').text(text); | ||
| 65 | + setTimeout(() => { | ||
| 66 | + successNotification.show(); | ||
| 67 | + }, 1); | ||
| 68 | + }); | ||
| 69 | + window.Events.listen('error', function (text) { | ||
| 70 | + errorNotification.find('span').text(text); | ||
| 71 | + errorNotification.show(); | ||
| 72 | + }); | ||
| 73 | + | ||
| 35 | // Notification hiding | 74 | // Notification hiding |
| 36 | - $('.notification').click(function () { | 75 | + notifications.click(function () { |
| 37 | $(this).fadeOut(100); | 76 | $(this).fadeOut(100); |
| 38 | }); | 77 | }); |
| 39 | 78 | ||
| ... | @@ -44,6 +83,29 @@ $(function () { | ... | @@ -44,6 +83,29 @@ $(function () { |
| 44 | $(this).closest('.chapter').find('.inset-list').slideToggle(180); | 83 | $(this).closest('.chapter').find('.inset-list').slideToggle(180); |
| 45 | }); | 84 | }); |
| 46 | 85 | ||
| 86 | + // Back to top button | ||
| 87 | + $('#back-to-top').click(function() { | ||
| 88 | + $('#header').smoothScrollTo(); | ||
| 89 | + }); | ||
| 90 | + var scrollTopShowing = false; | ||
| 91 | + var scrollTop = document.getElementById('back-to-top'); | ||
| 92 | + var scrollTopBreakpoint = 1200; | ||
| 93 | + window.addEventListener('scroll', function() { | ||
| 94 | + if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) { | ||
| 95 | + scrollTop.style.display = 'block'; | ||
| 96 | + scrollTopShowing = true; | ||
| 97 | + setTimeout(() => { | ||
| 98 | + scrollTop.style.opacity = 1; | ||
| 99 | + }, 1); | ||
| 100 | + } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) { | ||
| 101 | + scrollTop.style.opacity = 0; | ||
| 102 | + scrollTopShowing = false; | ||
| 103 | + setTimeout(() => { | ||
| 104 | + scrollTop.style.display = 'none'; | ||
| 105 | + }, 500); | ||
| 106 | + } | ||
| 107 | + }); | ||
| 108 | + | ||
| 47 | }); | 109 | }); |
| 48 | 110 | ||
| 49 | 111 | ... | ... |
| ... | @@ -8,7 +8,6 @@ module.exports = { | ... | @@ -8,7 +8,6 @@ module.exports = { |
| 8 | statusbar: false, | 8 | statusbar: false, |
| 9 | menubar: false, | 9 | menubar: false, |
| 10 | paste_data_images: false, | 10 | paste_data_images: false, |
| 11 | - //height: 700, | ||
| 12 | extended_valid_elements: 'pre[*]', | 11 | extended_valid_elements: 'pre[*]', |
| 13 | automatic_uploads: false, | 12 | automatic_uploads: false, |
| 14 | valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]", | 13 | valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]", |
| ... | @@ -31,7 +30,7 @@ module.exports = { | ... | @@ -31,7 +30,7 @@ module.exports = { |
| 31 | alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, | 30 | alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, |
| 32 | }, | 31 | }, |
| 33 | file_browser_callback: function (field_name, url, type, win) { | 32 | file_browser_callback: function (field_name, url, type, win) { |
| 34 | - ImageManager.show(function (image) { | 33 | + window.ImageManager.showExternal(function (image) { |
| 35 | win.document.getElementById(field_name).value = image.url; | 34 | win.document.getElementById(field_name).value = image.url; |
| 36 | if ("createEvent" in document) { | 35 | if ("createEvent" in document) { |
| 37 | var evt = document.createEvent("HTMLEvents"); | 36 | var evt = document.createEvent("HTMLEvents"); |
| ... | @@ -40,6 +39,10 @@ module.exports = { | ... | @@ -40,6 +39,10 @@ module.exports = { |
| 40 | } else { | 39 | } else { |
| 41 | win.document.getElementById(field_name).fireEvent("onchange"); | 40 | win.document.getElementById(field_name).fireEvent("onchange"); |
| 42 | } | 41 | } |
| 42 | + var html = '<a href="' + image.url + '" target="_blank">'; | ||
| 43 | + html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">'; | ||
| 44 | + html += '</a>'; | ||
| 45 | + win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html); | ||
| 43 | }); | 46 | }); |
| 44 | }, | 47 | }, |
| 45 | paste_preprocess: function (plugin, args) { | 48 | paste_preprocess: function (plugin, args) { | ... | ... |
| ... | @@ -21,7 +21,6 @@ | ... | @@ -21,7 +21,6 @@ |
| 21 | border-radius: 4px; | 21 | border-radius: 4px; |
| 22 | box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3); | 22 | box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3); |
| 23 | overflow: hidden; | 23 | overflow: hidden; |
| 24 | - max-width: 1340px; | ||
| 25 | position: fixed; | 24 | position: fixed; |
| 26 | top: 0; | 25 | top: 0; |
| 27 | bottom: 0; | 26 | bottom: 0; |
| ... | @@ -44,18 +43,49 @@ | ... | @@ -44,18 +43,49 @@ |
| 44 | right: 0; | 43 | right: 0; |
| 45 | } | 44 | } |
| 46 | 45 | ||
| 47 | -.image-manager-list img { | 46 | +.image-manager-list .image { |
| 48 | display: block; | 47 | display: block; |
| 48 | + position: relative; | ||
| 49 | border-radius: 0; | 49 | border-radius: 0; |
| 50 | float: left; | 50 | float: left; |
| 51 | margin: 0; | 51 | margin: 0; |
| 52 | cursor: pointer; | 52 | cursor: pointer; |
| 53 | width: (100%/6); | 53 | width: (100%/6); |
| 54 | height: auto; | 54 | height: auto; |
| 55 | - border: 1px solid #FFF; | 55 | + border: 1px solid #DDD; |
| 56 | + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); | ||
| 56 | transition: all cubic-bezier(.4, 0, 1, 1) 160ms; | 57 | transition: all cubic-bezier(.4, 0, 1, 1) 160ms; |
| 58 | + overflow: hidden; | ||
| 57 | &.selected { | 59 | &.selected { |
| 58 | transform: scale3d(0.92, 0.92, 0.92); | 60 | transform: scale3d(0.92, 0.92, 0.92); |
| 61 | + border: 1px solid #444; | ||
| 62 | + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2); | ||
| 63 | + } | ||
| 64 | + img { | ||
| 65 | + width: 100%; | ||
| 66 | + max-width: 100%; | ||
| 67 | + display: block; | ||
| 68 | + } | ||
| 69 | + .image-meta { | ||
| 70 | + position: absolute; | ||
| 71 | + width: 100%; | ||
| 72 | + bottom: 0; | ||
| 73 | + left: 0; | ||
| 74 | + color: #EEE; | ||
| 75 | + background-color: rgba(0, 0, 0, 0.4); | ||
| 76 | + font-size: 10px; | ||
| 77 | + padding: 3px 4px; | ||
| 78 | + span { | ||
| 79 | + display: block; | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + @include smaller-than($xl) { | ||
| 83 | + width: (100%/4); | ||
| 84 | + } | ||
| 85 | + @include smaller-than($m) { | ||
| 86 | + .image-meta { | ||
| 87 | + display: none; | ||
| 88 | + } | ||
| 59 | } | 89 | } |
| 60 | } | 90 | } |
| 61 | 91 | ... | ... |
| ... | @@ -105,8 +105,8 @@ | ... | @@ -105,8 +105,8 @@ |
| 105 | } | 105 | } |
| 106 | .book-tree .sidebar-page-list { | 106 | .book-tree .sidebar-page-list { |
| 107 | list-style: none; | 107 | list-style: none; |
| 108 | - margin: 0; | 108 | + margin: $-xs 0 0; |
| 109 | - margin-top: $-xs; | 109 | + padding-left: 0; |
| 110 | border-left: 5px solid $color-book; | 110 | border-left: 5px solid $color-book; |
| 111 | li a { | 111 | li a { |
| 112 | display: block; | 112 | display: block; | ... | ... |
| ... | @@ -223,13 +223,13 @@ span.highlight { | ... | @@ -223,13 +223,13 @@ span.highlight { |
| 223 | * Lists | 223 | * Lists |
| 224 | */ | 224 | */ |
| 225 | ul { | 225 | ul { |
| 226 | - list-style: disc; | 226 | + padding-left: $-m * 1.5; |
| 227 | - margin-left: $-m*1.5; | 227 | + list-style: disc inside; |
| 228 | } | 228 | } |
| 229 | 229 | ||
| 230 | ol { | 230 | ol { |
| 231 | - list-style: decimal; | 231 | + list-style: decimal inside; |
| 232 | - margin-left: $-m*1.5; | 232 | + padding-left: $-m * 1.5; |
| 233 | } | 233 | } |
| 234 | 234 | ||
| 235 | /* | 235 | /* | ... | ... |
| ... | @@ -9,4 +9,9 @@ | ... | @@ -9,4 +9,9 @@ |
| 9 | @import "tables"; | 9 | @import "tables"; |
| 10 | @import "header"; | 10 | @import "header"; |
| 11 | @import "lists"; | 11 | @import "lists"; |
| 12 | -@import "pages"; | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 12 | +@import "pages"; | ||
| 13 | + | ||
| 14 | +table { | ||
| 15 | + border-spacing: 0; | ||
| 16 | + border-collapse: collapse; | ||
| 17 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -126,4 +126,42 @@ $loadingSize: 10px; | ... | @@ -126,4 +126,42 @@ $loadingSize: 10px; |
| 126 | i { | 126 | i { |
| 127 | padding-right: $-s; | 127 | padding-right: $-s; |
| 128 | } | 128 | } |
| 129 | +} | ||
| 130 | + | ||
| 131 | +// Back to top link | ||
| 132 | +$btt-size: 40px; | ||
| 133 | +#back-to-top { | ||
| 134 | + background-color: rgba($primary, 0.4); | ||
| 135 | + position: fixed; | ||
| 136 | + bottom: $-m; | ||
| 137 | + right: $-m; | ||
| 138 | + padding: $-xs $-s; | ||
| 139 | + cursor: pointer; | ||
| 140 | + color: #FFF; | ||
| 141 | + width: $btt-size; | ||
| 142 | + height: $btt-size; | ||
| 143 | + border-radius: $btt-size; | ||
| 144 | + transition: all ease-in-out 180ms; | ||
| 145 | + opacity: 0; | ||
| 146 | + z-index: 999; | ||
| 147 | + &:hover { | ||
| 148 | + width: $btt-size*3.4; | ||
| 149 | + background-color: rgba($primary, 1); | ||
| 150 | + span { | ||
| 151 | + display: inline-block; | ||
| 152 | + } | ||
| 153 | + } | ||
| 154 | + .inner { | ||
| 155 | + width: $btt-size*3.4; | ||
| 156 | + } | ||
| 157 | + i { | ||
| 158 | + margin: 0; | ||
| 159 | + font-size: 28px; | ||
| 160 | + padding: 0 $-s 0 0; | ||
| 161 | + } | ||
| 162 | + span { | ||
| 163 | + line-height: 12px; | ||
| 164 | + position: relative; | ||
| 165 | + top: -5px; | ||
| 166 | + } | ||
| 129 | } | 167 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -77,6 +77,11 @@ | ... | @@ -77,6 +77,11 @@ |
| 77 | @yield('content') | 77 | @yield('content') |
| 78 | </section> | 78 | </section> |
| 79 | 79 | ||
| 80 | + <div id="back-to-top"> | ||
| 81 | + <div class="inner"> | ||
| 82 | + <i class="zmdi zmdi-chevron-up"></i> <span>Back to top</span> | ||
| 83 | + </div> | ||
| 84 | + </div> | ||
| 80 | @yield('bottom') | 85 | @yield('bottom') |
| 81 | <script src="{{ versioned_asset('js/common.js') }}"></script> | 86 | <script src="{{ versioned_asset('js/common.js') }}"></script> |
| 82 | @yield('scripts') | 87 | @yield('scripts') | ... | ... |
resources/views/errors/500.blade.php
0 → 100644
| ... | @@ -7,12 +7,12 @@ | ... | @@ -7,12 +7,12 @@ |
| 7 | <div class="faded-small"> | 7 | <div class="faded-small"> |
| 8 | <div class="container"> | 8 | <div class="container"> |
| 9 | <div class="row"> | 9 | <div class="row"> |
| 10 | - <div class="col-md-4 faded"> | 10 | + <div class="col-sm-4 faded"> |
| 11 | <div class="action-buttons text-left"> | 11 | <div class="action-buttons text-left"> |
| 12 | <a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>Toggle Header</a> | 12 | <a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>Toggle Header</a> |
| 13 | </div> | 13 | </div> |
| 14 | </div> | 14 | </div> |
| 15 | - <div class="col-md-8 faded"> | 15 | + <div class="col-sm-8 faded"> |
| 16 | <div class="action-buttons"> | 16 | <div class="action-buttons"> |
| 17 | <a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-close"></i>Cancel</a> | 17 | <a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-close"></i>Cancel</a> |
| 18 | <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button> | 18 | <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button> | ... | ... |
| ... | @@ -20,5 +20,11 @@ | ... | @@ -20,5 +20,11 @@ |
| 20 | table td { | 20 | table td { |
| 21 | width: auto !important; | 21 | width: auto !important; |
| 22 | } | 22 | } |
| 23 | + | ||
| 24 | + .page-content img.align-left, .page-content img.align-right { | ||
| 25 | + float: none !important; | ||
| 26 | + clear: both; | ||
| 27 | + display: block; | ||
| 28 | + } | ||
| 23 | </style> | 29 | </style> |
| 24 | @stop | 30 | @stop |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -22,9 +22,9 @@ | ... | @@ -22,9 +22,9 @@ |
| 22 | <span dropdown class="dropdown-container"> | 22 | <span dropdown class="dropdown-container"> |
| 23 | <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div> | 23 | <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div> |
| 24 | <ul class="wide"> | 24 | <ul class="wide"> |
| 25 | - <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted pull-right">.html</span></a></li> | 25 | + <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li> |
| 26 | - <li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted pull-right">.pdf</span></a></li> | 26 | + <li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li> |
| 27 | - <li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted pull-right">.txt</span></a></li> | 27 | + <li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li> |
| 28 | </ul> | 28 | </ul> |
| 29 | </span> | 29 | </span> |
| 30 | @if($currentUser->can('page-update')) | 30 | @if($currentUser->can('page-update')) | ... | ... |
| ... | @@ -5,11 +5,14 @@ | ... | @@ -5,11 +5,14 @@ |
| 5 | <div class="image-manager-content"> | 5 | <div class="image-manager-content"> |
| 6 | <div class="image-manager-list"> | 6 | <div class="image-manager-list"> |
| 7 | <div ng-repeat="image in images"> | 7 | <div ng-repeat="image in images"> |
| 8 | - <img class="anim fadeIn" | 8 | + <div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}" |
| 9 | - ng-class="{selected: (image==selectedImage)}" | 9 | + ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)"> |
| 10 | - ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}" | 10 | + <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"> |
| 11 | - ng-click="imageSelect(image)" | 11 | + <div class="image-meta"> |
| 12 | - ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"> | 12 | + <span class="name" ng-bind="image.name"></span> |
| 13 | + <span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span> | ||
| 14 | + </div> | ||
| 15 | + </div> | ||
| 13 | </div> | 16 | </div> |
| 14 | <div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div> | 17 | <div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div> |
| 15 | </div> | 18 | </div> |
| ... | @@ -19,18 +22,20 @@ | ... | @@ -19,18 +22,20 @@ |
| 19 | 22 | ||
| 20 | <div class="image-manager-sidebar"> | 23 | <div class="image-manager-sidebar"> |
| 21 | <h2>Images</h2> | 24 | <h2>Images</h2> |
| 22 | - <hr class="even"> | ||
| 23 | <drop-zone upload-url="@{{getUploadUrl()}}" event-success="uploadSuccess"></drop-zone> | 25 | <drop-zone upload-url="@{{getUploadUrl()}}" event-success="uploadSuccess"></drop-zone> |
| 24 | <div class="image-manager-details anim fadeIn" ng-show="selectedImage"> | 26 | <div class="image-manager-details anim fadeIn" ng-show="selectedImage"> |
| 25 | 27 | ||
| 26 | <hr class="even"> | 28 | <hr class="even"> |
| 27 | 29 | ||
| 28 | <form ng-submit="saveImageDetails($event)"> | 30 | <form ng-submit="saveImageDetails($event)"> |
| 31 | + <div> | ||
| 32 | + <a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;"> | ||
| 33 | + <img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}"> | ||
| 34 | + </a> | ||
| 35 | + </div> | ||
| 29 | <div class="form-group"> | 36 | <div class="form-group"> |
| 30 | <label for="name">Image Name</label> | 37 | <label for="name">Image Name</label> |
| 31 | <input type="text" id="name" name="name" ng-model="selectedImage.name"> | 38 | <input type="text" id="name" name="name" ng-model="selectedImage.name"> |
| 32 | - <p class="text-pos text-small" ng-show="imageUpdateSuccess"><i class="fa fa-check"></i> Image name updated</p> | ||
| 33 | - <p class="text-neg text-small" ng-show="imageUpdateFailure"><i class="fa fa-times"></i> <span ng-bind="imageUpdateFailure"></span></p> | ||
| 34 | </div> | 39 | </div> |
| 35 | </form> | 40 | </form> |
| 36 | 41 | ||
| ... | @@ -53,8 +58,6 @@ | ... | @@ -53,8 +58,6 @@ |
| 53 | </form> | 58 | </form> |
| 54 | </div> | 59 | </div> |
| 55 | 60 | ||
| 56 | - <p class="text-pos" ng-show="imageDeleteSuccess"><i class="fa fa-check"></i> Image deleted</p> | ||
| 57 | - | ||
| 58 | <div class="image-manager-bottom"> | 61 | <div class="image-manager-bottom"> |
| 59 | <button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()"> | 62 | <button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()"> |
| 60 | <i class="zmdi zmdi-square-right"></i>Select Image | 63 | <i class="zmdi zmdi-square-right"></i>Select Image | ... | ... |
| 1 | -@if(Session::has('success')) | ||
| 2 | - <div class="notification anim pos"> | ||
| 3 | - <i class="zmdi zmdi-mood"></i> <span>{{ Session::get('success') }}</span> | ||
| 4 | - </div> | ||
| 5 | -@endif | ||
| 6 | 1 | ||
| 7 | -@if(Session::has('error')) | ||
| 8 | - <div class="notification anim neg stopped"> | ||
| 9 | - <i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span> | ||
| 10 | - </div> | ||
| 11 | -@endif | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 2 | +<div class="notification anim pos" @if(!Session::has('success')) style="display:none;" @endif> | ||
| 3 | + <i class="zmdi zmdi-check-circle"></i> <span>{{ Session::get('success') }}</span> | ||
| 4 | +</div> | ||
| 5 | + | ||
| 6 | +<div class="notification anim neg stopped" @if(!Session::has('error')) style="display:none;" @endif> | ||
| 7 | + <i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span> | ||
| 8 | +</div> | ... | ... |
| ... | @@ -28,7 +28,7 @@ class LdapTest extends \TestCase | ... | @@ -28,7 +28,7 @@ class LdapTest extends \TestCase |
| 28 | ->andReturn(['count' => 1, 0 => [ | 28 | ->andReturn(['count' => 1, 0 => [ |
| 29 | 'uid' => [$this->mockUser->name], | 29 | 'uid' => [$this->mockUser->name], |
| 30 | 'cn' => [$this->mockUser->name], | 30 | 'cn' => [$this->mockUser->name], |
| 31 | - 'dn' => ['dc=test'.config('services.ldap.base_dn')] | 31 | + 'dn' => ['dc=test' . config('services.ldap.base_dn')] |
| 32 | ]]); | 32 | ]]); |
| 33 | $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true); | 33 | $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true); |
| 34 | 34 | ||
| ... | @@ -46,6 +46,30 @@ class LdapTest extends \TestCase | ... | @@ -46,6 +46,30 @@ class LdapTest extends \TestCase |
| 46 | ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]); | 46 | ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]); |
| 47 | } | 47 | } |
| 48 | 48 | ||
| 49 | + public function test_login_works_when_no_uid_provided_by_ldap_server() | ||
| 50 | + { | ||
| 51 | + $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); | ||
| 52 | + $this->mockLdap->shouldReceive('setOption')->once(); | ||
| 53 | + $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn'); | ||
| 54 | + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) | ||
| 55 | + ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array')) | ||
| 56 | + ->andReturn(['count' => 1, 0 => [ | ||
| 57 | + 'cn' => [$this->mockUser->name], | ||
| 58 | + 'dn' => $ldapDn, | ||
| 59 | + 'mail' => [$this->mockUser->email] | ||
| 60 | + ]]); | ||
| 61 | + $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true); | ||
| 62 | + | ||
| 63 | + $this->visit('/login') | ||
| 64 | + ->see('Username') | ||
| 65 | + ->type($this->mockUser->name, '#username') | ||
| 66 | + ->type($this->mockUser->password, '#password') | ||
| 67 | + ->press('Sign In') | ||
| 68 | + ->seePageIs('/') | ||
| 69 | + ->see($this->mockUser->name) | ||
| 70 | + ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $ldapDn]); | ||
| 71 | + } | ||
| 72 | + | ||
| 49 | public function test_initial_incorrect_details() | 73 | public function test_initial_incorrect_details() |
| 50 | { | 74 | { |
| 51 | $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); | 75 | $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); |
| ... | @@ -55,7 +79,7 @@ class LdapTest extends \TestCase | ... | @@ -55,7 +79,7 @@ class LdapTest extends \TestCase |
| 55 | ->andReturn(['count' => 1, 0 => [ | 79 | ->andReturn(['count' => 1, 0 => [ |
| 56 | 'uid' => [$this->mockUser->name], | 80 | 'uid' => [$this->mockUser->name], |
| 57 | 'cn' => [$this->mockUser->name], | 81 | 'cn' => [$this->mockUser->name], |
| 58 | - 'dn' => ['dc=test'.config('services.ldap.base_dn')] | 82 | + 'dn' => ['dc=test' . config('services.ldap.base_dn')] |
| 59 | ]]); | 83 | ]]); |
| 60 | $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false); | 84 | $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false); |
| 61 | 85 | ... | ... |
-
Please register or sign in to post a comment