feat(aio): add search to 404 page (#19682)
The 404 page will now run a search based on the given URL to offer suggestions for the page that the user really wanted. PR Close #19682
This commit is contained in:
parent
88c46feb20
commit
91fcfcb042
|
@ -7,3 +7,4 @@
|
||||||
<p>We're sorry. The page you are looking for cannot be found.</p>
|
<p>We're sorry. The page you are looking for cannot be found.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<aio-file-not-found-search></aio-file-not-found-search>
|
||||||
|
|
|
@ -100,4 +100,12 @@ describe('site App', function() {
|
||||||
expect(page.getSearchResults().map(link => link.getText())).toContain('ControlValueAccessor');
|
expect(page.getSearchResults().map(link => link.getText())).toContain('ControlValueAccessor');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('404 page', () => {
|
||||||
|
it('should search the index for words found in the url', () => {
|
||||||
|
page.navigateTo('http/router');
|
||||||
|
expect(page.getSearchResults().map(link => link.getText())).toContain('Http');
|
||||||
|
expect(page.getSearchResults().map(link => link.getText())).toContain('Router');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CodeTabsComponent } from './code/code-tabs.component';
|
||||||
import { ContributorListComponent } from './contributor/contributor-list.component';
|
import { ContributorListComponent } from './contributor/contributor-list.component';
|
||||||
import { ContributorComponent } from './contributor/contributor.component';
|
import { ContributorComponent } from './contributor/contributor.component';
|
||||||
import { CurrentLocationComponent } from './current-location.component';
|
import { CurrentLocationComponent } from './current-location.component';
|
||||||
|
import { FileNotFoundSearchComponent } from './search/file-not-found-search.component';
|
||||||
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
|
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
|
||||||
import { ResourceListComponent } from './resource/resource-list.component';
|
import { ResourceListComponent } from './resource/resource-list.component';
|
||||||
import { ResourceService } from './resource/resource.service';
|
import { ResourceService } from './resource/resource.service';
|
||||||
|
@ -30,7 +31,8 @@ import { TocComponent } from './toc/toc.component';
|
||||||
*/
|
*/
|
||||||
export const embeddedComponents: any[] = [
|
export const embeddedComponents: any[] = [
|
||||||
ApiListComponent, CodeExampleComponent, CodeTabsComponent, ContributorListComponent,
|
ApiListComponent, CodeExampleComponent, CodeTabsComponent, ContributorListComponent,
|
||||||
CurrentLocationComponent, LiveExampleComponent, ResourceListComponent, TocComponent
|
CurrentLocationComponent, FileNotFoundSearchComponent, LiveExampleComponent, ResourceListComponent,
|
||||||
|
TocComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Injectable class w/ property returning components that can be embedded in docs */
|
/** Injectable class w/ property returning components that can be embedded in docs */
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { Subject } from 'rxjs/Subject';
|
||||||
|
import { LocationService } from 'app/shared/location.service';
|
||||||
|
import { MockLocationService } from 'testing/location.service';
|
||||||
|
import { SearchResults } from 'app/search/interfaces';
|
||||||
|
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
|
||||||
|
import { SearchService } from 'app/search/search.service';
|
||||||
|
import { FileNotFoundSearchComponent } from './file-not-found-search.component';
|
||||||
|
|
||||||
|
|
||||||
|
describe('FileNotFoundSearchComponent', () => {
|
||||||
|
let element: HTMLElement;
|
||||||
|
let fixture: ComponentFixture<FileNotFoundSearchComponent>;
|
||||||
|
let searchService: SearchService;
|
||||||
|
let searchResultSubject: Subject<SearchResults>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ FileNotFoundSearchComponent, SearchResultsComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: LocationService, useValue: new MockLocationService('base/initial-url?some-query') },
|
||||||
|
SearchService
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FileNotFoundSearchComponent);
|
||||||
|
searchService = TestBed.get(SearchService);
|
||||||
|
searchResultSubject = new Subject<SearchResults>();
|
||||||
|
spyOn(searchService, 'search').and.callFake(() => searchResultSubject.asObservable());
|
||||||
|
fixture.detectChanges();
|
||||||
|
element = fixture.nativeElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run a search with a query built from the current url', () => {
|
||||||
|
expect(searchService.search).toHaveBeenCalledWith('base initial url');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass through any results to the `aio-search-results` component', () => {
|
||||||
|
const searchResultsComponent = fixture.debugElement.query(By.directive(SearchResultsComponent)).componentInstance;
|
||||||
|
expect(searchResultsComponent.searchResults).toBe(null);
|
||||||
|
|
||||||
|
const results = { query: 'base initial url', results: []};
|
||||||
|
searchResultSubject.next(results);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(searchResultsComponent.searchResults).toEqual(results);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { LocationService } from 'app/shared/location.service';
|
||||||
|
import { SearchResults } from 'app/search/interfaces';
|
||||||
|
import { SearchService } from 'app/search/search.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aio-file-not-found-search',
|
||||||
|
template:
|
||||||
|
`<p>Let's see if any of these search results help...</p>
|
||||||
|
<aio-search-results class="embedded" [searchResults]="searchResults | async"></aio-search-results>`
|
||||||
|
})
|
||||||
|
export class FileNotFoundSearchComponent implements OnInit {
|
||||||
|
searchResults: Observable<SearchResults>;
|
||||||
|
constructor(private location: LocationService, private search: SearchService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.searchResults = this.location.currentPath.switchMap(path => {
|
||||||
|
const query = path.split(/\W+/).join(' ');
|
||||||
|
return this.search.search(query);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,24 @@ aio-search-results {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aio-search-results.embedded .search-results {
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
width: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
position: relative;
|
||||||
|
background-color: inherit;
|
||||||
|
box-shadow: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.search-area a {
|
||||||
|
color: lighten($darkgray, 10);
|
||||||
|
&:hover {
|
||||||
|
color: $accentblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.search-area {
|
.search-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
Loading…
Reference in New Issue