mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
commit
9de85283cd
@ -56,4 +56,13 @@ class Book extends Entity
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,4 +51,13 @@ class Chapter extends Entity
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class RegeneratePermissions extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:regenerate-permissions';
|
||||
protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -46,7 +46,14 @@ class RegeneratePermissions extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = \DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
}
|
||||
|
||||
$this->permissionService->buildJointPermissions();
|
||||
|
||||
\DB::setDefaultConnection($connection);
|
||||
$this->comment('Permissions regenerated');
|
||||
}
|
||||
}
|
||||
|
53
app/Console/Commands/RegenerateSearch.php
Normal file
53
app/Console/Commands/RegenerateSearch.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\SearchService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegenerateSearch extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(SearchService $searchService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = \DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
}
|
||||
|
||||
$this->searchService->indexAllEntities();
|
||||
\DB::setDefaultConnection($connection);
|
||||
$this->comment('Search index regenerated');
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console;
|
||||
<?php namespace BookStack\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
@ -13,10 +11,11 @@ class Kernel extends ConsoleKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
\BookStack\Console\Commands\ClearViews::class,
|
||||
\BookStack\Console\Commands\ClearActivity::class,
|
||||
\BookStack\Console\Commands\ClearRevisions::class,
|
||||
\BookStack\Console\Commands\RegeneratePermissions::class,
|
||||
Commands\ClearViews::class,
|
||||
Commands\ClearActivity::class,
|
||||
Commands\ClearRevisions::class,
|
||||
Commands\RegeneratePermissions::class,
|
||||
Commands\RegenerateSearch::class
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
protected $fieldsToSearch = ['name', 'description'];
|
||||
public $textField = 'description';
|
||||
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
@ -65,6 +65,15 @@ class Entity extends Ownable
|
||||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related search terms.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function searchTerms()
|
||||
{
|
||||
return $this->morphMany(SearchTerm::class, 'entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this entities restrictions.
|
||||
*/
|
||||
@ -153,67 +162,19 @@ class Entity extends Ownable
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a full-text search on this entity.
|
||||
* @param string[] $fieldsToSearch
|
||||
* @param string[] $terms
|
||||
* @param string[] array $wheres
|
||||
* Get the body text of this entity.
|
||||
* @return mixed
|
||||
*/
|
||||
public function fullTextSearchQuery($terms, $wheres = [])
|
||||
public function getText()
|
||||
{
|
||||
$exactTerms = [];
|
||||
$fuzzyTerms = [];
|
||||
$search = static::newQuery();
|
||||
|
||||
foreach ($terms as $key => $term) {
|
||||
$term = htmlentities($term, ENT_QUOTES);
|
||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||
if (preg_match('/".*?"/', $term) || is_numeric($term)) {
|
||||
$term = str_replace('"', '', $term);
|
||||
$exactTerms[] = '%' . $term . '%';
|
||||
} else {
|
||||
$term = '' . $term . '*';
|
||||
if ($term !== '*') $fuzzyTerms[] = $term;
|
||||
}
|
||||
}
|
||||
|
||||
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
|
||||
|
||||
|
||||
// Perform fulltext search if relevant terms exist.
|
||||
if ($isFuzzy) {
|
||||
$termString = implode(' ', $fuzzyTerms);
|
||||
$fields = implode(',', $this->fieldsToSearch);
|
||||
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
}
|
||||
|
||||
// Ensure at least one exact term matches if in search
|
||||
if (count($exactTerms) > 0) {
|
||||
$search = $search->where(function ($query) use ($exactTerms) {
|
||||
foreach ($exactTerms as $exactTerm) {
|
||||
foreach ($this->fieldsToSearch as $field) {
|
||||
$query->orWhere($field, 'like', $exactTerm);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
|
||||
|
||||
// Add additional where terms
|
||||
foreach ($wheres as $whereTerm) {
|
||||
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
||||
}
|
||||
|
||||
// Load in relations
|
||||
if ($this->isA('page')) {
|
||||
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
||||
} else if ($this->isA('chapter')) {
|
||||
$search = $search->with('book');
|
||||
}
|
||||
|
||||
return $search->orderBy($orderBy, 'desc');
|
||||
return $this->{$this->textField};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery(){return '';}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\SearchService;
|
||||
use BookStack\Services\ViewService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -8,16 +9,19 @@ class SearchController extends Controller
|
||||
{
|
||||
protected $entityRepo;
|
||||
protected $viewService;
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* SearchController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param ViewService $viewService
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, ViewService $viewService)
|
||||
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->viewService = $viewService;
|
||||
$this->searchService = $searchService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@ -27,84 +31,26 @@ class SearchController extends Controller
|
||||
* @return \Illuminate\View\View
|
||||
* @internal param string $searchTerm
|
||||
*/
|
||||
public function searchAll(Request $request)
|
||||
public function search(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) {
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
|
||||
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
|
||||
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
|
||||
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
|
||||
|
||||
$page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
|
||||
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
|
||||
|
||||
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
||||
$hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
|
||||
|
||||
return view('search/all', [
|
||||
'pages' => $pages,
|
||||
'books' => $books,
|
||||
'chapters' => $chapters,
|
||||
'searchTerm' => $searchTerm
|
||||
'entities' => $results['results'],
|
||||
'totalResults' => $results['total'],
|
||||
'searchTerm' => $searchTerm,
|
||||
'hasNextPage' => $hasNextPage,
|
||||
'nextPageLink' => $nextPageLink
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the pages in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchPages(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $pages,
|
||||
'title' => trans('entities.search_results_page'),
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the chapters in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchChapters(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm]));
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $chapters,
|
||||
'title' => trans('entities.search_results_chapter'),
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the books in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchBooks(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $books,
|
||||
'title' => trans('entities.search_results_book'),
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches all entities within a book.
|
||||
@ -115,16 +61,24 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchBook(Request $request, $bookId)
|
||||
{
|
||||
if (!$request->has('term')) {
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$searchWhereTerms = [['book_id', '=', $bookId]];
|
||||
$pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
|
||||
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
|
||||
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
$term = $request->get('term', '');
|
||||
$results = $this->searchService->searchBook($bookId, $term);
|
||||
return view('partials/entity-list', ['entities' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches all entities within a chapter.
|
||||
* @param Request $request
|
||||
* @param integer $chapterId
|
||||
* @return \Illuminate\View\View
|
||||
* @internal param string $searchTerm
|
||||
*/
|
||||
public function searchChapter(Request $request, $chapterId)
|
||||
{
|
||||
$term = $request->get('term', '');
|
||||
$results = $this->searchService->searchChapter($chapterId, $term);
|
||||
return view('partials/entity-list', ['entities' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a list of entities and return a partial HTML response of matching entities.
|
||||
@ -134,18 +88,13 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchEntitiesAjax(Request $request)
|
||||
{
|
||||
$entities = collect();
|
||||
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
|
||||
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
foreach (['page', 'chapter', 'book'] as $entityType) {
|
||||
if ($entityTypes->contains($entityType)) {
|
||||
$entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
|
||||
}
|
||||
}
|
||||
$entities = $entities->sortByDesc('title_relevance');
|
||||
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
|
||||
$entities = $this->searchService->searchEntities($searchTerm)['results'];
|
||||
} else {
|
||||
$entityNames = $entityTypes->map(function ($type) {
|
||||
return 'BookStack\\' . ucfirst($type);
|
||||
|
13
app/Page.php
13
app/Page.php
@ -8,8 +8,7 @@ class Page extends Entity
|
||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||
|
||||
protected $with = ['book'];
|
||||
|
||||
protected $fieldsToSearch = ['name', 'text'];
|
||||
public $textField = 'text';
|
||||
|
||||
/**
|
||||
* Converts this page into a simplified array.
|
||||
@ -96,4 +95,14 @@ class Page extends Entity
|
||||
return mb_convert_encoding($text, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @param bool $withContent
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery($withContent = false)
|
||||
{ $htmlQuery = $withContent ? 'html' : "'' as html";
|
||||
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ use BookStack\Page;
|
||||
use BookStack\PageRevision;
|
||||
use BookStack\Services\AttachmentService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Services\SearchService;
|
||||
use BookStack\Services\ViewService;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
@ -59,13 +60,12 @@ class EntityRepo
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
* @var array
|
||||
* @var SearchService
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* EntityService constructor.
|
||||
* EntityRepo constructor.
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
@ -73,10 +73,12 @@ class EntityRepo
|
||||
* @param ViewService $viewService
|
||||
* @param PermissionService $permissionService
|
||||
* @param TagRepo $tagRepo
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(
|
||||
Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
|
||||
ViewService $viewService, PermissionService $permissionService, TagRepo $tagRepo
|
||||
ViewService $viewService, PermissionService $permissionService,
|
||||
TagRepo $tagRepo, SearchService $searchService
|
||||
)
|
||||
{
|
||||
$this->book = $book;
|
||||
@ -91,6 +93,7 @@ class EntityRepo
|
||||
$this->viewService = $viewService;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->tagRepo = $tagRepo;
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -216,6 +219,7 @@ class EntityRepo
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|callable $additionalQuery
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
@ -234,6 +238,7 @@ class EntityRepo
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|callable $additionalQuery
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
@ -327,7 +332,7 @@ class EntityRepo
|
||||
if ($rawEntity->entity_type === 'BookStack\\Page') {
|
||||
$entities[$index] = $this->page->newFromBuilder($rawEntity);
|
||||
if ($renderPages) {
|
||||
$entities[$index]->html = $rawEntity->description;
|
||||
$entities[$index]->html = $rawEntity->html;
|
||||
$entities[$index]->html = $this->renderPage($entities[$index]);
|
||||
};
|
||||
} else if ($rawEntity->entity_type === 'BookStack\\Chapter') {
|
||||
@ -354,6 +359,7 @@ class EntityRepo
|
||||
* Get the child items for a chapter sorted by priority but
|
||||
* with draft items floated to the top.
|
||||
* @param Chapter $chapter
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getChapterChildren(Chapter $chapter)
|
||||
{
|
||||
@ -361,56 +367,6 @@ class EntityRepo
|
||||
->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search entities of a type via a given query.
|
||||
* @param string $type
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms));
|
||||
$q = $this->addAdvancedSearchQueries($q, $term);
|
||||
$entities = $q->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
|
||||
// Highlight page content
|
||||
if ($type === 'page') {
|
||||
//lookahead/behind assertions ensures cut between words
|
||||
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
|
||||
|
||||
foreach ($entities as $page) {
|
||||
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
|
||||
//delimiter between occurrences
|
||||
$results = [];
|
||||
foreach ($matches as $line) {
|
||||
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
|
||||
}
|
||||
$matchLimit = 6;
|
||||
if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit);
|
||||
$result = join('... ', $results);
|
||||
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
|
||||
if (strlen($result) < 5) $result = $page->getExcerpt(80);
|
||||
|
||||
$page->searchSnippet = $result;
|
||||
}
|
||||
return $entities;
|
||||
}
|
||||
|
||||
// Highlight chapter/book content
|
||||
foreach ($entities as $entity) {
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $entity->getExcerpt(100));
|
||||
$entity->searchSnippet = $result;
|
||||
}
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next sequential priority for a new child element in the given book.
|
||||
@ -492,104 +448,7 @@ class EntityRepo
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a string of search terms by turning
|
||||
* it into an array of terms.
|
||||
* Keeps quoted terms together.
|
||||
* @param $termString
|
||||
* @return array
|
||||
*/
|
||||
public function prepareSearchTerms($termString)
|
||||
{
|
||||
$termString = $this->cleanSearchTermString($termString);
|
||||
preg_match_all('/(".*?")/', $termString, $matches);
|
||||
$terms = [];
|
||||
if (count($matches[1]) > 0) {
|
||||
foreach ($matches[1] as $match) {
|
||||
$terms[] = $match;
|
||||
}
|
||||
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
|
||||
}
|
||||
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any special search notation that should not
|
||||
* be used in a full-text search.
|
||||
* @param $termString
|
||||
* @return mixed
|
||||
*/
|
||||
protected function cleanSearchTermString($termString)
|
||||
{
|
||||
// Strip tag searches
|
||||
$termString = preg_replace('/\[.*?\]/', '', $termString);
|
||||
// Reduced multiple spacing into single spacing
|
||||
$termString = preg_replace("/\s{2,}/", " ", $termString);
|
||||
return $termString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available query operators as a regex escaped list.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRegexEscapedOperators()
|
||||
{
|
||||
$escapedOperators = [];
|
||||
foreach ($this->queryOperators as $operator) {
|
||||
$escapedOperators[] = preg_quote($operator);
|
||||
}
|
||||
return join('|', $escapedOperators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses advanced search notations and adds them to the db query.
|
||||
* @param $query
|
||||
* @param $termString
|
||||
* @return mixed
|
||||
*/
|
||||
protected function addAdvancedSearchQueries($query, $termString)
|
||||
{
|
||||
$escapedOperators = $this->getRegexEscapedOperators();
|
||||
// Look for tag searches
|
||||
preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
|
||||
if (count($tags[0]) > 0) {
|
||||
$this->applyTagSearches($query, $tags);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply extracted tag search terms onto a entity query.
|
||||
* @param $query
|
||||
* @param $tags
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearches($query, $tags) {
|
||||
$query->where(function($query) use ($tags) {
|
||||
foreach ($tags[1] as $index => $tagName) {
|
||||
$query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
|
||||
$tagOperator = $tags[3][$index];
|
||||
$tagValue = $tags[4][$index];
|
||||
if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
|
||||
if (is_numeric($tagValue) && $tagOperator !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
|
||||
$query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
|
||||
} else {
|
||||
$query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
} else {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new entity from request input.
|
||||
@ -608,12 +467,13 @@ class EntityRepo
|
||||
$entity->updated_by = user()->id;
|
||||
$isChapter ? $book->chapters()->save($entity) : $entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
$this->searchService->indexEntity($entity);
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entity details from request input.
|
||||
* Use for books and chapters
|
||||
* Used for books and chapters
|
||||
* @param string $type
|
||||
* @param Entity $entityModel
|
||||
* @param array $input
|
||||
@ -628,6 +488,7 @@ class EntityRepo
|
||||
$entityModel->updated_by = user()->id;
|
||||
$entityModel->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entityModel);
|
||||
$this->searchService->indexEntity($entityModel);
|
||||
return $entityModel;
|
||||
}
|
||||
|
||||
@ -711,7 +572,7 @@ class EntityRepo
|
||||
|
||||
$draftPage->save();
|
||||
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
|
||||
|
||||
$this->searchService->indexEntity($draftPage);
|
||||
return $draftPage;
|
||||
}
|
||||
|
||||
@ -961,6 +822,8 @@ class EntityRepo
|
||||
$this->savePageRevision($page, $input['summary']);
|
||||
}
|
||||
|
||||
$this->searchService->indexEntity($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
@ -1064,6 +927,7 @@ class EntityRepo
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->updated_by = user()->id;
|
||||
$page->save();
|
||||
$this->searchService->indexEntity($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
@ -1156,6 +1020,7 @@ class EntityRepo
|
||||
$book->views()->delete();
|
||||
$book->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($book);
|
||||
$this->searchService->deleteEntityTerms($book);
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
@ -1175,6 +1040,7 @@ class EntityRepo
|
||||
$chapter->views()->delete();
|
||||
$chapter->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($chapter);
|
||||
$this->searchService->deleteEntityTerms($chapter);
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
@ -1190,6 +1056,7 @@ class EntityRepo
|
||||
$page->revisions()->delete();
|
||||
$page->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($page);
|
||||
$this->searchService->deleteEntityTerms($page);
|
||||
|
||||
// Delete Attached Files
|
||||
$attachmentService = app(AttachmentService::class);
|
||||
|
18
app/SearchTerm.php
Normal file
18
app/SearchTerm.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class SearchTerm extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the entity that this term belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
}
|
@ -479,8 +479,7 @@ class PermissionService
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
|
||||
$pageContentSelect = $fetchPageContent ? 'html' : "''";
|
||||
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
|
||||
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
|
||||
$query->where('draft', '=', 0);
|
||||
if (!$filterDrafts) {
|
||||
$query->orWhere(function($query) {
|
||||
@ -488,7 +487,7 @@ class PermissionService
|
||||
});
|
||||
}
|
||||
});
|
||||
$chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id);
|
||||
$chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
|
||||
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
|
||||
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
|
||||
|
||||
@ -514,7 +513,7 @@ class PermissionService
|
||||
* @param string $entityType
|
||||
* @param Builder|Entity $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
* @return Builder
|
||||
*/
|
||||
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
|
||||
{
|
||||
@ -540,7 +539,7 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set a a polymorphic relation.
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
|
472
app/Services/SearchService.php
Normal file
472
app/Services/SearchService.php
Normal file
@ -0,0 +1,472 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\SearchTerm;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SearchService
|
||||
{
|
||||
protected $searchTerm;
|
||||
protected $book;
|
||||
protected $chapter;
|
||||
protected $page;
|
||||
protected $db;
|
||||
protected $permissionService;
|
||||
protected $entities;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
* @var array
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
|
||||
/**
|
||||
* SearchService constructor.
|
||||
* @param SearchTerm $searchTerm
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Connection $db
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
|
||||
{
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->db = $db;
|
||||
$this->entities = [
|
||||
'page' => $this->page,
|
||||
'chapter' => $this->chapter,
|
||||
'book' => $this->book
|
||||
];
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all entities in the system.
|
||||
* @param string $searchString
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @return array[int, Collection];
|
||||
*/
|
||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = array_keys($this->entities);
|
||||
$entityTypesToSearch = $entityTypes;
|
||||
$results = collect();
|
||||
|
||||
if ($entityType !== 'all') {
|
||||
$entityTypesToSearch = $entityType;
|
||||
} else if (isset($terms['filters']['type'])) {
|
||||
$entityTypesToSearch = explode('|', $terms['filters']['type']);
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
|
||||
foreach ($entityTypesToSearch as $entityType) {
|
||||
if (!in_array($entityType, $entityTypes)) continue;
|
||||
$search = $this->searchEntityTable($terms, $entityType, $page, $count);
|
||||
$total += $this->searchEntityTable($terms, $entityType, $page, $count, true);
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'count' => count($results),
|
||||
'results' => $results->sortByDesc('score')
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search a book for entities
|
||||
* @param integer $bookId
|
||||
* @param string $searchString
|
||||
* @return Collection
|
||||
*/
|
||||
public function searchBook($bookId, $searchString)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = ['page', 'chapter'];
|
||||
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
|
||||
|
||||
$results = collect();
|
||||
foreach ($entityTypesToSearch as $entityType) {
|
||||
if (!in_array($entityType, $entityTypes)) continue;
|
||||
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
return $results->sortByDesc('score')->take(20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a book for entities
|
||||
* @param integer $chapterId
|
||||
* @param string $searchString
|
||||
* @return Collection
|
||||
*/
|
||||
public function searchChapter($chapterId, $searchString)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
|
||||
return $pages->sortByDesc('score');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search across a particular entity type.
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param bool $getCount Return the total count of the search
|
||||
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
||||
*/
|
||||
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
|
||||
{
|
||||
$query = $this->buildEntitySearchQuery($terms, $entityType);
|
||||
if ($getCount) return $query->count();
|
||||
|
||||
$query = $query->skip(($page-1) * $count)->take($count);
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a search query for an entity
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function buildEntitySearchQuery($terms, $entityType = 'page')
|
||||
{
|
||||
$entity = $this->getEntity($entityType);
|
||||
$entitySelect = $entity->newQuery();
|
||||
|
||||
// Handle normal search terms
|
||||
if (count($terms['search']) > 0) {
|
||||
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
||||
$subQuery->where(function(Builder $query) use ($terms) {
|
||||
foreach ($terms['search'] as $inputTerm) {
|
||||
$query->orWhere('term', 'like', $inputTerm .'%');
|
||||
}
|
||||
})->groupBy('entity_type', 'entity_id');
|
||||
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
|
||||
$join->on('id', '=', 'entity_id');
|
||||
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
|
||||
$entitySelect->mergeBindings($subQuery);
|
||||
}
|
||||
|
||||
// Handle exact term matching
|
||||
if (count($terms['exact']) > 0) {
|
||||
$entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
|
||||
foreach ($terms['exact'] as $inputTerm) {
|
||||
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
|
||||
$query->where('name', 'like', '%'.$inputTerm .'%')
|
||||
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle tag searches
|
||||
foreach ($terms['tags'] as $inputTerm) {
|
||||
$this->applyTagSearch($entitySelect, $inputTerm);
|
||||
}
|
||||
|
||||
// Handle filters
|
||||
foreach ($terms['filters'] as $filterTerm => $filterValue) {
|
||||
$functionName = camel_case('filter_' . $filterTerm);
|
||||
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
|
||||
}
|
||||
|
||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse a search string into components.
|
||||
* @param $searchString
|
||||
* @return array
|
||||
*/
|
||||
protected function parseSearchString($searchString)
|
||||
{
|
||||
$terms = [
|
||||
'search' => [],
|
||||
'exact' => [],
|
||||
'tags' => [],
|
||||
'filters' => []
|
||||
];
|
||||
|
||||
$patterns = [
|
||||
'exact' => '/"(.*?)"/',
|
||||
'tags' => '/\[(.*?)\]/',
|
||||
'filters' => '/\{(.*?)\}/'
|
||||
];
|
||||
|
||||
// Parse special terms
|
||||
foreach ($patterns as $termType => $pattern) {
|
||||
$matches = [];
|
||||
preg_match_all($pattern, $searchString, $matches);
|
||||
if (count($matches) > 0) {
|
||||
$terms[$termType] = $matches[1];
|
||||
$searchString = preg_replace($pattern, '', $searchString);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse standard terms
|
||||
foreach (explode(' ', trim($searchString)) as $searchTerm) {
|
||||
if ($searchTerm !== '') $terms['search'][] = $searchTerm;
|
||||
}
|
||||
|
||||
// Split filter values out
|
||||
$splitFilters = [];
|
||||
foreach ($terms['filters'] as $filter) {
|
||||
$explodedFilter = explode(':', $filter, 2);
|
||||
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
|
||||
}
|
||||
$terms['filters'] = $splitFilters;
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available query operators as a regex escaped list.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRegexEscapedOperators()
|
||||
{
|
||||
$escapedOperators = [];
|
||||
foreach ($this->queryOperators as $operator) {
|
||||
$escapedOperators[] = preg_quote($operator);
|
||||
}
|
||||
return join('|', $escapedOperators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a tag search term onto a entity query.
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $tagTerm
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
|
||||
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||
$query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
|
||||
$tagName = $tagSplit[1];
|
||||
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
|
||||
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
|
||||
$validOperator = in_array($tagOperator, $this->queryOperators);
|
||||
if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
|
||||
if (!empty($tagName)) $query->where('name', '=', $tagName);
|
||||
if (is_numeric($tagValue) && $tagOperator !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
|
||||
$query->whereRaw("value ${tagOperator} ${tagValue}");
|
||||
} else {
|
||||
$query->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
} else {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
});
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance via type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
protected function getEntity($type)
|
||||
{
|
||||
return $this->entities[strtolower($type)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Index the given entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function indexEntity(Entity $entity)
|
||||
{
|
||||
$this->deleteEntityTerms($entity);
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
|
||||
$terms = array_merge($nameTerms, $bodyTerms);
|
||||
foreach ($terms as $index => $term) {
|
||||
$terms[$index]['entity_type'] = $entity->getMorphClass();
|
||||
$terms[$index]['entity_id'] = $entity->id;
|
||||
}
|
||||
$this->searchTerm->newQuery()->insert($terms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index multiple Entities at once
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function indexEntities($entities) {
|
||||
$terms = [];
|
||||
foreach ($entities as $entity) {
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
|
||||
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
|
||||
$term['entity_id'] = $entity->id;
|
||||
$term['entity_type'] = $entity->getMorphClass();
|
||||
$terms[] = $term;
|
||||
}
|
||||
}
|
||||
|
||||
$chunkedTerms = array_chunk($terms, 500);
|
||||
foreach ($chunkedTerms as $termChunk) {
|
||||
$this->searchTerm->newQuery()->insert($termChunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete and re-index the terms for all entities in the system.
|
||||
*/
|
||||
public function indexAllEntities()
|
||||
{
|
||||
$this->searchTerm->truncate();
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->chunk(1000, function ($books) {
|
||||
$this->indexEntities($books);
|
||||
});
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->chunk(1000, function ($chapters) {
|
||||
$this->indexEntities($chapters);
|
||||
});
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->chunk(1000, function ($pages) {
|
||||
$this->indexEntities($pages);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete related Entity search terms.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function deleteEntityTerms(Entity $entity)
|
||||
{
|
||||
$entity->searchTerms()->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scored term array from the given text.
|
||||
* @param $text
|
||||
* @param float|int $scoreAdjustment
|
||||
* @return array
|
||||
*/
|
||||
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
|
||||
{
|
||||
$tokenMap = []; // {TextToken => OccurrenceCount}
|
||||
$splitText = explode(' ', $text);
|
||||
foreach ($splitText as $token) {
|
||||
if ($token === '') continue;
|
||||
if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
|
||||
$tokenMap[$token]++;
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($tokenMap as $token => $count) {
|
||||
$terms[] = [
|
||||
'term' => $token,
|
||||
'score' => $count * $scoreAdjustment
|
||||
];
|
||||
}
|
||||
return $terms;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Custom entity search filters
|
||||
*/
|
||||
|
||||
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('updated_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('updated_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('created_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
$query->where('created_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') return;
|
||||
if ($input === 'me') $input = user()->id;
|
||||
$query->where('created_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') return;
|
||||
if ($input === 'me') $input = user()->id;
|
||||
$query->where('updated_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('name', 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
|
||||
|
||||
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where($model->textField, 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('restricted', '=', true);
|
||||
}
|
||||
|
||||
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereHas('views', function($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereDoesntHave('views', function($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -100,7 +100,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'log' => 'single',
|
||||
'log' => env('APP_LOGGING', 'single'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -12,9 +12,10 @@ class AddSearchIndexes extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::statement('ALTER TABLE pages ADD FULLTEXT search(name, text)');
|
||||
DB::statement('ALTER TABLE books ADD FULLTEXT search(name, description)');
|
||||
DB::statement('ALTER TABLE chapters ADD FULLTEXT search(name, description)');
|
||||
$prefix = DB::getTablePrefix();
|
||||
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
|
||||
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
|
||||
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,9 +12,10 @@ class FulltextWeighting extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::statement('ALTER TABLE pages ADD FULLTEXT name_search(name)');
|
||||
DB::statement('ALTER TABLE books ADD FULLTEXT name_search(name)');
|
||||
DB::statement('ALTER TABLE chapters ADD FULLTEXT name_search(name)');
|
||||
$prefix = DB::getTablePrefix();
|
||||
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
|
||||
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
|
||||
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateSearchIndexTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('search_terms', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('term', 200);
|
||||
$table->string('entity_type', 100);
|
||||
$table->integer('entity_id');
|
||||
$table->integer('score');
|
||||
|
||||
$table->index('term');
|
||||
$table->index('entity_type');
|
||||
$table->index(['entity_type', 'entity_id']);
|
||||
$table->index('score');
|
||||
});
|
||||
|
||||
// Drop search indexes
|
||||
Schema::table('pages', function(Blueprint $table) {
|
||||
$table->dropIndex('search');
|
||||
$table->dropIndex('name_search');
|
||||
});
|
||||
Schema::table('books', function(Blueprint $table) {
|
||||
$table->dropIndex('search');
|
||||
$table->dropIndex('name_search');
|
||||
});
|
||||
Schema::table('chapters', function(Blueprint $table) {
|
||||
$table->dropIndex('search');
|
||||
$table->dropIndex('name_search');
|
||||
});
|
||||
|
||||
app(\BookStack\Services\SearchService::class)->indexAllEntities();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$prefix = DB::getTablePrefix();
|
||||
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
|
||||
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
|
||||
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
|
||||
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
|
||||
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
|
||||
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
|
||||
|
||||
Schema::dropIfExists('search_terms');
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ class DummyContentSeeder extends Seeder
|
||||
$user->attachRole($role);
|
||||
|
||||
|
||||
$books = factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
|
||||
factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
|
||||
->each(function($book) use ($user) {
|
||||
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
|
||||
->each(function($chapter) use ($user, $book){
|
||||
@ -28,7 +28,7 @@ class DummyContentSeeder extends Seeder
|
||||
$book->pages()->saveMany($pages);
|
||||
});
|
||||
|
||||
$restrictionService = app(\BookStack\Services\PermissionService::class);
|
||||
$restrictionService->buildJointPermissions();
|
||||
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
|
||||
app(\BookStack\Services\SearchService::class)->indexAllEntities();
|
||||
}
|
||||
}
|
||||
|
67
gulpfile.js
67
gulpfile.js
@ -1,8 +1,63 @@
|
||||
var elixir = require('laravel-elixir');
|
||||
const argv = require('yargs').argv;
|
||||
const gulp = require('gulp'),
|
||||
plumber = require('gulp-plumber');
|
||||
const autoprefixer = require('gulp-autoprefixer');
|
||||
const uglify = require('gulp-uglify');
|
||||
const minifycss = require('gulp-clean-css');
|
||||
const sass = require('gulp-sass');
|
||||
const browserify = require("browserify");
|
||||
const source = require('vinyl-source-stream');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const babelify = require("babelify");
|
||||
const watchify = require("watchify");
|
||||
const envify = require("envify");
|
||||
const gutil = require("gulp-util");
|
||||
|
||||
elixir(mix => {
|
||||
mix.sass('styles.scss');
|
||||
mix.sass('print-styles.scss');
|
||||
mix.sass('export-styles.scss');
|
||||
mix.browserify('global.js', './public/js/common.js');
|
||||
if (argv.production) process.env.NODE_ENV = 'production';
|
||||
|
||||
gulp.task('styles', () => {
|
||||
let chain = gulp.src(['resources/assets/sass/**/*.scss'])
|
||||
.pipe(plumber({
|
||||
errorHandler: function (error) {
|
||||
console.log(error.message);
|
||||
this.emit('end');
|
||||
}}))
|
||||
.pipe(sass())
|
||||
.pipe(autoprefixer('last 2 versions'));
|
||||
if (argv.production) chain = chain.pipe(minifycss());
|
||||
return chain.pipe(gulp.dest('public/css/'));
|
||||
});
|
||||
|
||||
|
||||
function scriptTask(watch=false) {
|
||||
|
||||
let props = {
|
||||
basedir: 'resources/assets/js',
|
||||
debug: true,
|
||||
entries: ['global.js']
|
||||
};
|
||||
|
||||
let bundler = watch ? watchify(browserify(props), { poll: true }) : browserify(props);
|
||||
bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']});
|
||||
function rebundle() {
|
||||
let stream = bundler.bundle();
|
||||
stream = stream.pipe(source('common.js'));
|
||||
if (argv.production) stream = stream.pipe(buffer()).pipe(uglify());
|
||||
return stream.pipe(gulp.dest('public/js/'));
|
||||
}
|
||||
bundler.on('update', function() {
|
||||
rebundle();
|
||||
gutil.log('Rebundle...');
|
||||
});
|
||||
bundler.on('log', gutil.log);
|
||||
return rebundle();
|
||||
}
|
||||
|
||||
gulp.task('scripts', () => {scriptTask(false)});
|
||||
gulp.task('scripts-watch', () => {scriptTask(true)});
|
||||
|
||||
gulp.task('default', ['styles', 'scripts-watch'], () => {
|
||||
gulp.watch("resources/assets/sass/**/*.scss", ['styles']);
|
||||
});
|
||||
|
||||
gulp.task('build', ['styles', 'scripts']);
|
42
package.json
42
package.json
@ -1,24 +1,44 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "gulp --production",
|
||||
"dev": "gulp watch",
|
||||
"watch": "gulp watch"
|
||||
"build": "gulp build",
|
||||
"production": "gulp build --production",
|
||||
"dev": "gulp",
|
||||
"watch": "gulp"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babelify": "^7.3.0",
|
||||
"browserify": "^14.3.0",
|
||||
"envify": "^4.0.0",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-autoprefixer": "3.1.1",
|
||||
"gulp-clean-css": "^3.0.4",
|
||||
"gulp-minify-css": "1.2.4",
|
||||
"gulp-plumber": "1.1.0",
|
||||
"gulp-sass": "3.1.0",
|
||||
"gulp-uglify": "2.1.2",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"watchify": "^3.9.0",
|
||||
"yargs": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"angular": "^1.5.5",
|
||||
"angular-animate": "^1.5.5",
|
||||
"angular-resource": "^1.5.5",
|
||||
"angular-sanitize": "^1.5.5",
|
||||
"angular-ui-sortable": "^0.15.0",
|
||||
"angular-ui-sortable": "^0.17.0",
|
||||
"axios": "^0.16.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"clipboard": "^1.5.16",
|
||||
"dropzone": "^4.0.1",
|
||||
"gulp": "^3.9.0",
|
||||
"laravel-elixir": "^6.0.0-11",
|
||||
"laravel-elixir-browserify-official": "^0.1.3",
|
||||
"marked": "^0.3.5",
|
||||
"moment": "^2.12.0"
|
||||
"gulp-util": "^3.0.8",
|
||||
"markdown-it": "^8.3.1",
|
||||
"markdown-it-task-lists": "^2.0.0",
|
||||
"moment": "^2.12.0",
|
||||
"vue": "^2.2.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^1.5.16"
|
||||
"browser": {
|
||||
"vue": "vue/dist/vue.common.js"
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ These are the great projects used to help build BookStack:
|
||||
* [Dropzone.js](http://www.dropzonejs.com/)
|
||||
* [ZeroClipboard](http://zeroclipboard.org/)
|
||||
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
|
||||
* [Marked](https://github.com/chjj/marked)
|
||||
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
|
||||
* [Moment.js](http://momentjs.com/)
|
||||
* [BarryVD](https://github.com/barryvdh)
|
||||
* [Debugbar](https://github.com/barryvdh/laravel-debugbar)
|
||||
|
@ -1,12 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/en-gb';
|
||||
import editorOptions from "./pages/page-form";
|
||||
const moment = require('moment');
|
||||
require('moment/locale/en-gb');
|
||||
const editorOptions = require("./pages/page-form");
|
||||
|
||||
moment.locale('en-gb');
|
||||
|
||||
export default function (ngApp, events) {
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
|
||||
function ($scope, $attrs, $http, $timeout, imageManagerService) {
|
||||
@ -259,39 +259,6 @@ export default function (ngApp, events) {
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', '$sce', function ($scope, $http, $attrs, $sce) {
|
||||
$scope.searching = false;
|
||||
$scope.searchTerm = '';
|
||||
$scope.searchResults = '';
|
||||
|
||||
$scope.searchBook = function (e) {
|
||||
e.preventDefault();
|
||||
let term = $scope.searchTerm;
|
||||
if (term.length == 0) return;
|
||||
$scope.searching = true;
|
||||
$scope.searchResults = '';
|
||||
let searchUrl = window.baseUrl('/search/book/' + $attrs.bookId);
|
||||
searchUrl += '?term=' + encodeURIComponent(term);
|
||||
$http.get(searchUrl).then((response) => {
|
||||
$scope.searchResults = $sce.trustAsHtml(response.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.checkSearchForm = function () {
|
||||
if ($scope.searchTerm.length < 1) {
|
||||
$scope.searching = false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clearSearch = function () {
|
||||
$scope.searching = false;
|
||||
$scope.searchTerm = '';
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
|
||||
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
"use strict";
|
||||
import DropZone from "dropzone";
|
||||
import markdown from "marked";
|
||||
const DropZone = require("dropzone");
|
||||
const MarkdownIt = require("markdown-it");
|
||||
const mdTasksLists = require('markdown-it-task-lists');
|
||||
|
||||
export default function (ngApp, events) {
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
/**
|
||||
* Common tab controls using simple jQuery functions.
|
||||
@ -214,18 +215,8 @@ export default function (ngApp, events) {
|
||||
}
|
||||
}]);
|
||||
|
||||
let renderer = new markdown.Renderer();
|
||||
// Custom markdown checkbox list item
|
||||
// Attribution: https://github.com/chjj/marked/issues/107#issuecomment-44542001
|
||||
renderer.listitem = function(text) {
|
||||
if (/^\s*\[[x ]\]\s*/.test(text)) {
|
||||
text = text
|
||||
.replace(/^\s*\[ \]\s*/, '<input type="checkbox"/>')
|
||||
.replace(/^\s*\[x\]\s*/, '<input type="checkbox" checked/>');
|
||||
return `<li class="checkbox-item">${text}</li>`;
|
||||
}
|
||||
return `<li>${text}</li>`;
|
||||
};
|
||||
const md = new MarkdownIt();
|
||||
md.use(mdTasksLists, {label: true});
|
||||
|
||||
/**
|
||||
* Markdown input
|
||||
@ -244,20 +235,20 @@ export default function (ngApp, events) {
|
||||
element = element.find('textarea').first();
|
||||
let content = element.val();
|
||||
scope.mdModel = content;
|
||||
scope.mdChange(markdown(content, {renderer: renderer}));
|
||||
scope.mdChange(md.render(content));
|
||||
|
||||
element.on('change input', (event) => {
|
||||
content = element.val();
|
||||
$timeout(() => {
|
||||
scope.mdModel = content;
|
||||
scope.mdChange(markdown(content, {renderer: renderer}));
|
||||
scope.mdChange(md.render(content));
|
||||
});
|
||||
});
|
||||
|
||||
scope.$on('markdown-update', (event, value) => {
|
||||
element.val(value);
|
||||
scope.mdModel = value;
|
||||
scope.mdChange(markdown(value));
|
||||
scope.mdChange(md.render(value));
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
// AngularJS - Create application and load components
|
||||
import angular from "angular";
|
||||
import "angular-resource";
|
||||
import "angular-animate";
|
||||
import "angular-sanitize";
|
||||
import "angular-ui-sortable";
|
||||
|
||||
// Url retrieval function
|
||||
window.baseUrl = function(path) {
|
||||
let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
|
||||
@ -15,11 +8,33 @@ window.baseUrl = function(path) {
|
||||
return basePath + '/' + path;
|
||||
};
|
||||
|
||||
const Vue = require("vue");
|
||||
const axios = require("axios");
|
||||
|
||||
let axiosInstance = axios.create({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
|
||||
'baseURL': window.baseUrl('')
|
||||
}
|
||||
});
|
||||
|
||||
Vue.prototype.$http = axiosInstance;
|
||||
|
||||
require("./vues/vues");
|
||||
|
||||
|
||||
// AngularJS - Create application and load components
|
||||
const angular = require("angular");
|
||||
require("angular-resource");
|
||||
require("angular-animate");
|
||||
require("angular-sanitize");
|
||||
require("angular-ui-sortable");
|
||||
|
||||
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
|
||||
|
||||
// Translation setup
|
||||
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
|
||||
import Translations from "./translations"
|
||||
const Translations = require("./translations");
|
||||
let translator = new Translations(window.translations);
|
||||
window.trans = translator.get.bind(translator);
|
||||
|
||||
@ -47,11 +62,12 @@ class EventManager {
|
||||
}
|
||||
|
||||
window.Events = new EventManager();
|
||||
Vue.prototype.$events = window.Events;
|
||||
|
||||
// Load in angular specific items
|
||||
import Services from './services';
|
||||
import Directives from './directives';
|
||||
import Controllers from './controllers';
|
||||
const Services = require('./services');
|
||||
const Directives = require('./directives');
|
||||
const Controllers = require('./controllers');
|
||||
Services(ngApp, window.Events);
|
||||
Directives(ngApp, window.Events);
|
||||
Controllers(ngApp, window.Events);
|
||||
@ -154,4 +170,4 @@ if(navigator.userAgent.indexOf('MSIE')!==-1
|
||||
}
|
||||
|
||||
// Page specific items
|
||||
import "./pages/page-show";
|
||||
require("./pages/page-show");
|
||||
|
@ -60,7 +60,7 @@ function registerEditorShortcuts(editor) {
|
||||
editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']);
|
||||
}
|
||||
|
||||
export default function() {
|
||||
module.exports = function() {
|
||||
let settings = {
|
||||
selector: '#html-editor',
|
||||
content_css: [
|
||||
@ -68,6 +68,7 @@ export default function() {
|
||||
window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
|
||||
],
|
||||
body_class: 'page-content',
|
||||
browser_spellcheck: true,
|
||||
relative_urls: false,
|
||||
remove_script_host: false,
|
||||
document_base_url: window.baseUrl('/'),
|
||||
@ -213,4 +214,4 @@ export default function() {
|
||||
}
|
||||
};
|
||||
return settings;
|
||||
}
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
"use strict";
|
||||
// Configure ZeroClipboard
|
||||
import Clipboard from "clipboard";
|
||||
const Clipboard = require("clipboard");
|
||||
|
||||
export default window.setupPageShow = function (pageId) {
|
||||
let setupPageShow = window.setupPageShow = function (pageId) {
|
||||
|
||||
// Set up pointer
|
||||
let $pointer = $('#pointer').detach();
|
||||
@ -81,6 +81,12 @@ export default window.setupPageShow = function (pageId) {
|
||||
let $idElem = $(idElem);
|
||||
let color = $('#custom-styles').attr('data-color-light');
|
||||
$idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo();
|
||||
setTimeout(() => {
|
||||
$idElem.addClass('anim').addClass('selectFade').css('background-color', '');
|
||||
setTimeout(() => {
|
||||
$idElem.removeClass('selectFade');
|
||||
}, 3000);
|
||||
}, 100);
|
||||
} else {
|
||||
$('.page-content').find(':contains("' + text + '")').smoothScrollTo();
|
||||
}
|
||||
@ -151,3 +157,5 @@ export default window.setupPageShow = function (pageId) {
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports = setupPageShow;
|
@ -44,4 +44,4 @@ class Translator {
|
||||
|
||||
}
|
||||
|
||||
export default Translator
|
||||
module.exports = Translator;
|
||||
|
44
resources/assets/js/vues/entity-search.js
Normal file
44
resources/assets/js/vues/entity-search.js
Normal file
@ -0,0 +1,44 @@
|
||||
let data = {
|
||||
id: null,
|
||||
type: '',
|
||||
searching: false,
|
||||
searchTerm: '',
|
||||
searchResults: '',
|
||||
};
|
||||
|
||||
let computed = {
|
||||
|
||||
};
|
||||
|
||||
let methods = {
|
||||
|
||||
searchBook() {
|
||||
if (this.searchTerm.trim().length === 0) return;
|
||||
this.searching = true;
|
||||
this.searchResults = '';
|
||||
let url = window.baseUrl(`/search/${this.type}/${this.id}`);
|
||||
url += `?term=${encodeURIComponent(this.searchTerm)}`;
|
||||
this.$http.get(url).then(resp => {
|
||||
this.searchResults = resp.data;
|
||||
});
|
||||
},
|
||||
|
||||
checkSearchForm() {
|
||||
this.searching = this.searchTerm > 0;
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.searching = false;
|
||||
this.searchTerm = '';
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function mounted() {
|
||||
this.id = Number(this.$el.getAttribute('entity-id'));
|
||||
this.type = this.$el.getAttribute('entity-type');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data, computed, methods, mounted
|
||||
};
|
195
resources/assets/js/vues/search.js
Normal file
195
resources/assets/js/vues/search.js
Normal file
@ -0,0 +1,195 @@
|
||||
const moment = require('moment');
|
||||
|
||||
let data = {
|
||||
terms: '',
|
||||
termString : '',
|
||||
search: {
|
||||
type: {
|
||||
page: true,
|
||||
chapter: true,
|
||||
book: true
|
||||
},
|
||||
exactTerms: [],
|
||||
tagTerms: [],
|
||||
option: {},
|
||||
dates: {
|
||||
updated_after: false,
|
||||
updated_before: false,
|
||||
created_after: false,
|
||||
created_before: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let computed = {
|
||||
|
||||
};
|
||||
|
||||
let methods = {
|
||||
|
||||
appendTerm(term) {
|
||||
this.termString += ' ' + term;
|
||||
this.termString = this.termString.replace(/\s{2,}/g, ' ');
|
||||
this.termString = this.termString.replace(/^\s+/, '');
|
||||
this.termString = this.termString.replace(/\s+$/, '');
|
||||
},
|
||||
|
||||
exactParse(searchString) {
|
||||
this.search.exactTerms = [];
|
||||
let exactFilter = /"(.+?)"/g;
|
||||
let matches;
|
||||
while ((matches = exactFilter.exec(searchString)) !== null) {
|
||||
this.search.exactTerms.push(matches[1]);
|
||||
}
|
||||
},
|
||||
|
||||
exactChange() {
|
||||
let exactFilter = /"(.+?)"/g;
|
||||
this.termString = this.termString.replace(exactFilter, '');
|
||||
let matchesTerm = this.search.exactTerms.filter(term => {
|
||||
return term.trim() !== '';
|
||||
}).map(term => {
|
||||
return `"${term}"`
|
||||
}).join(' ');
|
||||
this.appendTerm(matchesTerm);
|
||||
},
|
||||
|
||||
addExact() {
|
||||
this.search.exactTerms.push('');
|
||||
setTimeout(() => {
|
||||
let exactInputs = document.querySelectorAll('.exact-input');
|
||||
exactInputs[exactInputs.length - 1].focus();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
removeExact(index) {
|
||||
this.search.exactTerms.splice(index, 1);
|
||||
this.exactChange();
|
||||
},
|
||||
|
||||
tagParse(searchString) {
|
||||
this.search.tagTerms = [];
|
||||
let tagFilter = /\[(.+?)\]/g;
|
||||
let matches;
|
||||
while ((matches = tagFilter.exec(searchString)) !== null) {
|
||||
this.search.tagTerms.push(matches[1]);
|
||||
}
|
||||
},
|
||||
|
||||
tagChange() {
|
||||
let tagFilter = /\[(.+?)\]/g;
|
||||
this.termString = this.termString.replace(tagFilter, '');
|
||||
let matchesTerm = this.search.tagTerms.filter(term => {
|
||||
return term.trim() !== '';
|
||||
}).map(term => {
|
||||
return `[${term}]`
|
||||
}).join(' ');
|
||||
this.appendTerm(matchesTerm);
|
||||
},
|
||||
|
||||
addTag() {
|
||||
this.search.tagTerms.push('');
|
||||
setTimeout(() => {
|
||||
let tagInputs = document.querySelectorAll('.tag-input');
|
||||
tagInputs[tagInputs.length - 1].focus();
|
||||
}, 100);
|
||||
},
|
||||
|
||||
removeTag(index) {
|
||||
this.search.tagTerms.splice(index, 1);
|
||||
this.tagChange();
|
||||
},
|
||||
|
||||
typeParse(searchString) {
|
||||
let typeFilter = /{\s?type:\s?(.*?)\s?}/;
|
||||
let match = searchString.match(typeFilter);
|
||||
let type = this.search.type;
|
||||
if (!match) {
|
||||
type.page = type.book = type.chapter = true;
|
||||
return;
|
||||
}
|
||||
let splitTypes = match[1].replace(/ /g, '').split('|');
|
||||
type.page = (splitTypes.indexOf('page') !== -1);
|
||||
type.chapter = (splitTypes.indexOf('chapter') !== -1);
|
||||
type.book = (splitTypes.indexOf('book') !== -1);
|
||||
},
|
||||
|
||||
typeChange() {
|
||||
let typeFilter = /{\s?type:\s?(.*?)\s?}/;
|
||||
let type = this.search.type;
|
||||
if (type.page === type.chapter && type.page === type.book) {
|
||||
this.termString = this.termString.replace(typeFilter, '');
|
||||
return;
|
||||
}
|
||||
let selectedTypes = Object.keys(type).filter(type => {return this.search.type[type];}).join('|');
|
||||
let typeTerm = '{type:'+selectedTypes+'}';
|
||||
if (this.termString.match(typeFilter)) {
|
||||
this.termString = this.termString.replace(typeFilter, typeTerm);
|
||||
return;
|
||||
}
|
||||
this.appendTerm(typeTerm);
|
||||
},
|
||||
|
||||
optionParse(searchString) {
|
||||
let optionFilter = /{([a-z_\-:]+?)}/gi;
|
||||
let matches;
|
||||
while ((matches = optionFilter.exec(searchString)) !== null) {
|
||||
this.search.option[matches[1].toLowerCase()] = true;
|
||||
}
|
||||
},
|
||||
|
||||
optionChange(optionName) {
|
||||
let isChecked = this.search.option[optionName];
|
||||
if (isChecked) {
|
||||
this.appendTerm(`{${optionName}}`);
|
||||
} else {
|
||||
this.termString = this.termString.replace(`{${optionName}}`, '');
|
||||
}
|
||||
},
|
||||
|
||||
updateSearch(e) {
|
||||
e.preventDefault();
|
||||
window.location = '/search?term=' + encodeURIComponent(this.termString);
|
||||
},
|
||||
|
||||
enableDate(optionName) {
|
||||
this.search.dates[optionName.toLowerCase()] = moment().format('YYYY-MM-DD');
|
||||
this.dateChange(optionName);
|
||||
},
|
||||
|
||||
dateParse(searchString) {
|
||||
let dateFilter = /{([a-z_\-]+?):([a-z_\-0-9]+?)}/gi;
|
||||
let dateTags = Object.keys(this.search.dates);
|
||||
let matches;
|
||||
while ((matches = dateFilter.exec(searchString)) !== null) {
|
||||
if (dateTags.indexOf(matches[1]) === -1) continue;
|
||||
this.search.dates[matches[1].toLowerCase()] = matches[2];
|
||||
}
|
||||
},
|
||||
|
||||
dateChange(optionName) {
|
||||
let dateFilter = new RegExp('{\\s?'+optionName+'\\s?:([a-z_\\-0-9]+?)}', 'gi');
|
||||
this.termString = this.termString.replace(dateFilter, '');
|
||||
if (!this.search.dates[optionName]) return;
|
||||
this.appendTerm(`{${optionName}:${this.search.dates[optionName]}}`);
|
||||
},
|
||||
|
||||
dateRemove(optionName) {
|
||||
this.search.dates[optionName] = false;
|
||||
this.dateChange(optionName);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function created() {
|
||||
this.termString = document.querySelector('[name=searchTerm]').value;
|
||||
this.typeParse(this.termString);
|
||||
this.exactParse(this.termString);
|
||||
this.tagParse(this.termString);
|
||||
this.optionParse(this.termString);
|
||||
this.dateParse(this.termString);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data, computed, methods, created
|
||||
};
|
18
resources/assets/js/vues/vues.js
Normal file
18
resources/assets/js/vues/vues.js
Normal file
@ -0,0 +1,18 @@
|
||||
const Vue = require("vue");
|
||||
|
||||
function exists(id) {
|
||||
return document.getElementById(id) !== null;
|
||||
}
|
||||
|
||||
let vueMapping = {
|
||||
'search-system': require('./search'),
|
||||
'entity-dashboard': require('./entity-search'),
|
||||
};
|
||||
|
||||
Object.keys(vueMapping).forEach(id => {
|
||||
if (exists(id)) {
|
||||
let config = vueMapping[id];
|
||||
config.el = '#' + id;
|
||||
new Vue(config);
|
||||
}
|
||||
});
|
@ -2,7 +2,7 @@
|
||||
.anim.fadeIn {
|
||||
opacity: 0;
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 160ms;
|
||||
animation-duration: 180ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@ -126,4 +126,8 @@
|
||||
animation-duration: 180ms;
|
||||
animation-delay: 0s;
|
||||
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
|
||||
}
|
||||
|
||||
.anim.selectFade {
|
||||
transition: background-color ease-in-out 3000ms;
|
||||
}
|
@ -98,19 +98,36 @@ label {
|
||||
|
||||
label.radio, label.checkbox {
|
||||
font-weight: 400;
|
||||
user-select: none;
|
||||
input[type="radio"], input[type="checkbox"] {
|
||||
margin-right: $-xs;
|
||||
}
|
||||
}
|
||||
|
||||
label.inline.checkbox {
|
||||
margin-right: $-m;
|
||||
}
|
||||
|
||||
label + p.small {
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="number"], input[type="email"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
|
||||
table.form-table {
|
||||
max-width: 100%;
|
||||
td {
|
||||
overflow: hidden;
|
||||
padding: $-xxs/2 0;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
|
||||
@extend .input-base;
|
||||
}
|
||||
|
||||
input[type=date] {
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
display: inline-block;
|
||||
background-color: #BBB;
|
||||
|
@ -109,6 +109,7 @@
|
||||
transition-property: right, border;
|
||||
border-left: 0px solid #FFF;
|
||||
background-color: #FFF;
|
||||
max-width: 320px;
|
||||
&.fixed {
|
||||
background-color: #FFF;
|
||||
z-index: 5;
|
||||
|
@ -269,19 +269,31 @@ span.highlight {
|
||||
/*
|
||||
* Lists
|
||||
*/
|
||||
ul, ol {
|
||||
overflow: hidden;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
padding-left: $-m * 1.3;
|
||||
list-style: disc;
|
||||
overflow: hidden;
|
||||
ul {
|
||||
list-style: circle;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
label {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal;
|
||||
padding-left: $-m * 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
li.checkbox-item {
|
||||
li.checkbox-item, li.task-list-item {
|
||||
list-style: none;
|
||||
margin-left: - ($-m * 1.3);
|
||||
input[type="checkbox"] {
|
||||
|
@ -7,8 +7,8 @@
|
||||
@import "grid";
|
||||
@import "blocks";
|
||||
@import "buttons";
|
||||
@import "forms";
|
||||
@import "tables";
|
||||
@import "forms";
|
||||
@import "animations";
|
||||
@import "tinymce";
|
||||
@import "highlightjs";
|
||||
@ -17,7 +17,11 @@
|
||||
@import "lists";
|
||||
@import "pages";
|
||||
|
||||
[v-cloak], [v-show] {display: none;}
|
||||
[v-cloak], [v-show] {
|
||||
display: none; opacity: 0;
|
||||
animation-name: none !important;
|
||||
}
|
||||
|
||||
|
||||
[ng\:cloak], [ng-cloak], .ng-cloak {
|
||||
display: none !important;
|
||||
@ -272,8 +276,3 @@ $btt-size: 40px;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -43,18 +43,9 @@ return [
|
||||
* Search
|
||||
*/
|
||||
'search_results' => 'Suchergebnisse',
|
||||
'search_results_page' => 'Seiten-Suchergebnisse',
|
||||
'search_results_chapter' => 'Kapitel-Suchergebnisse',
|
||||
'search_results_book' => 'Buch-Suchergebnisse',
|
||||
'search_clear' => 'Suche zurücksetzen',
|
||||
'search_view_pages' => 'Zeige alle passenden Seiten',
|
||||
'search_view_chapters' => 'Zeige alle passenden Kapitel',
|
||||
'search_view_books' => 'Zeige alle passenden Bücher',
|
||||
'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden',
|
||||
'search_for_term' => 'Suche nach :term',
|
||||
'search_page_for_term' => 'Suche nach :term in Seiten',
|
||||
'search_chapter_for_term' => 'Suche nach :term in Kapiteln',
|
||||
'search_book_for_term' => 'Suche nach :term in Büchern',
|
||||
|
||||
/**
|
||||
* Books
|
||||
|
@ -33,6 +33,7 @@ return [
|
||||
'search_clear' => 'Clear Search',
|
||||
'reset' => 'Reset',
|
||||
'remove' => 'Remove',
|
||||
'add' => 'Add',
|
||||
|
||||
|
||||
/**
|
||||
|
@ -43,18 +43,26 @@ return [
|
||||
* Search
|
||||
*/
|
||||
'search_results' => 'Search Results',
|
||||
'search_results_page' => 'Page Search Results',
|
||||
'search_results_chapter' => 'Chapter Search Results',
|
||||
'search_results_book' => 'Book Search Results',
|
||||
'search_total_results_found' => ':count result found|:count total results found',
|
||||
'search_clear' => 'Clear Search',
|
||||
'search_view_pages' => 'View all matches pages',
|
||||
'search_view_chapters' => 'View all matches chapters',
|
||||
'search_view_books' => 'View all matches books',
|
||||
'search_no_pages' => 'No pages matched this search',
|
||||
'search_for_term' => 'Search for :term',
|
||||
'search_page_for_term' => 'Page search for :term',
|
||||
'search_chapter_for_term' => 'Chapter search for :term',
|
||||
'search_book_for_term' => 'Books search for :term',
|
||||
'search_more' => 'More Results',
|
||||
'search_filters' => 'Search Filters',
|
||||
'search_content_type' => 'Content Type',
|
||||
'search_exact_matches' => 'Exact Matches',
|
||||
'search_tags' => 'Tag Searches',
|
||||
'search_viewed_by_me' => 'Viewed by me',
|
||||
'search_not_viewed_by_me' => 'Not viewed by me',
|
||||
'search_permissions_set' => 'Permissions set',
|
||||
'search_created_by_me' => 'Created by me',
|
||||
'search_updated_by_me' => 'Updated by me',
|
||||
'search_updated_before' => 'Updated before',
|
||||
'search_updated_after' => 'Updated after',
|
||||
'search_created_before' => 'Created before',
|
||||
'search_created_after' => 'Created after',
|
||||
'search_set_date' => 'Set Date',
|
||||
'search_update' => 'Update Search',
|
||||
|
||||
/**
|
||||
* Books
|
||||
@ -112,6 +120,7 @@ return [
|
||||
'chapters_empty' => 'No pages are currently in this chapter.',
|
||||
'chapters_permissions_active' => 'Chapter Permissions Active',
|
||||
'chapters_permissions_success' => 'Chapter Permissions Updated',
|
||||
'chapters_search_this' => 'Search this chapter',
|
||||
|
||||
/**
|
||||
* Pages
|
||||
|
@ -120,6 +120,7 @@ return [
|
||||
'fr' => 'Français',
|
||||
'nl' => 'Nederlands',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'sk' => 'Slovensky',
|
||||
]
|
||||
///////////////////////////////////
|
||||
];
|
||||
|
@ -43,18 +43,9 @@ return [
|
||||
* Search
|
||||
*/
|
||||
'search_results' => 'Buscar resultados',
|
||||
'search_results_page' => 'resultados de búsqueda en página',
|
||||
'search_results_chapter' => 'Resultados de búsqueda en capítulo ',
|
||||
'search_results_book' => 'Resultados de búsqueda en libro',
|
||||
'search_clear' => 'Limpiar resultados',
|
||||
'search_view_pages' => 'Ver todas las páginas que concuerdan',
|
||||
'search_view_chapters' => 'Ver todos los capítulos que concuerdan',
|
||||
'search_view_books' => 'Ver todos los libros que concuerdan',
|
||||
'search_no_pages' => 'Ninguna página encontrada para la búsqueda',
|
||||
'search_for_term' => 'Busqueda por :term',
|
||||
'search_page_for_term' => 'Búsqueda de página por :term',
|
||||
'search_chapter_for_term' => 'Búsqueda por capítulo de :term',
|
||||
'search_book_for_term' => 'Búsqueda en libro de :term',
|
||||
|
||||
/**
|
||||
* Books
|
||||
|
@ -43,18 +43,9 @@ return [
|
||||
* Search
|
||||
*/
|
||||
'search_results' => 'Résultats de recherche',
|
||||
'search_results_page' => 'Résultats de recherche des pages',
|
||||
'search_results_chapter' => 'Résultats de recherche des chapitres',
|
||||
'search_results_book' => 'Résultats de recherche des livres',
|
||||
'search_clear' => 'Réinitialiser la recherche',
|
||||
'search_view_pages' => 'Voir toutes les pages correspondantes',
|
||||
'search_view_chapters' => 'Voir tous les chapitres correspondants',
|
||||
'search_view_books' => 'Voir tous les livres correspondants',
|
||||
'search_no_pages' => 'Aucune page correspondant à cette recherche',
|
||||
'search_for_term' => 'recherche pour :term',
|
||||
'search_page_for_term' => 'Recherche de page pour :term',
|
||||
'search_chapter_for_term' => 'Recherche de chapitre pour :term',
|
||||
'search_book_for_term' => 'Recherche de livres pour :term',
|
||||
|
||||
/**
|
||||
* Books
|
||||
|
@ -43,18 +43,9 @@ return [
|
||||
* Search
|
||||
*/
|
||||
'search_results' => 'Zoekresultaten',
|
||||
'search_results_page' => 'Pagina Zoekresultaten',
|
||||
'search_results_chapter' => 'Hoofdstuk Zoekresultaten',
|
||||
'search_results_book' => 'Boek Zoekresultaten',
|
||||
'search_clear' => 'Zoekopdracht wissen',
|
||||
'search_view_pages' => 'Bekijk alle gevonden pagina\'s',
|
||||
'search_view_chapters' => 'Bekijk alle gevonden hoofdstukken',
|
||||
'search_view_books' => 'Bekijk alle gevonden boeken',
|
||||
'search_no_pages' => 'Er zijn geen pagina\'s gevonden',
|
||||
'search_for_term' => 'Zoeken op :term',
|
||||
'search_page_for_term' => 'Pagina doorzoeken op :term',
|
||||
'search_chapter_for_term' => 'Hoofdstuk doorzoeken op :term',
|
||||
'search_book_for_term' => 'Boeken doorzoeken op :term',
|
||||
|
||||
/**
|
||||
* Books
|
||||
|
@ -43,18 +43,9 @@ return [
|
||||
* Search
|
||||
*/
|
||||
'search_results' => 'Resultado(s) da Pesquisa',
|
||||
'search_results_page' => 'Resultado(s) de Pesquisa de Página',
|
||||
'search_results_chapter' => 'Resultado(s) de Pesquisa de Capítulo',
|
||||
'search_results_book' => 'Resultado(s) de Pesquisa de Livro',
|
||||
'search_clear' => 'Limpar Pesquisa',
|
||||
'search_view_pages' => 'Visualizar todas as páginas correspondentes',
|
||||
'search_view_chapters' => 'Visualizar todos os capítulos correspondentes',
|
||||
'search_view_books' => 'Visualizar todos os livros correspondentes',
|
||||
'search_no_pages' => 'Nenhuma página corresponde à pesquisa',
|
||||
'search_for_term' => 'Pesquisar por :term',
|
||||
'search_page_for_term' => 'Pesquisar Página por :term',
|
||||
'search_chapter_for_term' => 'Pesquisar Capítulo por :term',
|
||||
'search_book_for_term' => 'Pesquisar Livros por :term',
|
||||
|
||||
/**
|
||||
* Books
|
||||
|
40
resources/lang/sk/activities.php
Normal file
40
resources/lang/sk/activities.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
* Activity text strings.
|
||||
* Is used for all the text within activity logs & notifications.
|
||||
*/
|
||||
|
||||
// Pages
|
||||
'page_create' => 'vytvoril stránku',
|
||||
'page_create_notification' => 'Stránka úspešne vytvorená',
|
||||
'page_update' => 'aktualizoval stránku',
|
||||
'page_update_notification' => 'Stránka úspešne aktualizovaná',
|
||||
'page_delete' => 'odstránil stránku',
|
||||
'page_delete_notification' => 'Stránka úspešne odstránená',
|
||||
'page_restore' => 'obnovil stránku',
|
||||
'page_restore_notification' => 'Stránka úspešne obnovená',
|
||||
'page_move' => 'presunul stránku',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'vytvoril kapitolu',
|
||||
'chapter_create_notification' => 'Kapitola úspešne vytvorená',
|
||||
'chapter_update' => 'aktualizoval kapitolu',
|
||||
'chapter_update_notification' => 'Kapitola úspešne aktualizovaná',
|
||||
'chapter_delete' => 'odstránil kapitolu',
|
||||
'chapter_delete_notification' => 'Kapitola úspešne odstránená',
|
||||
'chapter_move' => 'presunul kapitolu',
|
||||
|
||||
// Books
|
||||
'book_create' => 'vytvoril knihu',
|
||||
'book_create_notification' => 'Kniha úspešne vytvorená',
|
||||
'book_update' => 'aktualizoval knihu',
|
||||
'book_update_notification' => 'Kniha úspešne aktualizovaná',
|
||||
'book_delete' => 'odstránil knihu',
|
||||
'book_delete_notification' => 'Kniha úspešne odstránená',
|
||||
'book_sort' => 'zoradil knihu',
|
||||
'book_sort_notification' => 'Kniha úspešne znovu zoradená',
|
||||
|
||||
];
|
76
resources/lang/sk/auth.php
Normal file
76
resources/lang/sk/auth.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
'failed' => 'Tieto údaje nesedia s našimi záznamami.',
|
||||
'throttle' => 'Priveľa pokusov o prihlásenie. Skúste znova o :seconds sekúnd.',
|
||||
|
||||
/**
|
||||
* Login & Register
|
||||
*/
|
||||
'sign_up' => 'Registrácia',
|
||||
'log_in' => 'Prihlásenie',
|
||||
'log_in_with' => 'Prihlásiť sa cez :socialDriver',
|
||||
'sign_up_with' => 'Registrovať sa cez :socialDriver',
|
||||
'logout' => 'Odhlásenie',
|
||||
|
||||
'name' => 'Meno',
|
||||
'username' => 'Používateľské meno',
|
||||
'email' => 'Email',
|
||||
'password' => 'Heslo',
|
||||
'password_confirm' => 'Potvrdiť heslo',
|
||||
'password_hint' => 'Musí mať viac ako 5 znakov',
|
||||
'forgot_password' => 'Zabudli ste heslo?',
|
||||
'remember_me' => 'Zapamätať si ma',
|
||||
'ldap_email_hint' => 'Zadajte prosím email, ktorý sa má použiť pre tento účet.',
|
||||
'create_account' => 'Vytvoriť účet',
|
||||
'social_login' => 'Sociálne prihlásenie',
|
||||
'social_registration' => 'Sociálna registrácia',
|
||||
'social_registration_text' => 'Registrovať sa a prihlásiť sa použitím inej služby.',
|
||||
|
||||
'register_thanks' => 'Ďakujeme zaregistráciu!',
|
||||
'register_confirm' => 'Skontrolujte prosím svoj email a kliknite na potvrdzujúce tlačidlo pre prístup k :appName.',
|
||||
'registrations_disabled' => 'Registrácie sú momentálne zablokované',
|
||||
'registration_email_domain_invalid' => 'Táto emailová doména nemá prístup k tejto aplikácii',
|
||||
'register_success' => 'Ďakujeme za registráciu! Teraz ste registrovaný a prihlásený.',
|
||||
|
||||
|
||||
/**
|
||||
* Password Reset
|
||||
*/
|
||||
'reset_password' => 'Reset hesla',
|
||||
'reset_password_send_instructions' => 'Zadajte svoj email nižšie a bude Vám odoslaný email s odkazom pre reset hesla.',
|
||||
'reset_password_send_button' => 'Poslať odkaz na reset hesla',
|
||||
'reset_password_sent_success' => 'Odkaz na reset hesla bol poslaný na :email.',
|
||||
'reset_password_success' => 'Vaše heslo bolo úspešne resetované.',
|
||||
|
||||
'email_reset_subject' => 'Reset Vášho :appName hesla',
|
||||
'email_reset_text' => 'Tento email Ste dostali pretože sme dostali požiadavku na reset hesla pre Váš účet.',
|
||||
'email_reset_not_requested' => 'Ak ste nepožiadali o reset hesla, nemusíte nič robiť.',
|
||||
|
||||
|
||||
/**
|
||||
* Email Confirmation
|
||||
*/
|
||||
'email_confirm_subject' => 'Potvrdiť email na :appName',
|
||||
'email_confirm_greeting' => 'Ďakujeme za pridanie sa k :appName!',
|
||||
'email_confirm_text' => 'Prosím potvrďte Vašu emailovú adresu kliknutím na tlačidlo nižšie:',
|
||||
'email_confirm_action' => 'Potvrdiť email',
|
||||
'email_confirm_send_error' => 'Je požadované overenie emailu, ale systém nemohol odoslať email. Kontaktujte administrátora by ste sa uistili, že email je nastavený správne.',
|
||||
'email_confirm_success' => 'Váš email bol overený!',
|
||||
'email_confirm_resent' => 'Potvrdzujúci email bol poslaný znovu, skontrolujte prosím svoju emailovú schránku.',
|
||||
|
||||
'email_not_confirmed' => 'Emailová adresa nebola overená',
|
||||
'email_not_confirmed_text' => 'Vaša emailová adresa nebola zatiaľ overená.',
|
||||
'email_not_confirmed_click_link' => 'Prosím, kliknite na odkaz v emaili, ktorý bol poslaný krátko po Vašej registrácii.',
|
||||
'email_not_confirmed_resend' => 'Ak nemôžete násť email, môžete znova odoslať overovací email odoslaním doleuvedeného formulára.',
|
||||
'email_not_confirmed_resend_button' => 'Znova odoslať overovací email',
|
||||
];
|
58
resources/lang/sk/common.php
Normal file
58
resources/lang/sk/common.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
/**
|
||||
* Buttons
|
||||
*/
|
||||
'cancel' => 'Zrušiť',
|
||||
'confirm' => 'Potvrdiť',
|
||||
'back' => 'Späť',
|
||||
'save' => 'Uložiť',
|
||||
'continue' => 'Pokračovať',
|
||||
'select' => 'Vybrať',
|
||||
|
||||
/**
|
||||
* Form Labels
|
||||
*/
|
||||
'name' => 'Meno',
|
||||
'description' => 'Popis',
|
||||
'role' => 'Rola',
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
'actions' => 'Akcie',
|
||||
'view' => 'Zobraziť',
|
||||
'create' => 'Vytvoriť',
|
||||
'update' => 'Aktualizovať',
|
||||
'edit' => 'Editovať',
|
||||
'sort' => 'Zoradiť',
|
||||
'move' => 'Presunúť',
|
||||
'delete' => 'Zmazať',
|
||||
'search' => 'Hľadť',
|
||||
'search_clear' => 'Vyčistiť hľadanie',
|
||||
'reset' => 'Reset',
|
||||
'remove' => 'Odstrániť',
|
||||
|
||||
|
||||
/**
|
||||
* Misc
|
||||
*/
|
||||
'deleted_user' => 'Odstránený používateľ',
|
||||
'no_activity' => 'Žiadna aktivita na zobrazenie',
|
||||
'no_items' => 'Žiadne položky nie sú dostupné',
|
||||
'back_to_top' => 'Späť nahor',
|
||||
'toggle_details' => 'Prepnúť detaily',
|
||||
|
||||
/**
|
||||
* Header
|
||||
*/
|
||||
'view_profile' => 'Zobraziť profil',
|
||||
'edit_profile' => 'Upraviť profil',
|
||||
|
||||
/**
|
||||
* Email Content
|
||||
*/
|
||||
'email_action_help' => 'Ak máte problém klinkúť na tlačidlo ":actionText", skopírujte a vložte URL uvedenú nižšie do Vášho prehliadača:',
|
||||
'email_rights' => 'Všetky práva vyhradené',
|
||||
];
|
24
resources/lang/sk/components.php
Normal file
24
resources/lang/sk/components.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
/**
|
||||
* Image Manager
|
||||
*/
|
||||
'image_select' => 'Vybrať obrázok',
|
||||
'image_all' => 'Všetko',
|
||||
'image_all_title' => 'Zobraziť všetky obrázky',
|
||||
'image_book_title' => 'Zobraziť obrázky nahrané do tejto knihy',
|
||||
'image_page_title' => 'Zobraziť obrázky nahrané do tejto stránky',
|
||||
'image_search_hint' => 'Hľadať obrázok podľa názvu',
|
||||
'image_uploaded' => 'Nahrané :uploadedDate',
|
||||
'image_load_more' => 'Načítať viac',
|
||||
'image_image_name' => 'Názov obrázka',
|
||||
'image_delete_confirm' => 'Tento obrázok je použitý na stránkach uvedených nižšie, kliknite znova na zmazať pre potvrdenie zmazania tohto obrázka.',
|
||||
'image_select_image' => 'Vybrať obrázok',
|
||||
'image_dropzone' => 'Presuňte obrázky sem alebo kliknite sem pre nahranie',
|
||||
'images_deleted' => 'Obrázky zmazané',
|
||||
'image_preview' => 'Náhľad obrázka',
|
||||
'image_upload_success' => 'Obrázok úspešne nahraný',
|
||||
'image_update_success' => 'Detaily obrázka úspešne aktualizované',
|
||||
'image_delete_success' => 'Obrázok úspešne zmazaný'
|
||||
];
|
226
resources/lang/sk/entities.php
Normal file
226
resources/lang/sk/entities.php
Normal file
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
/**
|
||||
* Shared
|
||||
*/
|
||||
'recently_created' => 'Nedávno vytvorené',
|
||||
'recently_created_pages' => 'Nedávno vytvorené stránky',
|
||||
'recently_updated_pages' => 'Nedávno aktualizované stránky',
|
||||
'recently_created_chapters' => 'Nedávno vytvorené kapitoly',
|
||||
'recently_created_books' => 'Nedávno vytvorené knihy',
|
||||
'recently_update' => 'Nedávno aktualizované',
|
||||
'recently_viewed' => 'Nedávno zobrazené',
|
||||
'recent_activity' => 'Nedávna aktivita',
|
||||
'create_now' => 'Vytvoriť teraz',
|
||||
'revisions' => 'Revízie',
|
||||
'meta_created' => 'Vytvorené :timeLength',
|
||||
'meta_created_name' => 'Vytvorené :timeLength používateľom :user',
|
||||
'meta_updated' => 'Aktualizované :timeLength',
|
||||
'meta_updated_name' => 'Aktualizované :timeLength používateľom :user',
|
||||
'x_pages' => ':count stránok',
|
||||
'entity_select' => 'Entita vybraná',
|
||||
'images' => 'Obrázky',
|
||||
'my_recent_drafts' => 'Moje nedávne koncepty',
|
||||
'my_recently_viewed' => 'Nedávno mnou zobrazené',
|
||||
'no_pages_viewed' => 'Nepozreli ste si žiadne stránky',
|
||||
'no_pages_recently_created' => 'Žiadne stránky neboli nedávno vytvorené',
|
||||
'no_pages_recently_updated' => 'Žiadne stránky neboli nedávno aktualizované',
|
||||
'export' => 'Export',
|
||||
'export_html' => 'Contained Web File',
|
||||
'export_pdf' => 'PDF súbor',
|
||||
'export_text' => 'Súbor s čistým textom',
|
||||
|
||||
/**
|
||||
* Permissions and restrictions
|
||||
*/
|
||||
'permissions' => 'Oprávnenia',
|
||||
'permissions_intro' => 'Ak budú tieto oprávnenia povolené, budú mať prioritu pred oprávneniami roly.',
|
||||
'permissions_enable' => 'Povoliť vlastné oprávnenia',
|
||||
'permissions_save' => 'Uložiť oprávnenia',
|
||||
|
||||
/**
|
||||
* Search
|
||||
*/
|
||||
'search_results' => 'Výsledky hľadania',
|
||||
'search_results_page' => 'Výsledky hľadania stránky',
|
||||
'search_results_chapter' => 'Výsledky hľadania kapitoly',
|
||||
'search_results_book' => 'Výsledky hľadania knihy',
|
||||
'search_clear' => 'Vyčistiť hľadanie',
|
||||
'search_view_pages' => 'Zobraziť všetky vyhovujúce stránky',
|
||||
'search_view_chapters' => 'Zobraziť všetky vyhovujúce kapitoly',
|
||||
'search_view_books' => 'Zobraziť všetky vyhovujúce knihy',
|
||||
'search_no_pages' => 'Žiadne stránky nevyhovujú tomuto hľadaniu',
|
||||
'search_for_term' => 'Hľadať :term',
|
||||
'search_page_for_term' => 'Hľadať :term medzi stránkami',
|
||||
'search_chapter_for_term' => 'Hľadať :term medzi kapitolami',
|
||||
'search_book_for_term' => 'Hľadať :term medzi knihami',
|
||||
|
||||
/**
|
||||
* Books
|
||||
*/
|
||||
'book' => 'Kniha',
|
||||
'books' => 'Knihy',
|
||||
'books_empty' => 'Žiadne knihy neboli vytvorené',
|
||||
'books_popular' => 'Populárne knihy',
|
||||
'books_recent' => 'Nedávne knihy',
|
||||
'books_popular_empty' => 'Najpopulárnejšie knihy sa objavia tu.',
|
||||
'books_create' => 'Vytvoriť novú knihu',
|
||||
'books_delete' => 'Zmazať knihu',
|
||||
'books_delete_named' => 'Zmazať knihu :bookName',
|
||||
'books_delete_explain' => 'Toto zmaže knihu s názvom \':bookName\', všetky stránky a kapitoly budú odstránené.',
|
||||
'books_delete_confirmation' => 'Ste si istý, že chcete zmazať túto knihu?',
|
||||
'books_edit' => 'Upraviť knihu',
|
||||
'books_edit_named' => 'Upraviť knihu :bookName',
|
||||
'books_form_book_name' => 'Názov knihy',
|
||||
'books_save' => 'Uložiť knihu',
|
||||
'books_permissions' => 'Oprávnenia knihy',
|
||||
'books_permissions_updated' => 'Oprávnenia knihy aktualizované',
|
||||
'books_empty_contents' => 'Pre túto knihu neboli vytvorené žiadne stránky alebo kapitoly.',
|
||||
'books_empty_create_page' => 'Vytvoriť novú stránku',
|
||||
'books_empty_or' => 'alebo',
|
||||
'books_empty_sort_current_book' => 'Zoradiť aktuálnu knihu',
|
||||
'books_empty_add_chapter' => 'Pridať kapitolu',
|
||||
'books_permissions_active' => 'Oprávnenia knihy aktívne',
|
||||
'books_search_this' => 'Hľadať v tejto knihe',
|
||||
'books_navigation' => 'Navigácia knihy',
|
||||
'books_sort' => 'Zoradiť obsah knihy',
|
||||
'books_sort_named' => 'Zoradiť knihu :bookName',
|
||||
'books_sort_show_other' => 'Zobraziť ostatné knihy',
|
||||
'books_sort_save' => 'Uložiť nové zoradenie',
|
||||
|
||||
/**
|
||||
* Chapters
|
||||
*/
|
||||
'chapter' => 'Kapitola',
|
||||
'chapters' => 'Kapitoly',
|
||||
'chapters_popular' => 'Populárne kapitoly',
|
||||
'chapters_new' => 'Nová kapitola',
|
||||
'chapters_create' => 'Vytvoriť novú kapitolu',
|
||||
'chapters_delete' => 'Zmazať kapitolu',
|
||||
'chapters_delete_named' => 'Zmazať kapitolu :chapterName',
|
||||
'chapters_delete_explain' => 'Toto zmaže kapitolu menom \':chapterName\', všetky stránky budú ostránené
|
||||
a pridané priamo do rodičovskej knihy.',
|
||||
'chapters_delete_confirm' => 'Ste si istý, že chcete zmazať túto kapitolu?',
|
||||
'chapters_edit' => 'Upraviť kapitolu',
|
||||
'chapters_edit_named' => 'Upraviť kapitolu :chapterName',
|
||||
'chapters_save' => 'Uložiť kapitolu',
|
||||
'chapters_move' => 'Presunúť kapitolu',
|
||||
'chapters_move_named' => 'Presunúť kapitolu :chapterName',
|
||||
'chapter_move_success' => 'Kapitola presunutá do :bookName',
|
||||
'chapters_permissions' => 'Oprávnenia kapitoly',
|
||||
'chapters_empty' => 'V tejto kapitole nie sú teraz žiadne stránky.',
|
||||
'chapters_permissions_active' => 'Oprávnenia kapitoly aktívne',
|
||||
'chapters_permissions_success' => 'Oprávnenia kapitoly aktualizované',
|
||||
|
||||
/**
|
||||
* Pages
|
||||
*/
|
||||
'page' => 'Stránka',
|
||||
'pages' => 'Stránky',
|
||||
'pages_popular' => 'Populárne stránky',
|
||||
'pages_new' => 'Nová stránka',
|
||||
'pages_attachments' => 'Prílohy',
|
||||
'pages_navigation' => 'Navigácia',
|
||||
'pages_delete' => 'Zmazať stránku',
|
||||
'pages_delete_named' => 'Zmazať stránku :pageName',
|
||||
'pages_delete_draft_named' => 'Zmazať koncept :pageName',
|
||||
'pages_delete_draft' => 'Zmazať koncept',
|
||||
'pages_delete_success' => 'Stránka zmazaná',
|
||||
'pages_delete_draft_success' => 'Koncept stránky zmazaný',
|
||||
'pages_delete_confirm' => 'Ste si istý, že chcete zmazať túto stránku?',
|
||||
'pages_delete_draft_confirm' => 'Ste si istý, že chcete zmazať tento koncept stránky?',
|
||||
'pages_editing_named' => 'Upraviť stránku :pageName',
|
||||
'pages_edit_toggle_header' => 'Prepnúť hlavičku',
|
||||
'pages_edit_save_draft' => 'Uložiť koncept',
|
||||
'pages_edit_draft' => 'Upraviť koncept stránky',
|
||||
'pages_editing_draft' => 'Upravuje sa koncept',
|
||||
'pages_editing_page' => 'Upravuje sa stránka',
|
||||
'pages_edit_draft_save_at' => 'Koncept uložený pod ',
|
||||
'pages_edit_delete_draft' => 'Uložiť koncept',
|
||||
'pages_edit_discard_draft' => 'Zrušiť koncept',
|
||||
'pages_edit_set_changelog' => 'Nastaviť záznam zmien',
|
||||
'pages_edit_enter_changelog_desc' => 'Zadajte krátky popis zmien, ktoré ste urobili',
|
||||
'pages_edit_enter_changelog' => 'Zadať záznam zmien',
|
||||
'pages_save' => 'Uložiť stránku',
|
||||
'pages_title' => 'Titulok stránky',
|
||||
'pages_name' => 'Názov stránky',
|
||||
'pages_md_editor' => 'Editor',
|
||||
'pages_md_preview' => 'Náhľad',
|
||||
'pages_md_insert_image' => 'Vložiť obrázok',
|
||||
'pages_md_insert_link' => 'Vložiť odkaz na entitu',
|
||||
'pages_not_in_chapter' => 'Stránka nie je v kapitole',
|
||||
'pages_move' => 'Presunúť stránku',
|
||||
'pages_move_success' => 'Stránka presunutá do ":parentName"',
|
||||
'pages_permissions' => 'Oprávnenia stránky',
|
||||
'pages_permissions_success' => 'Oprávnenia stránky aktualizované',
|
||||
'pages_revisions' => 'Revízie stránky',
|
||||
'pages_revisions_named' => 'Revízie stránky :pageName',
|
||||
'pages_revision_named' => 'Revízia stránky :pageName',
|
||||
'pages_revisions_created_by' => 'Vytvoril',
|
||||
'pages_revisions_date' => 'Dátum revízie',
|
||||
'pages_revisions_changelog' => 'Záznam zmien',
|
||||
'pages_revisions_changes' => 'Zmeny',
|
||||
'pages_revisions_current' => 'Aktuálna verzia',
|
||||
'pages_revisions_preview' => 'Náhľad',
|
||||
'pages_revisions_restore' => 'Obnoviť',
|
||||
'pages_revisions_none' => 'Táto stránka nemá žiadne revízie',
|
||||
'pages_copy_link' => 'Kopírovať odkaz',
|
||||
'pages_permissions_active' => 'Oprávnienia stránky aktívne',
|
||||
'pages_initial_revision' => 'Prvé zverejnenie',
|
||||
'pages_initial_name' => 'Nová stránka',
|
||||
'pages_editing_draft_notification' => 'Práve upravujete koncept, ktorý bol naposledy uložený :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Táto stránka bola odvtedy upravená. Odporúča sa odstrániť tento koncept.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count používateľov začalo upravovať túto stránku',
|
||||
'start_b' => ':userName začal upravovať túto stránku',
|
||||
'time_a' => 'odkedy boli stránky naposledy aktualizované',
|
||||
'time_b' => 'za posledných :minCount minút',
|
||||
'message' => ':start :time. Dávajte pozor aby ste si navzájom neprepísali zmeny!',
|
||||
],
|
||||
'pages_draft_discarded' => 'Koncept ostránený, aktuálny obsah stránky bol nahraný do editora',
|
||||
|
||||
/**
|
||||
* Editor sidebar
|
||||
*/
|
||||
'page_tags' => 'Štítky stránok',
|
||||
'tag' => 'Štítok',
|
||||
'tags' => 'Štítky',
|
||||
'tag_value' => 'Hodnota štítku (Voliteľné)',
|
||||
'tags_explain' => "Pridajte pár štítkov pre uľahčenie kategorizácie Vášho obsahu. \n Štítku môžete priradiť hodnotu pre ešte lepšiu organizáciu.",
|
||||
'tags_add' => 'Pridať ďalší štítok',
|
||||
'attachments' => 'Prílohy',
|
||||
'attachments_explain' => 'Nahrajte nejaké súbory alebo priložte zopár odkazov pre zobrazenie na Vašej stránke. Budú viditeľné v bočnom paneli.',
|
||||
'attachments_explain_instant_save' => 'Zmeny budú okamžite uložené.',
|
||||
'attachments_items' => 'Priložené položky',
|
||||
'attachments_upload' => 'Nahrať súbor',
|
||||
'attachments_link' => 'Priložiť odkaz',
|
||||
'attachments_set_link' => 'Nastaviť odkaz',
|
||||
'attachments_delete_confirm' => 'Kliknite znova na zmazať pre potvrdenie zmazania prílohy.',
|
||||
'attachments_dropzone' => 'Presuňte súbory alebo klinknite sem pre priloženie súboru',
|
||||
'attachments_no_files' => 'Žiadne súbory neboli nahrané',
|
||||
'attachments_explain_link' => 'Ak nechcete priložiť súbor, môžete priložiť odkaz. Môže to byť odkaz na inú stránku alebo odkaz na súbor v cloude.',
|
||||
'attachments_link_name' => 'Názov odkazu',
|
||||
'attachment_link' => 'Odkaz na prílohu',
|
||||
'attachments_link_url' => 'Odkaz na súbor',
|
||||
'attachments_link_url_hint' => 'Url stránky alebo súboru',
|
||||
'attach' => 'Priložiť',
|
||||
'attachments_edit_file' => 'Upraviť súbor',
|
||||
'attachments_edit_file_name' => 'Názov súboru',
|
||||
'attachments_edit_drop_upload' => 'Presuňte súbory sem alebo klinknite pre nahranie a prepis',
|
||||
'attachments_order_updated' => 'Poradie príloh aktualizované',
|
||||
'attachments_updated_success' => 'Detaily prílohy aktualizované',
|
||||
'attachments_deleted' => 'Príloha zmazaná',
|
||||
'attachments_file_uploaded' => 'Súbor úspešne nahraný',
|
||||
'attachments_file_updated' => 'Súbor úspešne aktualizovaný',
|
||||
'attachments_link_attached' => 'Odkaz úspešne pripojený k stránke',
|
||||
|
||||
/**
|
||||
* Profile View
|
||||
*/
|
||||
'profile_user_for_x' => 'Používateľ už :time',
|
||||
'profile_created_content' => 'Vytvorený obsah',
|
||||
'profile_not_created_pages' => ':userName nevytvoril žiadne stránky',
|
||||
'profile_not_created_chapters' => ':userName nevytvoril žiadne kapitoly',
|
||||
'profile_not_created_books' => ':userName nevytvoril žiadne knihy',
|
||||
];
|
70
resources/lang/sk/errors.php
Normal file
70
resources/lang/sk/errors.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
* Error text strings.
|
||||
*/
|
||||
|
||||
// Permissions
|
||||
'permission' => 'Nemáte oprávnenie pre prístup k požadovanej stránke.',
|
||||
'permissionJson' => 'Nemáte oprávnenie pre vykonanie požadovaného úkonu.',
|
||||
|
||||
// Auth
|
||||
'error_user_exists_different_creds' => 'Používateľ s emailom :email už existuje, ale s inými údajmi.',
|
||||
'email_already_confirmed' => 'Email bol už overený, skúste sa prihlásiť.',
|
||||
'email_confirmation_invalid' => 'Tento potvrdzujúci token nie je platný alebo už bol použitý, skúste sa prosím registrovať znova.',
|
||||
'email_confirmation_expired' => 'Potvrdzujúci token expiroval, bol odoslaný nový potvrdzujúci email.',
|
||||
'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',
|
||||
'ldap_fail_authed' => 'LDAP access failed using given dn & password details',
|
||||
'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
|
||||
'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
|
||||
'social_no_action_defined' => 'Nebola definovaná žiadna akcia',
|
||||
'social_account_in_use' => 'Tento :socialAccount účet sa už používa, skúste sa prihlásiť pomocou možnosti :socialAccount.',
|
||||
'social_account_email_in_use' => 'Email :email sa už používa. Ak už máte účet, môžete pripojiť svoj :socialAccount účet v nastaveniach profilu.',
|
||||
'social_account_existing' => 'Tento :socialAccount účet je už spojený s Vaším profilom.',
|
||||
'social_account_already_used_existing' => 'Tento :socialAccount účet už používa iný používateľ.',
|
||||
'social_account_not_used' => 'Tento :socialAccount účet nie je spojený so žiadnym používateľom. Pripojte ho prosím v nastaveniach Vášho profilu. ',
|
||||
'social_account_register_instructions' => 'Ak zatiaľ nemáte účet, môžete sa registrovať pomocou možnosti :socialAccount.',
|
||||
'social_driver_not_found' => 'Ovládač socialnych sietí nebol nájdený',
|
||||
'social_driver_not_configured' => 'Nastavenia Vášho :socialAccount účtu nie sú správne.',
|
||||
|
||||
// System
|
||||
'path_not_writable' => 'Do cesty :filePath sa nedá nahrávať. Uistite sa, že je zapisovateľná serverom.',
|
||||
'cannot_get_image_from_url' => 'Nedá sa získať obrázok z :url',
|
||||
'cannot_create_thumbs' => 'Server nedokáže vytvoriť náhľady. Skontrolujte prosím, či máte nainštalované GD rozšírenie PHP.',
|
||||
'server_upload_limit' => 'Server nedovoľuje nahrávanie súborov s takouto veľkosťou. Skúste prosím menší súbor.',
|
||||
'image_upload_error' => 'Pri nahrávaní obrázka nastala chyba',
|
||||
|
||||
// Attachments
|
||||
'attachment_page_mismatch' => 'Page mismatch during attachment update',
|
||||
|
||||
// Pages
|
||||
'page_draft_autosave_fail' => 'Koncept nemohol byť uložený. Uistite sa, že máte pripojenie k internetu pre uložením tejto stránky',
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'Entita nenájdená',
|
||||
'book_not_found' => 'Kniha nenájdená',
|
||||
'page_not_found' => 'Stránka nenájdená',
|
||||
'chapter_not_found' => 'Kapitola nenájdená',
|
||||
'selected_book_not_found' => 'Vybraná kniha nebola nájdená',
|
||||
'selected_book_chapter_not_found' => 'Vybraná kniha alebo kapitola nebola nájdená',
|
||||
'guests_cannot_save_drafts' => 'Hosť nemôže ukladať koncepty',
|
||||
|
||||
// Users
|
||||
'users_cannot_delete_only_admin' => 'Nemôžete zmazať posledného správcu',
|
||||
'users_cannot_delete_guest' => 'Nemôžete zmazať hosťa',
|
||||
|
||||
// Roles
|
||||
'role_cannot_be_edited' => 'Táto rola nemôže byť upravovaná',
|
||||
'role_system_cannot_be_deleted' => 'Táto rola je systémová rola a nemôže byť zmazaná',
|
||||
'role_registration_default_cannot_delete' => 'Táto rola nemôže byť zmazaná, pretože je nastavená ako prednastavená rola pri registrácii',
|
||||
|
||||
// Error pages
|
||||
'404_page_not_found' => 'Stránka nenájdená',
|
||||
'sorry_page_not_found' => 'Prepáčte, stránka ktorú hľadáte nebola nájdená.',
|
||||
'return_home' => 'Vrátiť sa domov',
|
||||
'error_occurred' => 'Nastala chyba',
|
||||
'app_down' => ':appName je momentálne nedostupná',
|
||||
'back_soon' => 'Čoskoro bude opäť dostupná.',
|
||||
];
|
19
resources/lang/sk/pagination.php
Normal file
19
resources/lang/sk/pagination.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Predchádzajúca',
|
||||
'next' => 'Ďalšia »',
|
||||
|
||||
];
|
22
resources/lang/sk/passwords.php
Normal file
22
resources/lang/sk/passwords.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reminder Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| has failed, such as for an invalid token or invalid new password.
|
||||
|
|
||||
*/
|
||||
|
||||
'password' => 'Heslo musí obsahovať aspoň šesť znakov a musí byť rovnaké ako potvrdzujúce.',
|
||||
'user' => "Nenašli sme používateľa s takou emailovou adresou.",
|
||||
'token' => 'Tento token pre reset hesla je neplatný.',
|
||||
'sent' => 'Poslali sme Vám email s odkazom na reset hesla!',
|
||||
'reset' => 'Vaše heslo bolo resetované!',
|
||||
|
||||
];
|
111
resources/lang/sk/settings.php
Normal file
111
resources/lang/sk/settings.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
* Settings text strings
|
||||
* Contains all text strings used in the general settings sections of BookStack
|
||||
* including users and roles.
|
||||
*/
|
||||
|
||||
'settings' => 'Nastavenia',
|
||||
'settings_save' => 'Uložiť nastavenia',
|
||||
'settings_save_success' => 'Nastavenia uložené',
|
||||
|
||||
/**
|
||||
* App settings
|
||||
*/
|
||||
|
||||
'app_settings' => 'Nastavenia aplikácie',
|
||||
'app_name' => 'Názov aplikácia',
|
||||
'app_name_desc' => 'Tento názov sa zobrazuje v hlavičke a v emailoch.',
|
||||
'app_name_header' => 'Zobraziť názov aplikácie v hlavičke?',
|
||||
'app_public_viewing' => 'Povoliť verejné zobrazenie?',
|
||||
'app_secure_images' => 'Povoliť nahrávanie súborov so zvýšeným zabezpečením?',
|
||||
'app_secure_images_desc' => 'Kvôli výkonu sú všetky obrázky verejné. Táto možnosť pridá pred URL obrázka náhodný, ťažko uhádnuteľný reťazec. Aby ste zabránili jednoduchému prístupu, uistite sa, že indexy priečinkov nie sú povolené.',
|
||||
'app_editor' => 'Editor stránky',
|
||||
'app_editor_desc' => 'Vyberte editor, ktorý bude používaný všetkými používateľmi na editáciu stránok.',
|
||||
'app_custom_html' => 'Vlastný HTML obsah hlavičky',
|
||||
'app_custom_html_desc' => 'Všetok text pridaný sem bude vložený naspodok <head> sekcie na každej stránke. Môže sa to zísť pri zmene štýlu alebo pre pridanie analytického kódu.',
|
||||
'app_logo' => 'Logo aplikácie',
|
||||
'app_logo_desc' => 'Tento obrázok by mal mať 43px na výšku. <br>Veľké obrázky budú preškálované na menší rozmer.',
|
||||
'app_primary_color' => 'Primárna farba pre aplikáciu',
|
||||
'app_primary_color_desc' => 'Toto by mala byť hodnota v hex tvare. <br>Nechajte prázdne ak chcete použiť prednastavenú farbu.',
|
||||
|
||||
/**
|
||||
* Registration settings
|
||||
*/
|
||||
|
||||
'reg_settings' => 'Nastavenia registrácie',
|
||||
'reg_allow' => 'Povoliť registráciu?',
|
||||
'reg_default_role' => 'Prednastavená používateľská rola po registrácii',
|
||||
'reg_confirm_email' => 'Vyžadovať overenie emailu?',
|
||||
'reg_confirm_email_desc' => 'Ak je použité obmedzenie domény, potom bude vyžadované overenie emailu a hodnota nižšie bude ignorovaná.',
|
||||
'reg_confirm_restrict_domain' => 'Obmedziť registráciu na doménu',
|
||||
'reg_confirm_restrict_domain_desc' => 'Zadajte zoznam domén, pre ktoré chcete povoliť registráciu oddelených čiarkou. Používatelia dostanú email kvôli overeniu adresy predtým ako im bude dovolené používať aplikáciu. <br> Používatelia si budú môcť po úspešnej registrácii zmeniť svoju emailovú adresu.',
|
||||
'reg_confirm_restrict_domain_placeholder' => 'Nie sú nastavené žiadne obmedzenia',
|
||||
|
||||
/**
|
||||
* Role settings
|
||||
*/
|
||||
|
||||
'roles' => 'Roly',
|
||||
'role_user_roles' => 'Používateľské roly',
|
||||
'role_create' => 'Vytvoriť novú rolu',
|
||||
'role_create_success' => 'Rola úspešne vytvorená',
|
||||
'role_delete' => 'Zmazať rolu',
|
||||
'role_delete_confirm' => 'Toto zmaže rolu menom \':roleName\'.',
|
||||
'role_delete_users_assigned' => 'Túto rolu má priradenú :userCount používateľov. Ak chcete premigrovať používateľov z tejto roly, vyberte novú rolu nižšie.',
|
||||
'role_delete_no_migration' => "Nemigrovať používateľov",
|
||||
'role_delete_sure' => 'Ste si istý, že chcete zmazať túto rolu?',
|
||||
'role_delete_success' => 'Rola úspešne zmazaná',
|
||||
'role_edit' => 'Upraviť rolu',
|
||||
'role_details' => 'Detaily roly',
|
||||
'role_name' => 'Názov roly',
|
||||
'role_desc' => 'Krátky popis roly',
|
||||
'role_system' => 'Systémové oprávnenia',
|
||||
'role_manage_users' => 'Spravovať používateľov',
|
||||
'role_manage_roles' => 'Spravovať role a oprávnenia rolí',
|
||||
'role_manage_entity_permissions' => 'Spravovať všetky oprávnenia kníh, kapitol a stránok',
|
||||
'role_manage_own_entity_permissions' => 'Spravovať oprávnenia vlastných kníh, kapitol a stránok',
|
||||
'role_manage_settings' => 'Spravovať nastavenia aplikácie',
|
||||
'role_asset' => 'Oprávnenia majetku',
|
||||
'role_asset_desc' => 'Tieto oprávnenia regulujú prednastavený prístup k zdroju v systéme. Oprávnenia pre knihy, kapitoly a stránky majú vyššiu prioritu.',
|
||||
'role_all' => 'Všetko',
|
||||
'role_own' => 'Vlastné',
|
||||
'role_controlled_by_asset' => 'Regulované zdrojom, do ktorého sú nahrané',
|
||||
'role_save' => 'Uložiť rolu',
|
||||
'role_update_success' => 'Roly úspešne aktualizované',
|
||||
'role_users' => 'Používatelia s touto rolou',
|
||||
'role_users_none' => 'Žiadni používatelia nemajú priradenú túto rolu',
|
||||
|
||||
/**
|
||||
* Users
|
||||
*/
|
||||
|
||||
'users' => 'Používatelia',
|
||||
'user_profile' => 'Profil používateľa',
|
||||
'users_add_new' => 'Pridať nového používateľa',
|
||||
'users_search' => 'Hľadať medzi používateľmi',
|
||||
'users_role' => 'Používateľské roly',
|
||||
'users_external_auth_id' => 'Externé autentifikačné ID',
|
||||
'users_password_warning' => 'Pole nižšie vyplňte iba ak chcete zmeniť heslo:',
|
||||
'users_system_public' => 'Tento účet reprezentuje každého hosťovského používateľa, ktorý navštívi Vašu inštanciu. Nedá sa pomocou neho prihlásiť a je priradený automaticky.',
|
||||
'users_delete' => 'Zmazať používateľa',
|
||||
'users_delete_named' => 'Zmazať používateľa :userName',
|
||||
'users_delete_warning' => ' Toto úplne odstráni používateľa menom \':userName\' zo systému.',
|
||||
'users_delete_confirm' => 'Ste si istý, že chcete zmazať tohoto používateľa?',
|
||||
'users_delete_success' => 'Používateľ úspešne zmazaný',
|
||||
'users_edit' => 'Upraviť používateľa',
|
||||
'users_edit_profile' => 'Upraviť profil',
|
||||
'users_edit_success' => 'Používateľ úspešne upravený',
|
||||
'users_avatar' => 'Avatar používateľa',
|
||||
'users_avatar_desc' => 'Tento obrázok by mal byť štvorec s rozmerom približne 256px.',
|
||||
'users_preferred_language' => 'Preferovaný jazyk',
|
||||
'users_social_accounts' => 'Sociálne účty',
|
||||
'users_social_accounts_info' => 'Tu si môžete pripojiť iné účty pre rýchlejšie a jednoduchšie prihlásenie. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',
|
||||
'users_social_connect' => 'Pripojiť účet',
|
||||
'users_social_disconnect' => 'Odpojiť účet',
|
||||
'users_social_connected' => ':socialAccount účet bol úspešne pripojený k Vášmu profilu.',
|
||||
'users_social_disconnected' => ':socialAccount účet bol úspešne odpojený od Vášho profilu.',
|
||||
];
|
108
resources/lang/sk/validation.php
Normal file
108
resources/lang/sk/validation.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => ':attribute musí byť akceptovaný.',
|
||||
'active_url' => ':attribute nie je platná URL.',
|
||||
'after' => ':attribute musí byť dátum po :date.',
|
||||
'alpha' => ':attribute môže obsahovať iba písmená.',
|
||||
'alpha_dash' => ':attribute môže obsahovať iba písmená, čísla a pomlčky.',
|
||||
'alpha_num' => ':attribute môže obsahovať iba písmená a čísla.',
|
||||
'array' => ':attribute musí byť pole.',
|
||||
'before' => ':attribute musí byť dátum pred :date.',
|
||||
'between' => [
|
||||
'numeric' => ':attribute musí byť medzi :min a :max.',
|
||||
'file' => ':attribute musí byť medzi :min a :max kilobajtmi.',
|
||||
'string' => ':attribute musí byť medzi :min a :max znakmi.',
|
||||
'array' => ':attribute musí byť medzi :min a :max položkami.',
|
||||
],
|
||||
'boolean' => ':attribute pole musí byť true alebo false.',
|
||||
'confirmed' => ':attribute potvrdenie nesedí.',
|
||||
'date' => ':attribute nie je platný dátum.',
|
||||
'date_format' => ':attribute nesedí s formátom :format.',
|
||||
'different' => ':attribute a :other musia byť rozdielne.',
|
||||
'digits' => ':attribute musí mať :digits číslic.',
|
||||
'digits_between' => ':attribute musí mať medzi :min a :max číslicami.',
|
||||
'email' => ':attribute musí byť platná emailová adresa.',
|
||||
'filled' => 'Políčko :attribute je povinné.',
|
||||
'exists' => 'Vybraný :attribute nie je platný.',
|
||||
'image' => ':attribute musí byť obrázok.',
|
||||
'in' => 'Vybraný :attribute je neplatný.',
|
||||
'integer' => ':attribute musí byť celé číslo.',
|
||||
'ip' => ':attribute musí byť platná IP adresa.',
|
||||
'max' => [
|
||||
'numeric' => ':attribute nesmie byť väčší ako :max.',
|
||||
'file' => ':attribute nesmie byť väčší ako :max kilobajtov.',
|
||||
'string' => ':attribute nesmie byť dlhší ako :max znakov.',
|
||||
'array' => ':attribute nesmie mať viac ako :max položiek.',
|
||||
],
|
||||
'mimes' => ':attribute musí byť súbor typu: :values.',
|
||||
'min' => [
|
||||
'numeric' => ':attribute musí byť aspoň :min.',
|
||||
'file' => ':attribute musí mať aspoň :min kilobajtov.',
|
||||
'string' => ':attribute musí mať aspoň :min znakov.',
|
||||
'array' => ':attribute musí mať aspoň :min položiek.',
|
||||
],
|
||||
'not_in' => 'Vybraný :attribute je neplatný.',
|
||||
'numeric' => ':attribute musí byť číslo.',
|
||||
'regex' => ':attribute formát je neplatný.',
|
||||
'required' => 'Políčko :attribute je povinné.',
|
||||
'required_if' => 'Políčko :attribute je povinné ak :other je :value.',
|
||||
'required_with' => 'Políčko :attribute je povinné ak :values existuje.',
|
||||
'required_with_all' => 'Políčko :attribute je povinné ak :values existuje.',
|
||||
'required_without' => 'Políčko :attribute je povinné aj :values neexistuje.',
|
||||
'required_without_all' => 'Políčko :attribute je povinné ak ani jedno z :values neexistuje.',
|
||||
'same' => ':attribute a :other musia byť rovnaké.',
|
||||
'size' => [
|
||||
'numeric' => ':attribute musí byť :size.',
|
||||
'file' => ':attribute musí mať :size kilobajtov.',
|
||||
'string' => ':attribute musí mať :size znakov.',
|
||||
'array' => ':attribute musí obsahovať :size položiek.',
|
||||
],
|
||||
'string' => ':attribute musí byť reťazec.',
|
||||
'timezone' => ':attribute musí byť plantá časová zóna.',
|
||||
'unique' => ':attribute je už použité.',
|
||||
'url' => ':attribute formát je neplatný.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify custom validation messages for attributes using the
|
||||
| convention "attribute.rule" to name the lines. This makes it quick to
|
||||
| specify a specific custom language line for a given attribute rule.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'password-confirm' => [
|
||||
'required_with' => 'Vyžaduje sa potvrdenie hesla',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap attribute place-holders
|
||||
| with something more reader friendly such as E-Mail Address instead
|
||||
| of "email". This simply helps us make messages a little cleaner.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
@ -47,7 +47,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-4 col-sm-3 text-center">
|
||||
<form action="{{ baseUrl('/search/all') }}" method="GET" class="search-box">
|
||||
<form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
|
||||
<input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
|
||||
<button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button>
|
||||
</form>
|
||||
|
@ -50,15 +50,15 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div class="container" id="book-dashboard" ng-controller="BookShowController" book-id="{{ $book->id }}">
|
||||
<div class="container" id="entity-dashboard" entity-id="{{ $book->id }}" entity-type="book">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
|
||||
<h1>{{$book->name}}</h1>
|
||||
<div class="book-content" ng-show="!searching">
|
||||
<p class="text-muted" ng-non-bindable>{{$book->description}}</p>
|
||||
<div class="book-content" v-if="!searching">
|
||||
<p class="text-muted" v-pre>{{$book->description}}</p>
|
||||
|
||||
<div class="page-list" ng-non-bindable>
|
||||
<div class="page-list" v-pre>
|
||||
<hr>
|
||||
@if(count($bookChildren) > 0)
|
||||
@foreach($bookChildren as $childElement)
|
||||
@ -81,12 +81,12 @@
|
||||
@include('partials.entity-meta', ['entity' => $book])
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-results" ng-cloak ng-show="searching">
|
||||
<h3 class="text-muted">{{ trans('entities.search_results') }} <a ng-if="searching" ng-click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
|
||||
<div ng-if="!searchResults">
|
||||
<div class="search-results" v-cloak v-if="searching">
|
||||
<h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
|
||||
<div v-if="!searchResults">
|
||||
@include('partials/loading-icon')
|
||||
</div>
|
||||
<div ng-bind-html="searchResults"></div>
|
||||
<div v-html="searchResults"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -94,6 +94,7 @@
|
||||
|
||||
<div class="col-md-4 col-md-offset-1">
|
||||
<div class="margin-top large"></div>
|
||||
|
||||
@if($book->restricted)
|
||||
<p class="text-muted">
|
||||
@if(userCan('restrictions-manage', $book))
|
||||
@ -103,14 +104,16 @@
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="search-box">
|
||||
<form ng-submit="searchBook($event)">
|
||||
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
|
||||
<form v-on:submit="searchBook">
|
||||
<input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
|
||||
<button type="submit"><i class="zmdi zmdi-search"></i></button>
|
||||
<button ng-if="searching" ng-click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
|
||||
<button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="activity anim fadeIn">
|
||||
|
||||
<div class="activity">
|
||||
<h3>{{ trans('entities.recent_activity') }}</h3>
|
||||
@include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
|
||||
</div>
|
||||
|
@ -47,40 +47,50 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div class="container" ng-non-bindable>
|
||||
<div class="container" id="entity-dashboard" entity-id="{{ $chapter->id }}" entity-type="chapter">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-7">
|
||||
<h1>{{ $chapter->name }}</h1>
|
||||
<p class="text-muted">{{ $chapter->description }}</p>
|
||||
<div class="chapter-content" v-if="!searching">
|
||||
<p class="text-muted">{{ $chapter->description }}</p>
|
||||
|
||||
@if(count($pages) > 0)
|
||||
<div class="page-list">
|
||||
<hr>
|
||||
@foreach($pages as $page)
|
||||
@include('pages/list-item', ['page' => $page])
|
||||
@if(count($pages) > 0)
|
||||
<div class="page-list">
|
||||
<hr>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<hr>
|
||||
<p class="text-muted">{{ trans('entities.chapters_empty') }}</p>
|
||||
<p>
|
||||
@if(userCan('page-create', $chapter))
|
||||
<a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
|
||||
@endif
|
||||
@if(userCan('page-create', $chapter) && userCan('book-update', $book))
|
||||
<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>
|
||||
@endif
|
||||
@if(userCan('book-update', $book))
|
||||
<a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a>
|
||||
@endif
|
||||
</p>
|
||||
<hr>
|
||||
@endif
|
||||
@foreach($pages as $page)
|
||||
@include('pages/list-item', ['page' => $page])
|
||||
<hr>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<hr>
|
||||
<p class="text-muted">{{ trans('entities.chapters_empty') }}</p>
|
||||
<p>
|
||||
@if(userCan('page-create', $chapter))
|
||||
<a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
|
||||
@endif
|
||||
@if(userCan('page-create', $chapter) && userCan('book-update', $book))
|
||||
<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>
|
||||
@endif
|
||||
@if(userCan('book-update', $book))
|
||||
<a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a>
|
||||
@endif
|
||||
</p>
|
||||
<hr>
|
||||
@endif
|
||||
|
||||
@include('partials.entity-meta', ['entity' => $chapter])
|
||||
@include('partials.entity-meta', ['entity' => $chapter])
|
||||
</div>
|
||||
|
||||
<div class="search-results" v-cloak v-if="searching">
|
||||
<h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
|
||||
<div v-if="!searchResults">
|
||||
@include('partials/loading-icon')
|
||||
</div>
|
||||
<div v-html="searchResults"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-md-offset-1">
|
||||
<div class="col-md-4 col-md-offset-1">
|
||||
<div class="margin-top large"></div>
|
||||
@if($book->restricted || $chapter->restricted)
|
||||
<div class="text-muted">
|
||||
@ -105,7 +115,16 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="search-box">
|
||||
<form v-on:submit="searchBook">
|
||||
<input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.chapters_search_this') }}">
|
||||
<button type="submit"><i class="zmdi zmdi-search"></i></button>
|
||||
<button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,13 +3,13 @@
|
||||
|
||||
@if(isset($page) && $page->tags->count() > 0)
|
||||
<div class="tag-display">
|
||||
<h6 class="text-muted">Page Tags</h6>
|
||||
<h6 class="text-muted">{{ trans('entities.page_tags') }}</h6>
|
||||
<table>
|
||||
<tbody>
|
||||
@foreach($page->tags as $tag)
|
||||
<tr class="tag">
|
||||
<td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
|
||||
@if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search/all?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
|
||||
<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>
|
||||
|
@ -2,59 +2,212 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<input type="hidden" name="searchTerm" value="{{$searchTerm}}">
|
||||
|
||||
<div id="search-system">
|
||||
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 faded">
|
||||
<div class="breadcrumbs">
|
||||
<a href="{{ baseUrl("/search/all?term={$searchTerm}") }}" class="text-button"><i class="zmdi zmdi-search"></i>{{ $searchTerm }}</a>
|
||||
<a href="{{ baseUrl("/search?term=" . urlencode($searchTerm)) }}" class="text-button"><i class="zmdi zmdi-search"></i>{{ trans('entities.search_for_term', ['term' => $searchTerm]) }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="container" ng-non-bindable>
|
||||
|
||||
<h1>{{ trans('entities.search_results') }}</h1>
|
||||
|
||||
<p>
|
||||
@if(count($pages) > 0)
|
||||
<a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.search_view_pages') }}</a>
|
||||
@endif
|
||||
|
||||
@if(count($chapters) > 0)
|
||||
|
||||
<a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>{{ trans('entities.search_view_chapters') }}</a>
|
||||
@endif
|
||||
|
||||
@if(count($books) > 0)
|
||||
|
||||
<a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.search_view_books') }}</a>
|
||||
@endif
|
||||
</p>
|
||||
<div class="container" ng-non-bindable id="searchSystem">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3><a href="{{ baseUrl("/search/pages?term={$searchTerm}") }}" class="no-color">{{ trans('entities.pages') }}</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $pages, 'style' => 'detailed'])
|
||||
</div>
|
||||
<div class="col-md-5 col-md-offset-1">
|
||||
@if(count($books) > 0)
|
||||
<h3><a href="{{ baseUrl("/search/books?term={$searchTerm}") }}" class="no-color">{{ trans('entities.books') }}</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $books])
|
||||
@endif
|
||||
|
||||
@if(count($chapters) > 0)
|
||||
<h3><a href="{{ baseUrl("/search/chapters?term={$searchTerm}") }}" class="no-color">{{ trans('entities.chapters') }}</a></h3>
|
||||
@include('partials/entity-list', ['entities' => $chapters])
|
||||
<div class="col-md-6">
|
||||
<h1>{{ trans('entities.search_results') }}</h1>
|
||||
<h6 class="text-muted">{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}</h6>
|
||||
@include('partials/entity-list', ['entities' => $entities])
|
||||
@if ($hasNextPage)
|
||||
<a href="{{ $nextPageLink }}" class="button">{{ trans('entities.search_more') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="col-md-5 col-md-offset-1">
|
||||
<h3>{{ trans('entities.search_filters') }}</h3>
|
||||
|
||||
<form v-on:submit="updateSearch" v-cloak class="v-cloak anim fadeIn">
|
||||
<h6 class="text-muted">{{ trans('entities.search_content_type') }}</h6>
|
||||
<div class="form-group">
|
||||
<label class="inline checkbox text-page"><input type="checkbox" v-on:change="typeChange" v-model="search.type.page" value="page">{{ trans('entities.page') }}</label>
|
||||
<label class="inline checkbox text-chapter"><input type="checkbox" v-on:change="typeChange" v-model="search.type.chapter" value="chapter">{{ trans('entities.chapter') }}</label>
|
||||
<label class="inline checkbox text-book"><input type="checkbox" v-on:change="typeChange" v-model="search.type.book" value="book">{{ trans('entities.book') }}</label>
|
||||
</div>
|
||||
|
||||
<h6 class="text-muted">{{ trans('entities.search_exact_matches') }}</h6>
|
||||
<table cellpadding="0" cellspacing="0" border="0" class="no-style">
|
||||
<tr v-for="(term, i) in search.exactTerms">
|
||||
<td style="padding: 0 12px 6px 0;">
|
||||
<input class="exact-input outline" v-on:input="exactChange" type="text" v-model="search.exactTerms[i]"></td>
|
||||
<td>
|
||||
<button type="button" class="text-neg text-button" v-on:click="removeExact(i)">
|
||||
<i class="zmdi zmdi-close"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<button type="button" class="text-button" v-on:click="addExact">
|
||||
<i class="zmdi zmdi-plus-circle-o"></i>{{ trans('common.add') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h6 class="text-muted">{{ trans('entities.search_tags') }}</h6>
|
||||
<table cellpadding="0" cellspacing="0" border="0" class="no-style">
|
||||
<tr v-for="(term, i) in search.tagTerms">
|
||||
<td style="padding: 0 12px 6px 0;">
|
||||
<input class="tag-input outline" v-on:input="tagChange" type="text" v-model="search.tagTerms[i]"></td>
|
||||
<td>
|
||||
<button type="button" class="text-neg text-button" v-on:click="removeTag(i)">
|
||||
<i class="zmdi zmdi-close"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<button type="button" class="text-button" v-on:click="addTag">
|
||||
<i class="zmdi zmdi-plus-circle-o"></i>{{ trans('common.add') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h6 class="text-muted">Options</h6>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-on:change="optionChange('viewed_by_me')"
|
||||
v-model="search.option.viewed_by_me" value="page">
|
||||
{{ trans('entities.search_viewed_by_me') }}
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-on:change="optionChange('not_viewed_by_me')"
|
||||
v-model="search.option.not_viewed_by_me" value="page">
|
||||
{{ trans('entities.search_not_viewed_by_me') }}
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-on:change="optionChange('is_restricted')"
|
||||
v-model="search.option.is_restricted" value="page">
|
||||
{{ trans('entities.search_permissions_set') }}
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-on:change="optionChange('created_by:me')"
|
||||
v-model="search.option['created_by:me']" value="page">
|
||||
{{ trans('entities.search_created_by_me') }}
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-on:change="optionChange('updated_by:me')"
|
||||
v-model="search.option['updated_by:me']" value="page">
|
||||
{{ trans('entities.search_updated_by_me') }}
|
||||
</label>
|
||||
|
||||
<h6 class="text-muted">Date Options</h6>
|
||||
<table cellpadding="0" cellspacing="0" border="0" class="no-style form-table">
|
||||
<tr>
|
||||
<td width="200">{{ trans('entities.search_updated_after') }}</td>
|
||||
<td width="80">
|
||||
<button type="button" class="text-button" v-if="!search.dates.updated_after"
|
||||
v-on:click="enableDate('updated_after')">{{ trans('entities.search_set_date') }}</button>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="search.dates.updated_after">
|
||||
<td>
|
||||
<input v-if="search.dates.updated_after" class="tag-input"
|
||||
v-on:input="dateChange('updated_after')" type="date" v-model="search.dates.updated_after"
|
||||
pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="search.dates.updated_after" type="button" class="text-neg text-button"
|
||||
v-on:click="dateRemove('updated_after')">
|
||||
<i class="zmdi zmdi-close"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ trans('entities.search_updated_before') }}</td>
|
||||
<td>
|
||||
<button type="button" class="text-button" v-if="!search.dates.updated_before"
|
||||
v-on:click="enableDate('updated_before')">{{ trans('entities.search_set_date') }}</button>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="search.dates.updated_before">
|
||||
<td>
|
||||
<input v-if="search.dates.updated_before" class="tag-input"
|
||||
v-on:input="dateChange('updated_before')" type="date" v-model="search.dates.updated_before"
|
||||
pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="search.dates.updated_before" type="button" class="text-neg text-button"
|
||||
v-on:click="dateRemove('updated_before')">
|
||||
<i class="zmdi zmdi-close"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ trans('entities.search_created_after') }}</td>
|
||||
<td>
|
||||
<button type="button" class="text-button" v-if="!search.dates.created_after"
|
||||
v-on:click="enableDate('created_after')">{{ trans('entities.search_set_date') }}</button>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="search.dates.created_after">
|
||||
<td>
|
||||
<input v-if="search.dates.created_after" class="tag-input"
|
||||
v-on:input="dateChange('created_after')" type="date" v-model="search.dates.created_after"
|
||||
pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="search.dates.created_after" type="button" class="text-neg text-button"
|
||||
v-on:click="dateRemove('created_after')">
|
||||
<i class="zmdi zmdi-close"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ trans('entities.search_created_before') }}</td>
|
||||
<td>
|
||||
<button type="button" class="text-button" v-if="!search.dates.created_before"
|
||||
v-on:click="enableDate('created_before')">{{ trans('entities.search_set_date') }}</button>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="search.dates.created_before">
|
||||
<td>
|
||||
<input v-if="search.dates.created_before" class="tag-input"
|
||||
v-on:input="dateChange('created_before')" type="date" v-model="search.dates.created_before"
|
||||
pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="search.dates.created_before" type="button" class="text-neg text-button"
|
||||
v-on:click="dateRemove('created_before')">
|
||||
<i class="zmdi zmdi-close"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<button type="submit" class="button primary">{{ trans('entities.search_update') }}</button>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@stop
|
@ -123,11 +123,9 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::get('/link/{id}', 'PageController@redirectFromLink');
|
||||
|
||||
// Search
|
||||
Route::get('/search/all', 'SearchController@searchAll');
|
||||
Route::get('/search/pages', 'SearchController@searchPages');
|
||||
Route::get('/search/books', 'SearchController@searchBooks');
|
||||
Route::get('/search/chapters', 'SearchController@searchChapters');
|
||||
Route::get('/search', 'SearchController@search');
|
||||
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
|
||||
Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
|
||||
|
||||
// Other Pages
|
||||
Route::get('/', 'HomeController@index');
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php namespace Tests;
|
||||
|
||||
class EntitySearchTest extends BrowserKitTest
|
||||
|
||||
class EntitySearchTest extends TestCase
|
||||
{
|
||||
|
||||
public function test_page_search()
|
||||
@ -8,32 +9,35 @@ class EntitySearchTest extends BrowserKitTest
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$page = $book->pages->first();
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
->type($page->name, 'term')
|
||||
->press('header-search-box-button')
|
||||
->see('Search Results')
|
||||
->seeInElement('.entity-list', $page->name)
|
||||
->clickInElement('.entity-list', $page->name)
|
||||
->seePageIs($page->getUrl());
|
||||
$search = $this->asEditor()->get('/search?term=' . urlencode($page->name));
|
||||
$search->assertSee('Search Results');
|
||||
$search->assertSee($page->name);
|
||||
}
|
||||
|
||||
public function test_invalid_page_search()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
->type('<p>test</p>', 'term')
|
||||
->press('header-search-box-button')
|
||||
->see('Search Results')
|
||||
->seeStatusCode(200);
|
||||
$resp = $this->asEditor()->get('/search?term=' . urlencode('<p>test</p>'));
|
||||
$resp->assertSee('Search Results');
|
||||
$resp->assertStatus(200);
|
||||
$this->get('/search?term=cat+-')->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_empty_search_redirects_back()
|
||||
public function test_empty_search_shows_search_page()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
->visit('/search/all')
|
||||
->seePageIs('/');
|
||||
$res = $this->asEditor()->get('/search');
|
||||
$res->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_searching_accents_and_small_terms()
|
||||
{
|
||||
$page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'some áéííúü¿¡ test content {a2 orange dog']);
|
||||
$this->asEditor();
|
||||
|
||||
$accentSearch = $this->get('/search?term=' . urlencode('áéíí'));
|
||||
$accentSearch->assertStatus(200)->assertSee($page->name);
|
||||
|
||||
$smallSearch = $this->get('/search?term=' . urlencode('{a'));
|
||||
$smallSearch->assertStatus(200)->assertSee($page->name);
|
||||
}
|
||||
|
||||
public function test_book_search()
|
||||
@ -42,57 +46,20 @@ class EntitySearchTest extends BrowserKitTest
|
||||
$page = $book->pages->last();
|
||||
$chapter = $book->chapters->last();
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/search/book/' . $book->id . '?term=' . urlencode($page->name))
|
||||
->see($page->name)
|
||||
$pageTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($page->name));
|
||||
$pageTestResp->assertSee($page->name);
|
||||
|
||||
->visit('/search/book/' . $book->id . '?term=' . urlencode($chapter->name))
|
||||
->see($chapter->name);
|
||||
$chapterTestResp = $this->asEditor()->get('/search/book/' . $book->id . '?term=' . urlencode($chapter->name));
|
||||
$chapterTestResp->assertSee($chapter->name);
|
||||
}
|
||||
|
||||
public function test_empty_book_search_redirects_back()
|
||||
public function test_chapter_search()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$this->asAdmin()
|
||||
->visit('/books')
|
||||
->visit('/search/book/' . $book->id . '?term=')
|
||||
->seePageIs('/books');
|
||||
}
|
||||
$chapter = \BookStack\Chapter::has('pages')->first();
|
||||
$page = $chapter->pages[0];
|
||||
|
||||
|
||||
public function test_pages_search_listing()
|
||||
{
|
||||
$page = \BookStack\Page::all()->last();
|
||||
$this->asAdmin()->visit('/search/pages?term=' . $page->name)
|
||||
->see('Page Search Results')->see('.entity-list', $page->name);
|
||||
}
|
||||
|
||||
public function test_chapters_search_listing()
|
||||
{
|
||||
$chapter = \BookStack\Chapter::all()->last();
|
||||
$this->asAdmin()->visit('/search/chapters?term=' . $chapter->name)
|
||||
->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name);
|
||||
}
|
||||
|
||||
public function test_search_quote_term_preparation()
|
||||
{
|
||||
$termString = '"192" cat "dog hat"';
|
||||
$repo = $this->app[\BookStack\Repos\EntityRepo::class];
|
||||
$preparedTerms = $repo->prepareSearchTerms($termString);
|
||||
$this->assertTrue($preparedTerms === ['"192"','"dog hat"', 'cat']);
|
||||
}
|
||||
|
||||
public function test_books_search_listing()
|
||||
{
|
||||
$book = \BookStack\Book::all()->last();
|
||||
$this->asAdmin()->visit('/search/books?term=' . $book->name)
|
||||
->see('Book Search Results')->see('.entity-list', $book->name);
|
||||
}
|
||||
|
||||
public function test_searching_hypen_doesnt_break()
|
||||
{
|
||||
$this->visit('/search/all?term=cat+-')
|
||||
->seeStatusCode(200);
|
||||
$pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name));
|
||||
$pageTestResp->assertSee($page->name);
|
||||
}
|
||||
|
||||
public function test_tag_search()
|
||||
@ -114,27 +81,99 @@ class EntitySearchTest extends BrowserKitTest
|
||||
$pageB = \BookStack\Page::all()->last();
|
||||
$pageB->tags()->create(['name' => 'animal', 'value' => 'dog']);
|
||||
|
||||
$this->asAdmin()->visit('/search/all?term=%5Banimal%5D')
|
||||
->seeLink($pageA->name)
|
||||
->seeLink($pageB->name);
|
||||
$this->asEditor();
|
||||
$tNameSearch = $this->get('/search?term=%5Banimal%5D');
|
||||
$tNameSearch->assertSee($pageA->name)->assertSee($pageB->name);
|
||||
|
||||
$this->visit('/search/all?term=%5Bcolor%5D')
|
||||
->seeLink($pageA->name)
|
||||
->dontSeeLink($pageB->name);
|
||||
$tNameSearch2 = $this->get('/search?term=%5Bcolor%5D');
|
||||
$tNameSearch2->assertSee($pageA->name)->assertDontSee($pageB->name);
|
||||
|
||||
$this->visit('/search/all?term=%5Banimal%3Dcat%5D')
|
||||
->seeLink($pageA->name)
|
||||
->dontSeeLink($pageB->name);
|
||||
$tNameValSearch = $this->get('/search?term=%5Banimal%3Dcat%5D');
|
||||
$tNameValSearch->assertSee($pageA->name)->assertDontSee($pageB->name);
|
||||
}
|
||||
|
||||
public function test_exact_searches()
|
||||
{
|
||||
$page = $this->newPage(['name' => 'My new test page', 'html' => 'this is a story about an orange donkey']);
|
||||
|
||||
$exactSearchA = $this->asEditor()->get('/search?term=' . urlencode('"story about an orange"'));
|
||||
$exactSearchA->assertStatus(200)->assertSee($page->name);
|
||||
|
||||
$exactSearchB = $this->asEditor()->get('/search?term=' . urlencode('"story not about an orange"'));
|
||||
$exactSearchB->assertStatus(200)->assertDontSee($page->name);
|
||||
}
|
||||
|
||||
public function test_search_filters()
|
||||
{
|
||||
$page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']);
|
||||
$this->asEditor();
|
||||
$editorId = $this->getEditor()->id;
|
||||
|
||||
// Viewed filter searches
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertDontSee($page->name);
|
||||
$this->get($page->getUrl());
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertDontSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {viewed_by_me}'))->assertSee($page->name);
|
||||
|
||||
// User filters
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorId.'}'))->assertDontSee($page->name);
|
||||
$page->created_by = $editorId;
|
||||
$page->save();
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_by:'.$editorId.'}'))->assertSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
|
||||
$page->updated_by = $editorId;
|
||||
$page->save();
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorId.'}'))->assertSee($page->name);
|
||||
|
||||
// Content filters
|
||||
$this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('{in_body:danzorbhsing}'))->assertSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('{in_name:test quaffleachits}'))->assertSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('{in_body:test quaffleachits}'))->assertDontSee($page->name);
|
||||
|
||||
// Restricted filter
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name);
|
||||
$page->restricted = true;
|
||||
$page->save();
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name);
|
||||
|
||||
// Date filters
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertDontSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertSee($page->name);
|
||||
$page->updated_at = '2037-02-01';
|
||||
$page->save();
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_after:2037-01-01}'))->assertSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_before:2037-01-01}'))->assertDontSee($page->name);
|
||||
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertDontSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertSee($page->name);
|
||||
$page->created_at = '2037-02-01';
|
||||
$page->save();
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_after:2037-01-01}'))->assertSee($page->name);
|
||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name);
|
||||
}
|
||||
|
||||
public function test_ajax_entity_search()
|
||||
{
|
||||
$page = \BookStack\Page::all()->last();
|
||||
$notVisitedPage = \BookStack\Page::first();
|
||||
$this->visit($page->getUrl());
|
||||
$this->asAdmin()->visit('/ajax/search/entities?term=' . $page->name)->see('.entity-list', $page->name);
|
||||
$this->asAdmin()->visit('/ajax/search/entities?types=book&term=' . $page->name)->dontSee('.entity-list', $page->name);
|
||||
$this->asAdmin()->visit('/ajax/search/entities')->see('.entity-list', $page->name)->dontSee($notVisitedPage->name);
|
||||
|
||||
// Visit the page to make popular
|
||||
$this->asEditor()->get($page->getUrl());
|
||||
|
||||
$normalSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name));
|
||||
$normalSearch->assertSee($page->name);
|
||||
|
||||
$bookSearch = $this->get('/ajax/search/entities?types=book&term=' . urlencode($page->name));
|
||||
$bookSearch->assertDontSee($page->name);
|
||||
|
||||
$defaultListTest = $this->get('/ajax/search/entities');
|
||||
$defaultListTest->assertSee($page->name);
|
||||
$defaultListTest->assertDontSee($notVisitedPage->name);
|
||||
}
|
||||
}
|
||||
|
@ -76,4 +76,16 @@ abstract class TestCase extends BaseTestCase
|
||||
public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
|
||||
return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new test page
|
||||
* @param array $input
|
||||
* @return Chapter
|
||||
*/
|
||||
public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
|
||||
$book = Book::first();
|
||||
$entityRepo = $this->app[EntityRepo::class];
|
||||
$draftPage = $entityRepo->getDraftPage($book);
|
||||
return $entityRepo->publishPageDraft($draftPage, $input);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user