* @param $bookSlug
* @param $chapterSlug
* @return Response
+ * @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, $bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
- if ($chapter->name !== $request->get('name')) {
- $chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
- }
- $chapter->fill($request->all());
- $chapter->updated_by = user()->id;
- $chapter->save();
+
+ $this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
Activity::add($chapter, 'chapter_update', $chapter->book->id);
return redirect($chapter->getUrl());
}
public function createFromInput($type, $input = [], $book = false)
{
$isChapter = strtolower($type) === 'chapter';
- $entity = $this->getEntity($type)->newInstance($input);
- $entity->slug = $this->findSuitableSlug($type, $entity->name, false, $isChapter ? $book->id : false);
- $entity->created_by = user()->id;
- $entity->updated_by = user()->id;
- $isChapter ? $book->chapters()->save($entity) : $entity->save();
- $this->permissionService->buildJointPermissionsForEntity($entity);
- $this->searchService->indexEntity($entity);
- return $entity;
+ $entityModel = $this->getEntity($type)->newInstance($input);
+ $entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
+ $entityModel->created_by = user()->id;
+ $entityModel->updated_by = user()->id;
+ $isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
+
+ if (isset($input['tags'])) {
+ $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
+ }
+
+ $this->permissionService->buildJointPermissionsForEntity($entityModel);
+ $this->searchService->indexEntity($entityModel);
+ return $entityModel;
}
/**
$entityModel->fill($input);
$entityModel->updated_by = user()->id;
$entityModel->save();
+
+ if (isset($input['tags'])) {
+ $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
+ }
+
$this->permissionService->buildJointPermissionsForEntity($entityModel);
$this->searchService->indexEntity($entityModel);
return $entityModel;
const autosuggest = require('./components/autosuggest');
let data = {
- pageId: false,
+ entityId: false,
+ entityType: null,
tags: [],
};
};
function mounted() {
- this.pageId = Number(this.$el.getAttribute('page-id'));
+ this.entityId = Number(this.$el.getAttribute('entity-id'));
+ this.entityType = this.$el.getAttribute('entity-type');
- let url = window.baseUrl(`/ajax/tags/get/page/${this.pageId}`);
+ let url = window.baseUrl(`/ajax/tags/get/${this.entityType}/${this.entityId}`);
this.$http.get(url).then(response => {
let tags = response.data;
for (let i = 0, len = tags.length; i < len; i++) {
text-align: center;
justify-content: center;
width: 28px;
+ flex-grow: 0;
padding-left: $-xs;
padding-right: $-xs;
&:hover {
}
> div .outline input {
margin: $-s 0;
+ width: 100%;
}
> div.padded {
padding: $-s 0 !important;
> div {
padding: 0 $-s;
max-width: 80%;
+ flex: 1;
}
}
color: #999;
}
}
+
+#tag-manager .drag-card {
+ max-width: 500px;
+}
\ No newline at end of file
&.open .collapse-title label:before {
transform: rotate(90deg);
}
+ &+.form-group[collapsible] {
+ margin-top: -($-s + 1);
+ }
}
.inline-input-style {
* Editor sidebar
*/
'page_tags' => 'Page Tags',
+ 'chapter_tags' => 'Chapter Tags',
+ 'book_tags' => 'Book Tags',
'tag' => 'Tag',
- 'tags' => '',
+ 'tags' => 'Tags',
'tag_value' => 'Tag Value (Optional)',
'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
'tags_add' => 'Add another tag',
</div>
<div class="form-group" collapsible id="logo-control">
- <div class="collapse-title text-primary" collapsible-trigger>
- <label for="user-avatar">{{ trans('common.cover_image') }}</label>
- </div>
- <div class="collapse-content" collapsible-content>
- <p class="small">{{ trans('common.cover_image_description') }}</p>
+ <div class="collapse-title text-primary" collapsible-trigger>
+ <label for="user-avatar">{{ trans('common.cover_image') }}</label>
+ </div>
+ <div class="collapse-content" collapsible-content>
+ <p class="small">{{ trans('common.cover_image_description') }}</p>
- @include('components.image-picker', [
- 'resizeHeight' => '512',
- 'resizeWidth' => '512',
- 'showRemove' => false,
- 'defaultImage' => baseUrl('/book_default_cover.png'),
- 'currentImage' => @isset($model) ? $model->getBookCover() : baseUrl('/book_default_cover.png') ,
- 'currentId' => @isset($model) ? $model->image_id : 0,
- 'name' => 'image_id',
- 'imageClass' => 'cover'
- ])
- </div>
+ @include('components.image-picker', [
+ 'resizeHeight' => '512',
+ 'resizeWidth' => '512',
+ 'showRemove' => false,
+ 'defaultImage' => baseUrl('/book_default_cover.png'),
+ 'currentImage' => @isset($model) ? $model->getBookCover() : baseUrl('/book_default_cover.png') ,
+ 'currentId' => @isset($model) ? $model->image_id : 0,
+ 'name' => 'image_id',
+ 'imageClass' => 'cover'
+ ])
+ </div>
+</div>
+
+<div class="form-group" collapsible id="logo-control">
+ <div class="collapse-title text-primary" collapsible-trigger>
+ <label for="user-avatar">{{ trans('entities.book_tags') }}</label>
+ </div>
+ <div class="collapse-content" collapsible-content>
+ @include('components.tag-manager', ['entity' => isset($book)?$book:null, 'entityType' => 'chapter'])
+ </div>
</div>
<div class="form-group text-right">
</div>
@endif
- @if(count($activity) > 0)
- <div class="activity card">
- <h3>@icon('time') {{ trans('entities.recent_activity') }}</h3>
- @include('partials/activity-list', ['activity' => $activity])
- </div>
- @endif
-
<div class="card">
<h3>@icon('info') {{ trans('common.details') }}</h3>
<div class="body">
@include('partials.entity-meta', ['entity' => $book])
</div>
</div>
+
+ @if($book->tags->count() > 0)
+ <div class="card tag-display">
+ <h3>@icon('tag') {{ trans('entities.book_tags') }}</h3>
+ <div class="body">
+ @include('components.tag-list', ['entity' => $book])
+ </div>
+ </div>
+ @endif
+
+ @if(count($activity) > 0)
+ <div class="activity card">
+ <h3>@icon('time') {{ trans('entities.recent_activity') }}</h3>
+ @include('partials/activity-list', ['activity' => $activity])
+ </div>
+ @endif
@stop
@section('container-attrs')
@include('form/textarea', ['name' => 'description'])
</div>
+<div class="form-group" collapsible id="logo-control">
+ <div class="collapse-title text-primary" collapsible-trigger>
+ <label for="user-avatar">{{ trans('entities.chapter_tags') }}</label>
+ </div>
+ <div class="collapse-content" collapsible-content>
+ @include('components.tag-manager', ['entity' => isset($chapter)?$chapter:null, 'entityType' => 'chapter'])
+ </div>
+</div>
+
<div class="form-group text-right">
<a href="{{ isset($chapter) ? $chapter->getUrl() : $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
<button type="submit" class="button pos">{{ trans('entities.chapters_save') }}</button>
</div>
</div>
+ @if($chapter->tags->count() > 0)
+ <div class="card tag-display">
+ <h3>@icon('tag') {{ trans('entities.chapter_tags') }}</h3>
+ <div class="body">
+ @include('components.tag-list', ['entity' => $chapter])
+ </div>
+ </div>
+ @endif
+
@include('partials/book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
@stop
--- /dev/null
+<table>
+ <tbody>
+ @foreach($entity->tags as $tag)
+ <tr class="tag">
+ <td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
+ @if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
+ </tr>
+ @endforeach
+ </tbody>
+</table>
\ No newline at end of file
--- /dev/null
+<div id="tag-manager" entity-id="{{ isset($entity) ? $entity->id : 0 }}" entity-type="{{ $entity ? $entity->getType() : $entityType }}">
+ <div class="tags">
+ <p class="muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p>
+
+
+ <draggable :options="{handle: '.handle'}" :list="tags" element="div">
+ <div v-for="(tag, i) in tags" :key="tag.key" class="card drag-card">
+ <div class="handle" >@icon('grip')</div>
+ <div>
+ <autosuggest url="{{ baseUrl('/ajax/tags/suggest/names') }}" type="name" class="outline" :name="getTagFieldName(i, 'name')"
+ v-model="tag.name" @input="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"/>
+ </div>
+ <div>
+ <autosuggest url="{{ baseUrl('/ajax/tags/suggest/values') }}" type="value" class="outline" :name="getTagFieldName(i, 'value')"
+ v-model="tag.value" @change="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_value') }}"/>
+ </div>
+ <div v-show="tags.length !== 1" class="text-center drag-card-action text-neg" @click="removeTag(tag)">@icon('close')</div>
+ </div>
+ </draggable>
+
+ <button @click="addEmptyTag" type="button" class="text-button">{{ trans('entities.tags_add') }}</button>
+ </div>
+</div>
\ No newline at end of file
@endif
</div>
- <div toolbox-tab-content="tags" id="tag-manager" page-id="{{ $page->id or 0 }}">
+ <div toolbox-tab-content="tags">
<h4>{{ trans('entities.page_tags') }}</h4>
- <div class="padded tags">
- <p class="muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p>
-
-
- <draggable :options="{handle: '.handle'}" :list="tags" element="div">
- <div v-for="(tag, i) in tags" :key="tag.key" class="card drag-card">
- <div class="handle" >@icon('grip')</div>
- <div>
- <autosuggest url="{{ baseUrl('/ajax/tags/suggest/names') }}" type="name" class="outline" :name="getTagFieldName(i, 'name')"
- v-model="tag.name" @input="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"/>
- </div>
- <div>
- <autosuggest url="{{ baseUrl('/ajax/tags/suggest/values') }}" type="value" class="outline" :name="getTagFieldName(i, 'value')"
- v-model="tag.value" @change="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_value') }}"/>
- </div>
- <div v-show="tags.length !== 1" class="text-center drag-card-action text-neg" @click="removeTag(tag)">@icon('close')</div>
- </div>
- </draggable>
-
- <button @click="addEmptyTag" type="button" class="text-button">{{ trans('entities.tags_add') }}</button>
-
+ <div class="padded">
+ @include('components.tag-manager', ['entity' => $page, 'entityType' => 'page'])
</div>
</div>
<div class="card tag-display">
<h3>@icon('tag') {{ trans('entities.page_tags') }}</h3>
<div class="body">
- <table>
- <tbody>
- @foreach($page->tags as $tag)
- <tr class="tag">
- <td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
- @if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
- </tr>
- @endforeach
- </tbody>
- </table>
+ @include('components.tag-list', ['entity' => $page])
</div>
</div>
@endif
<?php namespace Tests;
-use BookStack\Role;
+use BookStack\Book;
+use BookStack\Chapter;
use BookStack\Tag;
use BookStack\Page;
use BookStack\Services\PermissionService;
* @param Tag[]|bool $tags
* @return mixed
*/
- protected function getPageWithTags($tags = false)
+ protected function getEntityWithTags($class, $tags = false)
{
- $page = Page::first();
+ $entity = $class::first();
if (!$tags) {
$tags = factory(Tag::class, $this->defaultTagCount)->make();
}
- $page->tags()->saveMany($tags);
- return $page;
+ $entity->tags()->saveMany($tags);
+ return $entity;
}
public function test_get_page_tags()
{
- $page = $this->getPageWithTags();
+ $page = $this->getEntityWithTags(Page::class);
// Add some other tags to check they don't interfere
factory(Tag::class, $this->defaultTagCount)->create();
$this->assertTrue(count($json) === $this->defaultTagCount, "Returned JSON item count is not as expected");
}
+ public function test_get_chapter_tags()
+ {
+ $chapter = $this->getEntityWithTags(Chapter::class);
+
+ // Add some other tags to check they don't interfere
+ factory(Tag::class, $this->defaultTagCount)->create();
+
+ $this->asAdmin()->get("/ajax/tags/get/chapter/" . $chapter->id)
+ ->shouldReturnJson();
+
+ $json = json_decode($this->response->getContent());
+ $this->assertTrue(count($json) === $this->defaultTagCount, "Returned JSON item count is not as expected");
+ }
+
+ public function test_get_book_tags()
+ {
+ $book = $this->getEntityWithTags(Book::class);
+
+ // Add some other tags to check they don't interfere
+ factory(Tag::class, $this->defaultTagCount)->create();
+
+ $this->asAdmin()->get("/ajax/tags/get/book/" . $book->id)
+ ->shouldReturnJson();
+
+ $json = json_decode($this->response->getContent());
+ $this->assertTrue(count($json) === $this->defaultTagCount, "Returned JSON item count is not as expected");
+ }
+
public function test_tag_name_suggestions()
{
// Create some tags with similar names to test with
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'county']));
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'planet']));
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'plans']));
- $page = $this->getPageWithTags($attrs);
+ $page = $this->getEntityWithTags(Page::class, $attrs);
$this->asAdmin()->get('/ajax/tags/suggest/names?search=dog')->seeJsonEquals([]);
$this->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country', 'county']);
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'county', 'value' => 'dog']));
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'planet', 'value' => 'catapult']));
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'plans', 'value' => 'dodgy']));
- $page = $this->getPageWithTags($attrs);
+ $page = $this->getEntityWithTags(Page::class, $attrs);
$this->asAdmin()->get('/ajax/tags/suggest/values?search=ora')->seeJsonEquals([]);
$this->get('/ajax/tags/suggest/values?search=cat')->seeJsonEquals(['cats', 'cattery', 'catapult']);
$attrs = collect();
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country']));
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color']));
- $page = $this->getPageWithTags($attrs);
+ $page = $this->getEntityWithTags(Page::class, $attrs);
$this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']);
$this->asEditor()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']);