mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-03-26 00:16:20 +08:00
parent
a8f18c0102
commit
582158f70e
app
resources
assets
lang/en
views
tests/Entity
@ -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());
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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++) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -604,3 +604,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
#tag-manager .drag-card {
|
||||
max-width: 500px;
|
||||
}
|
@ -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 {
|
||||
|
@ -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',
|
||||
|
@ -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">
|
||||
|
@ -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')
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
10
resources/views/components/tag-list.blade.php
Normal file
10
resources/views/components/tag-list.blade.php
Normal 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>
|
23
resources/views/components/tag-manager.blade.php
Normal file
23
resources/views/components/tag-manager.blade.php
Normal 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>
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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']);
|
||||
|
Loading…
x
Reference in New Issue
Block a user