Compare commits

..

15 Commits

Author SHA1 Message Date
Dan Brown
9b55a52b85 Updated assets for release 0.7.4 2016-02-11 22:35:01 +00:00
Dan Brown
db1d10e80f Merge branch 'master' into release 2016-02-11 22:29:29 +00:00
Dan Brown
354912a1df Made book-navigation sidebar on pages sticky 2016-02-11 22:23:19 +00:00
Nick Walke
eacff3a9f0 Fixes #45 2016-02-11 14:02:17 -06:00
Nick Walke
17d4533e45 Fixes #58 2016-02-11 01:18:01 -06:00
Dan Brown
d6c00a85ad Fixed incorrect notification when deleting a page 2016-02-10 12:48:29 +00:00
Dan Brown
1be576966f Updated assets for release 0.7.3 2016-02-08 20:47:33 +00:00
Dan Brown
b97e792c5f Merge branch 'master' into release 2016-02-08 20:45:48 +00:00
Dan Brown
e0279f93f9 Added a back-to-top button on all pages
The new back-to-top button will show after scrolling a short distance down a long page.
Closes #44.
2016-02-08 20:42:41 +00:00
Dan Brown
9b83c57316 Fixed some design issues and improved page export styling
Fixed alignment on export options dropdown.
Fixed bullet list items sitting too close next to floated content. Fixes #34.
Fixed text overlaying images in PDF exports (Floats removed for now). Fixes #53.
Fixed spaced table cells on html & PDF exports.
2016-02-08 20:41:40 +00:00
Dan Brown
5d73d17c74 Fixed bug preventing LDAP users updating thier profile
Made email not required when a profile is updated. LDAP users without admin privileges could did not have an email field to submit therefore could previously not update thier profile.
2016-02-08 20:35:23 +00:00
Dan Brown
d32460070f Made ldap auth use the 'dn' if a 'uid' is not present.
Fixes #56
2016-02-08 19:45:01 +00:00
Dan Brown
105500e506 Tweaked page form header and added public uploads folder into repo 2016-02-07 10:21:09 +00:00
Dan Brown
8296782149 Updated image controller styling and added preview option
The notification system was also updated so it can be used from JavaScript events such as image manager uploads.

Closes #25
2016-02-07 10:17:38 +00:00
Dan Brown
8e8d582bc6 Updated app requirements & Added some friendlier errors 2016-02-03 20:55:37 +00:00
48 changed files with 448 additions and 143 deletions

1
.gitignore vendored
View File

