]> BookStack Code Mirror - bookstack/commitdiff
Added tags to chapters and books
authorDan Brown <redacted>
Fri, 30 Mar 2018 13:09:51 +0000 (14:09 +0100)
committerDan Brown <redacted>
Fri, 30 Mar 2018 13:09:51 +0000 (14:09 +0100)
Closes #121

16 files changed:
app/Http/Controllers/ChapterController.php
app/Repos/EntityRepo.php
resources/assets/js/vues/tag-manager.js
resources/assets/sass/_blocks.scss
resources/assets/sass/_components.scss
resources/assets/sass/_forms.scss
resources/lang/en/entities.php
resources/views/books/form.blade.php
resources/views/books/show.blade.php
resources/views/chapters/form.blade.php
resources/views/chapters/show.blade.php
resources/views/components/tag-list.blade.php [new file with mode: 0644]
resources/views/components/tag-manager.blade.php [new file with mode: 0644]
resources/views/pages/form-toolbox.blade.php
resources/views/pages/show.blade.php
tests/Entity/TagTest.php

index a4e0b64091efafc3fe22f1e034f4acb24d07786b..b737afc6df21e5ff8697dd0b4fc681c2d7d71a23 100644 (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());
     }
index ece9aa3057572fd14f7549181eec0b94b65e7f92..e94d3436991cf425f3ba80ac4c0a8380a012d1ed 100644 (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;
index 97c00487e0abe1f8ea158e0ebd69192404759da6..177af681fad09fb73cd306239133f972e9dbf9ef 100644 (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++) {
index 4cf2397bca8bc0457f76f31f6aefc304d5f3031b..f876ff281a0b298b19100bd12ec31b48991662ab 100644 (file)
     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;
   }
 }
 
index 84eebc89bc91036143be70d53f39dc1e832615c6..31e006e27d7b492f64d3fe41741886b2043da7be 100644 (file)
@@ -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
index 11adc7951917aea665df3272f23578a2186bdbc4..3ab2de522f64bcff88a879d6980fd98d19f8f849 100644 (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 {
index 8a47ae011d6d83872505ff7edd5739ff556683c2..c25dbb623e152d6e76da9b4bb7b80fb312ab1d5d 100644 (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',
index 0620ae9761b6eae9849f111798214d31219b2ce7..880149777c2c2482990ab2e87dd16264da4ee791 100644 (file)
 </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">
index d3a51cb3ac637a9523dd4c3c1f6bcbede1dded63..9f021b2b080eb12656fe7e931e37d56e333aa029 100644 (file)
         </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')
index 19cf65a61f8fe47ea0ad761898791b1d1f543172..fde46084449d31ec0fd732ad4d7043ee9fc6bedf 100644 (file)
     @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>
index 62bff243b7b0c3d6e2995c49e8bf14fcc733e9f2..ea98200226da2f5603a11efeeaa5049962e219ae 100644 (file)
         </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 (file)
index 0000000..9f4273c
--- /dev/null
@@ -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 (file)
index 0000000..801919a
--- /dev/null
@@ -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
index dd847f297cee300324913eb2cf49c0682ebb0420..f6ee2510d0b4f5040b56e18eaac9ef2241d0741a 100644 (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>
 
index f11da0f4f8f0daadcbfec9c6447188794f72423f..a6c4f329daf506608801a1ec02a5510b0052f1f8 100644 (file)
         <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
index 1ef7b7bde0fcfcf1556b8d82bd60b647efde5772..7e11663882533e120dbcd1138bb47a9b735dceab 100644 (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']);