Added tags to chapters and books

Closes #121
This commit is contained in:
Dan Brown 2018-03-30 14:09:51 +01:00
parent a8f18c0102
commit 582158f70e
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
16 changed files with 173 additions and 82 deletions

View File

@ -107,17 +107,14 @@ class ChapterController extends Controller
* @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());
}

View File

@ -492,14 +492,19 @@ class EntityRepo
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;
}
/**
@ -518,6 +523,11 @@ class EntityRepo
$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;

View File

@ -2,7 +2,8 @@ const draggable = require('vuedraggable');
const autosuggest = require('./components/autosuggest');
let data = {
pageId: false,
entityId: false,
entityType: null,
tags: [],
};
@ -48,9 +49,10 @@ let methods = {
};
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++) {

View File

@ -226,6 +226,7 @@
text-align: center;
justify-content: center;
width: 28px;
flex-grow: 0;
padding-left: $-xs;
padding-right: $-xs;
&:hover {
@ -237,6 +238,7 @@
}
> div .outline input {
margin: $-s 0;
width: 100%;
}
> div.padded {
padding: $-s 0 !important;
@ -251,6 +253,7 @@
> div {
padding: 0 $-s;
max-width: 80%;
flex: 1;
}
}

View File

@ -604,3 +604,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
color: #999;
}
}
#tag-manager .drag-card {
max-width: 500px;
}

View File

@ -237,6 +237,9 @@ input:checked + .toggle-switch {
&.open .collapse-title label:before {
transform: rotate(90deg);
}
&+.form-group[collapsible] {
margin-top: -($-s + 1);
}
}
.inline-input-style {

View File

@ -200,8 +200,10 @@ return [
* 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',

View File

@ -11,23 +11,32 @@
</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">

View File

@ -68,19 +68,28 @@
</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')

View File

@ -11,6 +11,15 @@
@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>

View File

@ -91,6 +91,15 @@
</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

View File

@ -0,0 +1,10 @@
<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>

View File

@ -0,0 +1,23 @@
<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>

View File

@ -9,29 +9,10 @@
@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>

View File

@ -81,16 +81,7 @@
<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

View File

@ -1,6 +1,7 @@
<?php namespace Tests;
use BookStack\Role;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Tag;
use BookStack\Page;
use BookStack\Services\PermissionService;
@ -15,21 +16,21 @@ class TagTest extends BrowserKitTest
* @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();
@ -41,6 +42,34 @@ class TagTest extends BrowserKitTest
$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
@ -51,7 +80,7 @@ class TagTest extends BrowserKitTest
$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']);
@ -69,7 +98,7 @@ class TagTest extends BrowserKitTest
$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']);
@ -85,7 +114,7 @@ class TagTest extends BrowserKitTest
$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']);