mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Converted search filters to not be vue based
This commit is contained in:
parent
76d02cd472
commit
715dee2d0e
137
app/Entities/SearchOptions.php
Normal file
137
app/Entities/SearchOptions.php
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<?php namespace BookStack\Entities;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SearchOptions
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $searches = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $exacts = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $tags = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $filters = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance from a search string.
|
||||||
|
*/
|
||||||
|
public static function fromString(string $search): SearchOptions
|
||||||
|
{
|
||||||
|
$decoded = static::decode($search);
|
||||||
|
$instance = new static();
|
||||||
|
foreach ($decoded as $type => $value) {
|
||||||
|
$instance->$type = $value;
|
||||||
|
}
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance from a request.
|
||||||
|
* Will look for a classic string term and use that
|
||||||
|
* Otherwise we'll use the details from an advanced search form.
|
||||||
|
*/
|
||||||
|
public static function fromRequest(Request $request): SearchOptions
|
||||||
|
{
|
||||||
|
if ($request->has('term')) {
|
||||||
|
return static::fromString($request->get('term'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = new static();
|
||||||
|
$inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']);
|
||||||
|
$instance->searches = explode(' ', $inputs['search'] ?? []);
|
||||||
|
$instance->exacts = array_filter($inputs['exact'] ?? []);
|
||||||
|
$instance->tags = array_filter($inputs['tags'] ?? []);
|
||||||
|
foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) {
|
||||||
|
if (empty($filterVal)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$instance->filters[$filterKey] = $filterVal === 'true' ? '' : $filterVal;
|
||||||
|
}
|
||||||
|
if (isset($inputs['types']) && count($inputs['types']) < 4) {
|
||||||
|
$instance->filters['type'] = implode('|', $inputs['types']);
|
||||||
|
}
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a search string into an array of terms.
|
||||||
|
*/
|
||||||
|
protected static function decode(string $searchString): array
|
||||||
|
{
|
||||||
|
$terms = [
|
||||||
|
'searches' => [],
|
||||||
|
'exacts' => [],
|
||||||
|
'tags' => [],
|
||||||
|
'filters' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
$patterns = [
|
||||||
|
'exacts' => '/"(.*?)"/',
|
||||||
|
'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['searches'][] = $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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode this instance to a search string.
|
||||||
|
*/
|
||||||
|
public function toString(): string
|
||||||
|
{
|
||||||
|
$string = implode(' ', $this->searches ?? []);
|
||||||
|
|
||||||
|
foreach ($this->exacts as $term) {
|
||||||
|
$string .= ' "' . $term . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->tags as $term) {
|
||||||
|
$string .= " [{$term}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->filters as $filterName => $filterVal) {
|
||||||
|
$string .= ' {' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -39,10 +39,6 @@ class SearchService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchService constructor.
|
* SearchService constructor.
|
||||||
* @param SearchTerm $searchTerm
|
|
||||||
* @param EntityProvider $entityProvider
|
|
||||||
* @param Connection $db
|
|
||||||
* @param PermissionService $permissionService
|
|
||||||
*/
|
*/
|
||||||
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
|
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
|
||||||
{
|
{
|
||||||
@ -54,7 +50,6 @@ class SearchService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the database connection
|
* Set the database connection
|
||||||
* @param Connection $connection
|
|
||||||
*/
|
*/
|
||||||
public function setConnection(Connection $connection)
|
public function setConnection(Connection $connection)
|
||||||
{
|
{
|
||||||
@ -63,23 +58,18 @@ class SearchService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search all entities in the system.
|
* Search all entities in the system.
|
||||||
* @param string $searchString
|
* The provided count is for each entity to search,
|
||||||
* @param string $entityType
|
* Total returned could can be larger and not guaranteed.
|
||||||
* @param int $page
|
|
||||||
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
|
|
||||||
* @param string $action
|
|
||||||
* @return array[int, Collection];
|
|
||||||
*/
|
*/
|
||||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
|
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20, string $action = 'view'): array
|
||||||
{
|
{
|
||||||
$terms = $this->parseSearchString($searchString);
|
|
||||||
$entityTypes = array_keys($this->entityProvider->all());
|
$entityTypes = array_keys($this->entityProvider->all());
|
||||||
$entityTypesToSearch = $entityTypes;
|
$entityTypesToSearch = $entityTypes;
|
||||||
|
|
||||||
if ($entityType !== 'all') {
|
if ($entityType !== 'all') {
|
||||||
$entityTypesToSearch = $entityType;
|
$entityTypesToSearch = $entityType;
|
||||||
} else if (isset($terms['filters']['type'])) {
|
} else if (isset($searchOpts->filters['type'])) {
|
||||||
$entityTypesToSearch = explode('|', $terms['filters']['type']);
|
$entityTypesToSearch = explode('|', $searchOpts->filters['type']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$results = collect();
|
$results = collect();
|
||||||
@ -90,8 +80,8 @@ class SearchService
|
|||||||
if (!in_array($entityType, $entityTypes)) {
|
if (!in_array($entityType, $entityTypes)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$search = $this->searchEntityTable($terms, $entityType, $page, $count, $action);
|
$search = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action);
|
||||||
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true);
|
$entityTotal = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action, true);
|
||||||
if ($entityTotal > $page * $count) {
|
if ($entityTotal > $page * $count) {
|
||||||
$hasMore = true;
|
$hasMore = true;
|
||||||
}
|
}
|
||||||
@ -103,29 +93,26 @@ class SearchService
|
|||||||
'total' => $total,
|
'total' => $total,
|
||||||
'count' => count($results),
|
'count' => count($results),
|
||||||
'has_more' => $hasMore,
|
'has_more' => $hasMore,
|
||||||
'results' => $results->sortByDesc('score')->values()
|
'results' => $results->sortByDesc('score')->values(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search a book for entities
|
* Search a book for entities
|
||||||
* @param integer $bookId
|
|
||||||
* @param string $searchString
|
|
||||||
* @return Collection
|
|
||||||
*/
|
*/
|
||||||
public function searchBook($bookId, $searchString)
|
public function searchBook(int $bookId, string $searchString): Collection
|
||||||
{
|
{
|
||||||
$terms = $this->parseSearchString($searchString);
|
$opts = SearchOptions::fromString($searchString);
|
||||||
$entityTypes = ['page', 'chapter'];
|
$entityTypes = ['page', 'chapter'];
|
||||||
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
|
$entityTypesToSearch = isset($opts->filters['type']) ? explode('|', $opts->filters['type']) : $entityTypes;
|
||||||
|
|
||||||
$results = collect();
|
$results = collect();
|
||||||
foreach ($entityTypesToSearch as $entityType) {
|
foreach ($entityTypesToSearch as $entityType) {
|
||||||
if (!in_array($entityType, $entityTypes)) {
|
if (!in_array($entityType, $entityTypes)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
$search = $this->buildEntitySearchQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
||||||
$results = $results->merge($search);
|
$results = $results->merge($search);
|
||||||
}
|
}
|
||||||
return $results->sortByDesc('score')->take(20);
|
return $results->sortByDesc('score')->take(20);
|
||||||
@ -133,30 +120,23 @@ class SearchService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search a book for entities
|
* Search a book for entities
|
||||||
* @param integer $chapterId
|
|
||||||
* @param string $searchString
|
|
||||||
* @return Collection
|
|
||||||
*/
|
*/
|
||||||
public function searchChapter($chapterId, $searchString)
|
public function searchChapter(int $chapterId, string $searchString): Collection
|
||||||
{
|
{
|
||||||
$terms = $this->parseSearchString($searchString);
|
$opts = SearchOptions::fromString($searchString);
|
||||||
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
|
$pages = $this->buildEntitySearchQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
|
||||||
return $pages->sortByDesc('score');
|
return $pages->sortByDesc('score');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search across a particular entity type.
|
* Search across a particular entity type.
|
||||||
* @param array $terms
|
* Setting getCount = true will return the total
|
||||||
* @param string $entityType
|
* matching instead of the items themselves.
|
||||||
* @param int $page
|
|
||||||
* @param int $count
|
|
||||||
* @param string $action
|
|
||||||
* @param bool $getCount Return the total count of the search
|
|
||||||
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
||||||
*/
|
*/
|
||||||
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false)
|
public function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false)
|
||||||
{
|
{
|
||||||
$query = $this->buildEntitySearchQuery($terms, $entityType, $action);
|
$query = $this->buildEntitySearchQuery($searchOpts, $entityType, $action);
|
||||||
if ($getCount) {
|
if ($getCount) {
|
||||||
return $query->count();
|
return $query->count();
|
||||||
}
|
}
|
||||||
@ -167,22 +147,18 @@ class SearchService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a search query for an entity
|
* Create a search query for an entity
|
||||||
* @param array $terms
|
|
||||||
* @param string $entityType
|
|
||||||
* @param string $action
|
|
||||||
* @return EloquentBuilder
|
|
||||||
*/
|
*/
|
||||||
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
|
protected function buildEntitySearchQuery(SearchOptions $searchOpts, string $entityType = 'page', string $action = 'view'): EloquentBuilder
|
||||||
{
|
{
|
||||||
$entity = $this->entityProvider->get($entityType);
|
$entity = $this->entityProvider->get($entityType);
|
||||||
$entitySelect = $entity->newQuery();
|
$entitySelect = $entity->newQuery();
|
||||||
|
|
||||||
// Handle normal search terms
|
// Handle normal search terms
|
||||||
if (count($terms['search']) > 0) {
|
if (count($searchOpts->searches) > 0) {
|
||||||
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
||||||
$subQuery->where('entity_type', '=', $entity->getMorphClass());
|
$subQuery->where('entity_type', '=', $entity->getMorphClass());
|
||||||
$subQuery->where(function (Builder $query) use ($terms) {
|
$subQuery->where(function (Builder $query) use ($searchOpts) {
|
||||||
foreach ($terms['search'] as $inputTerm) {
|
foreach ($searchOpts->searches as $inputTerm) {
|
||||||
$query->orWhere('term', 'like', $inputTerm .'%');
|
$query->orWhere('term', 'like', $inputTerm .'%');
|
||||||
}
|
}
|
||||||
})->groupBy('entity_type', 'entity_id');
|
})->groupBy('entity_type', 'entity_id');
|
||||||
@ -193,9 +169,9 @@ class SearchService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle exact term matching
|
// Handle exact term matching
|
||||||
if (count($terms['exact']) > 0) {
|
if (count($searchOpts->exacts) > 0) {
|
||||||
$entitySelect->where(function (EloquentBuilder $query) use ($terms, $entity) {
|
$entitySelect->where(function (EloquentBuilder $query) use ($searchOpts, $entity) {
|
||||||
foreach ($terms['exact'] as $inputTerm) {
|
foreach ($searchOpts->exacts as $inputTerm) {
|
||||||
$query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
|
$query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
|
||||||
$query->where('name', 'like', '%'.$inputTerm .'%')
|
$query->where('name', 'like', '%'.$inputTerm .'%')
|
||||||
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
||||||
@ -205,12 +181,12 @@ class SearchService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle tag searches
|
// Handle tag searches
|
||||||
foreach ($terms['tags'] as $inputTerm) {
|
foreach ($searchOpts->tags as $inputTerm) {
|
||||||
$this->applyTagSearch($entitySelect, $inputTerm);
|
$this->applyTagSearch($entitySelect, $inputTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle filters
|
// Handle filters
|
||||||
foreach ($terms['filters'] as $filterTerm => $filterValue) {
|
foreach ($searchOpts->filters as $filterTerm => $filterValue) {
|
||||||
$functionName = Str::camel('filter_' . $filterTerm);
|
$functionName = Str::camel('filter_' . $filterTerm);
|
||||||
if (method_exists($this, $functionName)) {
|
if (method_exists($this, $functionName)) {
|
||||||
$this->$functionName($entitySelect, $entity, $filterValue);
|
$this->$functionName($entitySelect, $entity, $filterValue);
|
||||||
@ -220,60 +196,10 @@ class SearchService
|
|||||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
|
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Get the available query operators as a regex escaped list.
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
protected function getRegexEscapedOperators()
|
protected function getRegexEscapedOperators(): string
|
||||||
{
|
{
|
||||||
$escapedOperators = [];
|
$escapedOperators = [];
|
||||||
foreach ($this->queryOperators as $operator) {
|
foreach ($this->queryOperators as $operator) {
|
||||||
@ -284,11 +210,8 @@ class SearchService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a tag search term onto a entity query.
|
* Apply a tag search term onto a entity query.
|
||||||
* @param EloquentBuilder $query
|
|
||||||
* @param string $tagTerm
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
protected function applyTagSearch(EloquentBuilder $query, $tagTerm)
|
protected function applyTagSearch(EloquentBuilder $query, string $tagTerm): EloquentBuilder
|
||||||
{
|
{
|
||||||
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||||
$query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) {
|
$query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) {
|
||||||
@ -318,7 +241,6 @@ class SearchService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Index the given entity.
|
* Index the given entity.
|
||||||
* @param Entity $entity
|
|
||||||
*/
|
*/
|
||||||
public function indexEntity(Entity $entity)
|
public function indexEntity(Entity $entity)
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ use BookStack\Entities\Bookshelf;
|
|||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Entity;
|
||||||
use BookStack\Entities\Managers\EntityContext;
|
use BookStack\Entities\Managers\EntityContext;
|
||||||
use BookStack\Entities\SearchService;
|
use BookStack\Entities\SearchService;
|
||||||
|
use BookStack\Entities\SearchOptions;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SearchController extends Controller
|
class SearchController extends Controller
|
||||||
@ -33,20 +34,22 @@ class SearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function search(Request $request)
|
public function search(Request $request)
|
||||||
{
|
{
|
||||||
$searchTerm = $request->get('term');
|
$searchOpts = SearchOptions::fromRequest($request);
|
||||||
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
|
$fullSearchString = $searchOpts->toString();
|
||||||
|
$this->setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString]));
|
||||||
|
|
||||||
$page = intval($request->get('page', '0')) ?: 1;
|
$page = intval($request->get('page', '0')) ?: 1;
|
||||||
$nextPageLink = url('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
|
$nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page+1));
|
||||||
|
|
||||||
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
$results = $this->searchService->searchEntities($searchOpts, 'all', $page, 20);
|
||||||
|
|
||||||
return view('search.all', [
|
return view('search.all', [
|
||||||
'entities' => $results['results'],
|
'entities' => $results['results'],
|
||||||
'totalResults' => $results['total'],
|
'totalResults' => $results['total'],
|
||||||
'searchTerm' => $searchTerm,
|
'searchTerm' => $fullSearchString,
|
||||||
'hasNextPage' => $results['has_more'],
|
'hasNextPage' => $results['has_more'],
|
||||||
'nextPageLink' => $nextPageLink
|
'nextPageLink' => $nextPageLink,
|
||||||
|
'options' => $searchOpts,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +87,7 @@ class SearchController extends Controller
|
|||||||
// Search for entities otherwise show most popular
|
// Search for entities otherwise show most popular
|
||||||
if ($searchTerm !== false) {
|
if ($searchTerm !== false) {
|
||||||
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
|
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
|
||||||
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
|
$entities = $this->searchService->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
|
||||||
} else {
|
} else {
|
||||||
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
|
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
|
||||||
}
|
}
|
||||||
|
31
resources/js/components/add-remove-rows.js
Normal file
31
resources/js/components/add-remove-rows.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {onChildEvent} from "../services/dom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AddRemoveRows
|
||||||
|
* Allows easy row add/remove controls onto a table.
|
||||||
|
* Needs a model row to use when adding a new row.
|
||||||
|
* @extends {Component}
|
||||||
|
*/
|
||||||
|
class AddRemoveRows {
|
||||||
|
setup() {
|
||||||
|
this.modelRow = this.$refs.model;
|
||||||
|
this.addButton = this.$refs.add;
|
||||||
|
this.removeSelector = this.$opts.removeSelector;
|
||||||
|
this.setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupListeners() {
|
||||||
|
this.addButton.addEventListener('click', e => {
|
||||||
|
const clone = this.modelRow.cloneNode(true);
|
||||||
|
clone.classList.remove('hidden');
|
||||||
|
this.modelRow.parentNode.insertBefore(clone, this.modelRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
onChildEvent(this.$el, this.removeSelector, 'click', (e) => {
|
||||||
|
const row = e.target.closest('tr');
|
||||||
|
row.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddRemoveRows;
|
28
resources/js/components/optional-input.js
Normal file
28
resources/js/components/optional-input.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {onSelect} from "../services/dom";
|
||||||
|
|
||||||
|
class OptionalInput {
|
||||||
|
setup() {
|
||||||
|
this.removeButton = this.$refs.remove;
|
||||||
|
this.showButton = this.$refs.show;
|
||||||
|
this.input = this.$refs.input;
|
||||||
|
this.setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupListeners() {
|
||||||
|
onSelect(this.removeButton, () => {
|
||||||
|
this.input.value = '';
|
||||||
|
this.input.classList.add('hidden');
|
||||||
|
this.removeButton.classList.add('hidden');
|
||||||
|
this.showButton.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
onSelect(this.showButton, () => {
|
||||||
|
this.input.classList.remove('hidden');
|
||||||
|
this.removeButton.classList.remove('hidden');
|
||||||
|
this.showButton.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OptionalInput;
|
@ -1,193 +0,0 @@
|
|||||||
import * as Dates from "../services/dates";
|
|
||||||
|
|
||||||
let data = {
|
|
||||||
terms: '',
|
|
||||||
termString : '',
|
|
||||||
search: {
|
|
||||||
type: {
|
|
||||||
page: true,
|
|
||||||
chapter: true,
|
|
||||||
book: true,
|
|
||||||
bookshelf: 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 => term.trim() !== '').map(term => `"${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 = type.bookshelf = 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);
|
|
||||||
type.bookshelf = (splitTypes.indexOf('bookshelf') !== -1);
|
|
||||||
},
|
|
||||||
|
|
||||||
typeChange() {
|
|
||||||
let typeFilter = /{\s?type:\s?(.*?)\s?}/;
|
|
||||||
let type = this.search.type;
|
|
||||||
if (type.page === type.chapter === type.book === type.bookshelf) {
|
|
||||||
this.termString = this.termString.replace(typeFilter, '');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let selectedTypes = Object.keys(type).filter(type => 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 = window.baseUrl('/search?term=' + encodeURIComponent(this.termString));
|
|
||||||
},
|
|
||||||
|
|
||||||
enableDate(optionName) {
|
|
||||||
this.search.dates[optionName.toLowerCase()] = Dates.getCurrentDay();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data, computed, methods, created
|
|
||||||
};
|
|
@ -4,7 +4,6 @@ function exists(id) {
|
|||||||
return document.getElementById(id) !== null;
|
return document.getElementById(id) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
import searchSystem from "./search";
|
|
||||||
import entityDashboard from "./entity-dashboard";
|
import entityDashboard from "./entity-dashboard";
|
||||||
import codeEditor from "./code-editor";
|
import codeEditor from "./code-editor";
|
||||||
import imageManager from "./image-manager";
|
import imageManager from "./image-manager";
|
||||||
@ -13,7 +12,6 @@ import attachmentManager from "./attachment-manager";
|
|||||||
import pageEditor from "./page-editor";
|
import pageEditor from "./page-editor";
|
||||||
|
|
||||||
let vueMapping = {
|
let vueMapping = {
|
||||||
'search-system': searchSystem,
|
|
||||||
'entity-dashboard': entityDashboard,
|
'entity-dashboard': entityDashboard,
|
||||||
'code-editor': codeEditor,
|
'code-editor': codeEditor,
|
||||||
'image-manager': imageManager,
|
'image-manager': imageManager,
|
||||||
|
@ -47,7 +47,8 @@ return [
|
|||||||
'search_no_pages' => 'No pages matched this search',
|
'search_no_pages' => 'No pages matched this search',
|
||||||
'search_for_term' => 'Search for :term',
|
'search_for_term' => 'Search for :term',
|
||||||
'search_more' => 'More Results',
|
'search_more' => 'More Results',
|
||||||
'search_filters' => 'Search Filters',
|
'search_advanced' => 'Advanced Search',
|
||||||
|
'search_terms' => 'Search Terms',
|
||||||
'search_content_type' => 'Content Type',
|
'search_content_type' => 'Content Type',
|
||||||
'search_exact_matches' => 'Exact Matches',
|
'search_exact_matches' => 'Exact Matches',
|
||||||
'search_tags' => 'Tag Searches',
|
'search_tags' => 'Tag Searches',
|
||||||
|
@ -141,7 +141,7 @@ body.flexbox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.float {
|
.float {
|
||||||
|
@ -1,186 +1,62 @@
|
|||||||
@extends('simple-layout')
|
@extends('simple-layout')
|
||||||
|
|
||||||
@section('body')
|
@section('body')
|
||||||
<input type="hidden" name="searchTerm" value="{{$searchTerm}}">
|
<div class="container mt-xl" id="search-system">
|
||||||
|
|
||||||
<div class="container" id="search-system">
|
|
||||||
|
|
||||||
<div class="my-s">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid right-focus reverse-collapse gap-xl">
|
<div class="grid right-focus reverse-collapse gap-xl">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<h5>{{ trans('entities.search_filters') }}</h5>
|
<h5>{{ trans('entities.search_advanced') }}</h5>
|
||||||
|
|
||||||
<form v-on:submit="updateSearch" v-cloak class="v-cloak anim fadeIn">
|
<form method="get" action="{{ url('/search') }}">
|
||||||
<h6 class="text-muted">{{ trans('entities.search_content_type') }}</h6>
|
<h6>{{ trans('entities.search_terms') }}</h6>
|
||||||
|
<input type="text" name="search" value="{{ implode(' ', $options->searches) }}">
|
||||||
|
|
||||||
|
<h6>{{ trans('entities.search_content_type') }}</h6>
|
||||||
<div class="form-group">
|
<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>
|
<?php
|
||||||
|
$types = explode('|', $options->filters['type'] ?? '');
|
||||||
|
$hasTypes = $types[0] !== '';
|
||||||
|
?>
|
||||||
|
@include('search.form.type-filter', ['checked' => !$hasTypes || in_array('page', $types), 'entity' => 'page', 'transKey' => 'page'])
|
||||||
|
@include('search.form.type-filter', ['checked' => !$hasTypes || in_array('chapter', $types), 'entity' => 'chapter', 'transKey' => 'chapter'])
|
||||||
<br>
|
<br>
|
||||||
<label class="inline checkbox text-book"><input type="checkbox" v-on:change="typeChange" v-model="search.type.book" value="book">{{ trans('entities.book') }}</label>
|
@include('search.form.type-filter', ['checked' => !$hasTypes || in_array('book', $types), 'entity' => 'book', 'transKey' => 'book'])
|
||||||
<label class="inline checkbox text-bookshelf"><input type="checkbox" v-on:change="typeChange" v-model="search.type.bookshelf" value="bookshelf">{{ trans('entities.shelf') }}</label>
|
@include('search.form.type-filter', ['checked' => !$hasTypes || in_array('bookshelf', $types), 'entity' => 'bookshelf', 'transKey' => 'shelf'])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h6 class="text-muted">{{ trans('entities.search_exact_matches') }}</h6>
|
<h6>{{ trans('entities.search_exact_matches') }}</h6>
|
||||||
<table cellpadding="0" cellspacing="0" border="0" class="no-style">
|
@include('search.form.term-list', ['type' => 'exact', 'currentList' => $options->exacts])
|
||||||
<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)">
|
|
||||||
@icon('close')
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">
|
|
||||||
<button type="button" class="text-button" v-on:click="addExact">
|
|
||||||
@icon('add-circle'){{ trans('common.add') }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h6 class="text-muted">{{ trans('entities.search_tags') }}</h6>
|
<h6>{{ trans('entities.search_tags') }}</h6>
|
||||||
<table cellpadding="0" cellspacing="0" border="0" class="no-style">
|
@include('search.form.term-list', ['type' => 'tags', 'currentList' => $options->tags])
|
||||||
<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)">
|
|
||||||
@icon('close')
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">
|
|
||||||
<button type="button" class="text-button" v-on:click="addTag">
|
|
||||||
@icon('add-circle'){{ trans('common.add') }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
@if(signedInUser())
|
@if(signedInUser())
|
||||||
<h6 class="text-muted">{{ trans('entities.search_options') }}</h6>
|
<h6>{{ trans('entities.search_options') }}</h6>
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" v-on:change="optionChange('viewed_by_me')"
|
@component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'viewed_by_me', 'value' => null])
|
||||||
v-model="search.option.viewed_by_me" value="page">
|
|
||||||
{{ trans('entities.search_viewed_by_me') }}
|
{{ trans('entities.search_viewed_by_me') }}
|
||||||
</label>
|
@endcomponent
|
||||||
<label class="checkbox">
|
@component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'not_viewed_by_me', 'value' => null])
|
||||||
<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') }}
|
{{ trans('entities.search_not_viewed_by_me') }}
|
||||||
</label>
|
@endcomponent
|
||||||
<label class="checkbox">
|
@component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'is_restricted', 'value' => null])
|
||||||
<input type="checkbox" v-on:change="optionChange('is_restricted')"
|
|
||||||
v-model="search.option.is_restricted" value="page">
|
|
||||||
{{ trans('entities.search_permissions_set') }}
|
{{ trans('entities.search_permissions_set') }}
|
||||||
</label>
|
@endcomponent
|
||||||
<label class="checkbox">
|
@component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'created_by', 'value' => 'me'])
|
||||||
<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') }}
|
{{ trans('entities.search_created_by_me') }}
|
||||||
</label>
|
@endcomponent
|
||||||
<label class="checkbox">
|
@component('search.form.boolean-filter', ['filters' => $options->filters, 'name' => 'updated_by', 'value' => 'me'])
|
||||||
<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') }}
|
{{ trans('entities.search_updated_by_me') }}
|
||||||
</label>
|
@endcomponent
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<h6 class="text-muted">{{ trans('entities.search_date_options') }}</h6>
|
<h6>{{ trans('entities.search_date_options') }}</h6>
|
||||||
<table cellpadding="0" cellspacing="0" border="0" class="no-style form-table">
|
@include('search.form.date-filter', ['name' => 'updated_after', 'filters' => $options->filters])
|
||||||
<tr>
|
@include('search.form.date-filter', ['name' => 'updated_before', 'filters' => $options->filters])
|
||||||
<td width="200">{{ trans('entities.search_updated_after') }}</td>
|
@include('search.form.date-filter', ['name' => 'created_after', 'filters' => $options->filters])
|
||||||
<td width="80">
|
@include('search.form.date-filter', ['name' => 'created_before', 'filters' => $options->filters])
|
||||||
<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')">
|
|
||||||
@icon('close')
|
|
||||||
</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')">
|
|
||||||
@icon('close')
|
|
||||||
</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')">
|
|
||||||
@icon('close')
|
|
||||||
</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')">
|
|
||||||
@icon('close')
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
<button type="submit" class="button">{{ trans('entities.search_update') }}</button>
|
<button type="submit" class="button">{{ trans('entities.search_update') }}</button>
|
||||||
</form>
|
</form>
|
||||||
@ -188,17 +64,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div v-pre class="card content-wrap">
|
<div class="card content-wrap">
|
||||||
<h1 class="list-heading">{{ trans('entities.search_results') }}</h1>
|
<h1 class="list-heading">{{ trans('entities.search_results') }}</h1>
|
||||||
|
|
||||||
<form action="{{ url('/search') }}" method="GET" class="search-box flexible hide-over-l">
|
<form action="{{ url('/search') }}" method="GET" class="search-box flexible hide-over-l">
|
||||||
<input value="{{$searchTerm}}" type="text" name="term" placeholder="{{ trans('common.search') }}">
|
<input value="{{$searchTerm}}" type="text" name="term" placeholder="{{ trans('common.search') }}">
|
||||||
<button type="submit">@icon('search')</button>
|
<button type="submit">@icon('search')</button>
|
||||||
<button v-if="searching" v-cloak class="search-box-cancel text-neg" v-on:click="clearSearch" type="button">@icon('close')</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h6 class="text-muted">{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}</h6>
|
<h6 class="text-muted">{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}</h6>
|
||||||
<div class="book-contents">
|
<div class="book-contents">
|
||||||
@include('partials.entity-list', ['entities' => $entities, 'showPath' => true])
|
@include('partials.entity-list', ['entities' => $entities, 'showPath' => true])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($hasNextPage)
|
@if($hasNextPage)
|
||||||
<div class="text-right mt-m">
|
<div class="text-right mt-m">
|
||||||
<a href="{{ $nextPageLink }}" class="button outline">{{ trans('entities.search_more') }}</a>
|
<a href="{{ $nextPageLink }}" class="button outline">{{ trans('entities.search_more') }}</a>
|
||||||
|
12
resources/views/search/form/boolean-filter.blade.php
Normal file
12
resources/views/search/form/boolean-filter.blade.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{{--
|
||||||
|
$filters - Array of search filter values
|
||||||
|
$name - Name of filter to limit use.
|
||||||
|
$value - Value of filter to use
|
||||||
|
--}}
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="filters[{{ $name }}]"
|
||||||
|
@if (isset($filters[$name]) && (!$value || ($value && $value === $filters[$name]))) checked="checked" @endif
|
||||||
|
value="{{ $value ?: 'true' }}">
|
||||||
|
{{ $slot }}
|
||||||
|
</label>
|
29
resources/views/search/form/date-filter.blade.php
Normal file
29
resources/views/search/form/date-filter.blade.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{{--
|
||||||
|
@filters - Active search filters
|
||||||
|
@name - Name of filter
|
||||||
|
--}}
|
||||||
|
<table class="no-style form-table mb-xs">
|
||||||
|
<tr>
|
||||||
|
<td width="200">{{ trans('entities.search_' . $name) }}</td>
|
||||||
|
<td width="80"></td>
|
||||||
|
</tr>
|
||||||
|
<tr component="optional-input">
|
||||||
|
<td>
|
||||||
|
<button type="button" refs="optional-input@show"
|
||||||
|
class="text-button {{ ($filters[$name] ?? false) ? 'hidden' : '' }}">{{ trans('entities.search_set_date') }}</button>
|
||||||
|
<input class="tag-input {{ ($filters[$name] ?? false) ? '' : 'hidden' }}"
|
||||||
|
refs="optional-input@input"
|
||||||
|
value="{{ $filters[$name] ?? '' }}"
|
||||||
|
type="date"
|
||||||
|
name="filters[{{ $name }}]"
|
||||||
|
pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button"
|
||||||
|
refs="optional-input@remove"
|
||||||
|
class="text-neg text-button {{ ($filters[$name] ?? false) ? '' : 'hidden' }}">
|
||||||
|
@icon('close')
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
25
resources/views/search/form/term-list.blade.php
Normal file
25
resources/views/search/form/term-list.blade.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{{--
|
||||||
|
@type - Type of term (exact, tag)
|
||||||
|
@currentList
|
||||||
|
--}}
|
||||||
|
<table component="add-remove-rows"
|
||||||
|
option:add-remove-rows:remove-selector="button.text-neg"
|
||||||
|
class="no-style">
|
||||||
|
@foreach(array_merge($currentList, ['']) as $term)
|
||||||
|
<tr @if(empty($term)) class="hidden" refs="add-remove-rows@model" @endif>
|
||||||
|
<td class="pb-s pr-m">
|
||||||
|
<input class="exact-input outline" type="text" name="{{$type}}[]" value="{{ $term }}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="text-neg text-button">@icon('close')</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<button refs="add-remove-rows@add" type="button" class="text-button">
|
||||||
|
@icon('add-circle'){{ trans('common.add') }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
10
resources/views/search/form/type-filter.blade.php
Normal file
10
resources/views/search/form/type-filter.blade.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{{--
|
||||||
|
@checked - If the option should be pre-checked
|
||||||
|
@entity - Entity Name
|
||||||
|
@transKey - Translation Key
|
||||||
|
--}}
|
||||||
|
<label class="inline checkbox text-{{$entity}}">
|
||||||
|
<input type="checkbox" name="types[]"
|
||||||
|
@if($checked) checked @endif
|
||||||
|
value="{{$entity}}">{{ trans('entities.' . $transKey) }}
|
||||||
|
</label>
|
43
tests/Entity/SearchOptionsTest.php
Normal file
43
tests/Entity/SearchOptionsTest.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php namespace Tests\Entity;
|
||||||
|
|
||||||
|
use BookStack\Entities\SearchOptions;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SearchOptionsTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_from_string_parses_a_search_string_properly()
|
||||||
|
{
|
||||||
|
$options = SearchOptions::fromString('cat "dog" [tag=good] {is_tree}');
|
||||||
|
|
||||||
|
$this->assertEquals(['cat'], $options->searches);
|
||||||
|
$this->assertEquals(['dog'], $options->exacts);
|
||||||
|
$this->assertEquals(['tag=good'], $options->tags);
|
||||||
|
$this->assertEquals(['is_tree' => ''], $options->filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_to_string_includes_all_items_in_the_correct_format()
|
||||||
|
{
|
||||||
|
$expected = 'cat "dog" [tag=good] {is_tree}';
|
||||||
|
$options = new SearchOptions;
|
||||||
|
$options->searches = ['cat'];
|
||||||
|
$options->exacts = ['dog'];
|
||||||
|
$options->tags = ['tag=good'];
|
||||||
|
$options->filters = ['is_tree' => ''];
|
||||||
|
|
||||||
|
$output = $options->toString();
|
||||||
|
foreach (explode(' ', $expected) as $term) {
|
||||||
|
$this->assertStringContainsString($term, $output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_correct_filter_values_are_set_from_string()
|
||||||
|
{
|
||||||
|
$opts = SearchOptions::fromString('{is_tree} {name:dan} {cat:happy}');
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
'is_tree' => '',
|
||||||
|
'name' => 'dan',
|
||||||
|
'cat' => 'happy',
|
||||||
|
], $opts->filters);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user