From 582158f70e6c63980cce17d408a0cc435a0d985f Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Fri, 30 Mar 2018 14:09:51 +0100 Subject: [PATCH] Added tags to chapters and books Closes #121 --- app/Http/Controllers/ChapterController.php | 9 ++-- app/Repos/EntityRepo.php | 26 ++++++---- resources/assets/js/vues/tag-manager.js | 8 ++-- resources/assets/sass/_blocks.scss | 3 ++ resources/assets/sass/_components.scss | 4 ++ resources/assets/sass/_forms.scss | 3 ++ resources/lang/en/entities.php | 4 +- resources/views/books/form.blade.php | 41 +++++++++------- resources/views/books/show.blade.php | 23 ++++++--- resources/views/chapters/form.blade.php | 9 ++++ resources/views/chapters/show.blade.php | 9 ++++ resources/views/components/tag-list.blade.php | 10 ++++ .../views/components/tag-manager.blade.php | 23 +++++++++ resources/views/pages/form-toolbox.blade.php | 25 ++-------- resources/views/pages/show.blade.php | 11 +---- tests/Entity/TagTest.php | 47 +++++++++++++++---- 16 files changed, 173 insertions(+), 82 deletions(-) create mode 100644 resources/views/components/tag-list.blade.php create mode 100644 resources/views/components/tag-manager.blade.php diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index a4e0b6409..b737afc6d 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -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()); } diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index ece9aa305..e94d34369 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -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; diff --git a/resources/assets/js/vues/tag-manager.js b/resources/assets/js/vues/tag-manager.js index 97c00487e..177af681f 100644 --- a/resources/assets/js/vues/tag-manager.js +++ b/resources/assets/js/vues/tag-manager.js @@ -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++) { diff --git a/resources/assets/sass/_blocks.scss b/resources/assets/sass/_blocks.scss index 4cf2397bc..f876ff281 100644 --- a/resources/assets/sass/_blocks.scss +++ b/resources/assets/sass/_blocks.scss @@ -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; } } diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index 84eebc89b..31e006e27 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -604,3 +604,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { color: #999; } } + +#tag-manager .drag-card { + max-width: 500px; +} \ No newline at end of file diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 11adc7951..3ab2de522 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -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 { diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 8a47ae011..c25dbb623 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -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', diff --git a/resources/views/books/form.blade.php b/resources/views/books/form.blade.php index 0620ae976..880149777 100644 --- a/resources/views/books/form.blade.php +++ b/resources/views/books/form.blade.php @@ -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"> diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index d3a51cb3a..9f021b2b0 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -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') diff --git a/resources/views/chapters/form.blade.php b/resources/views/chapters/form.blade.php index 19cf65a61..fde460844 100644 --- a/resources/views/chapters/form.blade.php +++ b/resources/views/chapters/form.blade.php @@ -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> diff --git a/resources/views/chapters/show.blade.php b/resources/views/chapters/show.blade.php index 62bff243b..ea9820022 100644 --- a/resources/views/chapters/show.blade.php +++ b/resources/views/chapters/show.blade.php @@ -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 diff --git a/resources/views/components/tag-list.blade.php b/resources/views/components/tag-list.blade.php new file mode 100644 index 000000000..9f4273c5a --- /dev/null +++ b/resources/views/components/tag-list.blade.php @@ -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> \ No newline at end of file diff --git a/resources/views/components/tag-manager.blade.php b/resources/views/components/tag-manager.blade.php new file mode 100644 index 000000000..801919a14 --- /dev/null +++ b/resources/views/components/tag-manager.blade.php @@ -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> \ No newline at end of file diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index dd847f297..f6ee2510d 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -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> diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index f11da0f4f..a6c4f329d 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -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 diff --git a/tests/Entity/TagTest.php b/tests/Entity/TagTest.php index 1ef7b7bde..7e1166388 100644 --- a/tests/Entity/TagTest.php +++ b/tests/Entity/TagTest.php @@ -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']);