diff --git a/app/Entities/SearchOptions.php b/app/Entities/SearchOptions.php new file mode 100644 index 000000000..af9156953 --- /dev/null +++ b/app/Entities/SearchOptions.php @@ -0,0 +1,137 @@ + $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; + } + +} \ No newline at end of file diff --git a/app/Entities/SearchService.php b/app/Entities/SearchService.php index ee9b87786..11b731cd0 100644 --- a/app/Entities/SearchService.php +++ b/app/Entities/SearchService.php @@ -39,10 +39,6 @@ class SearchService /** * 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) { @@ -54,7 +50,6 @@ class SearchService /** * Set the database connection - * @param Connection $connection */ public function setConnection(Connection $connection) { @@ -63,23 +58,18 @@ class SearchService /** * Search all entities in the system. - * @param string $searchString - * @param string $entityType - * @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]; + * The provided count is for each entity to search, + * Total returned could can be larger and not guaranteed. */ - 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()); $entityTypesToSearch = $entityTypes; if ($entityType !== 'all') { $entityTypesToSearch = $entityType; - } else if (isset($terms['filters']['type'])) { - $entityTypesToSearch = explode('|', $terms['filters']['type']); + } else if (isset($searchOpts->filters['type'])) { + $entityTypesToSearch = explode('|', $searchOpts->filters['type']); } $results = collect(); @@ -90,8 +80,8 @@ class SearchService if (!in_array($entityType, $entityTypes)) { continue; } - $search = $this->searchEntityTable($terms, $entityType, $page, $count, $action); - $entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true); + $search = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action); + $entityTotal = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action, true); if ($entityTotal > $page * $count) { $hasMore = true; } @@ -103,29 +93,26 @@ class SearchService 'total' => $total, 'count' => count($results), 'has_more' => $hasMore, - 'results' => $results->sortByDesc('score')->values() + 'results' => $results->sortByDesc('score')->values(), ]; } /** * 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']; - $entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes; + $entityTypesToSearch = isset($opts->filters['type']) ? explode('|', $opts->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(); + $search = $this->buildEntitySearchQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get(); $results = $results->merge($search); } return $results->sortByDesc('score')->take(20); @@ -133,30 +120,23 @@ class SearchService /** * 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); - $pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get(); + $opts = SearchOptions::fromString($searchString); + $pages = $this->buildEntitySearchQuery($opts, '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 string $action - * @param bool $getCount Return the total count of the search + * Setting getCount = true will return the total + * matching instead of the items themselves. * @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) { return $query->count(); } @@ -167,22 +147,18 @@ class SearchService /** * 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); $entitySelect = $entity->newQuery(); // 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->where('entity_type', '=', $entity->getMorphClass()); - $subQuery->where(function (Builder $query) use ($terms) { - foreach ($terms['search'] as $inputTerm) { + $subQuery->where(function (Builder $query) use ($searchOpts) { + foreach ($searchOpts->searches as $inputTerm) { $query->orWhere('term', 'like', $inputTerm .'%'); } })->groupBy('entity_type', 'entity_id'); @@ -193,9 +169,9 @@ class SearchService } // Handle exact term matching - if (count($terms['exact']) > 0) { - $entitySelect->where(function (EloquentBuilder $query) use ($terms, $entity) { - foreach ($terms['exact'] as $inputTerm) { + if (count($searchOpts->exacts) > 0) { + $entitySelect->where(function (EloquentBuilder $query) use ($searchOpts, $entity) { + foreach ($searchOpts->exacts as $inputTerm) { $query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) { $query->where('name', 'like', '%'.$inputTerm .'%') ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%'); @@ -205,12 +181,12 @@ class SearchService } // Handle tag searches - foreach ($terms['tags'] as $inputTerm) { + foreach ($searchOpts->tags as $inputTerm) { $this->applyTagSearch($entitySelect, $inputTerm); } // Handle filters - foreach ($terms['filters'] as $filterTerm => $filterValue) { + foreach ($searchOpts->filters as $filterTerm => $filterValue) { $functionName = Str::camel('filter_' . $filterTerm); if (method_exists($this, $functionName)) { $this->$functionName($entitySelect, $entity, $filterValue); @@ -220,60 +196,10 @@ class SearchService 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. - * @return mixed */ - protected function getRegexEscapedOperators() + protected function getRegexEscapedOperators(): string { $escapedOperators = []; foreach ($this->queryOperators as $operator) { @@ -284,11 +210,8 @@ class SearchService /** * 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); $query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) { @@ -318,7 +241,6 @@ class SearchService /** * Index the given entity. - * @param Entity $entity */ public function indexEntity(Entity $entity) { diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index a5d57741d..8105843b5 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -6,6 +6,7 @@ use BookStack\Entities\Bookshelf; use BookStack\Entities\Entity; use BookStack\Entities\Managers\EntityContext; use BookStack\Entities\SearchService; +use BookStack\Entities\SearchOptions; use Illuminate\Http\Request; class SearchController extends Controller @@ -33,20 +34,22 @@ class SearchController extends Controller */ public function search(Request $request) { - $searchTerm = $request->get('term'); - $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm])); + $searchOpts = SearchOptions::fromRequest($request); + $fullSearchString = $searchOpts->toString(); + $this->setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString])); $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', [ 'entities' => $results['results'], 'totalResults' => $results['total'], - 'searchTerm' => $searchTerm, + 'searchTerm' => $fullSearchString, '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 if ($searchTerm !== false) { $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 { $entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission); } diff --git a/resources/js/components/add-remove-rows.js b/resources/js/components/add-remove-rows.js new file mode 100644 index 000000000..81eeb43c4 --- /dev/null +++ b/resources/js/components/add-remove-rows.js @@ -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; \ No newline at end of file diff --git a/resources/js/components/optional-input.js b/resources/js/components/optional-input.js new file mode 100644 index 000000000..eab58e42a --- /dev/null +++ b/resources/js/components/optional-input.js @@ -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; \ No newline at end of file diff --git a/resources/js/vues/search.js b/resources/js/vues/search.js deleted file mode 100644 index c0b828b96..000000000 --- a/resources/js/vues/search.js +++ /dev/null @@ -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 -}; diff --git a/resources/js/vues/vues.js b/resources/js/vues/vues.js index ec192372d..125d541ce 100644 --- a/resources/js/vues/vues.js +++ b/resources/js/vues/vues.js @@ -4,7 +4,6 @@ function exists(id) { return document.getElementById(id) !== null; } -import searchSystem from "./search"; import entityDashboard from "./entity-dashboard"; import codeEditor from "./code-editor"; import imageManager from "./image-manager"; @@ -13,7 +12,6 @@ import attachmentManager from "./attachment-manager"; import pageEditor from "./page-editor"; let vueMapping = { - 'search-system': searchSystem, 'entity-dashboard': entityDashboard, 'code-editor': codeEditor, 'image-manager': imageManager, diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 6bbc723b0..bb5c0078d 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -47,7 +47,8 @@ return [ 'search_no_pages' => 'No pages matched this search', 'search_for_term' => 'Search for :term', 'search_more' => 'More Results', - 'search_filters' => 'Search Filters', + 'search_advanced' => 'Advanced Search', + 'search_terms' => 'Search Terms', 'search_content_type' => 'Content Type', 'search_exact_matches' => 'Exact Matches', 'search_tags' => 'Tag Searches', diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index 595713feb..4d044245a 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -141,7 +141,7 @@ body.flexbox { } .hidden { - display: none; + display: none !important; } .float { diff --git a/resources/views/search/all.blade.php b/resources/views/search/all.blade.php index f19e560a2..df137bd2a 100644 --- a/resources/views/search/all.blade.php +++ b/resources/views/search/all.blade.php @@ -1,186 +1,62 @@ @extends('simple-layout') @section('body') - - -
- -
-   -
+
-
{{ trans('entities.search_filters') }}
+
{{ trans('entities.search_advanced') }}
-
-
{{ trans('entities.search_content_type') }}
+ +
{{ trans('entities.search_terms') }}
+ + +
{{ trans('entities.search_content_type') }}
- - + + 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'])
- - + @include('search.form.type-filter', ['checked' => !$hasTypes || in_array('book', $types), 'entity' => 'book', 'transKey' => 'book']) + @include('search.form.type-filter', ['checked' => !$hasTypes || in_array('bookshelf', $types), 'entity' => 'bookshelf', 'transKey' => 'shelf'])
-
{{ trans('entities.search_exact_matches') }}
- - - - - - - - -
- - -
- -
+
{{ trans('entities.search_exact_matches') }}
+ @include('search.form.term-list', ['type' => 'exact', 'currentList' => $options->exacts]) -
{{ trans('entities.search_tags') }}
- - - - - - - - -
- - -
- -
+
{{ trans('entities.search_tags') }}
+ @include('search.form.term-list', ['type' => 'tags', 'currentList' => $options->tags]) @if(signedInUser()) -
{{ trans('entities.search_options') }}
- - - - - + @endcomponent @endif -
{{ trans('entities.search_date_options') }}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ trans('entities.search_updated_after') }} - - -
- - - -
{{ trans('entities.search_updated_before') }} - - -
- - - -
{{ trans('entities.search_created_after') }} - - -
- - - -
{{ trans('entities.search_created_before') }} - - -
- - - -
- +
{{ trans('entities.search_date_options') }}
+ @include('search.form.date-filter', ['name' => 'updated_after', 'filters' => $options->filters]) + @include('search.form.date-filter', ['name' => 'updated_before', 'filters' => $options->filters]) + @include('search.form.date-filter', ['name' => 'created_after', 'filters' => $options->filters]) + @include('search.form.date-filter', ['name' => 'created_before', 'filters' => $options->filters])
@@ -188,17 +64,19 @@
-
+

{{ trans('entities.search_results') }}

+ +
{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}
@include('partials.entity-list', ['entities' => $entities, 'showPath' => true])
+ @if($hasNextPage)
{{ trans('entities.search_more') }} diff --git a/resources/views/search/form/boolean-filter.blade.php b/resources/views/search/form/boolean-filter.blade.php new file mode 100644 index 000000000..1dc9bf0c5 --- /dev/null +++ b/resources/views/search/form/boolean-filter.blade.php @@ -0,0 +1,12 @@ +{{-- +$filters - Array of search filter values +$name - Name of filter to limit use. +$value - Value of filter to use +--}} + \ No newline at end of file diff --git a/resources/views/search/form/date-filter.blade.php b/resources/views/search/form/date-filter.blade.php new file mode 100644 index 000000000..05ab4c134 --- /dev/null +++ b/resources/views/search/form/date-filter.blade.php @@ -0,0 +1,29 @@ +{{-- +@filters - Active search filters +@name - Name of filter +--}} + + + + + + + + + +
{{ trans('entities.search_' . $name) }}
+ + + + +
\ No newline at end of file diff --git a/resources/views/search/form/term-list.blade.php b/resources/views/search/form/term-list.blade.php new file mode 100644 index 000000000..435de73f1 --- /dev/null +++ b/resources/views/search/form/term-list.blade.php @@ -0,0 +1,25 @@ +{{-- +@type - Type of term (exact, tag) +@currentList +--}} + + @foreach(array_merge($currentList, ['']) as $term) + + + + + @endforeach + + + +
+ +
\ No newline at end of file diff --git a/resources/views/search/form/type-filter.blade.php b/resources/views/search/form/type-filter.blade.php new file mode 100644 index 000000000..b885ebd7a --- /dev/null +++ b/resources/views/search/form/type-filter.blade.php @@ -0,0 +1,10 @@ +{{-- +@checked - If the option should be pre-checked +@entity - Entity Name +@transKey - Translation Key +--}} + \ No newline at end of file diff --git a/tests/Entity/SearchOptionsTest.php b/tests/Entity/SearchOptionsTest.php new file mode 100644 index 000000000..727db5533 --- /dev/null +++ b/tests/Entity/SearchOptionsTest.php @@ -0,0 +1,43 @@ +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); + } +}