From 4acf0c4ee061c5db0f80573f0931dfa0760df9c9 Mon Sep 17 00:00:00 2001 From: robert Date: Wed, 25 May 2016 23:52:43 +0200 Subject: [PATCH 01/25] Making sure MyISAM is set for the tables that need it for new installtions that are using mariadb. --- database/migrations/2015_07_12_114933_create_books_table.php | 1 + database/migrations/2015_07_12_190027_create_pages_table.php | 1 + database/migrations/2015_07_27_172342_create_chapters_table.php | 1 + 3 files changed, 3 insertions(+) diff --git a/database/migrations/2015_07_12_114933_create_books_table.php b/database/migrations/2015_07_12_114933_create_books_table.php index 51fb55c48..121607f6a 100644 --- a/database/migrations/2015_07_12_114933_create_books_table.php +++ b/database/migrations/2015_07_12_114933_create_books_table.php @@ -13,6 +13,7 @@ class CreateBooksTable extends Migration public function up() { Schema::create('books', function (Blueprint $table) { + $table->engine = 'MyISAM'; $table->increments('id'); $table->string('name'); $table->string('slug')->indexed(); diff --git a/database/migrations/2015_07_12_190027_create_pages_table.php b/database/migrations/2015_07_12_190027_create_pages_table.php index b3b2b9244..5e6e7e812 100644 --- a/database/migrations/2015_07_12_190027_create_pages_table.php +++ b/database/migrations/2015_07_12_190027_create_pages_table.php @@ -13,6 +13,7 @@ class CreatePagesTable extends Migration public function up() { Schema::create('pages', function (Blueprint $table) { + $table->engine = 'MyISAM'; $table->increments('id'); $table->integer('book_id'); $table->integer('chapter_id'); diff --git a/database/migrations/2015_07_27_172342_create_chapters_table.php b/database/migrations/2015_07_27_172342_create_chapters_table.php index 7974759f2..74594121f 100644 --- a/database/migrations/2015_07_27_172342_create_chapters_table.php +++ b/database/migrations/2015_07_27_172342_create_chapters_table.php @@ -13,6 +13,7 @@ class CreateChaptersTable extends Migration public function up() { Schema::create('chapters', function (Blueprint $table) { + $table->engine = 'MyISAM'; $table->increments('id'); $table->integer('book_id'); $table->string('slug')->indexed(); From f602b088acd1ae8964be551ec2e3b4f922ce1e0b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 28 May 2016 13:24:07 +0100 Subject: [PATCH 02/25] Added basic markdown scroll syncing --- resources/assets/js/directives.js | 45 +++++++++++++++++++++++----- resources/views/pages/form.blade.php | 2 +- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 62557f976..f5cf7569b 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -265,17 +265,48 @@ module.exports = function (ngApp, events) { link: function (scope, element, attrs) { // Elements - var input = element.find('textarea[markdown-input]'); - var insertImage = element.find('button[data-action="insertImage"]'); + const input = element.find('textarea[markdown-input]'); + const display = element.find('.markdown-display').first(); + const insertImage = element.find('button[data-action="insertImage"]'); - var currentCaretPos = 0; + let currentCaretPos = 0; - input.blur((event) => { + input.blur(event => { currentCaretPos = input[0].selectionStart; }); + // Scroll sync + let inputScrollHeight, + inputHeight, + displayScrollHeight, + displayHeight; + + function setScrollHeights() { + inputScrollHeight = input[0].scrollHeight; + inputHeight = input.height(); + displayScrollHeight = display[0].scrollHeight; + displayHeight = display.height(); + } + + setTimeout(() => { + setScrollHeights(); + }, 200); + window.addEventListener('resize', setScrollHeights); + let scrollDebounceTime = 800; + let lastScroll = 0; + input.on('scroll', event => { + let now = Date.now(); + if (now - lastScroll > scrollDebounceTime) { + setScrollHeights() + } + let scrollPercent = (input.scrollTop() / (inputScrollHeight-inputHeight)); + let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent; + display.scrollTop(displayScrollY); + lastScroll = now; + }); + // Insert image shortcut - input.keydown((event) => { + input.keydown(event => { if (event.which === 73 && event.ctrlKey && event.shiftKey) { event.preventDefault(); var caretPos = input[0].selectionStart; @@ -289,8 +320,8 @@ module.exports = function (ngApp, events) { }); // Insert image from image manager - insertImage.click((event) => { - window.ImageManager.showExternal((image) => { + insertImage.click(event => { + window.ImageManager.showExternal(image => { var caretPos = currentCaretPos; var currentContent = input.val(); var mdImageText = "![" + image.name + "](" + image.url + ")"; diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index aa05a9014..4196e946f 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -61,7 +61,7 @@ - From 5b9362ab0b7bf4f6ee36e3cca4b35fda99e8b316 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 28 May 2016 13:51:07 +0100 Subject: [PATCH 03/25] Added (Ctrl+s) draft force save --- resources/assets/js/controllers.js | 9 +++++++++ resources/assets/js/directives.js | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 8f434bf7e..406fd7e77 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -379,6 +379,15 @@ module.exports = function (ngApp, events) { saveDraft(); }; + // Listen to shortcuts coming via events + $scope.$on('editor-keydown', (event, data) => { + // Save shortcut (ctrl+s) + if (data.keyCode == 83 && (navigator.platform.match("Mac") ? data.metaKey : data.ctrlKey)) { + data.preventDefault(); + saveDraft(); + } + }); + /** * Discard the current draft and grab the current page * content from the system via an AJAX request. diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index f5cf7569b..d41fdd83b 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -185,6 +185,10 @@ module.exports = function (ngApp, events) { scope.mceChange(content); }); + editor.on('keydown', (event) => { + scope.$emit('editor-keydown', event); + }); + editor.on('init', (e) => { scope.mceModel = editor.getContent(); }); @@ -305,8 +309,9 @@ module.exports = function (ngApp, events) { lastScroll = now; }); - // Insert image shortcut + // Editor key-presses input.keydown(event => { + // Insert image shortcut if (event.which === 73 && event.ctrlKey && event.shiftKey) { event.preventDefault(); var caretPos = input[0].selectionStart; @@ -316,7 +321,10 @@ module.exports = function (ngApp, events) { input.focus(); input[0].selectionStart = caretPos + ("![](".length); input[0].selectionEnd = caretPos + ('![](http://'.length); + return; } + // Pass key presses to controller via event + scope.$emit('editor-keydown', event); }); // Insert image from image manager From 947db95d1674eeb95c5c9bd6aabee00a39d66da0 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 28 May 2016 14:02:48 +0100 Subject: [PATCH 04/25] Fixed error with similar activity filtering --- app/Activity.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Activity.php b/app/Activity.php index d43419c17..af386700a 100644 --- a/app/Activity.php +++ b/app/Activity.php @@ -44,7 +44,7 @@ class Activity extends Model * @return bool */ public function isSimilarTo($activityB) { - return [$this->key, $this->entitiy_type, $this->entitiy_id] === [$activityB->key, $activityB->entitiy_type, $activityB->entitiy_id]; + return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id]; } } From 80d1c594cc13a7733501a91bd9cebc696e6a6d88 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 3 Jun 2016 18:50:20 +0100 Subject: [PATCH 05/25] Added licence and release badges --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 8a20d52d9..27709de75 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,7 @@ # BookStack +[![GitHub release](https://img.shields.io/github/release/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/releases/latest) +[![license](https://img.shields.io/github/license/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/blob/master/LICENCE) [![Build Status](https://travis-ci.org/ssddanbrown/BookStack.svg)](https://travis-ci.org/ssddanbrown/BookStack) A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. From 3c1e165134901e8f1e0546407e981c6d91850e02 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 3 Jun 2016 18:51:47 +0100 Subject: [PATCH 06/25] Add in LICENSE file --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..281814bb8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Dan Brown + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 6c1e06bf86dbc1591fe38f07db3b66e682a95c05 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 3 Jun 2016 18:52:49 +0100 Subject: [PATCH 07/25] Updated License file --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 27709de75..29ac44f5e 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # BookStack [![GitHub release](https://img.shields.io/github/release/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/releases/latest) -[![license](https://img.shields.io/github/license/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/blob/master/LICENCE) +[![license](https://img.shields.io/github/license/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE) [![Build Status](https://travis-ci.org/ssddanbrown/BookStack.svg)](https://travis-ci.org/ssddanbrown/BookStack) A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. From 246d1621f5165a030874d4f931feba72b7a662e5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 4 Jun 2016 14:54:31 +0100 Subject: [PATCH 08/25] Limited tag value autosuggestions based on tag name As requested on #121 --- app/Http/Controllers/TagController.php | 3 +- app/Repos/TagRepo.php | 7 ++++- resources/assets/js/directives.js | 32 ++++++++++++++------ resources/views/pages/form-toolbox.blade.php | 6 ++-- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index 1823b0dc8..b6749aec1 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -67,7 +67,8 @@ class TagController extends Controller public function getValueSuggestions(Request $request) { $searchTerm = $request->get('search'); - $suggestions = $this->tagRepo->getValueSuggestions($searchTerm); + $tagName = $request->has('name') ? $request->get('name') : false; + $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName); return response()->json($suggestions); } diff --git a/app/Repos/TagRepo.php b/app/Repos/TagRepo.php index 7d51d87f7..e87732cf5 100644 --- a/app/Repos/TagRepo.php +++ b/app/Repos/TagRepo.php @@ -72,15 +72,20 @@ class TagRepo /** * Get tag value suggestions from scanning existing tag values. * @param $searchTerm + * @param $tagName * @return array */ - public function getValueSuggestions($searchTerm) + public function getValueSuggestions($searchTerm, $tagName = false) { if ($searchTerm === '') return []; $query = $this->tag->where('value', 'LIKE', $searchTerm . '%')->groupBy('value')->orderBy('value', 'desc'); + if ($tagName !== false) { + $query = $query->where('name', '=', $tagName); + } $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); return $query->get(['value'])->pluck('value'); } + /** * Save an array of tags to an entity * @param Entity $entity diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index d41fdd83b..df5284a97 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -378,7 +378,7 @@ module.exports = function (ngApp, events) { } }]); - ngApp.directive('autosuggestions', ['$http', function($http) { + ngApp.directive('tagAutosuggestions', ['$http', function($http) { return { restrict: 'A', link: function(scope, elem, attrs) { @@ -403,6 +403,8 @@ module.exports = function (ngApp, events) { let $input = $(this); let val = $input.val(); let url = $input.attr('autosuggest'); + let type = $input.attr('autosuggest-type'); + // No suggestions until at least 3 chars if (val.length < 3) { if (isShowing) { @@ -410,12 +412,21 @@ module.exports = function (ngApp, events) { isShowing = false; } return; - }; + } + + // Add name param to request if for a value + if (type.toLowerCase() === 'value') { + let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first(); + let nameVal = $nameInput.val(); + if (nameVal === '') return; + url += '?name=' + encodeURIComponent(nameVal); + console.log(url); + } let suggestionPromise = getSuggestions(val.slice(0, 3), url); - suggestionPromise.then((suggestions) => { + suggestionPromise.then(suggestions => { if (val.length > 2) { - suggestions = suggestions.filter((item) => { + suggestions = suggestions.filter(item => { return item.toLowerCase().indexOf(val.toLowerCase()) !== -1; }).slice(0, 4); displaySuggestions($input, suggestions); @@ -448,15 +459,17 @@ module.exports = function (ngApp, events) { let newActive = (active === 0) ? suggestCount-1 : active - 1; changeActiveTo(newActive, suggestionElems); } - // Enter key - else if (event.keyCode === 13) { + // Enter or tab key + else if (event.keyCode === 13 || event.keyCode === 9) { let text = suggestionElems[active].textContent; currentInput[0].value = text; currentInput.focus(); $suggestionBox.hide(); isShowing = false; - event.preventDefault(); - return false; + if (event.keyCode === 13) { + event.preventDefault(); + return false; + } } }); @@ -523,7 +536,8 @@ module.exports = function (ngApp, events) { // Get suggestions & cache function getSuggestions(input, url) { - let searchUrl = url + '?search=' + encodeURIComponent(input); + let hasQuery = url.indexOf('?') !== -1; + let searchUrl = url + (hasQuery?'&':'?') + 'search=' + encodeURIComponent(input); // Get from local cache if exists if (localCache[searchUrl]) { diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index ae17045d1..b3fcd7c13 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -10,12 +10,12 @@

Page Tags

Add some tags to better categorise your content.
You can assign a value to a tag for more in-depth organisation.

- +
- - + + From eec9c05518ee2568639537543ee048ee4b287ed5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 4 Jun 2016 15:37:28 +0100 Subject: [PATCH 09/25] Added tag autosuggestion when no input provided Shows the most popular tag names/values. As requested on #121 --- app/Http/Controllers/TagController.php | 4 +- app/Repos/TagRepo.php | 32 ++++++--- resources/assets/js/directives.js | 92 +++++++++++++------------- 3 files changed, 72 insertions(+), 56 deletions(-) diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index b6749aec1..c8a356541 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -55,7 +55,7 @@ class TagController extends Controller */ public function getNameSuggestions(Request $request) { - $searchTerm = $request->get('search'); + $searchTerm = $request->has('search') ? $request->get('search') : false; $suggestions = $this->tagRepo->getNameSuggestions($searchTerm); return response()->json($suggestions); } @@ -66,7 +66,7 @@ class TagController extends Controller */ public function getValueSuggestions(Request $request) { - $searchTerm = $request->get('search'); + $searchTerm = $request->has('search') ? $request->get('search') : false; $tagName = $request->has('name') ? $request->get('name') : false; $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName); return response()->json($suggestions); diff --git a/app/Repos/TagRepo.php b/app/Repos/TagRepo.php index e87732cf5..6d0857f8b 100644 --- a/app/Repos/TagRepo.php +++ b/app/Repos/TagRepo.php @@ -58,34 +58,48 @@ class TagRepo /** * Get tag name suggestions from scanning existing tag names. + * If no search term is given the 50 most popular tag names are provided. * @param $searchTerm * @return array */ - public function getNameSuggestions($searchTerm) + public function getNameSuggestions($searchTerm = false) { - if ($searchTerm === '') return []; - $query = $this->tag->where('name', 'LIKE', $searchTerm . '%')->groupBy('name')->orderBy('name', 'desc'); + $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name'); + + if ($searchTerm) { + $query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc'); + } else { + $query = $query->orderBy('count', 'desc')->take(50); + } + $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); return $query->get(['name'])->pluck('name'); } /** * Get tag value suggestions from scanning existing tag values. + * If no search is given the 50 most popular values are provided. + * Passing a tagName will only find values for a tags with a particular name. * @param $searchTerm * @param $tagName * @return array */ - public function getValueSuggestions($searchTerm, $tagName = false) + public function getValueSuggestions($searchTerm = false, $tagName = false) { - if ($searchTerm === '') return []; - $query = $this->tag->where('value', 'LIKE', $searchTerm . '%')->groupBy('value')->orderBy('value', 'desc'); - if ($tagName !== false) { - $query = $query->where('name', '=', $tagName); + $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value'); + + if ($searchTerm) { + $query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc'); + } else { + $query = $query->orderBy('count', 'desc')->take(50); } + + if ($tagName !== false) $query = $query->where('name', '=', $tagName); + $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); return $query->get(['value'])->pluck('value'); } - + /** * Save an array of tags to an entity * @param Entity $entity diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index df5284a97..43d55f092 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -166,7 +166,7 @@ module.exports = function (ngApp, events) { }; }]); - ngApp.directive('tinymce', ['$timeout', function($timeout) { + ngApp.directive('tinymce', ['$timeout', function ($timeout) { return { restrict: 'A', scope: { @@ -204,8 +204,8 @@ module.exports = function (ngApp, events) { scope.tinymce.extraSetups.push(tinyMceSetup); // Custom tinyMCE plugins - tinymce.PluginManager.add('customhr', function(editor) { - editor.addCommand('InsertHorizontalRule', function() { + tinymce.PluginManager.add('customhr', function (editor) { + editor.addCommand('InsertHorizontalRule', function () { var hrElem = document.createElement('hr'); var cNode = editor.selection.getNode(); var parentNode = cNode.parentNode; @@ -231,7 +231,7 @@ module.exports = function (ngApp, events) { } }]); - ngApp.directive('markdownInput', ['$timeout', function($timeout) { + ngApp.directive('markdownInput', ['$timeout', function ($timeout) { return { restrict: 'A', scope: { @@ -255,7 +255,7 @@ module.exports = function (ngApp, events) { scope.$on('markdown-update', (event, value) => { element.val(value); - scope.mdModel= value; + scope.mdModel = value; scope.mdChange(markdown(value)); }); @@ -263,7 +263,7 @@ module.exports = function (ngApp, events) { } }]); - ngApp.directive('markdownEditor', ['$timeout', function($timeout) { + ngApp.directive('markdownEditor', ['$timeout', function ($timeout) { return { restrict: 'A', link: function (scope, element, attrs) { @@ -303,7 +303,7 @@ module.exports = function (ngApp, events) { if (now - lastScroll > scrollDebounceTime) { setScrollHeights() } - let scrollPercent = (input.scrollTop() / (inputScrollHeight-inputHeight)); + let scrollPercent = (input.scrollTop() / (inputScrollHeight - inputHeight)); let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent; display.scrollTop(displayScrollY); lastScroll = now; @@ -341,11 +341,11 @@ module.exports = function (ngApp, events) { } } }]); - - ngApp.directive('toolbox', [function() { + + ngApp.directive('toolbox', [function () { return { restrict: 'A', - link: function(scope, elem, attrs) { + link: function (scope, elem, attrs) { // Get common elements const $buttons = elem.find('[tab-button]'); @@ -356,7 +356,7 @@ module.exports = function (ngApp, events) { $toggle.click((e) => { elem.toggleClass('open'); }); - + // Set an active tab/content by name function setActive(tabName, openToolbox) { $buttons.removeClass('active'); @@ -370,7 +370,7 @@ module.exports = function (ngApp, events) { setActive($content.first().attr('tab-content'), false); // Handle tab button click - $buttons.click(function(e) { + $buttons.click(function (e) { let name = $(this).attr('tab-button'); setActive(name, true); }); @@ -378,11 +378,11 @@ module.exports = function (ngApp, events) { } }]); - ngApp.directive('tagAutosuggestions', ['$http', function($http) { + ngApp.directive('tagAutosuggestions', ['$http', function ($http) { return { restrict: 'A', - link: function(scope, elem, attrs) { - + link: function (scope, elem, attrs) { + // Local storage for quick caching. const localCache = {}; @@ -399,49 +399,49 @@ module.exports = function (ngApp, events) { let active = 0; // Listen to input events on autosuggest fields - elem.on('input', '[autosuggest]', function(event) { + elem.on('input focus', '[autosuggest]', function (event) { let $input = $(this); let val = $input.val(); let url = $input.attr('autosuggest'); let type = $input.attr('autosuggest-type'); - // No suggestions until at least 3 chars - if (val.length < 3) { - if (isShowing) { - $suggestionBox.hide(); - isShowing = false; - } - return; - } - // Add name param to request if for a value if (type.toLowerCase() === 'value') { let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first(); let nameVal = $nameInput.val(); - if (nameVal === '') return; - url += '?name=' + encodeURIComponent(nameVal); - console.log(url); + if (nameVal !== '') { + url += '?name=' + encodeURIComponent(nameVal); + } } let suggestionPromise = getSuggestions(val.slice(0, 3), url); suggestionPromise.then(suggestions => { - if (val.length > 2) { - suggestions = suggestions.filter(item => { - return item.toLowerCase().indexOf(val.toLowerCase()) !== -1; - }).slice(0, 4); - displaySuggestions($input, suggestions); - } + if (val.length === 0) { + displaySuggestions($input, suggestions.slice(0, 6)); + } else { + suggestions = suggestions.filter(item => { + return item.toLowerCase().indexOf(val.toLowerCase()) !== -1; + }).slice(0, 4); + displaySuggestions($input, suggestions); + } }); }); // Hide autosuggestions when input loses focus. // Slight delay to allow clicks. - elem.on('blur', '[autosuggest]', function(event) { + let lastFocusTime = 0; + elem.on('blur', '[autosuggest]', function (event) { + let startTime = Date.now(); setTimeout(() => { - $suggestionBox.hide(); - isShowing = false; + if (lastFocusTime < startTime) { + $suggestionBox.hide(); + isShowing = false; + } }, 200) }); + elem.on('focus', '[autosuggest]', function (event) { + lastFocusTime = Date.now(); + }); elem.on('keydown', '[autosuggest]', function (event) { if (!isShowing) return; @@ -451,12 +451,12 @@ module.exports = function (ngApp, events) { // Down arrow if (event.keyCode === 40) { - let newActive = (active === suggestCount-1) ? 0 : active + 1; + let newActive = (active === suggestCount - 1) ? 0 : active + 1; changeActiveTo(newActive, suggestionElems); } // Up arrow else if (event.keyCode === 38) { - let newActive = (active === 0) ? suggestCount-1 : active - 1; + let newActive = (active === 0) ? suggestCount - 1 : active - 1; changeActiveTo(newActive, suggestionElems); } // Enter or tab key @@ -482,6 +482,7 @@ module.exports = function (ngApp, events) { // Display suggestions on a field let prevSuggestions = []; + function displaySuggestions($input, suggestions) { // Hide if no suggestions @@ -518,7 +519,8 @@ module.exports = function (ngApp, events) { if (i === 0) { suggestion.className = 'active' active = 0; - }; + } + ; $suggestionBox[0].appendChild(suggestion); } @@ -537,17 +539,17 @@ module.exports = function (ngApp, events) { // Get suggestions & cache function getSuggestions(input, url) { let hasQuery = url.indexOf('?') !== -1; - let searchUrl = url + (hasQuery?'&':'?') + 'search=' + encodeURIComponent(input); + let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input); // Get from local cache if exists - if (localCache[searchUrl]) { + if (typeof localCache[searchUrl] !== 'undefined') { return new Promise((resolve, reject) => { - resolve(localCache[input]); + resolve(localCache[searchUrl]); }); } - return $http.get(searchUrl).then((response) => { - localCache[input] = response.data; + return $http.get(searchUrl).then(response => { + localCache[searchUrl] = response.data; return response.data; }); } From 1bec3eaa1e314c6382c034602f4d6027e02908af Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 4 Jun 2016 16:32:57 +0100 Subject: [PATCH 10/25] Added checks to use MyISAM if MySQL 5.5 is found --- .../migrations/2015_07_12_114933_create_books_table.php | 9 +++++++-- .../migrations/2015_07_12_190027_create_pages_table.php | 9 +++++++-- .../2015_07_27_172342_create_chapters_table.php | 8 ++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/database/migrations/2015_07_12_114933_create_books_table.php b/database/migrations/2015_07_12_114933_create_books_table.php index 121607f6a..4220809d5 100644 --- a/database/migrations/2015_07_12_114933_create_books_table.php +++ b/database/migrations/2015_07_12_114933_create_books_table.php @@ -12,8 +12,13 @@ class CreateBooksTable extends Migration */ public function up() { - Schema::create('books', function (Blueprint $table) { - $table->engine = 'MyISAM'; + $pdo = \DB::connection()->getPdo(); + $mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); + $requiresISAM = strpos($mysqlVersion, '5.5') === 0; + + Schema::create('books', function (Blueprint $table) use ($requiresISAM) { + if($requiresISAM) $table->engine = 'MyISAM'; + $table->increments('id'); $table->string('name'); $table->string('slug')->indexed(); diff --git a/database/migrations/2015_07_12_190027_create_pages_table.php b/database/migrations/2015_07_12_190027_create_pages_table.php index 5e6e7e812..0a29d1087 100644 --- a/database/migrations/2015_07_12_190027_create_pages_table.php +++ b/database/migrations/2015_07_12_190027_create_pages_table.php @@ -12,8 +12,13 @@ class CreatePagesTable extends Migration */ public function up() { - Schema::create('pages', function (Blueprint $table) { - $table->engine = 'MyISAM'; + $pdo = \DB::connection()->getPdo(); + $mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); + $requiresISAM = strpos($mysqlVersion, '5.5') === 0; + + Schema::create('pages', function (Blueprint $table) use ($requiresISAM) { + if($requiresISAM) $table->engine = 'MyISAM'; + $table->increments('id'); $table->integer('book_id'); $table->integer('chapter_id'); diff --git a/database/migrations/2015_07_27_172342_create_chapters_table.php b/database/migrations/2015_07_27_172342_create_chapters_table.php index 74594121f..3ec414480 100644 --- a/database/migrations/2015_07_27_172342_create_chapters_table.php +++ b/database/migrations/2015_07_27_172342_create_chapters_table.php @@ -12,8 +12,12 @@ class CreateChaptersTable extends Migration */ public function up() { - Schema::create('chapters', function (Blueprint $table) { - $table->engine = 'MyISAM'; + $pdo = \DB::connection()->getPdo(); + $mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); + $requiresISAM = strpos($mysqlVersion, '5.5') === 0; + + Schema::create('chapters', function (Blueprint $table) use ($requiresISAM) { + if($requiresISAM) $table->engine = 'MyISAM'; $table->increments('id'); $table->integer('book_id'); $table->string('slug')->indexed(); From 61596a8e214ae2e1ea432e65c531bcb1484da4a9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 10 Jun 2016 19:45:53 +0100 Subject: [PATCH 11/25] Prevent page redirect on draft saving Only an issue when using the non 'mysqlnd' extension. Fixes #120 --- app/Http/Controllers/PageController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index da9273743..230068884 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -92,7 +92,7 @@ class PageController extends Controller $draftPage = $this->pageRepo->getById($pageId, true); - $chapterId = $draftPage->chapter_id; + $chapterId = intval($draftPage->chapter_id); $parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book; $this->checkOwnablePermission('page-create', $parent); From 97d011ac8ea94ddb3dfc6c11d4dee78d4bcd3f5c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 11 Jun 2016 21:04:18 +0100 Subject: [PATCH 12/25] Started work on page move view and entity selector --- app/Http/Controllers/PageController.php | 18 ++++++++++ app/Http/routes.php | 1 + resources/assets/js/directives.js | 38 +++++++++++++++++++- resources/assets/sass/_text.scss | 6 ++-- resources/views/books/list-item.blade.php | 2 +- resources/views/chapters/list-item.blade.php | 2 +- resources/views/pages/list-item.blade.php | 6 ++-- resources/views/pages/move.blade.php | 35 ++++++++++++++++++ resources/views/pages/show.blade.php | 1 + 9 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 resources/views/pages/move.blade.php diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 230068884..52de19b5b 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -450,6 +450,24 @@ class PageController extends Controller ]); } + /** + * Show the view to choose a new parent to move a page into. + * @param $bookSlug + * @param $pageSlug + * @return mixed + * @throws NotFoundException + */ + public function showMove($bookSlug, $pageSlug) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $this->checkOwnablePermission('page-update', $page); + return view('pages/move', [ + 'book' => $book, + 'page' => $page + ]); + } + /** * Set the permissions for this page. * @param $bookSlug diff --git a/app/Http/routes.php b/app/Http/routes.php index 9f226efd7..90bcd593f 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -34,6 +34,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml'); Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText'); Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); + Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove'); Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft'); Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict'); diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 43d55f092..54df2d2bf 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -149,7 +149,10 @@ module.exports = function (ngApp, events) { }; }]); - + /** + * Dropdown + * Provides some simple logic to create small dropdown menus + */ ngApp.directive('dropdown', [function () { return { restrict: 'A', @@ -166,6 +169,10 @@ module.exports = function (ngApp, events) { }; }]); + /** + * TinyMCE + * An angular wrapper around the tinyMCE editor. + */ ngApp.directive('tinymce', ['$timeout', function ($timeout) { return { restrict: 'A', @@ -231,6 +238,10 @@ module.exports = function (ngApp, events) { } }]); + /** + * Markdown input + * Handles the logic for just the editor input field. + */ ngApp.directive('markdownInput', ['$timeout', function ($timeout) { return { restrict: 'A', @@ -263,6 +274,10 @@ module.exports = function (ngApp, events) { } }]); + /** + * Markdown Editor + * Handles all functionality of the markdown editor. + */ ngApp.directive('markdownEditor', ['$timeout', function ($timeout) { return { restrict: 'A', @@ -342,6 +357,11 @@ module.exports = function (ngApp, events) { } }]); + /** + * Page Editor Toolbox + * Controls all functionality for the sliding toolbox + * on the page edit view. + */ ngApp.directive('toolbox', [function () { return { restrict: 'A', @@ -378,6 +398,11 @@ module.exports = function (ngApp, events) { } }]); + /** + * Tag Autosuggestions + * Listens to child inputs and provides autosuggestions depending on field type + * and input. Suggestions provided by server. + */ ngApp.directive('tagAutosuggestions', ['$http', function ($http) { return { restrict: 'A', @@ -557,6 +582,17 @@ module.exports = function (ngApp, events) { } } }]); + + + ngApp.directive('entitySelector', ['$http', function ($http) { + return { + restrict: 'A', + link: function (scope, element, attrs) { + scope.loading = true; + + } + }; + }]); }; diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index 0095b91cb..493933135 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -3,7 +3,7 @@ */ h1 { - font-size: 3.625em; + font-size: 3.425em; line-height: 1.22222222em; margin-top: 0.48888889em; margin-bottom: 0.48888889em; @@ -33,10 +33,10 @@ h1, h2, h3, h4 { display: block; color: #555; .subheader { - display: block; + //display: block; font-size: 0.5em; line-height: 1em; - color: lighten($text-dark, 16%); + color: lighten($text-dark, 32%); } } diff --git a/resources/views/books/list-item.blade.php b/resources/views/books/list-item.blade.php index 5807bf461..d3e0ef56b 100644 --- a/resources/views/books/list-item.blade.php +++ b/resources/views/books/list-item.blade.php @@ -1,4 +1,4 @@ -
+

{{$book->name}}

@if(isset($book->searchSnippet))

{!! $book->searchSnippet !!}

diff --git a/resources/views/chapters/list-item.blade.php b/resources/views/chapters/list-item.blade.php index d5ac46338..71225e987 100644 --- a/resources/views/chapters/list-item.blade.php +++ b/resources/views/chapters/list-item.blade.php @@ -1,4 +1,4 @@ -
+

{{ $chapter->name }} diff --git a/resources/views/pages/list-item.blade.php b/resources/views/pages/list-item.blade.php index 87a7eabe5..100c1de48 100644 --- a/resources/views/pages/list-item.blade.php +++ b/resources/views/pages/list-item.blade.php @@ -1,4 +1,4 @@ -
+

{{ $page->name }}

@@ -11,11 +11,11 @@ @if(isset($style) && $style === 'detailed')
-
+
Created {{$page->created_at->diffForHumans()}} @if($page->createdBy)by {{$page->createdBy->name}}@endif
Last updated {{ $page->updated_at->diffForHumans() }} @if($page->updatedBy)by {{$page->updatedBy->name}} @endif
-
+
{{ $page->book->getShortName(30) }}
@if($page->chapter) diff --git a/resources/views/pages/move.blade.php b/resources/views/pages/move.blade.php new file mode 100644 index 000000000..2ccbdb438 --- /dev/null +++ b/resources/views/pages/move.blade.php @@ -0,0 +1,35 @@ +@extends('base') + +@section('content') + +
+
+
+ +
+
+
+ +
+

Move Page {{$page->name}}

+ +
+ +
@include('partials/loading-icon')
+
+
+ +@stop diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 4dbab0e29..8b9b63492 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -30,6 +30,7 @@ @if(userCan('page-update', $page)) Revisions Edit + Move @endif @if(userCan('restrictions-manage', $page)) Permissions From 7f99903fdbd3eb814f72319130bfc6ef130d0fe5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 12 Jun 2016 12:14:14 +0100 Subject: [PATCH 13/25] Finished off page move functionality --- app/Http/Controllers/PageController.php | 39 +++++++++++- app/Http/Controllers/SearchController.php | 62 ++++++++++++++----- app/Http/routes.php | 3 + app/Repos/PageRepo.php | 17 +++++ app/Services/ViewService.php | 8 ++- resources/assets/js/directives.js | 54 +++++++++++++++- resources/assets/sass/_forms.scss | 3 + resources/assets/sass/styles.scss | 56 +++++++++++++++++ resources/lang/en/activities.php | 1 + resources/views/books/list-item.blade.php | 2 +- resources/views/chapters/list-item.blade.php | 2 +- resources/views/pages/list-item.blade.php | 2 +- resources/views/pages/move.blade.php | 20 ++++-- resources/views/pages/show.blade.php | 24 ++++--- .../views/partials/custom-styles.blade.php | 2 +- 15 files changed, 260 insertions(+), 35 deletions(-) diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 52de19b5b..373e49de7 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -221,8 +221,8 @@ class PageController extends Controller $updateTime = $draft->updated_at->timestamp; $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset; return response()->json([ - 'status' => 'success', - 'message' => 'Draft saved at ', + 'status' => 'success', + 'message' => 'Draft saved at ', 'timestamp' => $utcUpdateTimestamp ]); } @@ -468,6 +468,41 @@ class PageController extends Controller ]); } + public function move($bookSlug, $pageSlug, Request $request) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $this->checkOwnablePermission('page-update', $page); + + $entitySelection = $request->get('entity_selection', null); + if ($entitySelection === null || $entitySelection === '') { + return redirect($page->getUrl()); + } + + $stringExploded = explode(':', $entitySelection); + $entityType = $stringExploded[0]; + $entityId = intval($stringExploded[1]); + + $parent = false; + + if ($entityType == 'chapter') { + $parent = $this->chapterRepo->getById($entityId); + } else if ($entityType == 'book') { + $parent = $this->bookRepo->getById($entityId); + } + + if ($parent === false || $parent === null) { + session()->flash('The selected Book or Chapter was not found'); + return redirect()->back(); + } + + $this->pageRepo->changePageParent($page, $parent); + Activity::add($page, 'page_move', $page->book->id); + session()->flash('success', sprintf('Page moved to "%s"', $parent->name)); + + return redirect($page->getUrl()); + } + /** * Set the permissions for this page. * @param $bookSlug diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index e198dc767..7c7d7b254 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -2,10 +2,10 @@ namespace BookStack\Http\Controllers; +use BookStack\Services\ViewService; use Illuminate\Http\Request; use BookStack\Http\Requests; -use BookStack\Http\Controllers\Controller; use BookStack\Repos\BookRepo; use BookStack\Repos\ChapterRepo; use BookStack\Repos\PageRepo; @@ -15,18 +15,21 @@ class SearchController extends Controller protected $pageRepo; protected $bookRepo; protected $chapterRepo; + protected $viewService; /** * SearchController constructor. - * @param $pageRepo - * @param $bookRepo - * @param $chapterRepo + * @param PageRepo $pageRepo + * @param BookRepo $bookRepo + * @param ChapterRepo $chapterRepo + * @param ViewService $viewService */ - public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo) + public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ViewService $viewService) { $this->pageRepo = $pageRepo; $this->bookRepo = $bookRepo; $this->chapterRepo = $chapterRepo; + $this->viewService = $viewService; parent::__construct(); } @@ -48,9 +51,9 @@ class SearchController extends Controller $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends); $this->setPageTitle('Search For ' . $searchTerm); return view('search/all', [ - 'pages' => $pages, - 'books' => $books, - 'chapters' => $chapters, + 'pages' => $pages, + 'books' => $books, + 'chapters' => $chapters, 'searchTerm' => $searchTerm ]); } @@ -69,8 +72,8 @@ class SearchController extends Controller $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends); $this->setPageTitle('Page Search For ' . $searchTerm); return view('search/entity-search-list', [ - 'entities' => $pages, - 'title' => 'Page Search Results', + 'entities' => $pages, + 'title' => 'Page Search Results', 'searchTerm' => $searchTerm ]); } @@ -89,8 +92,8 @@ class SearchController extends Controller $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends); $this->setPageTitle('Chapter Search For ' . $searchTerm); return view('search/entity-search-list', [ - 'entities' => $chapters, - 'title' => 'Chapter Search Results', + 'entities' => $chapters, + 'title' => 'Chapter Search Results', 'searchTerm' => $searchTerm ]); } @@ -109,8 +112,8 @@ class SearchController extends Controller $books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends); $this->setPageTitle('Book Search For ' . $searchTerm); return view('search/entity-search-list', [ - 'entities' => $books, - 'title' => 'Book Search Results', + 'entities' => $books, + 'title' => 'Book Search Results', 'searchTerm' => $searchTerm ]); } @@ -134,4 +137,35 @@ class SearchController extends Controller return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); } + + /** + * Search for a list of entities and return a partial HTML response of matching entities. + * Returns the most popular entities if no search is provided. + * @param Request $request + * @return mixed + */ + public function searchEntitiesAjax(Request $request) + { + $entities = collect(); + $entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']); + $searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false; + + // Search for entities otherwise show most popular + if ($searchTerm !== false) { + if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items()); + if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items()); + if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items()); + $entities = $entities->sortByDesc('title_relevance'); + } else { + $entityNames = $entityTypes->map(function ($type) { + return 'BookStack\\' . ucfirst($type); + })->toArray(); + $entities = $this->viewService->getPopular(20, 0, $entityNames); + } + + return view('partials/entity-list', ['entities' => $entities]); + } + } + + diff --git a/app/Http/routes.php b/app/Http/routes.php index 90bcd593f..d7c090953 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -35,6 +35,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText'); Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove'); + Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move'); Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft'); Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict'); @@ -94,6 +95,8 @@ Route::group(['middleware' => 'auth'], function () { Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity'); }); + Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax'); + // Links Route::get('/link/{id}', 'PageController@redirectFromLink'); diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 504c3fa3b..de050e1c7 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -3,6 +3,7 @@ use Activity; use BookStack\Book; use BookStack\Chapter; +use BookStack\Entity; use BookStack\Exceptions\NotFoundException; use Carbon\Carbon; use DOMDocument; @@ -572,6 +573,22 @@ class PageRepo extends EntityRepo return $page; } + + /** + * Change the page's parent to the given entity. + * @param Page $page + * @param Entity $parent + */ + public function changePageParent(Page $page, Entity $parent) + { + $book = $parent->isA('book') ? $parent : $parent->book; + $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0; + $page->save(); + $page = $this->changeBook($book->id, $page); + $page->load('book'); + $this->permissionService->buildJointPermissionsForEntity($book); + } + /** * Gets a suitable slug for the resource * @param $name diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php index 849a164cf..aac9831f7 100644 --- a/app/Services/ViewService.php +++ b/app/Services/ViewService.php @@ -50,7 +50,7 @@ class ViewService * Get the entities with the most views. * @param int $count * @param int $page - * @param bool|false $filterModel + * @param bool|false|array $filterModel */ public function getPopular($count = 10, $page = 0, $filterModel = false) { @@ -60,7 +60,11 @@ class ViewService ->groupBy('viewable_id', 'viewable_type') ->orderBy('view_count', 'desc'); - if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel)); + if ($filterModel && is_array($filterModel)) { + $query->whereIn('viewable_type', $filterModel); + } else if ($filterModel) { + $query->where('viewable_type', '=', get_class($filterModel)); + }; return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable'); } diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 54df2d2bf..f1fefd241 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -584,12 +584,62 @@ module.exports = function (ngApp, events) { }]); - ngApp.directive('entitySelector', ['$http', function ($http) { + ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) { return { restrict: 'A', + scope: true, link: function (scope, element, attrs) { scope.loading = true; - + scope.entityResults = false; + scope.search = ''; + + // Add input for forms + const input = element.find('[entity-selector-input]').first(); + + // Listen to entity item clicks + element.on('click', '.entity-list a', function(event) { + event.preventDefault(); + event.stopPropagation(); + let item = $(this).closest('[data-entity-type]'); + itemSelect(item); + }); + element.on('click', '[data-entity-type]', function(event) { + itemSelect($(this)); + }); + + // Select entity action + function itemSelect(item) { + let entityType = item.attr('data-entity-type'); + let entityId = item.attr('data-entity-id'); + let isSelected = !item.hasClass('selected'); + element.find('.selected').removeClass('selected').removeClass('primary-background'); + if (isSelected) item.addClass('selected').addClass('primary-background'); + let newVal = isSelected ? `${entityType}:${entityId}` : ''; + input.val(newVal); + } + + // Get search url with correct types + function getSearchUrl() { + let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter'); + return `/ajax/search/entities?types=${types}`; + } + + // Get initial contents + $http.get(getSearchUrl()).then(resp => { + scope.entityResults = $sce.trustAsHtml(resp.data); + scope.loading = false; + }); + + // Search when typing + scope.searchEntities = function() { + scope.loading = true; + input.val(''); + let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search); + $http.get(url).then(resp => { + scope.entityResults = $sce.trustAsHtml(resp.data); + scope.loading = false; + }); + }; } }; }]); diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index da015ec7c..4e643dcda 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -20,6 +20,9 @@ &.disabled, &[disabled] { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==); } + &:focus { + outline: 0; + } } #html-editor { diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 0a7da179b..770d5eeb4 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -207,3 +207,59 @@ $btt-size: 40px; color: #EEE; } } + +.entity-selector { + border: 1px solid #DDD; + border-radius: 3px; + overflow: hidden; + font-size: 0.8em; + input[type="text"] { + width: 100%; + display: block; + border-radius: 0; + border: 0; + border-bottom: 1px solid #DDD; + font-size: 16px; + padding: $-s $-m; + } + .entity-list { + overflow-y: scroll; + height: 400px; + background-color: #EEEEEE; + } + .loading { + height: 400px; + padding-top: $-l; + } + .entity-list > p { + text-align: center; + padding-top: $-l; + font-size: 1.333em; + } + .entity-list > div { + padding-left: $-m; + padding-right: $-m; + background-color: #FFF; + transition: all ease-in-out 120ms; + cursor: pointer; + } +} + +.entity-list-item.selected { + h3, i, p ,a { + color: #EEE; + } +} + + + + + + + + + + + + + diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php index 8356d8302..c59513aa9 100644 --- a/resources/lang/en/activities.php +++ b/resources/lang/en/activities.php @@ -16,6 +16,7 @@ return [ 'page_delete_notification' => 'Page Successfully Deleted', 'page_restore' => 'restored page', 'page_restore_notification' => 'Page Successfully Restored', + 'page_move' => 'moved page', // Chapters 'chapter_create' => 'created chapter', diff --git a/resources/views/books/list-item.blade.php b/resources/views/books/list-item.blade.php index d3e0ef56b..945eb9015 100644 --- a/resources/views/books/list-item.blade.php +++ b/resources/views/books/list-item.blade.php @@ -1,4 +1,4 @@ -
+

{{$book->name}}

@if(isset($book->searchSnippet))

{!! $book->searchSnippet !!}

diff --git a/resources/views/chapters/list-item.blade.php b/resources/views/chapters/list-item.blade.php index 71225e987..f66c3781f 100644 --- a/resources/views/chapters/list-item.blade.php +++ b/resources/views/chapters/list-item.blade.php @@ -1,4 +1,4 @@ -
+

{{ $chapter->name }} diff --git a/resources/views/pages/list-item.blade.php b/resources/views/pages/list-item.blade.php index 100c1de48..a95870db0 100644 --- a/resources/views/pages/list-item.blade.php +++ b/resources/views/pages/list-item.blade.php @@ -1,4 +1,4 @@ -
+

{{ $page->name }}

diff --git a/resources/views/pages/move.blade.php b/resources/views/pages/move.blade.php index 2ccbdb438..2c9437e77 100644 --- a/resources/views/pages/move.blade.php +++ b/resources/views/pages/move.blade.php @@ -26,10 +26,22 @@

Move Page {{$page->name}}

-
- -
@include('partials/loading-icon')
-
+
+ {!! csrf_field() !!} + + +
+
+ + +
@include('partials/loading-icon')
+
+
+
+ + Cancel + +
@stop diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 8b9b63492..f12ba58c6 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -28,16 +28,26 @@ @if(userCan('page-update', $page)) - Revisions Edit - Move @endif - @if(userCan('restrictions-manage', $page)) - Permissions - @endif - @if(userCan('page-delete', $page)) - Delete + @if(userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page)) + @endif +

diff --git a/resources/views/partials/custom-styles.blade.php b/resources/views/partials/custom-styles.blade.php index 4d3d0a6ef..e6f8f7fc2 100644 --- a/resources/views/partials/custom-styles.blade.php +++ b/resources/views/partials/custom-styles.blade.php @@ -1,7 +1,7 @@ @if(Setting::get('app-color')) -@endif \ No newline at end of file + \ No newline at end of file diff --git a/resources/views/partials/notifications.blade.php b/resources/views/partials/notifications.blade.php index 183934c66..c079080db 100644 --- a/resources/views/partials/notifications.blade.php +++ b/resources/views/partials/notifications.blade.php @@ -1,12 +1,12 @@ -