Added new endpoint for search suggestions

This commit is contained in:
Dan Brown 2022-11-21 10:29:12 +00:00
parent c617190905
commit e7e83a4109
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
9 changed files with 83 additions and 23 deletions

View File

@ -69,7 +69,7 @@ class SearchController extends Controller
* Search for a list of entities and return a partial HTML response of matching entities. * Search for a list of entities and return a partial HTML response of matching entities.
* Returns the most popular entities if no search is provided. * Returns the most popular entities if no search is provided.
*/ */
public function searchEntitiesAjax(Request $request) public function searchForSelector(Request $request)
{ {
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book']; $entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
$searchTerm = $request->get('term', false); $searchTerm = $request->get('term', false);
@ -83,7 +83,25 @@ class SearchController extends Controller
$entities = (new Popular())->run(20, 0, $entityTypes); $entities = (new Popular())->run(20, 0, $entityTypes);
} }
return view('search.parts.entity-ajax-list', ['entities' => $entities, 'permission' => $permission]); return view('search.parts.entity-selector-list', ['entities' => $entities, 'permission' => $permission]);
}
/**
* Search for a list of entities and return a partial HTML response of matching entities
* to be used as a result preview suggestion list for global system searches.
*/
public function searchSuggestions(Request $request)
{
$searchTerm = $request->get('term', '');
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 5)['results'];
foreach ($entities as $entity) {
$entity->setAttribute('preview_content', '');
}
return view('search.parts.entity-suggestion-list', [
'entities' => $entities->slice(0, 5)
]);
} }
/** /**

View File

@ -115,7 +115,7 @@ class EntitySelector {
} }
searchUrl() { searchUrl() {
return `/ajax/search/entities?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`; return `/search/entity-selector?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
} }
searchEntities(searchTerm) { searchEntities(searchTerm) {

View File

@ -12,6 +12,7 @@ class GlobalSearch {
this.suggestions = this.$refs.suggestions; this.suggestions = this.$refs.suggestions;
this.suggestionResultsWrap = this.$refs.suggestionResults; this.suggestionResultsWrap = this.$refs.suggestionResults;
this.loadingWrap = this.$refs.loading; this.loadingWrap = this.$refs.loading;
this.button = this.$refs.button;
this.setupListeners(); this.setupListeners();
} }
@ -34,7 +35,7 @@ class GlobalSearch {
// Allow double click to show auto-click suggestions // Allow double click to show auto-click suggestions
this.input.addEventListener('dblclick', () => { this.input.addEventListener('dblclick', () => {
this.input.setAttribute('autocomplete', 'on'); this.input.setAttribute('autocomplete', 'on');
this.input.blur(); this.button.focus();
this.input.focus(); this.input.focus();
}) })
} }
@ -43,18 +44,13 @@ class GlobalSearch {
* @param {String} search * @param {String} search
*/ */
async updateSuggestions(search) { async updateSuggestions(search) {
const {data: results} = await window.$http.get('/ajax/search/entities', {term: search, count: 5}); const {data: results} = await window.$http.get('/search/suggest', {term: search});
if (!this.input.value) { if (!this.input.value) {
return; return;
} }
const resultDom = htmlToDom(results); const resultDom = htmlToDom(results);
const childrenToTrim = Array.from(resultDom.children).slice(9);
for (const child of childrenToTrim) {
child.remove();
}
this.suggestionResultsWrap.innerHTML = ''; this.suggestionResultsWrap.innerHTML = '';
this.suggestionResultsWrap.style.opacity = '1'; this.suggestionResultsWrap.style.opacity = '1';
this.loadingWrap.style.display = 'none'; this.loadingWrap.style.display = 'none';

View File

@ -18,7 +18,7 @@
.search-suggestions-animation{ .search-suggestions-animation{
animation-name: searchSuggestions; animation-name: searchSuggestions;
animation-duration: 180ms; animation-duration: 120ms;
animation-fill-mode: forwards; animation-fill-mode: forwards;
animation-timing-function: cubic-bezier(.62, .28, .23, .99); animation-timing-function: cubic-bezier(.62, .28, .23, .99);
} }

View File

@ -20,7 +20,11 @@
<div class="flex-container-column items-center justify-center hide-under-l"> <div class="flex-container-column items-center justify-center hide-under-l">
@if (hasAppAccess()) @if (hasAppAccess())
<form component="global-search" action="{{ url('/search') }}" method="GET" class="search-box" role="search"> <form component="global-search" action="{{ url('/search') }}" method="GET" class="search-box" role="search">
<button id="header-search-box-button" type="submit" aria-label="{{ trans('common.search') }}" tabindex="-1">@icon('search') </button> <button id="header-search-box-button"
refs="global-search@button"
type="submit"
aria-label="{{ trans('common.search') }}"
tabindex="-1">@icon('search')</button>
<input id="header-search-box-input" <input id="header-search-box-input"
refs="global-search@input" refs="global-search@input"
type="text" type="text"

View File

@ -0,0 +1,21 @@
<div class="entity-list">
@if(count($entities) > 0)
@foreach($entities as $index => $entity)
@include('entities.list-item', [
'entity' => $entity,
'showPath' => true,
'locked' => false,
])
@if($index !== count($entities) - 1)
<hr>
@endif
@endforeach
@else
<div class="text-muted px-m py-m">
{{ trans('common.no_items') }}
</div>
@endif
</div>

View File

@ -184,8 +184,6 @@ Route::middleware('auth')->group(function () {
Route::get('/ajax/tags/suggest/names', [TagController::class, 'getNameSuggestions']); Route::get('/ajax/tags/suggest/names', [TagController::class, 'getNameSuggestions']);
Route::get('/ajax/tags/suggest/values', [TagController::class, 'getValueSuggestions']); Route::get('/ajax/tags/suggest/values', [TagController::class, 'getValueSuggestions']);
Route::get('/ajax/search/entities', [SearchController::class, 'searchEntitiesAjax']);
// Comments // Comments
Route::post('/comment/{pageId}', [CommentController::class, 'savePageComment']); Route::post('/comment/{pageId}', [CommentController::class, 'savePageComment']);
Route::put('/comment/{id}', [CommentController::class, 'update']); Route::put('/comment/{id}', [CommentController::class, 'update']);
@ -199,6 +197,8 @@ Route::middleware('auth')->group(function () {
Route::get('/search/book/{bookId}', [SearchController::class, 'searchBook']); Route::get('/search/book/{bookId}', [SearchController::class, 'searchBook']);
Route::get('/search/chapter/{bookId}', [SearchController::class, 'searchChapter']); Route::get('/search/chapter/{bookId}', [SearchController::class, 'searchChapter']);
Route::get('/search/entity/siblings', [SearchController::class, 'searchSiblings']); Route::get('/search/entity/siblings', [SearchController::class, 'searchSiblings']);
Route::get('/search/entity-selector', [SearchController::class, 'searchForSelector']);
Route::get('/search/suggest', [SearchController::class, 'searchSuggestions']);
// User Search // User Search
Route::get('/search/users/select', [UserSearchController::class, 'forSelect']); Route::get('/search/users/select', [UserSearchController::class, 'forSelect']);

View File

@ -190,7 +190,7 @@ class EntitySearchTest extends TestCase
$this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name); $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name);
} }
public function test_ajax_entity_search() public function test_entity_selector_search()
{ {
$page = $this->entities->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']); $page = $this->entities->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']);
$notVisitedPage = $this->entities->page(); $notVisitedPage = $this->entities->page();
@ -198,38 +198,38 @@ class EntitySearchTest extends TestCase
// Visit the page to make popular // Visit the page to make popular
$this->asEditor()->get($page->getUrl()); $this->asEditor()->get($page->getUrl());
$normalSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name)); $normalSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));
$normalSearch->assertSee($page->name); $normalSearch->assertSee($page->name);
$bookSearch = $this->get('/ajax/search/entities?types=book&term=' . urlencode($page->name)); $bookSearch = $this->get('/search/entity-selector?types=book&term=' . urlencode($page->name));
$bookSearch->assertDontSee($page->name); $bookSearch->assertDontSee($page->name);
$defaultListTest = $this->get('/ajax/search/entities'); $defaultListTest = $this->get('/search/entity-selector');
$defaultListTest->assertSee($page->name); $defaultListTest->assertSee($page->name);
$defaultListTest->assertDontSee($notVisitedPage->name); $defaultListTest->assertDontSee($notVisitedPage->name);
} }
public function test_ajax_entity_search_shows_breadcrumbs() public function test_entity_selector_search_shows_breadcrumbs()
{ {
$chapter = $this->entities->chapter(); $chapter = $this->entities->chapter();
$page = $chapter->pages->first(); $page = $chapter->pages->first();
$this->asEditor(); $this->asEditor();
$pageSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name)); $pageSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));
$pageSearch->assertSee($page->name); $pageSearch->assertSee($page->name);
$pageSearch->assertSee($chapter->getShortName(42)); $pageSearch->assertSee($chapter->getShortName(42));
$pageSearch->assertSee($page->book->getShortName(42)); $pageSearch->assertSee($page->book->getShortName(42));
$chapterSearch = $this->get('/ajax/search/entities?term=' . urlencode($chapter->name)); $chapterSearch = $this->get('/search/entity-selector?term=' . urlencode($chapter->name));
$chapterSearch->assertSee($chapter->name); $chapterSearch->assertSee($chapter->name);
$chapterSearch->assertSee($chapter->book->getShortName(42)); $chapterSearch->assertSee($chapter->book->getShortName(42));
} }
public function test_ajax_entity_search_reflects_items_without_permission() public function test_entity_selector_search_reflects_items_without_permission()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$baseSelector = 'a[data-entity-type="page"][data-entity-id="' . $page->id . '"]'; $baseSelector = 'a[data-entity-type="page"][data-entity-id="' . $page->id . '"]';
$searchUrl = '/ajax/search/entities?permission=update&term=' . urlencode($page->name); $searchUrl = '/search/entity-selector?permission=update&term=' . urlencode($page->name);
$resp = $this->asEditor()->get($searchUrl); $resp = $this->asEditor()->get($searchUrl);
$this->withHtml($resp)->assertElementContains($baseSelector, $page->name); $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);
@ -457,4 +457,25 @@ class EntitySearchTest extends TestCase
$this->withHtml($resp)->assertElementExists('form input[name="filters[updated_by]"][value="me"][checked="checked"]'); $this->withHtml($resp)->assertElementExists('form input[name="filters[updated_by]"][value="me"][checked="checked"]');
$this->withHtml($resp)->assertElementExists('form input[name="filters[created_by]"][value="me"][checked="checked"]'); $this->withHtml($resp)->assertElementExists('form input[name="filters[created_by]"][value="me"][checked="checked"]');
} }
public function test_search_suggestion_endpoint()
{
$this->entities->newPage(['name' => 'My suggestion page', 'html' => '<p>My supercool suggestion page</p>']);
// Test specific search
$resp = $this->asEditor()->get('/search/suggest?term="supercool+suggestion"');
$resp->assertSee('My suggestion page');
$resp->assertDontSee('My supercool suggestion page');
$resp->assertDontSee('No items available');
$this->withHtml($resp)->assertElementCount('a', 1);
// Test search limit
$resp = $this->asEditor()->get('/search/suggest?term=et');
$this->withHtml($resp)->assertElementCount('a', 5);
// Test empty state
$resp = $this->asEditor()->get('/search/suggest?term=spaghettisaurusrex');
$this->withHtml($resp)->assertElementCount('a', 0);
$resp->assertSee('No items available');
}
} }