Added new endpoint for search suggestions
This commit is contained in:
		
							parent
							
								
									c617190905
								
							
						
					
					
						commit
						e7e83a4109
					
				| 
						 | 
					@ -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)
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					@ -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']);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue