From e7e83a4109ab95e58f3772a2d9cd759b43ddbd3f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 21 Nov 2022 10:29:12 +0000 Subject: [PATCH] Added new endpoint for search suggestions --- app/Http/Controllers/SearchController.php | 22 ++++++++++- resources/js/components/entity-selector.js | 2 +- resources/js/components/global-search.js | 10 ++--- resources/sass/_animations.scss | 2 +- resources/views/common/header.blade.php | 6 ++- ...ade.php => entity-selector-list.blade.php} | 0 .../parts/entity-suggestion-list.blade.php | 21 ++++++++++ routes/web.php | 4 +- tests/Entity/EntitySearchTest.php | 39 ++++++++++++++----- 9 files changed, 83 insertions(+), 23 deletions(-) rename resources/views/search/parts/{entity-ajax-list.blade.php => entity-selector-list.blade.php} (100%) create mode 100644 resources/views/search/parts/entity-suggestion-list.blade.php diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 8fcde4a6d..8df5cfafb 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -69,7 +69,7 @@ class SearchController extends Controller * 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. */ - public function searchEntitiesAjax(Request $request) + public function searchForSelector(Request $request) { $entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book']; $searchTerm = $request->get('term', false); @@ -83,7 +83,25 @@ class SearchController extends Controller $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) + ]); } /** diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.js index e2596998a..2f009b2da 100644 --- a/resources/js/components/entity-selector.js +++ b/resources/js/components/entity-selector.js @@ -115,7 +115,7 @@ class EntitySelector { } 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) { diff --git a/resources/js/components/global-search.js b/resources/js/components/global-search.js index b3719c8fd..c940ad983 100644 --- a/resources/js/components/global-search.js +++ b/resources/js/components/global-search.js @@ -12,6 +12,7 @@ class GlobalSearch { this.suggestions = this.$refs.suggestions; this.suggestionResultsWrap = this.$refs.suggestionResults; this.loadingWrap = this.$refs.loading; + this.button = this.$refs.button; this.setupListeners(); } @@ -34,7 +35,7 @@ class GlobalSearch { // Allow double click to show auto-click suggestions this.input.addEventListener('dblclick', () => { this.input.setAttribute('autocomplete', 'on'); - this.input.blur(); + this.button.focus(); this.input.focus(); }) } @@ -43,18 +44,13 @@ class GlobalSearch { * @param {String} 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) { return; } const resultDom = htmlToDom(results); - const childrenToTrim = Array.from(resultDom.children).slice(9); - for (const child of childrenToTrim) { - child.remove(); - } - this.suggestionResultsWrap.innerHTML = ''; this.suggestionResultsWrap.style.opacity = '1'; this.loadingWrap.style.display = 'none'; diff --git a/resources/sass/_animations.scss b/resources/sass/_animations.scss index ca4977a89..eb9f4e767 100644 --- a/resources/sass/_animations.scss +++ b/resources/sass/_animations.scss @@ -18,7 +18,7 @@ .search-suggestions-animation{ animation-name: searchSuggestions; - animation-duration: 180ms; + animation-duration: 120ms; animation-fill-mode: forwards; animation-timing-function: cubic-bezier(.62, .28, .23, .99); } diff --git a/resources/views/common/header.blade.php b/resources/views/common/header.blade.php index d5f4fa224..71b73215b 100644 --- a/resources/views/common/header.blade.php +++ b/resources/views/common/header.blade.php @@ -20,7 +20,11 @@
@if (hasAppAccess())
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index de913c543..107484939 100644 --- a/routes/web.php +++ b/routes/web.php @@ -184,8 +184,6 @@ Route::middleware('auth')->group(function () { Route::get('/ajax/tags/suggest/names', [TagController::class, 'getNameSuggestions']); Route::get('/ajax/tags/suggest/values', [TagController::class, 'getValueSuggestions']); - Route::get('/ajax/search/entities', [SearchController::class, 'searchEntitiesAjax']); - // Comments Route::post('/comment/{pageId}', [CommentController::class, 'savePageComment']); 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/chapter/{bookId}', [SearchController::class, 'searchChapter']); 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 Route::get('/search/users/select', [UserSearchController::class, 'forSelect']); diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index c309f2167..2650b6743 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -190,7 +190,7 @@ class EntitySearchTest extends TestCase $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']); $notVisitedPage = $this->entities->page(); @@ -198,38 +198,38 @@ class EntitySearchTest extends TestCase // Visit the page to make popular $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); - $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); - $defaultListTest = $this->get('/ajax/search/entities'); + $defaultListTest = $this->get('/search/entity-selector'); $defaultListTest->assertSee($page->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(); $page = $chapter->pages->first(); $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($chapter->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->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(); $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); $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[created_by]"][value="me"][checked="checked"]'); } + + public function test_search_suggestion_endpoint() + { + $this->entities->newPage(['name' => 'My suggestion page', 'html' => '

My supercool suggestion page

']); + + // 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'); + } }