Finished off UI for search system

This commit is contained in:
Dan Brown 2017-04-15 15:04:30 +01:00
parent ad125327c0
commit 0e0945ef84
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
14 changed files with 225 additions and 120 deletions

View File

@ -34,14 +34,20 @@ class SearchController extends Controller
public function search(Request $request)
{
$searchTerm = $request->get('term');
// $paginationAppends = $request->only('term'); TODO - Check pagination
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
$entities = $this->searchService->searchEntities($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', [
'entities' => $entities,
'searchTerm' => $searchTerm
'entities' => $results['results'],
'totalResults' => $results['total'],
'searchTerm' => $searchTerm,
'hasNextPage' => $hasNextPage,
'nextPageLink' => $nextPageLink
]);
}

View File

@ -8,7 +8,6 @@ use BookStack\SearchTerm;
use Illuminate\Database\Connection;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
class SearchService
{
@ -56,9 +55,9 @@ class SearchService
* @param string $entityType
* @param int $page
* @param int $count
* @return Collection
* @return array[int, Collection];
*/
public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20)
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
{
$terms = $this->parseSearchString($searchString);
$entityTypes = array_keys($this->entities);
@ -71,14 +70,20 @@ class SearchService
$entityTypesToSearch = explode('|', $terms['filters']['type']);
}
// TODO - Check drafts don't show up in results
$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 $results->sortByDesc('score');
return [
'total' => $total,
'count' => count($results),
'results' => $results->sortByDesc('score')
];
}
/**
@ -87,9 +92,10 @@ class SearchService
* @param string $entityType
* @param int $page
* @param int $count
* @return \Illuminate\Database\Eloquent\Collection|static[]
* @param bool $getCount Return the total count of the search
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/
public function searchEntityTable($terms, $entityType = 'page', $page = 0, $count = 20)
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
{
$entity = $this->getEntity($entityType);
$entitySelect = $entity->newQuery();
@ -131,8 +137,10 @@ class SearchService
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
}
$entitySelect->skip($page * $count)->take($count);
$query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
if ($getCount) return $query->count();
$query = $query->skip(($page-1) * $count)->take($count);
return $query->get();
}
@ -371,13 +379,15 @@ class SearchService
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
if (!is_numeric($input)) return;
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)) return;
if (!is_numeric($input) && $input !== 'me') return;
if ($input === 'me') $input = user()->id;
$query->where('updated_by', '=', $input);
}

View File

@ -12,7 +12,12 @@ let data = {
exactTerms: [],
tagTerms: [],
option: {},
dates: {}
dates: {
updated_after: false,
updated_before: false,
created_after: false,
created_before: false,
}
}
};
@ -126,7 +131,7 @@ let methods = {
},
optionParse(searchString) {
let optionFilter = /{([a-z_-]+?)}/gi;
let optionFilter = /{([a-z_\-:]+?)}/gi;
let matches;
while ((matches = optionFilter.exec(searchString)) !== null) {
this.search.option[matches[1].toLowerCase()] = true;
@ -148,7 +153,30 @@ let methods = {
},
enableDate(optionName) {
this.search.dates[optionName] = moment().format('YYYY-MM-DD');
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);
}
};
@ -159,6 +187,7 @@ function created() {
this.exactParse(this.termString);
this.tagParse(this.termString);
this.optionParse(this.termString);
this.dateParse(this.termString);
}
module.exports = {

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -33,6 +33,7 @@ return [
'search_clear' => 'Clear Search',
'reset' => 'Reset',
'remove' => 'Remove',
'add' => 'Add',
/**

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -5,123 +5,203 @@
<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 id="searchSystem">
<h1>{{ trans('entities.search_results') }}</h1>
<input type="text" v-model="termString">
<div class="row">
<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>Search Filters</h3>
<h3>{{ trans('entities.search_filters') }}</h3>
<form v-on:submit="updateSearch" v-cloak>
<p><strong>Content Type</strong></p>
<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><input type="checkbox" v-on:change="typeChange" v-model="search.type.page" value="page"> Page</label>
<label><input type="checkbox" v-on:change="typeChange" v-model="search.type.chapter" value="chapter"> Chapter</label>
<label><input type="checkbox" v-on:change="typeChange" v-model="search.type.book" value="book"> Book</label>
<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>
<p><strong>Exact Matches</strong></p>
<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" v-on:input="exactChange" type="text" v-model="search.exactTerms[i]"></td>
<input class="exact-input outline" v-on:input="exactChange" type="text" v-model="search.exactTerms[i]"></td>
<td>
<button type="button" class="text-button" v-on:click="removeExact(i)">
<i class="zmdi zmdi-close-circle-o"></i>
<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>Add exact match term
<i class="zmdi zmdi-plus-circle-o"></i>{{ trans('common.add') }}
</button>
</td>
</tr>
</table>
<p><strong>Tag Searches</strong></p>
<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" v-on:input="tagChange" type="text" v-model="search.tagTerms[i]"></td>
<input class="tag-input outline" v-on:input="tagChange" type="text" v-model="search.tagTerms[i]"></td>
<td>
<button type="button" class="text-button" v-on:click="removeTag(i)">
<i class="zmdi zmdi-close-circle-o"></i>
<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>Add tag search
<i class="zmdi zmdi-plus-circle-o"></i>{{ trans('common.add') }}
</button>
</td>
</tr>
</table>
<p><strong>Options</strong></p>
<label>
<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">
Viewed by me
{{ trans('entities.search_viewed_by_me') }}
</label>
<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">
Not viewed by me
{{ 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>
<p><strong>Date Options</strong></p>
<table cellpadding="0" cellspacing="0" border="0" class="no-style">
<h6 class="text-muted">Date Options</h6>
<table cellpadding="0" cellspacing="0" border="0" class="no-style form-table">
<tr>
<td>Updated After</td>
<td style="padding: 0 12px 6px 0;">
<input v-if="search.dates.updated_after" class="tag-input" v-on:input="tagChange" type="date" v-model="search.dates.updated_after" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
<button type="button" class="text-button" v-if="!search.dates.updated_after" v-on:click="enableDate('updated_at')">Set Date</button>
<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-button" v-on:click="search.dates.updated_after = false">
<i class="zmdi zmdi-close-circle-o"></i>
<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 colspan="2">
<button type="button" class="text-button" v-on:click="addTag">
<i class="zmdi zmdi-plus-circle-o"></i>Add tag search
<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 pos">Update Search</button>
<button type="submit" class="button primary">{{ trans('entities.search_update') }}</button>
</form>
</div>
</div>