>
+ */
+ protected function idsByTypeToModelMap(array $idsByType): array
+ {
+ $modelMap = [];
+
+ foreach ($idsByType as $type => $ids) {
+ if (!isset($this->listAttributes[$type])) {
+ continue;
+ }
+
+ $instance = $this->entityProvider->get($type);
+ $models = $instance->newQuery()
+ ->select($this->listAttributes[$type])
+ ->scopes('visible')
+ ->whereIn('id', $ids)
+ ->with($this->getRelationsToEagerLoad($type))
+ ->get();
+
+ if (count($models) > 0) {
+ $modelMap[$type] = [];
+ }
+
+ foreach ($models as $model) {
+ $modelMap[$type][strval($model->id)] = $model;
+ }
+ }
+
+ return $modelMap;
+ }
+
+ protected function getRelationsToEagerLoad(string $type): array
+ {
+ $toLoad = [];
+ $loadVisible = fn (Relation $query) => $query->scopes('visible');
+
+ if ($type === 'chapter' || $type === 'page') {
+ $toLoad['book'] = $loadVisible;
+ }
+
+ if ($type === 'page') {
+ $toLoad['chapter'] = $loadVisible;
+ }
+
+ return $toLoad;
+ }
+}
diff --git a/app/References/ReferenceController.php b/app/References/ReferenceController.php
index d6978dd5b..991f47225 100644
--- a/app/References/ReferenceController.php
+++ b/app/References/ReferenceController.php
@@ -10,11 +10,9 @@ use BookStack\Http\Controller;
class ReferenceController extends Controller
{
- protected ReferenceFetcher $referenceFetcher;
-
- public function __construct(ReferenceFetcher $referenceFetcher)
- {
- $this->referenceFetcher = $referenceFetcher;
+ public function __construct(
+ protected ReferenceFetcher $referenceFetcher
+ ) {
}
/**
@@ -23,7 +21,7 @@ class ReferenceController extends Controller
public function page(string $bookSlug, string $pageSlug)
{
$page = Page::getBySlugs($bookSlug, $pageSlug);
- $references = $this->referenceFetcher->getPageReferencesToEntity($page);
+ $references = $this->referenceFetcher->getReferencesToEntity($page);
return view('pages.references', [
'page' => $page,
@@ -37,7 +35,7 @@ class ReferenceController extends Controller
public function chapter(string $bookSlug, string $chapterSlug)
{
$chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
- $references = $this->referenceFetcher->getPageReferencesToEntity($chapter);
+ $references = $this->referenceFetcher->getReferencesToEntity($chapter);
return view('chapters.references', [
'chapter' => $chapter,
@@ -51,7 +49,7 @@ class ReferenceController extends Controller
public function book(string $slug)
{
$book = Book::getBySlug($slug);
- $references = $this->referenceFetcher->getPageReferencesToEntity($book);
+ $references = $this->referenceFetcher->getReferencesToEntity($book);
return view('books.references', [
'book' => $book,
@@ -65,7 +63,7 @@ class ReferenceController extends Controller
public function shelf(string $slug)
{
$shelf = Bookshelf::getBySlug($slug);
- $references = $this->referenceFetcher->getPageReferencesToEntity($shelf);
+ $references = $this->referenceFetcher->getReferencesToEntity($shelf);
return view('shelves.references', [
'shelf' => $shelf,
diff --git a/app/References/ReferenceFetcher.php b/app/References/ReferenceFetcher.php
index c4a7d31b6..0d9883a3e 100644
--- a/app/References/ReferenceFetcher.php
+++ b/app/References/ReferenceFetcher.php
@@ -3,65 +3,51 @@
namespace BookStack\References;
use BookStack\Entities\Models\Entity;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
-use Illuminate\Database\Eloquent\Relations\Relation;
class ReferenceFetcher
{
- protected PermissionApplicator $permissions;
-
- public function __construct(PermissionApplicator $permissions)
- {
- $this->permissions = $permissions;
+ public function __construct(
+ protected PermissionApplicator $permissions,
+ protected MixedEntityListLoader $mixedEntityListLoader,
+ ) {
}
/**
- * Query and return the page references pointing to the given entity.
+ * Query and return the references pointing to the given entity.
* Loads the commonly required relations while taking permissions into account.
*/
- public function getPageReferencesToEntity(Entity $entity): Collection
+ public function getReferencesToEntity(Entity $entity): Collection
{
- $baseQuery = $this->queryPageReferencesToEntity($entity)
- ->with([
- 'from' => fn (Relation $query) => $query->select(Page::$listAttributes),
- 'from.book' => fn (Relation $query) => $query->scopes('visible'),
- 'from.chapter' => fn (Relation $query) => $query->scopes('visible'),
- ]);
-
- $references = $this->permissions->restrictEntityRelationQuery(
- $baseQuery,
- 'references',
- 'from_id',
- 'from_type'
- )->get();
+ $references = $this->queryReferencesToEntity($entity)->get();
+ $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from');
return $references;
}
/**
- * Returns the count of page references pointing to the given entity.
+ * Returns the count of references pointing to the given entity.
* Takes permissions into account.
*/
- public function getPageReferenceCountToEntity(Entity $entity): int
+ public function getReferenceCountToEntity(Entity $entity): int
{
- $count = $this->permissions->restrictEntityRelationQuery(
- $this->queryPageReferencesToEntity($entity),
+ return $this->queryReferencesToEntity($entity)->count();
+ }
+
+ protected function queryReferencesToEntity(Entity $entity): Builder
+ {
+ $baseQuery = Reference::query()
+ ->where('to_type', '=', $entity->getMorphClass())
+ ->where('to_id', '=', $entity->id);
+
+ return $this->permissions->restrictEntityRelationQuery(
+ $baseQuery,
'references',
'from_id',
'from_type'
- )->count();
-
- return $count;
- }
-
- protected function queryPageReferencesToEntity(Entity $entity): Builder
- {
- return Reference::query()
- ->where('to_type', '=', $entity->getMorphClass())
- ->where('to_id', '=', $entity->id)
- ->where('from_type', '=', (new Page())->getMorphClass());
+ );
}
}
diff --git a/app/References/ReferenceStore.php b/app/References/ReferenceStore.php
index 4c6db35c5..78595084b 100644
--- a/app/References/ReferenceStore.php
+++ b/app/References/ReferenceStore.php
@@ -2,60 +2,62 @@
namespace BookStack\References;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Entity;
use Illuminate\Database\Eloquent\Collection;
class ReferenceStore
{
- /**
- * Update the outgoing references for the given page.
- */
- public function updateForPage(Page $page): void
- {
- $this->updateForPages([$page]);
+ public function __construct(
+ protected EntityProvider $entityProvider
+ ) {
}
/**
- * Update the outgoing references for all pages in the system.
+ * Update the outgoing references for the given entity.
*/
- public function updateForAllPages(): void
+ public function updateForEntity(Entity $entity): void
{
- Reference::query()
- ->where('from_type', '=', (new Page())->getMorphClass())
- ->delete();
-
- Page::query()->select(['id', 'html'])->chunk(100, function (Collection $pages) {
- $this->updateForPages($pages->all());
- });
+ $this->updateForEntities([$entity]);
}
/**
- * Update the outgoing references for the pages in the given array.
+ * Update the outgoing references for all entities in the system.
+ */
+ public function updateForAll(): void
+ {
+ Reference::query()->delete();
+
+ foreach ($this->entityProvider->all() as $entity) {
+ $entity->newQuery()->select(['id', $entity->htmlField])->chunk(100, function (Collection $entities) {
+ $this->updateForEntities($entities->all());
+ });
+ }
+ }
+
+ /**
+ * Update the outgoing references for the entities in the given array.
*
- * @param Page[] $pages
+ * @param Entity[] $entities
*/
- protected function updateForPages(array $pages): void
+ protected function updateForEntities(array $entities): void
{
- if (count($pages) === 0) {
+ if (count($entities) === 0) {
return;
}
$parser = CrossLinkParser::createWithEntityResolvers();
$references = [];
- $pageIds = array_map(fn (Page $page) => $page->id, $pages);
- Reference::query()
- ->where('from_type', '=', $pages[0]->getMorphClass())
- ->whereIn('from_id', $pageIds)
- ->delete();
+ $this->dropReferencesFromEntities($entities);
- foreach ($pages as $page) {
- $models = $parser->extractLinkedModels($page->html);
+ foreach ($entities as $entity) {
+ $models = $parser->extractLinkedModels($entity->getAttribute($entity->htmlField));
foreach ($models as $model) {
$references[] = [
- 'from_id' => $page->id,
- 'from_type' => $page->getMorphClass(),
+ 'from_id' => $entity->id,
+ 'from_type' => $entity->getMorphClass(),
'to_id' => $model->id,
'to_type' => $model->getMorphClass(),
];
@@ -66,4 +68,29 @@ class ReferenceStore
Reference::query()->insert($referenceDataChunk);
}
}
+
+ /**
+ * Delete all the existing references originating from the given entities.
+ * @param Entity[] $entities
+ */
+ protected function dropReferencesFromEntities(array $entities): void
+ {
+ $IdsByType = [];
+
+ foreach ($entities as $entity) {
+ $type = $entity->getMorphClass();
+ if (!isset($IdsByType[$type])) {
+ $IdsByType[$type] = [];
+ }
+
+ $IdsByType[$type][] = $entity->id;
+ }
+
+ foreach ($IdsByType as $type => $entityIds) {
+ Reference::query()
+ ->where('from_type', '=', $type)
+ ->whereIn('from_id', $entityIds)
+ ->delete();
+ }
+ }
}
diff --git a/app/References/ReferenceUpdater.php b/app/References/ReferenceUpdater.php
index 248937339..db355f211 100644
--- a/app/References/ReferenceUpdater.php
+++ b/app/References/ReferenceUpdater.php
@@ -4,6 +4,7 @@ namespace BookStack\References;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\HasHtmlDescription;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\RevisionRepo;
use BookStack\Util\HtmlDocument;
@@ -12,20 +13,19 @@ class ReferenceUpdater
{
public function __construct(
protected ReferenceFetcher $referenceFetcher,
- protected RevisionRepo $revisionRepo
+ protected RevisionRepo $revisionRepo,
) {
}
- public function updateEntityPageReferences(Entity $entity, string $oldLink)
+ public function updateEntityReferences(Entity $entity, string $oldLink): void
{
$references = $this->getReferencesToUpdate($entity);
$newLink = $entity->getUrl();
- /** @var Reference $reference */
foreach ($references as $reference) {
- /** @var Page $page */
- $page = $reference->from;
- $this->updateReferencesWithinPage($page, $oldLink, $newLink);
+ /** @var Entity $entity */
+ $entity = $reference->from;
+ $this->updateReferencesWithinEntity($entity, $oldLink, $newLink);
}
}
@@ -35,7 +35,7 @@ class ReferenceUpdater
protected function getReferencesToUpdate(Entity $entity): array
{
/** @var Reference[] $references */
- $references = $this->referenceFetcher->getPageReferencesToEntity($entity)->values()->all();
+ $references = $this->referenceFetcher->getReferencesToEntity($entity)->values()->all();
if ($entity instanceof Book) {
$pages = $entity->pages()->get(['id']);
@@ -43,7 +43,7 @@ class ReferenceUpdater
$children = $pages->concat($chapters);
foreach ($children as $bookChild) {
/** @var Reference[] $childRefs */
- $childRefs = $this->referenceFetcher->getPageReferencesToEntity($bookChild)->values()->all();
+ $childRefs = $this->referenceFetcher->getReferencesToEntity($bookChild)->values()->all();
array_push($references, ...$childRefs);
}
}
@@ -57,7 +57,28 @@ class ReferenceUpdater
return array_values($deduped);
}
- protected function updateReferencesWithinPage(Page $page, string $oldLink, string $newLink)
+ protected function updateReferencesWithinEntity(Entity $entity, string $oldLink, string $newLink): void
+ {
+ if ($entity instanceof Page) {
+ $this->updateReferencesWithinPage($entity, $oldLink, $newLink);
+ return;
+ }
+
+ if (in_array(HasHtmlDescription::class, class_uses($entity))) {
+ $this->updateReferencesWithinDescription($entity, $oldLink, $newLink);
+ }
+ }
+
+ protected function updateReferencesWithinDescription(Entity $entity, string $oldLink, string $newLink): void
+ {
+ /** @var HasHtmlDescription&Entity $entity */
+ $entity = (clone $entity)->refresh();
+ $html = $this->updateLinksInHtml($entity->description_html ?: '', $oldLink, $newLink);
+ $entity->description_html = $html;
+ $entity->save();
+ }
+
+ protected function updateReferencesWithinPage(Page $page, string $oldLink, string $newLink): void
{
$page = (clone $page)->refresh();
$html = $this->updateLinksInHtml($page->html, $oldLink, $newLink);
diff --git a/app/Settings/MaintenanceController.php b/app/Settings/MaintenanceController.php
index 60e5fee28..62eeecf39 100644
--- a/app/Settings/MaintenanceController.php
+++ b/app/Settings/MaintenanceController.php
@@ -87,7 +87,7 @@ class MaintenanceController extends Controller
$this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'regenerate-references');
try {
- $referenceStore->updateForAllPages();
+ $referenceStore->updateForAll();
$this->showSuccessNotification(trans('settings.maint_regen_references_success'));
} catch (\Exception $exception) {
$this->showErrorNotification($exception->getMessage());
diff --git a/app/Util/HtmlDescriptionFilter.php b/app/Util/HtmlDescriptionFilter.php
new file mode 100644
index 000000000..7287586d1
--- /dev/null
+++ b/app/Util/HtmlDescriptionFilter.php
@@ -0,0 +1,79 @@
+
+ */
+ protected static array $allowedAttrsByElements = [
+ 'p' => [],
+ 'a' => ['href', 'title'],
+ 'ol' => [],
+ 'ul' => [],
+ 'li' => [],
+ 'strong' => [],
+ 'em' => [],
+ 'br' => [],
+ ];
+
+ public static function filterFromString(string $html): string
+ {
+ if (empty(trim($html))) {
+ return '';
+ }
+
+ $doc = new HtmlDocument($html);
+
+ $topLevel = [...$doc->getBodyChildren()];
+ foreach ($topLevel as $child) {
+ /** @var DOMNode $child */
+ if ($child instanceof DOMElement) {
+ static::filterElement($child);
+ } else {
+ $child->parentNode->removeChild($child);
+ }
+ }
+
+ return $doc->getBodyInnerHtml();
+ }
+
+ protected static function filterElement(DOMElement $element): void
+ {
+ $elType = strtolower($element->tagName);
+ $allowedAttrs = static::$allowedAttrsByElements[$elType] ?? null;
+ if (is_null($allowedAttrs)) {
+ $element->remove();
+ return;
+ }
+
+ /** @var DOMNamedNodeMap $attrs */
+ $attrs = $element->attributes;
+ for ($i = $attrs->length - 1; $i >= 0; $i--) {
+ /** @var DOMAttr $attr */
+ $attr = $attrs->item($i);
+ $name = strtolower($attr->name);
+ if (!in_array($name, $allowedAttrs)) {
+ $element->removeAttribute($attr->name);
+ }
+ }
+
+ foreach ($element->childNodes as $child) {
+ if ($child instanceof DOMElement) {
+ static::filterElement($child);
+ }
+ }
+ }
+}
diff --git a/database/factories/Entities/Models/BookFactory.php b/database/factories/Entities/Models/BookFactory.php
index 3bf157786..9cb8e971c 100644
--- a/database/factories/Entities/Models/BookFactory.php
+++ b/database/factories/Entities/Models/BookFactory.php
@@ -21,10 +21,12 @@ class BookFactory extends Factory
*/
public function definition()
{
+ $description = $this->faker->paragraph();
return [
'name' => $this->faker->sentence(),
'slug' => Str::random(10),
- 'description' => $this->faker->paragraph(),
+ 'description' => $description,
+ 'description_html' => '' . e($description) . '
'
];
}
}
diff --git a/database/factories/Entities/Models/BookshelfFactory.php b/database/factories/Entities/Models/BookshelfFactory.php
index 66dd1c111..edbefc3e7 100644
--- a/database/factories/Entities/Models/BookshelfFactory.php
+++ b/database/factories/Entities/Models/BookshelfFactory.php
@@ -21,10 +21,12 @@ class BookshelfFactory extends Factory
*/
public function definition()
{
+ $description = $this->faker->paragraph();
return [
'name' => $this->faker->sentence,
'slug' => Str::random(10),
- 'description' => $this->faker->paragraph,
+ 'description' => $description,
+ 'description_html' => '' . e($description) . '
'
];
}
}
diff --git a/database/factories/Entities/Models/ChapterFactory.php b/database/factories/Entities/Models/ChapterFactory.php
index 36379866e..1fc49933e 100644
--- a/database/factories/Entities/Models/ChapterFactory.php
+++ b/database/factories/Entities/Models/ChapterFactory.php
@@ -21,10 +21,12 @@ class ChapterFactory extends Factory
*/
public function definition()
{
+ $description = $this->faker->paragraph();
return [
'name' => $this->faker->sentence(),
'slug' => Str::random(10),
- 'description' => $this->faker->paragraph(),
+ 'description' => $description,
+ 'description_html' => '' . e($description) . '
'
];
}
}
diff --git a/database/migrations/2023_12_17_140913_add_description_html_to_entities.php b/database/migrations/2023_12_17_140913_add_description_html_to_entities.php
new file mode 100644
index 000000000..68c52e81b
--- /dev/null
+++ b/database/migrations/2023_12_17_140913_add_description_html_to_entities.php
@@ -0,0 +1,36 @@
+ $table->text('description_html');
+
+ Schema::table('books', $addColumn);
+ Schema::table('chapters', $addColumn);
+ Schema::table('bookshelves', $addColumn);
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ $removeColumn = fn(Blueprint $table) => $table->removeColumn('description_html');
+
+ Schema::table('books', $removeColumn);
+ Schema::table('chapters', $removeColumn);
+ Schema::table('bookshelves', $removeColumn);
+ }
+};
diff --git a/database/seeders/DummyContentSeeder.php b/database/seeders/DummyContentSeeder.php
index 47e8d1d7c..a4383be50 100644
--- a/database/seeders/DummyContentSeeder.php
+++ b/database/seeders/DummyContentSeeder.php
@@ -3,6 +3,7 @@
namespace Database\Seeders;
use BookStack\Api\ApiToken;
+use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
@@ -38,7 +39,7 @@ class DummyContentSeeder extends Seeder
$byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'owned_by' => $editorUser->id];
- \BookStack\Entities\Models\Book::factory()->count(5)->create($byData)
+ Book::factory()->count(5)->create($byData)
->each(function ($book) use ($byData) {
$chapters = Chapter::factory()->count(3)->create($byData)
->each(function ($chapter) use ($book, $byData) {
@@ -50,7 +51,7 @@ class DummyContentSeeder extends Seeder
$book->pages()->saveMany($pages);
});
- $largeBook = \BookStack\Entities\Models\Book::factory()->create(array_merge($byData, ['name' => 'Large book' . Str::random(10)]));
+ $largeBook = Book::factory()->create(array_merge($byData, ['name' => 'Large book' . Str::random(10)]));
$pages = Page::factory()->count(200)->make($byData);
$chapters = Chapter::factory()->count(50)->make($byData);
$largeBook->pages()->saveMany($pages);
diff --git a/dev/api/requests/books-create.json b/dev/api/requests/books-create.json
index 2a38dba83..71dbdcc65 100644
--- a/dev/api/requests/books-create.json
+++ b/dev/api/requests/books-create.json
@@ -1,7 +1,7 @@
{
"name": "My own book",
- "description": "This is my own little book",
- "default_template_id": 12,
+ "description_html": "This is my own little book created via the API
",
+ "default_template_id": 2427,
"tags": [
{"name": "Category", "value": "Top Content"},
{"name": "Rating", "value": "Highest"}
diff --git a/dev/api/requests/books-update.json b/dev/api/requests/books-update.json
index c026b7b49..30ce7e95a 100644
--- a/dev/api/requests/books-update.json
+++ b/dev/api/requests/books-update.json
@@ -1,7 +1,7 @@
{
"name": "My updated book",
- "description": "This is my book with updated details",
- "default_template_id": 12,
+ "description_html": "This is my book with updated details
",
+ "default_template_id": 2427,
"tags": [
{"name": "Subject", "value": "Updates"}
]
diff --git a/dev/api/requests/chapters-create.json b/dev/api/requests/chapters-create.json
index a7a0e072c..e9d903387 100644
--- a/dev/api/requests/chapters-create.json
+++ b/dev/api/requests/chapters-create.json
@@ -1,7 +1,7 @@
{
"book_id": 1,
"name": "My fantastic new chapter",
- "description": "This is a great new chapter that I've created via the API",
+ "description_html": "This is a great new chapter that I've created via the API
",
"priority": 15,
"tags": [
{"name": "Category", "value": "Top Content"},
diff --git a/dev/api/requests/chapters-update.json b/dev/api/requests/chapters-update.json
index 18c40301b..be675772b 100644
--- a/dev/api/requests/chapters-update.json
+++ b/dev/api/requests/chapters-update.json
@@ -1,7 +1,7 @@
{
"book_id": 1,
"name": "My fantastic updated chapter",
- "description": "This is an updated chapter that I've altered via the API",
+ "description_html": "This is an updated chapter that I've altered via the API
",
"priority": 16,
"tags": [
{"name": "Category", "value": "Kinda Good Content"},
diff --git a/dev/api/requests/shelves-create.json b/dev/api/requests/shelves-create.json
index 39b88af7e..8f35340f6 100644
--- a/dev/api/requests/shelves-create.json
+++ b/dev/api/requests/shelves-create.json
@@ -1,5 +1,8 @@
{
"name": "My shelf",
- "description": "This is my shelf with some books",
- "books": [5,1,3]
+ "description_html": "This is my shelf with some books
",
+ "books": [5,1,3],
+ "tags": [
+ {"name": "Category", "value": "Learning"}
+ ]
}
\ No newline at end of file
diff --git a/dev/api/requests/shelves-update.json b/dev/api/requests/shelves-update.json
index df5f5735d..081c8f4c1 100644
--- a/dev/api/requests/shelves-update.json
+++ b/dev/api/requests/shelves-update.json
@@ -1,5 +1,5 @@
{
"name": "My updated shelf",
- "description": "This is my update shelf with some books",
+ "description_html": "This is my updated shelf with some books
",
"books": [5,1,3]
}
\ No newline at end of file
diff --git a/dev/api/responses/books-create.json b/dev/api/responses/books-create.json
index 773879125..8895fb854 100644
--- a/dev/api/responses/books-create.json
+++ b/dev/api/responses/books-create.json
@@ -1,12 +1,26 @@
{
- "id": 15,
- "name": "My new book",
- "slug": "my-new-book",
- "description": "This is a book created via the API",
+ "id": 226,
+ "name": "My own book",
+ "slug": "my-own-book",
+ "description": "This is my own little book created via the API",
+ "created_at": "2023-12-22T14:22:28.000000Z",
+ "updated_at": "2023-12-22T14:22:28.000000Z",
"created_by": 1,
"updated_by": 1,
"owned_by": 1,
- "default_template_id": 12,
- "updated_at": "2020-01-12T14:05:11.000000Z",
- "created_at": "2020-01-12T14:05:11.000000Z"
+ "default_template_id": 2427,
+ "description_html": "This is my<\/strong> own little book created via the API<\/p>",
+ "tags": [
+ {
+ "name": "Category",
+ "value": "Top Content",
+ "order": 0
+ },
+ {
+ "name": "Rating",
+ "value": "Highest",
+ "order": 0
+ }
+ ],
+ "cover": null
}
\ No newline at end of file
diff --git a/dev/api/responses/books-read.json b/dev/api/responses/books-read.json
index 21e1829b8..afeebade6 100644
--- a/dev/api/responses/books-read.json
+++ b/dev/api/responses/books-read.json
@@ -3,6 +3,7 @@
"name": "My own book",
"slug": "my-own-book",
"description": "This is my own little book",
+ "description_html": "This is my own little book
",
"created_at": "2020-01-12T14:09:59.000000Z",
"updated_at": "2020-01-12T14:11:51.000000Z",
"created_by": {
diff --git a/dev/api/responses/books-update.json b/dev/api/responses/books-update.json
index f69677c4a..dafa2feb0 100644
--- a/dev/api/responses/books-update.json
+++ b/dev/api/responses/books-update.json
@@ -1,12 +1,21 @@
{
- "id": 16,
+ "id": 226,
"name": "My updated book",
"slug": "my-updated-book",
"description": "This is my book with updated details",
- "created_at": "2020-01-12T14:09:59.000000Z",
- "updated_at": "2020-01-12T14:16:10.000000Z",
+ "created_at": "2023-12-22T14:22:28.000000Z",
+ "updated_at": "2023-12-22T14:24:07.000000Z",
"created_by": 1,
"updated_by": 1,
"owned_by": 1,
- "default_template_id": 12
+ "default_template_id": 2427,
+ "description_html": "This is my book with updated<\/em> details<\/p>",
+ "tags": [
+ {
+ "name": "Subject",
+ "value": "Updates",
+ "order": 0
+ }
+ ],
+ "cover": null
}
\ No newline at end of file
diff --git a/dev/api/responses/chapters-create.json b/dev/api/responses/chapters-create.json
index cf47b123d..183186b0b 100644
--- a/dev/api/responses/chapters-create.json
+++ b/dev/api/responses/chapters-create.json
@@ -1,15 +1,16 @@
{
- "id": 74,
+ "id": 668,
"book_id": 1,
"slug": "my-fantastic-new-chapter",
"name": "My fantastic new chapter",
"description": "This is a great new chapter that I've created via the API",
"priority": 15,
+ "created_at": "2023-12-22T14:26:28.000000Z",
+ "updated_at": "2023-12-22T14:26:28.000000Z",
"created_by": 1,
"updated_by": 1,
"owned_by": 1,
- "updated_at": "2020-05-22T22:59:55.000000Z",
- "created_at": "2020-05-22T22:59:55.000000Z",
+ "description_html": "This is a great new chapter<\/strong> that I've created via the API<\/p>",
"tags": [
{
"name": "Category",
@@ -19,7 +20,7 @@
{
"name": "Rating",
"value": "Highest",
- "order": 1
+ "order": 0
}
]
}
\ No newline at end of file
diff --git a/dev/api/responses/chapters-read.json b/dev/api/responses/chapters-read.json
index 5f4de85f1..192ffce7c 100644
--- a/dev/api/responses/chapters-read.json
+++ b/dev/api/responses/chapters-read.json
@@ -4,6 +4,7 @@
"slug": "content-creation",
"name": "Content Creation",
"description": "How to create documentation on whatever subject you need to write about.",
+ "description_html": "How to create documentation on whatever subject you need to write about.
",
"priority": 3,
"created_at": "2019-05-05T21:49:56.000000Z",
"updated_at": "2019-09-28T11:24:23.000000Z",
diff --git a/dev/api/responses/chapters-update.json b/dev/api/responses/chapters-update.json
index a4940af2d..5ac3c64c1 100644
--- a/dev/api/responses/chapters-update.json
+++ b/dev/api/responses/chapters-update.json
@@ -1,16 +1,16 @@
{
- "id": 75,
+ "id": 668,
"book_id": 1,
"slug": "my-fantastic-updated-chapter",
"name": "My fantastic updated chapter",
"description": "This is an updated chapter that I've altered via the API",
"priority": 16,
- "created_at": "2020-05-22T23:03:35.000000Z",
- "updated_at": "2020-05-22T23:07:20.000000Z",
+ "created_at": "2023-12-22T14:26:28.000000Z",
+ "updated_at": "2023-12-22T14:27:59.000000Z",
"created_by": 1,
"updated_by": 1,
"owned_by": 1,
- "book_slug": "bookstack-demo-site",
+ "description_html": "This is an updated chapter<\/strong> that I've altered via the API<\/p>",
"tags": [
{
"name": "Category",
@@ -20,7 +20,7 @@
{
"name": "Rating",
"value": "Medium",
- "order": 1
+ "order": 0
}
]
}
\ No newline at end of file
diff --git a/dev/api/responses/shelves-create.json b/dev/api/responses/shelves-create.json
index 84caf8bdc..235557834 100644
--- a/dev/api/responses/shelves-create.json
+++ b/dev/api/responses/shelves-create.json
@@ -1,11 +1,20 @@
{
- "id": 14,
+ "id": 20,
"name": "My shelf",
"slug": "my-shelf",
"description": "This is my shelf with some books",
"created_by": 1,
"updated_by": 1,
+ "created_at": "2023-12-22T14:33:52.000000Z",
+ "updated_at": "2023-12-22T14:33:52.000000Z",
"owned_by": 1,
- "created_at": "2020-04-10T13:24:09.000000Z",
- "updated_at": "2020-04-10T13:24:09.000000Z"
+ "description_html": "This is my shelf<\/strong> with some books<\/p>",
+ "tags": [
+ {
+ "name": "Category",
+ "value": "Learning",
+ "order": 0
+ }
+ ],
+ "cover": null
}
\ No newline at end of file
diff --git a/dev/api/responses/shelves-read.json b/dev/api/responses/shelves-read.json
index 802045bd8..eca06a46b 100644
--- a/dev/api/responses/shelves-read.json
+++ b/dev/api/responses/shelves-read.json
@@ -3,6 +3,7 @@
"name": "My shelf",
"slug": "my-shelf",
"description": "This is my shelf with some books",
+ "description_html": "This is my shelf with some books
",
"created_by": {
"id": 1,
"name": "Admin",
diff --git a/dev/api/responses/shelves-update.json b/dev/api/responses/shelves-update.json
index e199d8d68..3b3f0538e 100644
--- a/dev/api/responses/shelves-update.json
+++ b/dev/api/responses/shelves-update.json
@@ -1,11 +1,20 @@
{
- "id": 14,
+ "id": 20,
"name": "My updated shelf",
"slug": "my-updated-shelf",
- "description": "This is my update shelf with some books",
+ "description": "This is my updated shelf with some books",
"created_by": 1,
"updated_by": 1,
+ "created_at": "2023-12-22T14:33:52.000000Z",
+ "updated_at": "2023-12-22T14:35:00.000000Z",
"owned_by": 1,
- "created_at": "2020-04-10T13:24:09.000000Z",
- "updated_at": "2020-04-10T13:48:22.000000Z"
+ "description_html": "This is my updated shelf<\/em> with some books<\/p>",
+ "tags": [
+ {
+ "name": "Category",
+ "value": "Learning",
+ "order": 0
+ }
+ ],
+ "cover": null
}
\ No newline at end of file
diff --git a/lang/en/entities.php b/lang/en/entities.php
index 354eee42e..f1f915544 100644
--- a/lang/en/entities.php
+++ b/lang/en/entities.php
@@ -23,7 +23,7 @@ return [
'meta_updated' => 'Updated :timeLength',
'meta_updated_name' => 'Updated :timeLength by :user',
'meta_owned_name' => 'Owned by :user',
- 'meta_reference_page_count' => 'Referenced on :count page|Referenced on :count pages',
+ 'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',
'entity_select' => 'Entity Select',
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
'images' => 'Images',
@@ -409,7 +409,7 @@ return [
// References
'references' => 'References',
'references_none' => 'There are no tracked references to this item.',
- 'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
+ 'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',
// Watch Options
'watch' => 'Watch',
diff --git a/resources/js/components/entity-selector-popup.js b/resources/js/components/entity-selector-popup.js
index 9ff67d53e..6fb461968 100644
--- a/resources/js/components/entity-selector-popup.js
+++ b/resources/js/components/entity-selector-popup.js
@@ -15,8 +15,15 @@ export class EntitySelectorPopup extends Component {
window.$events.listen('entity-select-confirm', this.handleConfirmedSelection.bind(this));
}
- show(callback, searchText = '') {
+ /**
+ * Show the selector popup.
+ * @param {Function} callback
+ * @param {String} searchText
+ * @param {EntitySelectorSearchOptions} searchOptions
+ */
+ show(callback, searchText = '', searchOptions = {}) {
this.callback = callback;
+ this.getSelector().configureSearchOptions(searchOptions);
this.getPopup().show();
if (searchText) {
diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.js
index b12eeb402..5ad991437 100644
--- a/resources/js/components/entity-selector.js
+++ b/resources/js/components/entity-selector.js
@@ -1,6 +1,13 @@
import {onChildEvent} from '../services/dom';
import {Component} from './component';
+/**
+ * @typedef EntitySelectorSearchOptions
+ * @property entityTypes string
+ * @property entityPermission string
+ * @property searchEndpoint string
+ */
+
/**
* Entity Selector
*/
@@ -8,21 +15,35 @@ export class EntitySelector extends Component {
setup() {
this.elem = this.$el;
- this.entityTypes = this.$opts.entityTypes || 'page,book,chapter';
- this.entityPermission = this.$opts.entityPermission || 'view';
- this.searchEndpoint = this.$opts.searchEndpoint || '/search/entity-selector';
this.input = this.$refs.input;
this.searchInput = this.$refs.search;
this.loading = this.$refs.loading;
this.resultsContainer = this.$refs.results;
+ this.searchOptions = {
+ entityTypes: this.$opts.entityTypes || 'page,book,chapter',
+ entityPermission: this.$opts.entityPermission || 'view',
+ searchEndpoint: this.$opts.searchEndpoint || '',
+ };
+
this.search = '';
this.lastClick = 0;
this.setupListeners();
this.showLoading();
- this.initialLoad();
+
+ if (this.searchOptions.searchEndpoint) {
+ this.initialLoad();
+ }
+ }
+
+ /**
+ * @param {EntitySelectorSearchOptions} options
+ */
+ configureSearchOptions(options) {
+ Object.assign(this.searchOptions, options);
+ this.reset();
}
setupListeners() {
@@ -103,6 +124,10 @@ export class EntitySelector extends Component {
}
initialLoad() {
+ if (!this.searchOptions.searchEndpoint) {
+ throw new Error('Search endpoint not set for entity-selector load');
+ }
+
window.$http.get(this.searchUrl()).then(resp => {
this.resultsContainer.innerHTML = resp.data;
this.hideLoading();
@@ -110,10 +135,15 @@ export class EntitySelector extends Component {
}
searchUrl() {
- return `${this.searchEndpoint}?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
+ const query = `types=${encodeURIComponent(this.searchOptions.entityTypes)}&permission=${encodeURIComponent(this.searchOptions.entityPermission)}`;
+ return `${this.searchOptions.searchEndpoint}?${query}`;
}
searchEntities(searchTerm) {
+ if (!this.searchOptions.searchEndpoint) {
+ throw new Error('Search endpoint not set for entity-selector load');
+ }
+
this.input.value = '';
const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
window.$http.get(url).then(resp => {
diff --git a/resources/js/components/index.js b/resources/js/components/index.js
index a56f18a5a..3a66079d7 100644
--- a/resources/js/components/index.js
+++ b/resources/js/components/index.js
@@ -58,3 +58,4 @@ export {TriLayout} from './tri-layout';
export {UserSelect} from './user-select';
export {WebhookEvents} from './webhook-events';
export {WysiwygEditor} from './wysiwyg-editor';
+export {WysiwygInput} from './wysiwyg-input';
diff --git a/resources/js/components/page-picker.js b/resources/js/components/page-picker.js
index 9bb0bee04..39af67229 100644
--- a/resources/js/components/page-picker.js
+++ b/resources/js/components/page-picker.js
@@ -14,6 +14,8 @@ export class PagePicker extends Component {
this.defaultDisplay = this.$refs.defaultDisplay;
this.buttonSep = this.$refs.buttonSeperator;
+ this.selectorEndpoint = this.$opts.selectorEndpoint;
+
this.value = this.input.value;
this.setupListeners();
}
@@ -33,6 +35,10 @@ export class PagePicker extends Component {
const selectorPopup = window.$components.first('entity-selector-popup');
selectorPopup.show(entity => {
this.setValue(entity.id, entity.name);
+ }, '', {
+ searchEndpoint: this.selectorEndpoint,
+ entityTypes: 'page',
+ entityPermission: 'view',
});
}
diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js
index 21db207e6..82f60827d 100644
--- a/resources/js/components/wysiwyg-editor.js
+++ b/resources/js/components/wysiwyg-editor.js
@@ -1,4 +1,4 @@
-import {build as buildEditorConfig} from '../wysiwyg/config';
+import {buildForEditor as buildEditorConfig} from '../wysiwyg/config';
import {Component} from './component';
export class WysiwygEditor extends Component {
@@ -6,17 +6,13 @@ export class WysiwygEditor extends Component {
setup() {
this.elem = this.$el;
- this.pageId = this.$opts.pageId;
- this.textDirection = this.$opts.textDirection;
- this.isDarkMode = document.documentElement.classList.contains('dark-mode');
-
this.tinyMceConfig = buildEditorConfig({
language: this.$opts.language,
containerElement: this.elem,
- darkMode: this.isDarkMode,
- textDirection: this.textDirection,
+ darkMode: document.documentElement.classList.contains('dark-mode'),
+ textDirection: this.$opts.textDirection,
drawioUrl: this.getDrawIoUrl(),
- pageId: Number(this.pageId),
+ pageId: Number(this.$opts.pageId),
translations: {
imageUploadErrorText: this.$opts.imageUploadErrorText,
serverUploadLimitText: this.$opts.serverUploadLimitText,
diff --git a/resources/js/components/wysiwyg-input.js b/resources/js/components/wysiwyg-input.js
new file mode 100644
index 000000000..88c06a334
--- /dev/null
+++ b/resources/js/components/wysiwyg-input.js
@@ -0,0 +1,26 @@
+import {Component} from './component';
+import {buildForInput} from '../wysiwyg/config';
+
+export class WysiwygInput extends Component {
+
+ setup() {
+ this.elem = this.$el;
+
+ const config = buildForInput({
+ language: this.$opts.language,
+ containerElement: this.elem,
+ darkMode: document.documentElement.classList.contains('dark-mode'),
+ textDirection: this.textDirection,
+ translations: {
+ imageUploadErrorText: this.$opts.imageUploadErrorText,
+ serverUploadLimitText: this.$opts.serverUploadLimitText,
+ },
+ translationMap: window.editor_translations,
+ });
+
+ window.tinymce.init(config).then(editors => {
+ this.editor = editors[0];
+ });
+ }
+
+}
diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js
index 4909a59d0..511f1ebda 100644
--- a/resources/js/markdown/actions.js
+++ b/resources/js/markdown/actions.js
@@ -73,7 +73,11 @@ export class Actions {
const selectedText = selectionText || entity.name;
const newText = `[${selectedText}](${entity.link})`;
this.#replaceSelection(newText, newText.length, selectionRange);
- }, selectionText);
+ }, selectionText, {
+ searchEndpoint: '/search/entity-selector',
+ entityTypes: 'page,book,chapter,bookshelf',
+ entityPermission: 'view',
+ });
}
// Show draw.io if enabled and handle save.
diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js
index 6973db8c8..963e2970d 100644
--- a/resources/js/wysiwyg/config.js
+++ b/resources/js/wysiwyg/config.js
@@ -85,7 +85,11 @@ function filePickerCallback(callback, value, meta) {
text: entity.name,
title: entity.name,
});
- }, selectionText);
+ }, selectionText, {
+ searchEndpoint: '/search/entity-selector',
+ entityTypes: 'page,book,chapter,bookshelf',
+ entityPermission: 'view',
+ });
}
if (meta.filetype === 'image') {
@@ -217,7 +221,7 @@ body {
* @param {WysiwygConfigOptions} options
* @return {Object}
*/
-export function build(options) {
+export function buildForEditor(options) {
// Set language
window.tinymce.addI18n(options.language, options.translationMap);
@@ -290,6 +294,54 @@ export function build(options) {
};
}
+/**
+ * @param {WysiwygConfigOptions} options
+ * @return {RawEditorOptions}
+ */
+export function buildForInput(options) {
+ // Set language
+ window.tinymce.addI18n(options.language, options.translationMap);
+
+ // BookStack Version
+ const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
+
+ // Return config object
+ return {
+ width: '100%',
+ height: '185px',
+ target: options.containerElement,
+ cache_suffix: `?version=${version}`,
+ content_css: [
+ window.baseUrl('/dist/styles.css'),
+ ],
+ branding: false,
+ skin: options.darkMode ? 'tinymce-5-dark' : 'tinymce-5',
+ body_class: 'wysiwyg-input',
+ browser_spellcheck: true,
+ relative_urls: false,
+ language: options.language,
+ directionality: options.textDirection,
+ remove_script_host: false,
+ document_base_url: window.baseUrl('/'),
+ end_container_on_empty_block: true,
+ remove_trailing_brs: false,
+ statusbar: false,
+ menubar: false,
+ plugins: 'link autolink lists',
+ contextmenu: false,
+ toolbar: 'bold italic link bullist numlist',
+ content_style: getContentStyle(options),
+ file_picker_types: 'file',
+ file_picker_callback: filePickerCallback,
+ init_instance_callback(editor) {
+ const head = editor.getDoc().querySelector('head');
+ head.innerHTML += fetchCustomHeadContent();
+
+ editor.contentDocument.documentElement.classList.toggle('dark-mode', options.darkMode);
+ },
+ };
+}
+
/**
* @typedef {Object} WysiwygConfigOptions
* @property {Element} containerElement
diff --git a/resources/js/wysiwyg/shortcuts.js b/resources/js/wysiwyg/shortcuts.js
index 147e3c2d5..da9e02270 100644
--- a/resources/js/wysiwyg/shortcuts.js
+++ b/resources/js/wysiwyg/shortcuts.js
@@ -58,6 +58,10 @@ export function register(editor) {
editor.selection.collapse(false);
editor.focus();
- }, selectionText);
+ }, selectionText, {
+ searchEndpoint: '/search/entity-selector',
+ entityTypes: 'page,book,chapter,bookshelf',
+ entityPermission: 'view',
+ });
});
}
diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss
index cd5d929f4..8c277c2b5 100644
--- a/resources/sass/_forms.scss
+++ b/resources/sass/_forms.scss
@@ -406,6 +406,15 @@ input[type=color] {
height: auto;
}
+.description-input > .tox-tinymce {
+ border: 1px solid #DDD !important;
+ @include lightDark(border-color, #DDD !important, #000 !important);
+ border-radius: 3px;
+ .tox-toolbar__primary {
+ justify-content: end;
+ }
+}
+
.search-box {
max-width: 100%;
position: relative;
diff --git a/resources/sass/_tinymce.scss b/resources/sass/_tinymce.scss
index 8e036fc46..c4336da7c 100644
--- a/resources/sass/_tinymce.scss
+++ b/resources/sass/_tinymce.scss
@@ -23,6 +23,13 @@
display: block;
}
+.wysiwyg-input.mce-content-body {
+ padding-block-start: 1rem;
+ padding-block-end: 1rem;
+ outline: 0;
+ display: block;
+}
+
// Default styles for our custom root nodes
.page-content.mce-content-body doc-root {
display: block;
diff --git a/resources/views/books/parts/form.blade.php b/resources/views/books/parts/form.blade.php
index e22be619d..fa8f16e52 100644
--- a/resources/views/books/parts/form.blade.php
+++ b/resources/views/books/parts/form.blade.php
@@ -1,3 +1,6 @@
+@push('head')
+
+@endpush
{{ csrf_field() }}
@@ -6,8 +9,8 @@
- {{ trans('common.description') }}
- @include('form.textarea', ['name' => 'description'])
+ {{ trans('common.description') }}
+ @include('form.description-html-input')
@@ -36,7 +39,7 @@
@@ -62,4 +66,5 @@
{{ trans('entities.books_save') }}
-@include('entities.selector-popup', ['entityTypes' => 'page', 'selectorEndpoint' => '/search/entity-selector-templates'])
\ No newline at end of file
+@include('entities.selector-popup')
+@include('form.editor-translations')
\ No newline at end of file
diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php
index 8f7c3f6cf..dbb09fc9e 100644
--- a/resources/views/books/show.blade.php
+++ b/resources/views/books/show.blade.php
@@ -26,7 +26,7 @@
{{$book->name}}
-
{!! nl2br(e($book->description)) !!}
+
{!! $book->descriptionHtml() !!}
@if(count($bookChildren) > 0)
@foreach($bookChildren as $childElement)
diff --git a/resources/views/chapters/parts/form.blade.php b/resources/views/chapters/parts/form.blade.php
index 8abcebe13..c6052c93a 100644
--- a/resources/views/chapters/parts/form.blade.php
+++ b/resources/views/chapters/parts/form.blade.php
@@ -1,14 +1,16 @@
+@push('head')
+
+@endpush
-{!! csrf_field() !!}
-
+{{ csrf_field() }}
{{ trans('common.name') }}
@include('form.text', ['name' => 'name', 'autofocus' => true])
- {{ trans('common.description') }}
- @include('form.textarea', ['name' => 'description'])
+ {{ trans('common.description') }}
+ @include('form.description-html-input')
+
+@include('entities.selector-popup')
+@include('form.editor-translations')
\ No newline at end of file
diff --git a/resources/views/chapters/show.blade.php b/resources/views/chapters/show.blade.php
index 0e5224d54..45e43ad96 100644
--- a/resources/views/chapters/show.blade.php
+++ b/resources/views/chapters/show.blade.php
@@ -24,7 +24,7 @@
{{ $chapter->name }}
-
{!! nl2br(e($chapter->description)) !!}
+
{!! $chapter->descriptionHtml() !!}
@if(count($pages) > 0)
@foreach($pages as $page)
diff --git a/resources/views/entities/meta.blade.php b/resources/views/entities/meta.blade.php
index 2298be8bb..9d3c4b956 100644
--- a/resources/views/entities/meta.blade.php
+++ b/resources/views/entities/meta.blade.php
@@ -64,7 +64,7 @@
@icon('reference')
- {!! trans_choice('entities.meta_reference_page_count', $referenceCount, ['count' => $referenceCount]) !!}
+ {{ trans_choice('entities.meta_reference_count', $referenceCount, ['count' => $referenceCount]) }}
@endif
diff --git a/resources/views/entities/selector-popup.blade.php b/resources/views/entities/selector-popup.blade.php
index d4c941e9a..ac91725d6 100644
--- a/resources/views/entities/selector-popup.blade.php
+++ b/resources/views/entities/selector-popup.blade.php
@@ -5,7 +5,7 @@
- @include('entities.selector', ['name' => 'entity-selector'])
+ @include('entities.selector', ['name' => 'entity-selector', 'selectorEndpoint' => ''])
diff --git a/resources/views/exports/book.blade.php b/resources/views/exports/book.blade.php
index 42e03ea01..9de7b8eba 100644
--- a/resources/views/exports/book.blade.php
+++ b/resources/views/exports/book.blade.php
@@ -5,7 +5,7 @@
@section('content')
{{$book->name}}
-
{{ $book->description }}
+
{!! $book->descriptionHtml() !!}
@include('exports.parts.book-contents-menu', ['children' => $bookChildren])
diff --git a/resources/views/exports/chapter.blade.php b/resources/views/exports/chapter.blade.php
index ae49fa918..515366d60 100644
--- a/resources/views/exports/chapter.blade.php
+++ b/resources/views/exports/chapter.blade.php
@@ -5,7 +5,7 @@
@section('content')
{{$chapter->name}}
-
{{ $chapter->description }}
+
{!! $chapter->descriptionHtml() !!}
@include('exports.parts.chapter-contents-menu', ['pages' => $pages])
diff --git a/resources/views/exports/parts/chapter-item.blade.php b/resources/views/exports/parts/chapter-item.blade.php
index f58068b5e..fa0b1f228 100644
--- a/resources/views/exports/parts/chapter-item.blade.php
+++ b/resources/views/exports/parts/chapter-item.blade.php
@@ -1,7 +1,7 @@
{{ $chapter->name }}
-
{{ $chapter->description }}
+
{!! $chapter->descriptionHtml() !!}
@if(count($chapter->visible_pages) > 0)
@foreach($chapter->visible_pages as $page)
diff --git a/resources/views/form/description-html-input.blade.php b/resources/views/form/description-html-input.blade.php
new file mode 100644
index 000000000..3cf726ba4
--- /dev/null
+++ b/resources/views/form/description-html-input.blade.php
@@ -0,0 +1,8 @@
+
+@if($errors->has('description_html'))
+
{{ $errors->first('description_html') }}
+@endif
\ No newline at end of file
diff --git a/resources/views/pages/parts/editor-translations.blade.php b/resources/views/form/editor-translations.blade.php
similarity index 100%
rename from resources/views/pages/parts/editor-translations.blade.php
rename to resources/views/form/editor-translations.blade.php
diff --git a/resources/views/form/page-picker.blade.php b/resources/views/form/page-picker.blade.php
index d9810d575..ad0a9d516 100644
--- a/resources/views/form/page-picker.blade.php
+++ b/resources/views/form/page-picker.blade.php
@@ -1,6 +1,7 @@
{{--Depends on entity selector popup--}}
-
+
@@ -168,5 +173,5 @@
@endsection
@section('after-content')
- @include('entities.selector-popup', ['entityTypes' => 'page'])
+ @include('entities.selector-popup')
@endsection
diff --git a/resources/views/shelves/parts/form.blade.php b/resources/views/shelves/parts/form.blade.php
index ad67cb85c..a75dd6ac1 100644
--- a/resources/views/shelves/parts/form.blade.php
+++ b/resources/views/shelves/parts/form.blade.php
@@ -1,13 +1,16 @@
-{{ csrf_field() }}
+@push('head')
+
+@endpush
+{{ csrf_field() }}
{{ trans('common.name') }}
@include('form.text', ['name' => 'name', 'autofocus' => true])
- {{ trans('common.description') }}
- @include('form.textarea', ['name' => 'description'])
+ {{ trans('common.description') }}
+ @include('form.description-html-input')
@@ -84,4 +87,7 @@
\ No newline at end of file
+
+
+@include('entities.selector-popup')
+@include('form.editor-translations')
\ No newline at end of file
diff --git a/resources/views/shelves/show.blade.php b/resources/views/shelves/show.blade.php
index 58fe1cd86..11baccaf4 100644
--- a/resources/views/shelves/show.blade.php
+++ b/resources/views/shelves/show.blade.php
@@ -28,7 +28,7 @@
-
{!! nl2br(e($shelf->description)) !!}
+
{!! $shelf->descriptionHtml() !!}
@if(count($sortedVisibleShelfBooks) > 0)
@if($view === 'list')
diff --git a/tests/Api/BooksApiTest.php b/tests/Api/BooksApiTest.php
index c648faaf2..b31bd7d37 100644
--- a/tests/Api/BooksApiTest.php
+++ b/tests/Api/BooksApiTest.php
@@ -33,8 +33,8 @@ class BooksApiTest extends TestCase
$this->actingAsApiEditor();
$templatePage = $this->entities->templatePage();
$details = [
- 'name' => 'My API book',
- 'description' => 'A book created via the API',
+ 'name' => 'My API book',
+ 'description' => 'A book created via the API',
'default_template_id' => $templatePage->id,
];
@@ -42,10 +42,35 @@ class BooksApiTest extends TestCase
$resp->assertStatus(200);
$newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
- $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
+ $resp->assertJson(array_merge($details, [
+ 'id' => $newItem->id,
+ 'slug' => $newItem->slug,
+ 'description_html' => '
A book created via the API
',
+ ]));
$this->assertActivityExists('book_create', $newItem);
}
+ public function test_create_endpoint_with_html()
+ {
+ $this->actingAsApiEditor();
+ $details = [
+ 'name' => 'My API book',
+ 'description_html' => '
A book created via the API
',
+ ];
+
+ $resp = $this->postJson($this->baseEndpoint, $details);
+ $resp->assertStatus(200);
+
+ $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
+ $expectedDetails = array_merge($details, [
+ 'id' => $newItem->id,
+ 'description' => 'A book created via the API',
+ ]);
+
+ $resp->assertJson($expectedDetails);
+ $this->assertDatabaseHas('books', $expectedDetails);
+ }
+
public function test_book_name_needed_to_create()
{
$this->actingAsApiEditor();
@@ -61,7 +86,7 @@ class BooksApiTest extends TestCase
'validation' => [
'name' => ['The name field is required.'],
],
- 'code' => 422,
+ 'code' => 422,
],
]);
}
@@ -128,7 +153,7 @@ class BooksApiTest extends TestCase
$templatePage = $this->entities->templatePage();
$details = [
'name' => 'My updated API book',
- 'description' => 'A book created via the API',
+ 'description' => 'A book updated via the API',
'default_template_id' => $templatePage->id,
];
@@ -136,10 +161,29 @@ class BooksApiTest extends TestCase
$book->refresh();
$resp->assertStatus(200);
- $resp->assertJson(array_merge($details, ['id' => $book->id, 'slug' => $book->slug]));
+ $resp->assertJson(array_merge($details, [
+ 'id' => $book->id,
+ 'slug' => $book->slug,
+ 'description_html' => '
A book updated via the API
',
+ ]));
$this->assertActivityExists('book_update', $book);
}
+ public function test_update_endpoint_with_html()
+ {
+ $this->actingAsApiEditor();
+ $book = $this->entities->book();
+ $details = [
+ 'name' => 'My updated API book',
+ 'description_html' => '
A book updated via the API
',
+ ];
+
+ $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
+ $resp->assertStatus(200);
+
+ $this->assertDatabaseHas('books', array_merge($details, ['id' => $book->id, 'description' => 'A book updated via the API']));
+ }
+
public function test_update_increments_updated_date_if_only_tags_are_sent()
{
$this->actingAsApiEditor();
diff --git a/tests/Api/ChaptersApiTest.php b/tests/Api/ChaptersApiTest.php
index 0629f3aed..81a918877 100644
--- a/tests/Api/ChaptersApiTest.php
+++ b/tests/Api/ChaptersApiTest.php
@@ -51,7 +51,11 @@ class ChaptersApiTest extends TestCase
$resp = $this->postJson($this->baseEndpoint, $details);
$resp->assertStatus(200);
$newItem = Chapter::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
- $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
+ $resp->assertJson(array_merge($details, [
+ 'id' => $newItem->id,
+ 'slug' => $newItem->slug,
+ 'description_html' => '
A chapter created via the API
',
+ ]));
$this->assertDatabaseHas('tags', [
'entity_id' => $newItem->id,
'entity_type' => $newItem->getMorphClass(),
@@ -62,6 +66,28 @@ class ChaptersApiTest extends TestCase
$this->assertActivityExists('chapter_create', $newItem);
}
+ public function test_create_endpoint_with_html()
+ {
+ $this->actingAsApiEditor();
+ $book = $this->entities->book();
+ $details = [
+ 'name' => 'My API chapter',
+ 'description_html' => '
A chapter created via the API
',
+ 'book_id' => $book->id,
+ ];
+
+ $resp = $this->postJson($this->baseEndpoint, $details);
+ $resp->assertStatus(200);
+ $newItem = Chapter::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
+
+ $expectedDetails = array_merge($details, [
+ 'id' => $newItem->id,
+ 'description' => 'A chapter created via the API',
+ ]);
+ $resp->assertJson($expectedDetails);
+ $this->assertDatabaseHas('chapters', $expectedDetails);
+ }
+
public function test_chapter_name_needed_to_create()
{
$this->actingAsApiEditor();
@@ -131,7 +157,7 @@ class ChaptersApiTest extends TestCase
$chapter = $this->entities->chapter();
$details = [
'name' => 'My updated API chapter',
- 'description' => 'A chapter created via the API',
+ 'description' => 'A chapter updated via the API',
'tags' => [
[
'name' => 'freshtag',
@@ -146,11 +172,31 @@ class ChaptersApiTest extends TestCase
$resp->assertStatus(200);
$resp->assertJson(array_merge($details, [
- 'id' => $chapter->id, 'slug' => $chapter->slug, 'book_id' => $chapter->book_id,
+ 'id' => $chapter->id,
+ 'slug' => $chapter->slug,
+ 'book_id' => $chapter->book_id,
+ 'description_html' => '
A chapter updated via the API
',
]));
$this->assertActivityExists('chapter_update', $chapter);
}
+ public function test_update_endpoint_with_html()
+ {
+ $this->actingAsApiEditor();
+ $chapter = $this->entities->chapter();
+ $details = [
+ 'name' => 'My updated API chapter',
+ 'description_html' => '
A chapter updated via the API
',
+ ];
+
+ $resp = $this->putJson($this->baseEndpoint . "/{$chapter->id}", $details);
+ $resp->assertStatus(200);
+
+ $this->assertDatabaseHas('chapters', array_merge($details, [
+ 'id' => $chapter->id, 'description' => 'A chapter updated via the API'
+ ]));
+ }
+
public function test_update_increments_updated_date_if_only_tags_are_sent()
{
$this->actingAsApiEditor();
diff --git a/tests/Api/SearchApiTest.php b/tests/Api/SearchApiTest.php
index cdc954ec3..2a186e8d6 100644
--- a/tests/Api/SearchApiTest.php
+++ b/tests/Api/SearchApiTest.php
@@ -52,7 +52,7 @@ class SearchApiTest extends TestCase
public function test_all_endpoint_returns_items_with_preview_html()
{
$book = $this->entities->book();
- $book->update(['name' => 'name with superuniquevalue within', 'description' => 'Description with superuniquevalue within']);
+ $book->forceFill(['name' => 'name with superuniquevalue within', 'description' => 'Description with superuniquevalue within'])->save();
$book->indexForSearch();
$resp = $this->actingAsApiAdmin()->getJson($this->baseEndpoint . '?query=superuniquevalue');
diff --git a/tests/Api/ShelvesApiTest.php b/tests/Api/ShelvesApiTest.php
index fbfc17cb4..f1b8ed985 100644
--- a/tests/Api/ShelvesApiTest.php
+++ b/tests/Api/ShelvesApiTest.php
@@ -42,7 +42,11 @@ class ShelvesApiTest extends TestCase
$resp = $this->postJson($this->baseEndpoint, array_merge($details, ['books' => [$books[0]->id, $books[1]->id]]));
$resp->assertStatus(200);
$newItem = Bookshelf::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
- $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
+ $resp->assertJson(array_merge($details, [
+ 'id' => $newItem->id,
+ 'slug' => $newItem->slug,
+ 'description_html' => '
A shelf created via the API
',
+ ]));
$this->assertActivityExists('bookshelf_create', $newItem);
foreach ($books as $index => $book) {
$this->assertDatabaseHas('bookshelves_books', [
@@ -53,6 +57,28 @@ class ShelvesApiTest extends TestCase
}
}
+ public function test_create_endpoint_with_html()
+ {
+ $this->actingAsApiEditor();
+
+ $details = [
+ 'name' => 'My API shelf',
+ 'description_html' => '
A shelf created via the API
',
+ ];
+
+ $resp = $this->postJson($this->baseEndpoint, $details);
+ $resp->assertStatus(200);
+ $newItem = Bookshelf::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
+
+ $expectedDetails = array_merge($details, [
+ 'id' => $newItem->id,
+ 'description' => 'A shelf created via the API',
+ ]);
+
+ $resp->assertJson($expectedDetails);
+ $this->assertDatabaseHas('bookshelves', $expectedDetails);
+ }
+
public function test_shelf_name_needed_to_create()
{
$this->actingAsApiEditor();
@@ -102,17 +128,36 @@ class ShelvesApiTest extends TestCase
$shelf = Bookshelf::visible()->first();
$details = [
'name' => 'My updated API shelf',
- 'description' => 'A shelf created via the API',
+ 'description' => 'A shelf updated via the API',
];
$resp = $this->putJson($this->baseEndpoint . "/{$shelf->id}", $details);
$shelf->refresh();
$resp->assertStatus(200);
- $resp->assertJson(array_merge($details, ['id' => $shelf->id, 'slug' => $shelf->slug]));
+ $resp->assertJson(array_merge($details, [
+ 'id' => $shelf->id,
+ 'slug' => $shelf->slug,
+ 'description_html' => '
A shelf updated via the API
',
+ ]));
$this->assertActivityExists('bookshelf_update', $shelf);
}
+ public function test_update_endpoint_with_html()
+ {
+ $this->actingAsApiEditor();
+ $shelf = Bookshelf::visible()->first();
+ $details = [
+ 'name' => 'My updated API shelf',
+ 'description_html' => '
A shelf updated via the API
',
+ ];
+
+ $resp = $this->putJson($this->baseEndpoint . "/{$shelf->id}", $details);
+ $resp->assertStatus(200);
+
+ $this->assertDatabaseHas('bookshelves', array_merge($details, ['id' => $shelf->id, 'description' => 'A shelf updated via the API']));
+ }
+
public function test_update_increments_updated_date_if_only_tags_are_sent()
{
$this->actingAsApiEditor();
diff --git a/tests/Commands/UpdateUrlCommandTest.php b/tests/Commands/UpdateUrlCommandTest.php
index 280c81feb..62c39c274 100644
--- a/tests/Commands/UpdateUrlCommandTest.php
+++ b/tests/Commands/UpdateUrlCommandTest.php
@@ -2,6 +2,7 @@
namespace Tests\Commands;
+use BookStack\Entities\Models\Entity;
use Illuminate\Support\Facades\Artisan;
use Symfony\Component\Console\Exception\RuntimeException;
use Tests\TestCase;
@@ -24,6 +25,28 @@ class UpdateUrlCommandTest extends TestCase
]);
}
+ public function test_command_updates_description_html()
+ {
+ /** @var Entity[] $models */
+ $models = [$this->entities->book(), $this->entities->chapter(), $this->entities->shelf()];
+
+ foreach ($models as $model) {
+ $model->description_html = '
';
+ $model->save();
+ }
+
+ $this->artisan('bookstack:update-url https://example.com https://cats.example.com')
+ ->expectsQuestion("This will search for \"https://example.com\" in your database and replace it with \"https://cats.example.com\".\nAre you sure you want to proceed?", 'y')
+ ->expectsQuestion('This operation could cause issues if used incorrectly. Have you made a backup of your existing database?', 'y');
+
+ foreach ($models as $model) {
+ $this->assertDatabaseHas($model->getTable(), [
+ 'id' => $model->id,
+ 'description_html' => '
',
+ ]);
+ }
+ }
+
public function test_command_requires_valid_url()
{
$badUrlMessage = 'The given urls are expected to be full urls starting with http:// or https://';
diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php
index c1842c175..fb9862931 100644
--- a/tests/Entity/BookShelfTest.php
+++ b/tests/Entity/BookShelfTest.php
@@ -77,8 +77,8 @@ class BookShelfTest extends TestCase
{
$booksToInclude = Book::take(2)->get();
$shelfInfo = [
- 'name' => 'My test book' . Str::random(4),
- 'description' => 'Test book description ' . Str::random(10),
+ 'name' => 'My test shelf' . Str::random(4),
+ 'description_html' => '
Test book description ' . Str::random(10) . '
',
];
$resp = $this->asEditor()->post('/shelves', array_merge($shelfInfo, [
'books' => $booksToInclude->implode('id', ','),
@@ -96,7 +96,7 @@ class BookShelfTest extends TestCase
$shelf = Bookshelf::where('name', '=', $shelfInfo['name'])->first();
$shelfPage = $this->get($shelf->getUrl());
$shelfPage->assertSee($shelfInfo['name']);
- $shelfPage->assertSee($shelfInfo['description']);
+ $shelfPage->assertSee($shelfInfo['description_html'], false);
$this->withHtml($shelfPage)->assertElementContains('.tag-item', 'Test Category');
$this->withHtml($shelfPage)->assertElementContains('.tag-item', 'Test Tag Value');
@@ -107,8 +107,8 @@ class BookShelfTest extends TestCase
public function test_shelves_create_sets_cover_image()
{
$shelfInfo = [
- 'name' => 'My test book' . Str::random(4),
- 'description' => 'Test book description ' . Str::random(10),
+ 'name' => 'My test shelf' . Str::random(4),
+ 'description_html' => '
Test book description ' . Str::random(10) . '
',
];
$imageFile = $this->files->uploadedImage('shelf-test.png');
@@ -174,7 +174,7 @@ class BookShelfTest extends TestCase
// Set book ordering
$this->asAdmin()->put($shelf->getUrl(), [
'books' => $books->implode('id', ','),
- 'tags' => [], 'description' => 'abc', 'name' => 'abc',
+ 'tags' => [], 'description_html' => 'abc', 'name' => 'abc',
]);
$this->assertEquals(3, $shelf->books()->count());
$shelf->refresh();
@@ -207,7 +207,7 @@ class BookShelfTest extends TestCase
// Set book ordering
$this->asAdmin()->put($shelf->getUrl(), [
'books' => $books->implode('id', ','),
- 'tags' => [], 'description' => 'abc', 'name' => 'abc',
+ 'tags' => [], 'description_html' => 'abc', 'name' => 'abc',
]);
$this->assertEquals(3, $shelf->books()->count());
$shelf->refresh();
@@ -229,8 +229,8 @@ class BookShelfTest extends TestCase
$booksToInclude = Book::take(2)->get();
$shelfInfo = [
- 'name' => 'My test book' . Str::random(4),
- 'description' => 'Test book description ' . Str::random(10),
+ 'name' => 'My test shelf' . Str::random(4),
+ 'description_html' => '
Test book description ' . Str::random(10) . '
',
];
$resp = $this->asEditor()->put($shelf->getUrl(), array_merge($shelfInfo, [
@@ -251,7 +251,7 @@ class BookShelfTest extends TestCase
$shelfPage = $this->get($shelf->getUrl());
$shelfPage->assertSee($shelfInfo['name']);
- $shelfPage->assertSee($shelfInfo['description']);
+ $shelfPage->assertSee($shelfInfo['description_html'], false);
$this->withHtml($shelfPage)->assertElementContains('.tag-item', 'Test Category');
$this->withHtml($shelfPage)->assertElementContains('.tag-item', 'Test Tag Value');
@@ -270,8 +270,8 @@ class BookShelfTest extends TestCase
$testName = 'Test Book in Shelf Name';
$createBookResp = $this->asEditor()->post($shelf->getUrl('/create-book'), [
- 'name' => $testName,
- 'description' => 'Book in shelf description',
+ 'name' => $testName,
+ 'description_html' => 'Book in shelf description',
]);
$createBookResp->assertRedirect();
@@ -372,8 +372,8 @@ class BookShelfTest extends TestCase
{
// Create shelf
$shelfInfo = [
- 'name' => 'My test shelf' . Str::random(4),
- 'description' => 'Test shelf description ' . Str::random(10),
+ 'name' => 'My test shelf' . Str::random(4),
+ 'description_html' => '
Test shelf description ' . Str::random(10) . '
',
];
$this->asEditor()->post('/shelves', $shelfInfo);
@@ -381,8 +381,8 @@ class BookShelfTest extends TestCase
// Create book and add to shelf
$this->asEditor()->post($shelf->getUrl('/create-book'), [
- 'name' => 'Test book name',
- 'description' => 'Book in shelf description',
+ 'name' => 'Test book name',
+ 'description_html' => '
Book in shelf description
',
]);
$newBook = Book::query()->orderBy('id', 'desc')->first();
@@ -403,4 +403,15 @@ class BookShelfTest extends TestCase
$resp = $this->asEditor()->get($shelf->getUrl('/create-book'));
$this->withHtml($resp)->assertElementContains('form a[href="' . $shelf->getUrl() . '"]', 'Cancel');
}
+
+ public function test_show_view_displays_description_if_no_description_html_set()
+ {
+ $shelf = $this->entities->shelf();
+ $shelf->description_html = '';
+ $shelf->description = "My great\ndescription\n\nwith newlines";
+ $shelf->save();
+
+ $resp = $this->asEditor()->get($shelf->getUrl());
+ $resp->assertSee("
My great \ndescription \n \nwith newlines
", false);
+ }
}
diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php
index 833cabaae..374089246 100644
--- a/tests/Entity/BookTest.php
+++ b/tests/Entity/BookTest.php
@@ -22,7 +22,7 @@ class BookTest extends TestCase
$resp = $this->get('/create-book');
$this->withHtml($resp)->assertElementContains('form[action="' . url('/books') . '"][method="POST"]', 'Save Book');
- $resp = $this->post('/books', $book->only('name', 'description'));
+ $resp = $this->post('/books', $book->only('name', 'description_html'));
$resp->assertRedirect('/books/my-first-book');
$resp = $this->get('/books/my-first-book');
@@ -36,8 +36,8 @@ class BookTest extends TestCase
'name' => 'My First Book',
]);
- $this->asEditor()->post('/books', $book->only('name', 'description'));
- $this->asEditor()->post('/books', $book->only('name', 'description'));
+ $this->asEditor()->post('/books', $book->only('name', 'description_html'));
+ $this->asEditor()->post('/books', $book->only('name', 'description_html'));
$books = Book::query()->where('name', '=', $book->name)
->orderBy('id', 'desc')
@@ -52,9 +52,9 @@ class BookTest extends TestCase
{
// Cheeky initial update to refresh slug
$this->asEditor()->post('books', [
- 'name' => 'My book with tags',
- 'description' => 'A book with tags',
- 'tags' => [
+ 'name' => 'My book with tags',
+ 'description_html' => '
A book with tags
',
+ 'tags' => [
[
'name' => 'Category',
'value' => 'Donkey Content',
@@ -79,23 +79,23 @@ class BookTest extends TestCase
{
$book = $this->entities->book();
// Cheeky initial update to refresh slug
- $this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description' => $book->description]);
+ $this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description_html' => $book->description_html]);
$book->refresh();
$newName = $book->name . ' Updated';
- $newDesc = $book->description . ' with more content';
+ $newDesc = $book->description_html . '
with more content
';
$resp = $this->get($book->getUrl('/edit'));
$resp->assertSee($book->name);
- $resp->assertSee($book->description);
+ $resp->assertSee($book->description_html);
$this->withHtml($resp)->assertElementContains('form[action="' . $book->getUrl() . '"]', 'Save Book');
- $resp = $this->put($book->getUrl(), ['name' => $newName, 'description' => $newDesc]);
+ $resp = $this->put($book->getUrl(), ['name' => $newName, 'description_html' => $newDesc]);
$resp->assertRedirect($book->getUrl() . '-updated');
$resp = $this->get($book->getUrl() . '-updated');
$resp->assertSee($newName);
- $resp->assertSee($newDesc);
+ $resp->assertSee($newDesc, false);
}
public function test_update_sets_tags()
@@ -184,7 +184,7 @@ class BookTest extends TestCase
public function test_recently_viewed_books_updates_as_expected()
{
- $books = Book::all()->take(2);
+ $books = Book::take(2)->get();
$resp = $this->asAdmin()->get('/books');
$this->withHtml($resp)->assertElementNotContains('#recents', $books[0]->name)
@@ -200,7 +200,7 @@ class BookTest extends TestCase
public function test_popular_books_updates_upon_visits()
{
- $books = Book::all()->take(2);
+ $books = Book::take(2)->get();
$resp = $this->asAdmin()->get('/books');
$this->withHtml($resp)->assertElementNotContains('#popular', $books[0]->name)
@@ -262,6 +262,33 @@ class BookTest extends TestCase
$this->assertEquals('parta-partb-partc', $book->slug);
}
+ public function test_description_limited_to_specific_html()
+ {
+ $book = $this->entities->book();
+
+ $input = '
Test Contenta
';
+ $expected = '
Contenta
';
+
+ $this->asEditor()->put($book->getUrl(), [
+ 'name' => $book->name,
+ 'description_html' => $input
+ ]);
+
+ $book->refresh();
+ $this->assertEquals($expected, $book->description_html);
+ }
+
+ public function test_show_view_displays_description_if_no_description_html_set()
+ {
+ $book = $this->entities->book();
+ $book->description_html = '';
+ $book->description = "My great\ndescription\n\nwith newlines";
+ $book->save();
+
+ $resp = $this->asEditor()->get($book->getUrl());
+ $resp->assertSee("
My great \ndescription \n \nwith newlines
", false);
+ }
+
public function test_show_view_has_copy_button()
{
$book = $this->entities->book();
@@ -291,6 +318,8 @@ class BookTest extends TestCase
$resp->assertRedirect($copy->getUrl());
$this->assertEquals($book->getDirectChildren()->count(), $copy->getDirectChildren()->count());
+
+ $this->get($copy->getUrl())->assertSee($book->description_html, false);
}
public function test_copy_does_not_copy_non_visible_content()
diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php
index 7fa32c252..1577cee76 100644
--- a/tests/Entity/ChapterTest.php
+++ b/tests/Entity/ChapterTest.php
@@ -23,12 +23,23 @@ class ChapterTest extends TestCase
$resp = $this->get($book->getUrl('/create-chapter'));
$this->withHtml($resp)->assertElementContains('form[action="' . $book->getUrl('/create-chapter') . '"][method="POST"]', 'Save Chapter');
- $resp = $this->post($book->getUrl('/create-chapter'), $chapter->only('name', 'description'));
+ $resp = $this->post($book->getUrl('/create-chapter'), $chapter->only('name', 'description_html'));
$resp->assertRedirect($book->getUrl('/chapter/my-first-chapter'));
$resp = $this->get($book->getUrl('/chapter/my-first-chapter'));
$resp->assertSee($chapter->name);
- $resp->assertSee($chapter->description);
+ $resp->assertSee($chapter->description_html, false);
+ }
+
+ public function test_show_view_displays_description_if_no_description_html_set()
+ {
+ $chapter = $this->entities->chapter();
+ $chapter->description_html = '';
+ $chapter->description = "My great\ndescription\n\nwith newlines";
+ $chapter->save();
+
+ $resp = $this->asEditor()->get($chapter->getUrl());
+ $resp->assertSee("
My great \ndescription \n \nwith newlines
", false);
}
public function test_delete()
diff --git a/tests/Entity/ConvertTest.php b/tests/Entity/ConvertTest.php
index decda5224..d9b1ee466 100644
--- a/tests/Entity/ConvertTest.php
+++ b/tests/Entity/ConvertTest.php
@@ -42,6 +42,7 @@ class ConvertTest extends TestCase
$this->assertEquals('Penguins', $newBook->tags->first()->value);
$this->assertEquals($chapter->name, $newBook->name);
$this->assertEquals($chapter->description, $newBook->description);
+ $this->assertEquals($chapter->description_html, $newBook->description_html);
$this->assertActivityExists(ActivityType::BOOK_CREATE_FROM_CHAPTER, $newBook);
}
@@ -105,6 +106,7 @@ class ConvertTest extends TestCase
$this->assertEquals('Ducks', $newShelf->tags->first()->value);
$this->assertEquals($book->name, $newShelf->name);
$this->assertEquals($book->description, $newShelf->description);
+ $this->assertEquals($book->description_html, $newShelf->description_html);
$this->assertEquals($newShelf->books()->count(), $bookChapterCount + 1);
$this->assertEquals($systemBookCount + $bookChapterCount, Book::query()->count());
$this->assertActivityExists(ActivityType::BOOKSHELF_CREATE_FROM_BOOK, $newShelf);
diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php
index 08bf17d0a..eedcb672c 100644
--- a/tests/Entity/ExportTest.php
+++ b/tests/Entity/ExportTest.php
@@ -107,18 +107,18 @@ class ExportTest extends TestCase
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
}
- public function test_book_html_export_shows_chapter_descriptions()
+ public function test_book_html_export_shows_html_descriptions()
{
- $chapterDesc = 'My custom test chapter description ' . Str::random(12);
- $chapter = $this->entities->chapter();
- $chapter->description = $chapterDesc;
+ $book = $this->entities->bookHasChaptersAndPages();
+ $chapter = $book->chapters()->first();
+ $book->description_html = '
A description with HTML within!
';
+ $chapter->description_html = '
A chapter description with HTML within!
';
+ $book->save();
$chapter->save();
- $book = $chapter->book;
- $this->asEditor();
-
- $resp = $this->get($book->getUrl('/export/html'));
- $resp->assertSee($chapterDesc);
+ $resp = $this->asEditor()->get($book->getUrl('/export/html'));
+ $resp->assertSee($book->description_html, false);
+ $resp->assertSee($chapter->description_html, false);
}
public function test_chapter_text_export()
@@ -174,6 +174,16 @@ class ExportTest extends TestCase
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html"');
}
+ public function test_chapter_html_export_shows_html_descriptions()
+ {
+ $chapter = $this->entities->chapter();
+ $chapter->description_html = '
A description with HTML within!
';
+ $chapter->save();
+
+ $resp = $this->asEditor()->get($chapter->getUrl('/export/html'));
+ $resp->assertSee($chapter->description_html, false);
+ }
+
public function test_page_html_export_contains_custom_head_if_set()
{
$page = $this->entities->page();
diff --git a/tests/References/ReferencesTest.php b/tests/References/ReferencesTest.php
index a19e1b901..715f71435 100644
--- a/tests/References/ReferencesTest.php
+++ b/tests/References/ReferencesTest.php
@@ -30,7 +30,30 @@ class ReferencesTest extends TestCase
]);
}
- public function test_references_deleted_on_entity_delete()
+ public function test_references_created_on_book_chapter_bookshelf_update()
+ {
+ $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->shelf()];
+ $shelf = $this->entities->shelf();
+
+ foreach ($entities as $entity) {
+ $entity->refresh();
+ $this->assertDatabaseMissing('references', ['from_id' => $entity->id, 'from_type' => $entity->getMorphClass()]);
+
+ $this->asEditor()->put($entity->getUrl(), [
+ 'name' => 'Reference test',
+ 'description_html' => '
Testing ',
+ ]);
+
+ $this->assertDatabaseHas('references', [
+ 'from_id' => $entity->id,
+ 'from_type' => $entity->getMorphClass(),
+ 'to_id' => $shelf->id,
+ 'to_type' => $shelf->getMorphClass(),
+ ]);
+ }
+ }
+
+ public function test_references_deleted_on_page_delete()
{
$pageA = $this->entities->page();
$pageB = $this->entities->page();
@@ -48,6 +71,25 @@ class ReferencesTest extends TestCase
$this->assertDatabaseMissing('references', ['to_id' => $pageA->id, 'to_type' => $pageA->getMorphClass()]);
}
+ public function test_references_from_deleted_on_book_chapter_shelf_delete()
+ {
+ $entities = [$this->entities->chapter(), $this->entities->book(), $this->entities->shelf()];
+ $shelf = $this->entities->shelf();
+
+ foreach ($entities as $entity) {
+ $this->createReference($entity, $shelf);
+ $this->assertDatabaseHas('references', ['from_id' => $entity->id, 'from_type' => $entity->getMorphClass()]);
+
+ $this->asEditor()->delete($entity->getUrl());
+ app(TrashCan::class)->empty();
+
+ $this->assertDatabaseMissing('references', [
+ 'from_id' => $entity->id,
+ 'from_type' => $entity->getMorphClass()
+ ]);
+ }
+ }
+
public function test_references_to_count_visible_on_entity_show_view()
{
$entities = $this->entities->all();
@@ -60,13 +102,13 @@ class ReferencesTest extends TestCase
foreach ($entities as $entity) {
$resp = $this->get($entity->getUrl());
- $resp->assertSee('Referenced on 1 page');
- $resp->assertDontSee('Referenced on 1 pages');
+ $resp->assertSee('Referenced by 1 item');
+ $resp->assertDontSee('Referenced by 1 items');
}
$this->createReference($otherPage, $entities['page']);
$resp = $this->get($entities['page']->getUrl());
- $resp->assertSee('Referenced on 2 pages');
+ $resp->assertSee('Referenced by 2 items');
}
public function test_references_to_visible_on_references_page()
@@ -203,6 +245,32 @@ class ReferencesTest extends TestCase
$this->assertEquals($expected, $page->markdown);
}
+ public function test_description_links_from_book_chapter_shelf_updated_on_url_change()
+ {
+ $entities = [$this->entities->chapter(), $this->entities->book(), $this->entities->shelf()];
+ $shelf = $this->entities->shelf();
+ $this->asEditor();
+
+ foreach ($entities as $entity) {
+ $this->put($entity->getUrl(), [
+ 'name' => 'Reference test',
+ 'description_html' => '
Testing ',
+ ]);
+ }
+
+ $oldUrl = $shelf->getUrl();
+ $this->put($shelf->getUrl(), ['name' => 'My updated shelf link']);
+ $shelf->refresh();
+ $this->assertNotEquals($oldUrl, $shelf->getUrl());
+
+ foreach ($entities as $entity) {
+ $oldHtml = $entity->description_html;
+ $entity->refresh();
+ $this->assertNotEquals($oldHtml, $entity->description_html);
+ $this->assertStringContainsString($shelf->getUrl(), $entity->description_html);
+ }
+ }
+
protected function createReference(Model $from, Model $to)
{
(new Reference())->forceFill([
diff --git a/tests/Settings/RegenerateReferencesTest.php b/tests/Settings/RegenerateReferencesTest.php
index 25832b03e..0511f2624 100644
--- a/tests/Settings/RegenerateReferencesTest.php
+++ b/tests/Settings/RegenerateReferencesTest.php
@@ -21,7 +21,7 @@ class RegenerateReferencesTest extends TestCase
public function test_action_runs_reference_regen()
{
$this->mock(ReferenceStore::class)
- ->shouldReceive('updateForAllPages')
+ ->shouldReceive('updateForAll')
->once();
$resp = $this->asAdmin()->post('/settings/maintenance/regenerate-references');
@@ -45,7 +45,7 @@ class RegenerateReferencesTest extends TestCase
public function test_action_failed_shown_as_error_notification()
{
$this->mock(ReferenceStore::class)
- ->shouldReceive('updateForAllPages')
+ ->shouldReceive('updateForAll')
->andThrow(\Exception::class, 'A badger stopped the task');
$resp = $this->asAdmin()->post('/settings/maintenance/regenerate-references');