build(docs-infra): expose deprecated status on items more clearly (#25750)

PR Close #25750
This commit is contained in:
Pete Bacon Darwin
2018-09-17 17:37:18 +01:00
committed by Kara Erickson
parent cea2e0477c
commit 026b60cd70
22 changed files with 321 additions and 93 deletions

View File

@ -775,7 +775,7 @@ describe('AppComponent', () => {
const searchService: MockSearchService = TestBed.get(SearchService);
const results = [
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false }
];
searchService.searchResults.next({ query: 'something', results: results });

View File

@ -22,11 +22,14 @@
<article class="api-list-container l-content-small docs-content">
<div *ngFor="let section of filteredSections | async" >
<h2 *ngIf="section.items"><a [href]="section.path">{{section.title}}</a></h2>
<h2 *ngIf="section.items"><a [href]="section.path" [class.deprecated-api-item]="section.deprecated">{{section.title}}</a></h2>
<ul class="api-list" *ngIf="section.items?.length">
<ng-container *ngFor="let item of section.items">
<li class="api-item">
<a [href]="item.path"><span class="symbol {{item.docType}}"></span> {{item.title}}</a>
<a [href]="item.path" [class.deprecated-api-item]="item.stability === 'deprecated'">
<span class="symbol {{item.docType}}"></span>
{{item.title}}
</a>
</li>
</ng-container>
</ul>

View File

@ -226,6 +226,7 @@ const apiSections: ApiSection[] = [
"name": "common",
"title": "common",
"path": "api/common",
"deprecated": false,
"items": [
{
"name": "class_1",
@ -233,7 +234,7 @@ const apiSections: ApiSection[] = [
"path": "api/common/class_1",
"docType": "class",
"stability": "experimental",
"securityRisk": false
"securityRisk": false,
},
{
"name": "class_2",
@ -241,7 +242,7 @@ const apiSections: ApiSection[] = [
"path": "api/common/class_2",
"docType": "class",
"stability": "stable",
"securityRisk": false
"securityRisk": false,
},
{
"name": "directive_1",
@ -249,7 +250,7 @@ const apiSections: ApiSection[] = [
"path": "api/common/directive_1",
"docType": "directive",
"stability": "stable",
"securityRisk": true
"securityRisk": true,
},
{
"name": "pipe_1",
@ -257,7 +258,7 @@ const apiSections: ApiSection[] = [
"path": "api/common/pipe_1",
"docType": "pipe",
"stability": "stable",
"securityRisk": true
"securityRisk": true,
},
]
},
@ -265,6 +266,7 @@ const apiSections: ApiSection[] = [
"name": "core",
"title": "core",
"path": "api/core",
"deprecated": false,
"items": [
{
"name": "class_3",
@ -272,7 +274,7 @@ const apiSections: ApiSection[] = [
"path": "api/core/class_3",
"docType": "class",
"stability": "experimental",
"securityRisk": false
"securityRisk": false,
},
{
"name": "function_1",
@ -280,7 +282,7 @@ const apiSections: ApiSection[] = [
"path": "api/core/function 1",
"docType": "function",
"stability": "deprecated",
"securityRisk": true
"securityRisk": true,
},
{
"name": "const_1",
@ -288,7 +290,7 @@ const apiSections: ApiSection[] = [
"path": "api/core/const_1",
"docType": "const",
"stability": "stable",
"securityRisk": false
"securityRisk": false,
}
]
}

View File

@ -50,7 +50,10 @@ describe('ApiService', () => {
describe('#sections', () => {
it('first subscriber should fetch sections', done => {
const data = [{name: 'a', title: 'A', path: '', items: []}, {name: 'b', title: 'B', path: '', items: []}];
const data = [
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
];
service.sections.subscribe(sections => {
expect(sections).toEqual(data);
@ -61,7 +64,10 @@ describe('ApiService', () => {
});
it('second subscriber should get previous sections and NOT trigger refetch', done => {
const data = [{name: 'a', title: 'A', path: '', items: []}, {name: 'b', title: 'B', path: '', items: []}];
const data = [
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
];
let subscriptions = 0;
service.sections.subscribe(sections => {
@ -91,7 +97,10 @@ describe('ApiService', () => {
let call = 0;
let data = [{name: 'a', title: 'A', path: '', items: []}, {name: 'b', title: 'B', path: '', items: []}];
let data = [
{name: 'a', title: 'A', path: '', items: [], deprecated: false},
{name: 'b', title: 'B', path: '', items: [], deprecated: false},
];
service.sections.subscribe(sections => {
// called twice during this test
@ -103,7 +112,7 @@ describe('ApiService', () => {
httpMock.expectOne({}).flush(data);
// refresh/refetch
data = [{name: 'c', title: 'C', path: '', items: []}];
data = [{name: 'c', title: 'C', path: '', items: [], deprecated: false}];
service.fetchSections();
httpMock.expectOne({}).flush(data);

View File

@ -20,6 +20,7 @@ export interface ApiSection {
path: string;
name: string;
title: string;
deprecated: boolean;
items: ApiItem[]|null;
}
@ -47,7 +48,12 @@ export class ApiService implements OnDestroy {
this._sections.subscribe(sections => this.logger.log('ApiService got API sections') );
}
return this._sections;
return this._sections.pipe(tap(sections => {
sections.forEach(section => {
section.deprecated = !!section.items &&
section.items.every(item => item.stability === 'deprecated');
});
}));
};
constructor(private http: HttpClient, private logger: Logger) { }

View File

@ -9,6 +9,7 @@ export interface SearchResult {
type: string;
titleWords: string;
keywords: string;
deprecated: boolean;
}
export interface SearchArea {

View File

@ -9,14 +9,16 @@
<ul class="priority-pages" >
<li class="search-page" *ngFor="let page of area.priorityPages">
<a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page, $event)">
<span class="symbol {{page.type}}" *ngIf="area.name === 'api'"></span>{{ page.title }}
<span class="symbol {{page.type}}" *ngIf="area.name === 'api'"></span>
<span [class.deprecated-api-item]="page.deprecated">{{ page.title }}</span>
</a>
</li>
</ul>
<ul>
<li class="search-page" *ngFor="let page of area.pages">
<a class="search-result-item" href="{{ page.path }}" (click)="onResultSelected(page, $event)">
<span class="symbol {{page.type}}" *ngIf="area.name === 'api'"></span>{{ page.title }}
<span class="symbol {{page.type}}" *ngIf="area.name === 'api'"></span>
<span [class.deprecated-api-item]="page.deprecated">{{ page.title }}</span>
</a>
</li>
</ul>

View File

@ -5,41 +5,59 @@ import { SearchResult } from 'app/search/interfaces';
import { SearchResultsComponent } from './search-results.component';
describe('SearchResultsComponent', () => {
let component: SearchResultsComponent;
let fixture: ComponentFixture<SearchResultsComponent>;
let guideA: SearchResult;
let apiD: SearchResult;
let guideB: SearchResult;
let guideAC: SearchResult;
let apiC: SearchResult;
let guideN: SearchResult;
let guideM: SearchResult;
let guideL: SearchResult;
let guideK: SearchResult;
let guideJ: SearchResult;
let guideI: SearchResult;
let guideH: SearchResult;
let guideG: SearchResult;
let guideF: SearchResult;
let guideE: SearchResult;
let standardResults: SearchResult[];
/** Get all text from component element */
/** Get all text from component element. */
function getText() { return fixture.debugElement.nativeElement.textContent; }
/** Get a full set of test results. "Take" what you need */
function getTestResults(take?: number) {
const results: SearchResult[] = [
{ path: 'guide/a', title: 'Guide A' },
{ path: 'api/d', title: 'API D' },
{ path: 'guide/b', title: 'Guide B' },
{ path: 'guide/a/c', title: 'Guide A - C' },
{ path: 'api/c', title: 'API C' }
]
// fill it out to exceed 10 guide pages
.concat('nmlkjihgfe'.split('').map(l => {
return { path: 'guide/' + l, title: 'Guide ' + l};
}))
// add these empty fields to satisfy interface
.map(r => ({...{ keywords: '', titleWords: '', type: '' }, ...r }));
return take === undefined ? results : results.slice(0, take);
}
function compareTitle(l: SearchResult, r: SearchResult) {
return l.title!.toUpperCase() > r.title!.toUpperCase() ? 1 : -1;
}
/** Pass the given search results to the component and trigger change detection. */
function setSearchResults(query: string, results: SearchResult[]) {
component.searchResults = {query, results};
component.ngOnChanges({});
fixture.detectChanges();
}
/** Get a full set of test results. "Take" what you need */
beforeEach(() => {
apiD = { path: 'api/d', title: 'API D', deprecated: false, keywords: '', titleWords: '', type: '' };
apiC = { path: 'api/c', title: 'API C', deprecated: false, keywords: '', titleWords: '', type: '' };
guideA = { path: 'guide/a', title: 'Guide A', deprecated: false, keywords: '', titleWords: '', type: '' };
guideB = { path: 'guide/b', title: 'Guide B', deprecated: false, keywords: '', titleWords: '', type: '' };
guideAC = { path: 'guide/a/c', title: 'Guide A - C', deprecated: false, keywords: '', titleWords: '', type: '' };
guideE = { path: 'guide/e', title: 'Guide e', deprecated: false, keywords: '', titleWords: '', type: '' };
guideF = { path: 'guide/f', title: 'Guide f', deprecated: false, keywords: '', titleWords: '', type: '' };
guideG = { path: 'guide/g', title: 'Guide g', deprecated: false, keywords: '', titleWords: '', type: '' };
guideH = { path: 'guide/h', title: 'Guide h', deprecated: false, keywords: '', titleWords: '', type: '' };
guideI = { path: 'guide/i', title: 'Guide i', deprecated: false, keywords: '', titleWords: '', type: '' };
guideJ = { path: 'guide/j', title: 'Guide j', deprecated: false, keywords: '', titleWords: '', type: '' };
guideK = { path: 'guide/k', title: 'Guide k', deprecated: false, keywords: '', titleWords: '', type: '' };
guideL = { path: 'guide/l', title: 'Guide l', deprecated: false, keywords: '', titleWords: '', type: '' };
guideM = { path: 'guide/m', title: 'Guide m', deprecated: false, keywords: '', titleWords: '', type: '' };
guideN = { path: 'guide/n', title: 'Guide n', deprecated: false, keywords: '', titleWords: '', type: '' };
standardResults = [
guideA, apiD, guideB, guideAC, apiC, guideN, guideM, guideL, guideK, guideJ, guideI, guideH, guideG, guideF, guideE,
];
});
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ SearchResultsComponent ]
@ -53,48 +71,42 @@ describe('SearchResultsComponent', () => {
});
it('should map the search results into groups based on their containing folder', () => {
setSearchResults('', getTestResults(3));
setSearchResults('', [guideA, apiD, guideB]);
expect(component.searchAreas).toEqual([
{ name: 'api', priorityPages: [
{ path: 'api/d', title: 'API D', type: '', keywords: '', titleWords: '' }
], pages: [] },
{ name: 'guide', priorityPages: [
{ path: 'guide/a', title: 'Guide A', type: '', keywords: '', titleWords: '' },
{ path: 'guide/b', title: 'Guide B', type: '', keywords: '', titleWords: '' },
], pages: [] }
{ name: 'api', priorityPages: [apiD], pages: [] },
{ name: 'guide', priorityPages: [guideA, guideB], pages: [] }
]);
});
it('should special case results that are top level folders', () => {
setSearchResults('', [
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' },
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '' },
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '', deprecated: false },
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '', deprecated: false },
]);
expect(component.searchAreas).toEqual([
{ name: 'tutorial', priorityPages: [
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' },
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '' },
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '', deprecated: false },
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '', deprecated: false },
], pages: [] }
]);
});
it('should put first 5 results for each area into priorityPages', () => {
const results = getTestResults();
setSearchResults('', results);
expect(component.searchAreas[0].priorityPages).toEqual(results.filter(p => p.path.startsWith('api')).slice(0, 5));
expect(component.searchAreas[1].priorityPages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(0, 5));
it('should put, at most, the first 5 results for each area into priorityPages, not sorted', () => {
setSearchResults('', standardResults);
expect(component.searchAreas[0].priorityPages).toEqual([apiD, apiC]);
expect(component.searchAreas[1].priorityPages).toEqual([guideA, guideB, guideAC, guideN, guideM]);
});
it('should put the nonPriorityPages into the pages array, sorted by title', () => {
const results = getTestResults();
setSearchResults('', results);
setSearchResults('', standardResults);
expect(component.searchAreas[0].pages).toEqual([]);
expect(component.searchAreas[1].pages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(5).sort(compareTitle));
expect(component.searchAreas[1].pages).toEqual([
guideE, guideF, guideG, guideH, guideI, guideJ, guideK, guideL
]);
});
it('should put a total count in the header of each area of search results', () => {
const results = getTestResults();
setSearchResults('', results);
setSearchResults('', standardResults);
fixture.detectChanges();
const headers = fixture.debugElement.queryAll(By.css('h3'));
expect(headers.length).toEqual(2);
@ -104,26 +116,55 @@ describe('SearchResultsComponent', () => {
it('should put search results with no containing folder into the default area (other)', () => {
const results = [
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false }
];
setSearchResults('', results);
expect(component.searchAreas).toEqual([
{ name: 'other', priorityPages: [
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false }
], pages: [] }
]);
});
it('should omit search results with no title', () => {
const results = [
{ path: 'news', title: '', type: 'marketing', keywords: '', titleWords: '' }
{ path: 'news', title: '', type: 'marketing', keywords: '', titleWords: '', deprecated: false }
];
setSearchResults('something', results);
expect(component.searchAreas).toEqual([]);
});
describe('when there are deprecated items', () => {
beforeEach(() => {
apiD.deprecated = true;
guideAC.deprecated = true;
guideJ.deprecated = true;
guideE.deprecated = true;
setSearchResults('something', standardResults);
});
it('should include deprecated items in priority pages unless there are fewer than 5 non-deprecated priority pages', () => {
// Priority pages do not include deprecated items:
expect(component.searchAreas[1].priorityPages).not.toContain(guideAC);
expect(component.searchAreas[1].priorityPages).not.toContain(guideJ);
// Except where there are too few priority pages:
expect(component.searchAreas[0].priorityPages).toContain(apiD);
});
it('should move the non-priority deprecated pages to the bottom of the pages list, unsorted', () => {
// Bottom pages are the deprecated ones (in original order)
expect(component.searchAreas[1].pages.slice(-3)).toEqual([guideAC, guideJ, guideE]);
});
it('should sort the non-deprecated, non-priority pages by title', () => {
// The rest of the pages are non-deprecated, sorted by title
expect(component.searchAreas[1].pages.slice(0, -3)).toEqual([
guideF, guideG, guideH, guideI, guideK,
]);
});
});
it('should display "Searching ..." while waiting for search results', () => {
fixture.detectChanges();
expect(getText()).toContain('Searching ...');
@ -138,7 +179,7 @@ describe('SearchResultsComponent', () => {
component.resultSelected.subscribe((result: SearchResult) => selected = result);
selected = null;
searchResult = { path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' };
searchResult = { path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '', deprecated: false };
setSearchResults('something', [searchResult]);
fixture.detectChanges();

View File

@ -53,12 +53,12 @@ export class SearchResultsComponent implements OnChanges {
});
const keys = Object.keys(searchAreaMap).sort((l, r) => l > r ? 1 : -1);
return keys.map(name => {
let pages: SearchResult[] = searchAreaMap[name];
// Extract the top 5 most relevant results as priorityPages
const priorityPages = pages.splice(0, 5);
pages = pages.sort(compareResults);
return { name, pages, priorityPages };
const {priorityPages, pages, deprecated} = splitPages(searchAreaMap[name]);
return {
name,
priorityPages,
pages: pages.concat(deprecated),
};
});
}
@ -72,6 +72,30 @@ export class SearchResultsComponent implements OnChanges {
}
}
function splitPages(allPages: SearchResult[]) {
const priorityPages: SearchResult[] = [];
const pages: SearchResult[] = [];
const deprecated: SearchResult[] = [];
allPages.forEach(page => {
if (page.deprecated) {
deprecated.push(page);
} else if (priorityPages.length < 5) {
priorityPages.push(page);
} else {
pages.push(page);
}
});
while (priorityPages.length < 5 && pages.length) {
priorityPages.push(pages.shift()!);
}
while (priorityPages.length < 5 && deprecated.length) {
priorityPages.push(deprecated.shift()!);
}
pages.sort(compareResults);
return { priorityPages, pages, deprecated };
}
function compareResults(l: SearchResult, r: SearchResult) {
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
}

View File

@ -198,6 +198,10 @@ aio-api-list {
background: $blue-grey-50;
color: $blue-500;
}
&.deprecated-api-item {
text-decoration: line-through;
}
}
}
}

View File

@ -114,3 +114,7 @@
padding: 0;
}
}
.deprecated-api-item {
text-decoration: line-through;
}

View File

@ -49,11 +49,7 @@ label.raised, .api-header label {
&.api-status-label {
background-color: $mediumgray;
&.security {
background-color: $brightred;
}
&.impure-pipe {
&.deprecated, &.security, &.impure-pipe {
background-color: $brightred;
}
}

View File

@ -23,6 +23,7 @@ $mediumgray: #6e6e6e;
$darkgray: #333;
$black: #0A1014;
$orange: #FF9800;
$darkorange: #940;
$anti-pattern: $brightred;
// API & CODE COLORS
@ -95,7 +96,7 @@ $api-symbols: (
),
ngmodule: (
content: 'M',
background: $darkred
background: $darkorange
),
package: (
content: 'Pk',