- composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
- phpenv config-rm xdebug.ini
- composer self-update
++ - composer dump-autoload --no-interaction
- composer install --prefer-dist --no-interaction
- npm install
- ./node_modules/.bin/gulp
$draftPage->draft = false;
$draftPage->save();
+ $this->saveRevision($draftPage, 'Initial Publish');
+
return $draftPage;
}
*/
public function updatePage(Page $page, $book_id, $input)
{
- // Save a revision before updating
- if ($page->html !== $input['html'] || $page->name !== $input['name']) {
- $this->saveRevision($page);
- }
+ // Hold the old details to compare later
+ $oldHtml = $page->html;
+ $oldName = $page->name;
// Prevent slug being updated if no name change
if ($page->name !== $input['name']) {
// Remove all update drafts for this user & page.
$this->userUpdateDraftsQuery($page, $userId)->delete();
+ // Save a revision after updating
+ if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
+ $this->saveRevision($page, $input['summary']);
+ }
+
return $page;
}
/**
* Saves a page revision into the system.
* @param Page $page
+ * @param null|string $summary
* @return $this
*/
- public function saveRevision(Page $page)
+ public function saveRevision(Page $page, $summary = null)
{
$revision = $this->pageRevision->fill($page->toArray());
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
$revision->created_by = auth()->user()->id;
$revision->created_at = $page->updated_at;
$revision->type = 'version';
+ $revision->summary = $summary;
$revision->save();
// Clear old revisions
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
/**
* Gets a suitable slug for the resource
- * @param $name
- * @param $bookId
+ * @param string $name
+ * @param int $bookId
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $bookId, $currentId = false)
{
$slug = Str::slug($name);
+ if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
try {
$storage->put($fullPath, $imageData);
+ $storage->setVisibility($fullPath, 'public');
} catch (Exception $e) {
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
}
$thumbData = (string)$thumb->encode();
$storage->put($thumbFilePath, $thumbData);
+ $storage->setVisibility($thumbFilePath, 'public');
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
return $this->getPublicUrl($thumbFilePath);
$storageUrl = config('filesystems.url');
// Get the standard public s3 url if s3 is set as storage type
+ // Uses the nice, short URL if bucket name has no periods in otherwise the longer
+ // region-based url will be used to prevent http issues.
if ($storageUrl == false && config('filesystems.default') === 's3') {
$storageDetails = config('filesystems.disks.s3');
- $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
+ if (strpos($storageDetails['bucket'], '.') === false) {
+ $storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
+ } else {
+ $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
+ }
}
$this->storageUrl = $storageUrl;
}
- return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath;
+ return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
}
-}
+}
"use strict";
- var DropZone = require('dropzone');
- var markdown = require('marked');
+ const DropZone = require('dropzone');
+ const markdown = require('marked');
- var toggleSwitchTemplate = require('./components/toggle-switch.html');
- var imagePickerTemplate = require('./components/image-picker.html');
- var dropZoneTemplate = require('./components/drop-zone.html');
+ const toggleSwitchTemplate = require('./components/toggle-switch.html');
+ const imagePickerTemplate = require('./components/image-picker.html');
+ const dropZoneTemplate = require('./components/drop-zone.html');
module.exports = function (ngApp, events) {
imageClass: '@'
},
link: function (scope, element, attrs) {
- var usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false';
+ let usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false';
scope.image = scope.currentImage;
scope.value = scope.currentImage || '';
if (usingIds) scope.value = scope.currentId;
};
scope.updateImageFromModel = function (model) {
- var isResized = scope.resizeWidth && scope.resizeHeight;
+ let isResized = scope.resizeWidth && scope.resizeHeight;
if (!isResized) {
scope.$apply(() => {
return;
}
- var cropped = scope.resizeCrop ? 'true' : 'false';
- var requestString = '/images/thumb/' + model.id + '/' + scope.resizeWidth + '/' + scope.resizeHeight + '/' + cropped;
+ let cropped = scope.resizeCrop ? 'true' : 'false';
+ let requestString = '/images/thumb/' + model.id + '/' + scope.resizeWidth + '/' + scope.resizeHeight + '/' + cropped;
+ requestString = window.baseUrl(requestString);
$http.get(requestString).then((response) => {
setImage(model, response.data.url);
});
return {
restrict: 'A',
link: function (scope, element, attrs) {
- var menu = element.find('ul');
+ const menu = element.find('ul');
element.find('[dropdown-toggle]').on('click', function () {
menu.show().addClass('anim menuIn');
+ let inputs = menu.find('input');
+ let hasInput = inputs.length > 0;
+ if (hasInput) {
+ inputs.first().focus();
+ element.on('keypress', 'input', event => {
+ if (event.keyCode === 13) {
+ event.preventDefault();
+ menu.hide();
+ menu.removeClass('anim menuIn');
+ return false;
+ }
+ });
+ }
element.mouseleave(function () {
menu.hide();
menu.removeClass('anim menuIn');
link: function (scope, element, attrs) {
// Set initial model content
- var content = element.val();
+ element = element.find('textarea').first();
+ let content = element.val();
scope.mdModel = content;
scope.mdChange(markdown(content));
- element.on('change input', (e) => {
+ console.log('test');
+
+ element.on('change input', (event) => {
content = element.val();
$timeout(() => {
scope.mdModel = content;
link: function (scope, element, attrs) {
// Elements
- const input = element.find('textarea[markdown-input]');
+ const input = element.find('[markdown-input] textarea').first();
const display = element.find('.markdown-display').first();
const insertImage = element.find('button[data-action="insertImage"]');
// Insert image shortcut
if (event.which === 73 && event.ctrlKey && event.shiftKey) {
event.preventDefault();
- var caretPos = input[0].selectionStart;
- var currentContent = input.val();
- var mdImageText = "";
+ let caretPos = input[0].selectionStart;
+ let currentContent = input.val();
+ const mdImageText = "";
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
input.focus();
input[0].selectionStart = caretPos + (";
// Insert image from image manager
insertImage.click(event => {
window.ImageManager.showExternal(image => {
- var caretPos = currentCaretPos;
- var currentContent = input.val();
- var mdImageText = "";
+ let caretPos = currentCaretPos;
+ let currentContent = input.val();
+ let mdImageText = "";
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
input.change();
});
// 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}`;
+ return window.baseUrl(`/ajax/search/entities?types=${types}`);
}
// Get initial contents
var ngSanitize = require('angular-sanitize');
require('angular-ui-sortable');
+ // Url retrieval function
+ window.baseUrl = function(path) {
+ let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
+ if (basePath[basePath.length-1] === '/') basePath = basePath.slice(0, basePath.length-1);
+ if (path[0] === '/') path = path.slice(1);
+ return basePath + '/' + path;
+ };
+
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Global Event System
-var Events = {
- listeners: {},
- emit: function (eventName, eventData) {
+class Events {
+ constructor() {
+ this.listeners = {};
+ }
+
+ emit(eventName, eventData) {
if (typeof this.listeners[eventName] === 'undefined') return this;
var eventsToStart = this.listeners[eventName];
for (let i = 0; i < eventsToStart.length; i++) {
event(eventData);
}
return this;
- },
- listen: function (eventName, callback) {
+ }
+
+ listen(eventName, callback) {
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
this.listeners[eventName].push(callback);
return this;
}
};
-window.Events = Events;
+window.Events = new Events();
+
var services = require('./services')(ngApp, Events);
var directives = require('./directives')(ngApp, Events);
var controllers = require('./controllers')(ngApp, Events);
// Smooth scrolling
jQuery.fn.smoothScrollTo = function () {
if (this.length === 0) return;
- $('body').animate({
+ let scrollElem = document.documentElement.scrollTop === 0 ? document.body : document.documentElement;
+ $(scrollElem).animate({
scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
}, 800); // Adjust to change animations speed (ms)
return this;
};
// Making contains text expression not worry about casing
-$.expr[":"].contains = $.expr.createPseudo(function (arg) {
+jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
return function (elem) {
return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
};
var scrollTop = document.getElementById('back-to-top');
var scrollTopBreakpoint = 1200;
window.addEventListener('scroll', function() {
- if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) {
+ let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
+ if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
scrollTop.style.display = 'block';
scrollTopShowing = true;
setTimeout(() => {
scrollTop.style.opacity = 0.4;
}, 1);
- } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
+ } else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
scrollTop.style.opacity = 0;
scrollTopShowing = false;
setTimeout(() => {
- xhr.open('POST', '/images/gallery/upload');
+"use strict";
+
+function editorPaste(e) {
+ if (!e.clipboardData) return
+ var items = e.clipboardData.items;
+ if (!items) return;
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].type.indexOf("image") !== -1) {
+
+ var file = items[i].getAsFile();
+ var formData = new FormData();
+ var ext = 'png';
+ var xhr = new XMLHttpRequest();
+
+ if (file.name) {
+ var fileNameMatches = file.name.match(/\.(.+)$/);
+ if (fileNameMatches) {
+ ext = fileNameMatches[1];
+ }
+ }
+
+ var id = "image-" + Math.random().toString(16).slice(2);
+ editor.execCommand('mceInsertContent', false, '<img src="/loading.gif" id="' + id + '">');
+
+ var remoteFilename = "image-" + Date.now() + "." + ext;
+ formData.append('file', file, remoteFilename);
+ formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
+
++ xhr.open('POST', window.baseUrl('/images/gallery/upload'));
+ xhr.onload = function () {
+ if (xhr.status === 200 || xhr.status === 201) {
+ var result = JSON.parse(xhr.responseText);
+ editor.dom.setAttrib(id, 'src', result.url);
+ } else {
+ console.log('An error occured uploading the image');
+ console.log(xhr.responseText);
+ editor.dom.remove(id);
+ }
+ };
+ xhr.send(formData);
+ }
+ }
+}
+
+function registerEditorShortcuts(editor) {
+ // Headers
+ for (let i = 1; i < 5; i++) {
+ editor.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
+ }
+
+ // Other block shortcuts
+ editor.addShortcut('ctrl+q', '', ['FormatBlock', false, 'blockquote']);
+ editor.addShortcut('ctrl+d', '', ['FormatBlock', false, 'p']);
+ editor.addShortcut('ctrl+e', '', ['FormatBlock', false, 'pre']);
+ editor.addShortcut('ctrl+s', '', ['FormatBlock', false, 'code']);
+}
+
var mceOptions = module.exports = {
selector: '#html-editor',
content_css: [
- '/css/styles.css',
- '/libs/material-design-iconic-font/css/material-design-iconic-font.min.css'
+ window.baseUrl('/css/styles.css'),
+ window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
],
body_class: 'page-content',
relative_urls: false,
mceOptions.extraSetups[i](editor);
}
+ registerEditorShortcuts(editor);
+
(function () {
var wrap;
});
// Paste image-uploads
- editor.on('paste', function (e) {
- if (e.clipboardData) {
- var items = e.clipboardData.items;
- if (items) {
- for (var i = 0; i < items.length; i++) {
- if (items[i].type.indexOf("image") !== -1) {
-
- var file = items[i].getAsFile();
- var formData = new FormData();
- var ext = 'png';
- var xhr = new XMLHttpRequest();
-
- if (file.name) {
- var fileNameMatches = file.name.match(/\.(.+)$/);
- if (fileNameMatches) {
- ext = fileNameMatches[1];
- }
- }
-
- var id = "image-" + Math.random().toString(16).slice(2);
- editor.execCommand('mceInsertContent', false, '<img src="/loading.gif" id="' + id + '">');
-
- var remoteFilename = "image-" + Date.now() + "." + ext;
- formData.append('file', file, remoteFilename);
- formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
-
- xhr.open('POST', window.baseUrl('/images/gallery/upload'));
- xhr.onload = function () {
- if (xhr.status === 200 || xhr.status === 201) {
- var result = JSON.parse(xhr.responseText);
- editor.dom.setAttrib(id, 'src', result.url);
- } else {
- console.log('An error occured uploading the image');
- console.log(xhr.responseText);
- editor.dom.remove(id);
- }
- };
- xhr.send(formData);
- }
- }
- }
-
- }
- });
+ editor.on('paste', editorPaste);
}
};
<a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>Save Draft</a>
</li>
<li ng-if="isNewPageDraft">
- <a href="{{$model->getUrl()}}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
+ <a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
</li>
+ <li>
+ <a type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</a>
+ </li>
</ul>
</div>
</div>
<div class="col-sm-4 faded">
<div class="action-buttons" ng-cloak>
+ <div dropdown class="dropdown-container">
+ <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-edit"></i> @{{(changeSummary | limitTo:16) + (changeSummary.length>16?'...':'') || 'Set Changelog'}}</a>
+ <ul class="wide">
+ <li class="padded">
+ <p class="text-muted">Enter a brief description of the changes you've made</p>
+ <input name="summary" id="summary-input" type="text" placeholder="Enter Changelog" ng-model="changeSummary" />
+ </li>
+ </ul>
+ </div>
- <button type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-button text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</button>
- <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
+ <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
</div>
</div>
</div>
<div class="edit-area flex-fill flex">
@if(setting('app-editor') === 'wysiwyg')
- <textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" name="html" rows="5"
- @if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
+ <div tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" class="flex-fill flex">
+ <textarea id="html-editor" name="html" rows="5" ng-non-bindable
+ @if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
+ </div>
+
@if($errors->has('html'))
<div class="text-neg text-small">{{ $errors->first('html') }}</div>
@endif
<button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
</div>
</div>
- <textarea markdown-input md-change="editorChange" id="markdown-editor-input" md-model="editContent" name="markdown" rows="5"
- @if($errors->has('markdown')) class="neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
+
+ <div markdown-input md-change="editorChange" md-model="editContent" class="flex flex-fill">
+ <textarea ng-non-bindable id="markdown-editor-input" name="markdown" rows="5"
+ @if($errors->has('markdown')) class="neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
+ </div>
+
</div>
<div class="markdown-editor-wrap">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
- <a href="{{$page->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
+ <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
@if($page->hasChapter())
<span class="sep">»</span>
<a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
<i class="zmdi zmdi-collection-bookmark"></i>
- {{$page->chapter->getShortName()}}
+ {{ $page->chapter->getShortName() }}
</a>
@endif
<span class="sep">»</span>
- <a href="{{$page->getUrl()}}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
- <a href="{{ $page->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
++ <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
</div>
</div>
</div>
<div class="faded-small toolbar">
<div class="container">
<div class="row">
- <div class="col-md-6 faded">
+ <div class="col-sm-12 faded">
<div class="breadcrumbs">
- <a href="{{$page->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
- <a href="{{ $page->getUrl() }}" class="text-primary text-button"><i class="zmdi zmdi-arrow-left"></i>Back to page</a>
++ <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
+ @if($page->hasChapter())
+ <span class="sep">»</span>
+ <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
+ <i class="zmdi zmdi-collection-bookmark"></i>
- {{$page->chapter->getShortName()}}
++ {{ $page->chapter->getShortName() }}
+ </a>
+ @endif
+ <span class="sep">»</span>
- <a href="{{$page->getUrl()}}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
++ <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
</div>
</div>
- <div class="col-md-6 faded">
- </div>
</div>
</div>
</div>
- <div class="container small" ng-non-bindable>
+
+ <div class="container" ng-non-bindable>
<h1>Page Revisions <span class="subheader">For "{{ $page->name }}"</span></h1>
@if(count($page->revisions) > 0)
<table class="table">
<tr>
- <th width="40%">Name</th>
- <th colspan="2" width="20%">Created By</th>
- <th width="20%">Revision Date</th>
- <th width="20%">Actions</th>
+ <th width="25%">Name</th>
+ <th colspan="2" width="10%">Created By</th>
+ <th width="15%">Revision Date</th>
+ <th width="25%">Changelog</th>
+ <th width="15%">Actions</th>
</tr>
- @foreach($page->revisions as $revision)
+ @foreach($page->revisions as $index => $revision)
<tr>
-- <td>{{$revision->name}}</td>
++ <td>{{ $revision->name }}</td>
<td style="line-height: 0;">
@if($revision->createdBy)
-- <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{$revision->createdBy->name}}">
++ <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
@endif
</td>
-- <td> @if($revision->createdBy) {{$revision->createdBy->name}} @else Deleted User @endif</td>
-- <td><small>{{$revision->created_at->format('jS F, Y H:i:s')}} <br> ({{$revision->created_at->diffForHumans()}})</small></td>
- <td>{{$revision->summary}}</td>
- <td>
- <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
- <span class="text-muted"> | </span>
- <a href="{{ $revision->getUrl('/restore') }}">Restore</a>
- </td>
++ <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td>
++ <td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
++ <td>{{ $revision->summary }}</td>
+ @if ($index !== 0)
+ <td>
- <a href="{{$revision->getUrl()}}" target="_blank">Preview</a>
++ <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
+ <span class="text-muted"> | </span>
- <a href="{{$revision->getUrl()}}/restore">Restore</a>
++ <a href="{{ $revision->getUrl() }}/restore">Restore</a>
+ </td>
+ @else
+ <td><a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a></td>
+ @endif
</tr>
@endforeach
</table>
->visit('/books/create')
->type($book->name, '#name')
->type($book->description, '#description')
- ->press('Save Book')
- ->seePageIs('/books/my-first-book-2');
+ ->press('Save Book');
+
+ $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/';
+ $this->assertRegExp($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n");
$book = \BookStack\Book::where('slug', '=', 'my-first-book')->first();
return $book;
public function test_old_page_slugs_redirect_to_new_pages()
{
- $page = \BookStack\Page::all()->first();
+ $page = \BookStack\Page::first();
$pageUrl = $page->getUrl();
$newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
+ // Need to save twice since revisions are not generated in seeder.
$this->asAdmin()->visit($pageUrl)
+ ->clickInElement('#content', 'Edit')
+ ->type('super test', '#name')
+ ->press('Save Page');
+
+ $page = \BookStack\Page::first();
+ $pageUrl = $page->getUrl();
+
+ // Second Save
+ $this->visit($pageUrl)
->clickInElement('#content', 'Edit')
->type('super test page', '#name')
->press('Save Page')
+ // Check redirect
->seePageIs($newPageUrl)
->visit($pageUrl)
->seePageIs($newPageUrl);