@@ -7,7 +7,6 @@ Homestead.yaml
/public/plugins
/public/css/*.map
/public/js/*.map
/public/uploads
/public/bower
/storage/images
_ide_helper.php

View File

@@ -1,7 +1,4 @@
<?php namespace BookStack\Exceptions;
class ConfirmationEmailException extends NotifyException
{
}
class ConfirmationEmailException extends NotifyException {}

View File

@@ -5,6 +5,7 @@ namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PhpSpec\Exception\Example\ErrorException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException;
@@ -38,17 +39,26 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if($e instanceof NotifyException) {
// Handle notify exceptions which will redirect to the
// specified location then show a notification message.
if ($e instanceof NotifyException) {
\Session::flash('error', $e->message);
return response()->redirectTo($e->redirectLocation);
}
// Handle pretty exceptions which will show a friendly application-fitting page
// Which will include the basic message to point the user roughly to the cause.
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
return response()->view('errors/500', ['message' => $message], 500);
}
return parent::render($request, $e);
}
}

View File

@@ -1,6 +1,3 @@
<?php namespace BookStack\Exceptions;
use Exception;
class ImageUploadException extends Exception {}
class ImageUploadException extends PrettyException {}

View File

@@ -1,9 +1,3 @@
<?php namespace BookStack\Exceptions;
use Exception;
class LdapException extends Exception
{
}
class LdapException extends PrettyException {}

View File

@@ -0,0 +1,5 @@
<?php namespace BookStack\Exceptions;
use Exception;
class PrettyException extends Exception {}

View File

@@ -1,6 +1,4 @@
<?php namespace BookStack\Exceptions;
class SocialDriverNotConfigured extends \Exception
{
}
class SocialDriverNotConfigured extends PrettyException {}

View File

@@ -1,7 +1,4 @@
<?php namespace BookStack\Exceptions;
class SocialSignInException extends NotifyException
{
}
class SocialSignInException extends NotifyException {}

View File

@@ -1,7 +1,4 @@
<?php namespace BookStack\Exceptions;
class UserRegistrationException extends NotifyException
{
}
class UserRegistrationException extends NotifyException {}

View File

@@ -130,8 +130,8 @@ class UserController extends Controller
});
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users,email,' . $id,
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'role' => 'exists:roles,id'

View File

@@ -4,6 +4,7 @@ use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use Exception;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
@@ -119,10 +120,12 @@ class ImageService
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
*
* @param Image $image
* @param int $width
* @param int $height
* @param bool $keepRatio
* @param int $width
* @param int $height
* @param bool $keepRatio
* @return string
* @throws Exception
* @throws ImageUploadException
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
@@ -139,8 +142,16 @@ class ImageService
return $this->getPublicUrl($thumbFilePath);
}
// Otherwise create the thumbnail
$thumb = $this->imageTool->make($storage->get($image->path));
try {
$thumb = $this->imageTool->make($storage->get($image->path));
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
} else {
throw $e;
}
}
if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) {
$constraint->aspectRatio();

View File

@@ -46,7 +46,7 @@ class LdapService
$user = $users[0];
return [
'uid' => $user['uid'][0],
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null

View File

@@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration
$table->dropColumn('external_auth_id');
});
}
}
}

View File

@@ -1,5 +1,5 @@
{
"css/styles.css": "css/styles.css?version=8d91c6c",
"css/print-styles.css": "css/print-styles.css?version=8d91c6c",
"js/common.js": "js/common.js?version=8d91c6c"
"css/styles.css": "css/styles.css?version=2748d88",
"css/print-styles.css": "css/print-styles.css?version=2748d88",
"js/common.js": "js/common.js?version=2748d88"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/uploads/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -17,19 +17,13 @@ A platform to create documentation/wiki content. General information about BookS
## Requirements
BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing.
BookStack has similar requirements to Laravel:
* PHP >= 5.5.9, Will need to be usable from the command line.
* OpenSSL PHP Extension
* PDO PHP Extension
* MBstring PHP Extension
* Tokenizer PHP Extension
* PHP Extensions: `OpenSSL`, `PDO`, `MBstring`, `Tokenizer`, `GD`
* MySQL >= 5.6
* Git (Not strictly required but helps manage updates)
* [Composer](https://getcomposer.org/)
* [Node.js](https://nodejs.org/en/) **Development Only**
* [Gulp](http://gulpjs.com/) **Development Only**
## Installation
@@ -144,7 +138,14 @@ A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user ui
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.
## Testing
## Development & Testing
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:
* [Node.js](https://nodejs.org/en/) **Development Only**
* [Gulp](http://gulpjs.com/) **Development Only**
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.
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.

View File

@@ -1,6 +1,6 @@
"use strict";
module.exports = function (ngApp) {
module.exports = function (ngApp, events) {
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
function ($scope, $attrs, $http, $timeout, imageManagerService) {
@@ -17,21 +17,40 @@ module.exports = function (ngApp) {
var dataLoaded = false;
var callback = false;
/**
* Simple returns the appropriate upload url depending on the image type set.
* @returns {string}
*/
$scope.getUploadUrl = function () {
return '/images/' + $scope.imageType + '/upload';
};
/**
* Runs on image upload, Adds an image to local list of images
* and shows a success message to the user.
* @param file
* @param data
*/
$scope.uploadSuccess = function (file, data) {
$scope.$apply(() => {
$scope.images.unshift(data);
});
events.emit('success', 'Image uploaded');
};
/**
* Runs the callback and hides the image manager.
* @param returnData
*/
function callbackAndHide(returnData) {
if (callback) callback(returnData);
$scope.showing = false;
}
/**
* Image select action. Checks if a double-click was fired.
* @param image
*/
$scope.imageSelect = function (image) {
var dblClickTime = 300;
var currentTime = Date.now();
@@ -48,10 +67,19 @@ module.exports = function (ngApp) {
previousClickTime = currentTime;
};
/**
* Action that runs when the 'Select image' button is clicked.
* Runs the callback and hides the image manager.
*/
$scope.selectButtonClick = function () {
callbackAndHide($scope.selectedImage);
};
/**
* Show the image manager.
* Takes a callback to execute later on.
* @param doneCallback
*/
function show(doneCallback) {
callback = doneCallback;
$scope.showing = true;
@@ -62,6 +90,8 @@ module.exports = function (ngApp) {
}
}
// Connects up the image manger so it can be used externally
// such as from TinyMCE.
imageManagerService.show = show;
imageManagerService.showExternal = function (doneCallback) {
$scope.$apply(() => {
@@ -70,10 +100,16 @@ module.exports = function (ngApp) {
};
window.ImageManager = imageManagerService;
/**
* Hide the image manager
*/
$scope.hide = function () {
$scope.showing = false;
};
/**
* Fetch the list image data from the server.
*/
function fetchData() {
var url = '/images/' + $scope.imageType + '/all/' + page;
$http.get(url).then((response) => {
@@ -82,28 +118,33 @@ module.exports = function (ngApp) {
page++;
});
}
$scope.fetchData = fetchData;
/**
* Save the details of an image.
* @param event
*/
$scope.saveImageDetails = function (event) {
event.preventDefault();
var url = '/images/update/' + $scope.selectedImage.id;
$http.put(url, this.selectedImage).then((response) => {
$scope.imageUpdateSuccess = true;
$timeout(() => {
$scope.imageUpdateSuccess = false;
}, 3000);
events.emit('success', 'Image details updated');
}, (response) => {
var errors = response.data;
var message = '';
Object.keys(errors).forEach((key) => {
message += errors[key].join('\n');
});
$scope.imageUpdateFailure = message;
$timeout(() => {
$scope.imageUpdateFailure = false;
}, 5000);
events.emit('error', message);
});
};
/**
* Delete an image from system and notify of success.
* Checks if it should force delete when an image
* has dependant pages.
* @param event
*/
$scope.deleteImage = function (event) {
event.preventDefault();
var force = $scope.dependantPages !== false;
@@ -112,10 +153,7 @@ module.exports = function (ngApp) {
$http.delete(url).then((response) => {
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
$scope.selectedImage = false;
$scope.imageDeleteSuccess = true;
$timeout(() => {
$scope.imageDeleteSuccess = false;
}, 3000);
events.emit('success', 'Image successfully deleted');
}, (response) => {
// Pages failure
if (response.status === 400) {
@@ -124,6 +162,15 @@ module.exports = function (ngApp) {
});
};
/**
* Simple date creator used to properly format dates.
* @param stringDate
* @returns {Date}
*/
$scope.getDate = function(stringDate) {
return new Date(stringDate);
};
}]);

View File

@@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html');
var imagePickerTemplate = require('./components/image-picker.html');
var dropZoneTemplate = require('./components/drop-zone.html');
module.exports = function (ngApp) {
module.exports = function (ngApp, events) {
/**
* Toggle Switches

View File

@@ -1,4 +1,4 @@
"use strict";
// AngularJS - Create application and load components
var angular = require('angular');
@@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate');
var ngSanitize = require('angular-sanitize');
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']);
var services = require('./services')(ngApp);
var directives = require('./directives')(ngApp);
var controllers = require('./controllers')(ngApp);
// Global Event System
var Events = {
listeners: {},
emit: function (eventName, eventData) {
if (typeof this.listeners[eventName] === 'undefined') return this;
var eventsToStart = this.listeners[eventName];
for (let i = 0; i < eventsToStart.length; i++) {
var event = eventsToStart[i];
event(eventData);
}
return this;
},
listen: function (eventName, callback) {
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
this.listeners[eventName].push(callback);
return this;
}
};
window.Events = Events;
var services = require('./services')(ngApp, Events);
var directives = require('./directives')(ngApp, Events);
var controllers = require('./controllers')(ngApp, Events);
//Global jQuery Config & Extensions
@@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) {
// Global jQuery Elements
$(function () {
var notifications = $('.notification');
var successNotification = notifications.filter('.pos');
var errorNotification = notifications.filter('.neg');
// Notification Events
window.Events.listen('success', function (text) {
successNotification.hide();
successNotification.find('span').text(text);
setTimeout(() => {
successNotification.show();
}, 1);
});
window.Events.listen('error', function (text) {
errorNotification.find('span').text(text);
errorNotification.show();
});
// Notification hiding
$('.notification').click(function () {
notifications.click(function () {
$(this).fadeOut(100);
});
@@ -44,6 +83,29 @@ $(function () {
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
});
// Back to top button
$('#back-to-top').click(function() {
$('#header').smoothScrollTo();
});
var scrollTopShowing = false;
var scrollTop = document.getElementById('back-to-top');
var scrollTopBreakpoint = 1200;
window.addEventListener('scroll', function() {
if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) {
scrollTop.style.display = 'block';
scrollTopShowing = true;
setTimeout(() => {
scrollTop.style.opacity = 1;
}, 1);
} else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
scrollTop.style.opacity = 0;
scrollTopShowing = false;
setTimeout(() => {
scrollTop.style.display = 'none';
}, 500);
}
});
});

View File

@@ -8,7 +8,6 @@ module.exports = {
statusbar: false,
menubar: false,
paste_data_images: false,
//height: 700,
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
@@ -31,7 +30,7 @@ module.exports = {
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
},
file_browser_callback: function (field_name, url, type, win) {
ImageManager.show(function (image) {
window.ImageManager.showExternal(function (image) {
win.document.getElementById(field_name).value = image.url;
if ("createEvent" in document) {
var evt = document.createEvent("HTMLEvents");
@@ -40,6 +39,10 @@ module.exports = {
} else {
win.document.getElementById(field_name).fireEvent("onchange");
}
var html = '<a href="' + image.url + '" target="_blank">';
html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">';
html += '</a>';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
});
},
paste_preprocess: function (plugin, args) {

View File

@@ -13,7 +13,7 @@ window.setupPageShow = module.exports = function (pageId) {
var isSelection = false;
// Select all contents on input click
$pointer.on('click', 'input', function(e) {
$pointer.on('click', 'input', function (e) {
$(this).select();
e.stopPropagation();
});
@@ -30,6 +30,7 @@ window.setupPageShow = module.exports = function (pageId) {
// Show pointer when selecting a single block of tagged content
$('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) {
e.stopPropagation();
var selection = window.getSelection();
if (selection.toString().length === 0) return;
@@ -47,8 +48,6 @@ window.setupPageShow = module.exports = function (pageId) {
var pointerLeftOffsetPercent = (pointerLeftOffset / $elem.width()) * 100;
$pointerInner.css('left', pointerLeftOffsetPercent + '%');
e.stopPropagation();
isSelection = true;
setTimeout(() => {
isSelection = false;
@@ -72,4 +71,43 @@ window.setupPageShow = module.exports = function (pageId) {
goToText(text);
}
};
// Make the book-tree sidebar stick in view on scroll
var $window = $(window);
var $bookTree = $(".book-tree");
// Check the page is scrollable and the content is taller than the tree
var pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height());
// Get current tree's width and header height
var headerHeight = $("#header").height() + $(".toolbar").height();
var isFixed = $window.scrollTop() > headerHeight;
var bookTreeWidth = $bookTree.width();
// Function to fix the tree as a sidebar
function stickTree() {
$bookTree.width(bookTreeWidth + 48 + 15);
$bookTree.addClass("fixed");
isFixed = true;
}
// Function to un-fix the tree back into position
function unstickTree() {
$bookTree.css('width', 'auto');
$bookTree.removeClass("fixed");
isFixed = false;
}
// Checks if the tree stickiness state should change
function checkTreeStickiness(skipCheck) {
var shouldBeFixed = $window.scrollTop() > headerHeight;
if (shouldBeFixed && (!isFixed || skipCheck)) {
stickTree();
} else if (!shouldBeFixed && (isFixed || skipCheck)) {
unstickTree();
}
}
// If the page is scrollable and the window is wide enough listen to scroll events
// and evaluate tree stickiness.
if (pageScrollable && $window.width() > 1000) {
$window.scroll(function() {
checkTreeStickiness(false);
});
checkTreeStickiness(true);
}
};

View File

@@ -1,6 +1,6 @@
"use strict";
module.exports = function(ngApp) {
module.exports = function(ngApp, events) {
ngApp.factory('imageManagerService', function() {
return {

View File

@@ -209,7 +209,7 @@ form.search-box {
.faded-small {
color: #000;
font-size: 0.9em;
background-color: rgba(21, 101, 192, 0.15);
background-color: $primary-faded;
}
.breadcrumbs .text-button, .action-buttons .text-button {

View File

@@ -21,7 +21,6 @@
border-radius: 4px;
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
overflow: hidden;
max-width: 1340px;
position: fixed;
top: 0;
bottom: 0;
@@ -44,18 +43,49 @@
right: 0;
}
.image-manager-list img {
.image-manager-list .image {
display: block;
position: relative;
border-radius: 0;
float: left;
margin: 0;
cursor: pointer;
width: (100%/6);
height: auto;
border: 1px solid #FFF;
border: 1px solid #DDD;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
transition: all cubic-bezier(.4, 0, 1, 1) 160ms;
overflow: hidden;
&.selected {
transform: scale3d(0.92, 0.92, 0.92);
border: 1px solid #444;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
}
img {
width: 100%;
max-width: 100%;
display: block;
}
.image-meta {
position: absolute;
width: 100%;
bottom: 0;
left: 0;
color: #EEE;
background-color: rgba(0, 0, 0, 0.4);
font-size: 10px;
padding: 3px 4px;
span {
display: block;
}
}
@include smaller-than($xl) {
width: (100%/4);
}
@include smaller-than($m) {
.image-meta {
display: none;
}
}
}

View File

@@ -95,7 +95,27 @@
// Sidebar list
.book-tree {
margin-top: $-xl;
padding: $-xl 0 0 0;
position: relative;
right: 0;
top: 0;
transition: ease-in-out 240ms;
transition-property: right, border;
border-left: 0px solid #FFF;
&.fixed {
position: fixed;
top: 0;
padding-left: $-l;
padding-right: $-l + 15;
width: 30%;
right: -15px;
height: 100%;
overflow-y: scroll;
-ms-overflow-style: none;
//background-color: $primary-faded;
border-left: 1px solid #DDD;
&::-webkit-scrollbar { width: 0 !important }
}
}
.book-tree h4 {
padding: $-m $-s 0 $-s;
@@ -105,16 +125,14 @@
}
.book-tree .sidebar-page-list {
list-style: none;
margin: 0;
margin-top: $-xs;
margin: $-xs 0 0;
padding-left: 0;
border-left: 5px solid $color-book;
li a {
display: block;
border-bottom: none;
padding-left: $-s;
padding: $-xs 0 $-xs $-s;
&:hover {
background-color: rgba(255, 255, 255, 0.2);
text-decoration: none;
}
}
@@ -165,6 +183,7 @@
}
.sub-menu {
display: none;
padding-left: 0;
}
.sub-menu.open {
display: block;

View File

@@ -223,13 +223,13 @@ span.highlight {
* Lists
*/
ul {
list-style: disc;
margin-left: $-m*1.5;
padding-left: $-m * 1.5;
list-style: disc inside;
}
ol {
list-style: decimal;
margin-left: $-m*1.5;
list-style: decimal inside;
padding-left: $-m * 1.5;
}
/*

View File

@@ -38,6 +38,7 @@ $primary-dark: #0288D1;
$secondary: #e27b41;
$positive: #52A256;
$negative: #E84F4F;
$primary-faded: rgba(21, 101, 192, 0.15);
// Item Colors
$color-book: #009688;

View File

@@ -9,4 +9,9 @@
@import "tables";
@import "header";
@import "lists";
@import "pages";
@import "pages";
table {
border-spacing: 0;
border-collapse: collapse;
}

View File

@@ -126,4 +126,43 @@ $loadingSize: 10px;
i {
padding-right: $-s;
}
}
// Back to top link
$btt-size: 40px;
#back-to-top {
background-color: rgba($primary, 0.4);
position: fixed;
bottom: $-m;
right: $-l;
padding: $-xs $-s;
cursor: pointer;
color: #FFF;
width: $btt-size;
height: $btt-size;
border-radius: $btt-size;
transition: all ease-in-out 180ms;
opacity: 0;
z-index: 999;
overflow: hidden;
&:hover {
width: $btt-size*3.4;
background-color: rgba($primary, 1);
span {
display: inline-block;
}
}
.inner {
width: $btt-size*3.4;
}
i {
margin: 0;
font-size: 28px;
padding: 0 $-s 0 0;
}
span {
line-height: 12px;
position: relative;
top: -5px;
}
}

View File

@@ -13,7 +13,7 @@ return [
'page_update' => 'updated page',
'page_update_notification' => 'Page Successfully Updated',
'page_delete' => 'deleted page',
'page_delete_notification' => 'Page Successfully Created',
'page_delete_notification' => 'Page Successfully Deleted',
'page_restore' => 'restored page',
'page_restore_notification' => 'Page Successfully Restored',
@@ -35,4 +35,4 @@ return [
'book_sort' => 'sorted book',
'book_sort_notification' => 'Book Successfully Re-sorted',
];
];

View File

@@ -77,6 +77,11 @@
@yield('content')
</section>
<div id="back-to-top">
<div class="inner">
<i class="zmdi zmdi-chevron-up"></i> <span>Back to top</span>
</div>
</div>
@yield('bottom')
<script src="{{ versioned_asset('js/common.js') }}"></script>
@yield('scripts')

View File

@@ -2,7 +2,7 @@
@section('content')
<div class="faded-small">
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-xs-1"></div>

View File

@@ -2,7 +2,7 @@
@section('content')
<div class="faded-small" ng-non-bindable>
<div class="faded-small toolbar" ng-non-bindable>
<div class="container">
<div class="row">
<div class="col-md-12">

View File

@@ -2,7 +2,7 @@
@section('content')
<div class="faded-small" ng-non-bindable>
<div class="faded-small toolbar" ng-non-bindable>
<div class="container">
<div class="row">
<div class="col-md-4 faded">

View File

@@ -0,0 +1,10 @@
@extends('base')
@section('content')
<div class="container">
<h1 class="text-muted">An Error Occurred</h1>
<p>{{ $message }}</p>
</div>
@stop

View File

@@ -4,15 +4,15 @@
<div class="page-editor flex-fill flex" ng-non-bindable>
{{ csrf_field() }}
<div class="faded-small">
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-md-4 faded">
<div class="col-sm-4 faded">
<div class="action-buttons text-left">
<a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>Toggle Header</a>
</div>
</div>
<div class="col-md-8 faded">
<div class="col-sm-8 faded">
<div class="action-buttons">
<a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-close"></i>Cancel</a>
<button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>

View File

@@ -20,5 +20,11 @@
table td {
width: auto !important;
}
.page-content img.align-left, .page-content img.align-right {
float: none !important;
clear: both;
display: block;
}
</style>
@stop

View File

@@ -2,7 +2,7 @@
@section('content')
<div class="faded-small">
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-md-6 faded">

View File

@@ -2,7 +2,7 @@
@section('content')
<div class="faded-small">
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-6 faded">
@@ -22,9 +22,9 @@
<span dropdown class="dropdown-container">
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
<ul class="wide">
<li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted pull-right">.html</span></a></li>
<li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted pull-right">.pdf</span></a></li>
<li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted pull-right">.txt</span></a></li>
<li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
<li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
<li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
</ul>
</span>
@if($currentUser->can('page-update'))

View File

@@ -5,11 +5,14 @@
<div class="image-manager-content">
<div class="image-manager-list">
<div ng-repeat="image in images">
<img class="anim fadeIn"
ng-class="{selected: (image==selectedImage)}"
ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"
ng-click="imageSelect(image)"
ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}">
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
<img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
<div class="image-meta">
<span class="name" ng-bind="image.name"></span>
<span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span>
</div>
</div>
</div>
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div>
</div>
@@ -19,18 +22,20 @@
<div class="image-manager-sidebar">
<h2>Images</h2>
<hr class="even">
<drop-zone upload-url="@{{getUploadUrl()}}" event-success="uploadSuccess"></drop-zone>
<div class="image-manager-details anim fadeIn" ng-show="selectedImage">
<hr class="even">
<form ng-submit="saveImageDetails($event)">
<div>
<a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;">
<img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}">
</a>
</div>
<div class="form-group">
<label for="name">Image Name</label>
<input type="text" id="name" name="name" ng-model="selectedImage.name">
<p class="text-pos text-small" ng-show="imageUpdateSuccess"><i class="fa fa-check"></i> Image name updated</p>
<p class="text-neg text-small" ng-show="imageUpdateFailure"><i class="fa fa-times"></i> <span ng-bind="imageUpdateFailure"></span></p>
</div>
</form>
@@ -53,8 +58,6 @@
</form>
</div>
<p class="text-pos" ng-show="imageDeleteSuccess"><i class="fa fa-check"></i> Image deleted</p>
<div class="image-manager-bottom">
<button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()">
<i class="zmdi zmdi-square-right"></i>Select Image

View File

@@ -1,11 +1,8 @@
@if(Session::has('success'))
<div class="notification anim pos">
<i class="zmdi zmdi-mood"></i> <span>{{ Session::get('success') }}</span>
</div>
@endif
@if(Session::has('error'))
<div class="notification anim neg stopped">
<i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span>
</div>
@endif
<div class="notification anim pos" @if(!Session::has('success')) style="display:none;" @endif>
<i class="zmdi zmdi-check-circle"></i> <span>{{ Session::get('success') }}</span>
</div>
<div class="notification anim neg stopped" @if(!Session::has('error')) style="display:none;" @endif>
<i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span>
</div>

View File

@@ -1,5 +1,5 @@
<div class="faded-small">
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-md-12 setting-nav">

View File

@@ -3,11 +3,11 @@
@section('content')
<div class="faded-small">
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-md-6"></div>
<div class="col-md-6 faded">
<div class="col-sm-6"></div>
<div class="col-sm-6 faded">
<div class="action-buttons">
<a href="/users/{{$user->id}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete User</a>
</div>

View File

@@ -27,12 +27,20 @@
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
<a href="/users/{{$user->id}}">
@endif
{{$user->name}}
{{ $user->name }}
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
</a>
@endif
</td>
<td>
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
<a href="/users/{{$user->id}}">
@endif
{{ $user->email }}
@if($currentUser->can('user-update') || $currentUser->id == $user->id)
</a>
@endif
</td>
<td>{{$user->email}}</td>
<td>{{ $user->role->display_name }}</td>
</tr>
@endforeach

View File

@@ -28,7 +28,7 @@ class LdapTest extends \TestCase
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test'.config('services.ldap.base_dn')]
'dn' => ['dc=test' . config('services.ldap.base_dn')]
]]);
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
@@ -46,6 +46,30 @@ class LdapTest extends \TestCase
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]);
}
public function test_login_works_when_no_uid_provided_by_ldap_server()
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setOption')->once();
$ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'cn' => [$this->mockUser->name],
'dn' => $ldapDn,
'mail' => [$this->mockUser->email]
]]);
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Sign In')
->seePageIs('/')
->see($this->mockUser->name)
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $ldapDn]);
}
public function test_initial_incorrect_details()
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
@@ -55,7 +79,7 @@ class LdapTest extends \TestCase
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test'.config('services.ldap.base_dn')]
'dn' => ['dc=test' . config('services.ldap.base_dn')]
]]);
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);