mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
committed by
Miško Hevery
parent
22b96b9690
commit
7c9b411777
@@ -0,0 +1,33 @@
|
||||
<div class="l-flex-wrap api-filter">
|
||||
|
||||
<aio-select (change)="setType($event.option)"
|
||||
[options]="types"
|
||||
[selected]="type"
|
||||
[showSymbol]="true"
|
||||
label="Type:">
|
||||
</aio-select>
|
||||
|
||||
<aio-select (change)="setStatus($event.option)"
|
||||
[options]="statuses"
|
||||
[selected]="status"
|
||||
label="Status:">
|
||||
</aio-select>
|
||||
|
||||
<div class="form-search">
|
||||
<input #filter placeholder="Filter" (input)="setQuery($event.target.value)">
|
||||
<i class="material-icons">search</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="api-list-container l-content-small docs-content">
|
||||
<div *ngFor="let section of filteredSections | async" >
|
||||
<h2>{{section.title}}</h2>
|
||||
<ul class="api-list">
|
||||
<ng-container *ngFor="let item of section.items">
|
||||
<li *ngIf="item.show" class="api-item">
|
||||
<a [href]="item.path"><span class="symbol {{item.docType}}"></span> {{item.title}}</a>
|
||||
</li>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,288 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
import { ApiListComponent } from './api-list.component';
|
||||
import { ApiItem, ApiSection, ApiService } from './api.service';
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { MockLogger } from 'testing/logger.service';
|
||||
import { ApiListModule } from './api-list.module';
|
||||
|
||||
describe('ApiListComponent', () => {
|
||||
let component: ApiListComponent;
|
||||
let fixture: ComponentFixture<ApiListComponent>;
|
||||
let sections: ApiSection[];
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ ApiListModule ],
|
||||
providers: [
|
||||
{ provide: ApiService, useClass: TestApiService },
|
||||
{ provide: Logger, useClass: MockLogger },
|
||||
{ provide: LocationService, useClass: TestLocationService }
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(ApiListComponent);
|
||||
component = fixture.componentInstance;
|
||||
sections = getApiSections();
|
||||
});
|
||||
|
||||
/**
|
||||
* Expectation Utility: Assert that filteredSections has the expected result for this test
|
||||
* @param itemTest - return true if the item passes the match test
|
||||
*
|
||||
* Subscibes to `filteredSections` and performs expectation within subscription callback.
|
||||
*/
|
||||
function expectFilteredResult(label: string, itemTest: (item: ApiItem) => boolean) {
|
||||
component.filteredSections.subscribe(filtered => {
|
||||
let badItem: ApiItem|undefined;
|
||||
expect(filtered.length).toBeGreaterThan(0, 'expected something');
|
||||
expect(filtered.every(section => section.items.every(
|
||||
item => {
|
||||
const ok = item.show === itemTest(item);
|
||||
if (!ok) { badItem = item; }
|
||||
return ok;
|
||||
}
|
||||
))).toBe(true, `${label} fail: ${JSON.stringify(badItem, null, 2)}`);
|
||||
});
|
||||
}
|
||||
|
||||
describe('#filteredSections', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should return all complete sections when no criteria', () => {
|
||||
let filtered: ApiSection[]|undefined;
|
||||
component.filteredSections.subscribe(f => filtered = f);
|
||||
expect(filtered).toEqual(sections);
|
||||
});
|
||||
|
||||
it('item.show should be true for all queried items', () => {
|
||||
component.setQuery('class');
|
||||
expectFilteredResult('query: class', item => /class/.test(item.name));
|
||||
});
|
||||
|
||||
it('item.show should be true for every item in section when query matches section name', () => {
|
||||
component.setQuery('core');
|
||||
component.filteredSections.subscribe(filtered => {
|
||||
expect(filtered.length).toBe(1, 'only one section');
|
||||
expect(filtered[0].name).toBe('core');
|
||||
expect(filtered[0].items.every(item => !!item.show)).toBe(true, 'all core items shown');
|
||||
});
|
||||
});
|
||||
|
||||
it('item.show should be true for items with selected status', () => {
|
||||
component.setStatus({value: 'stable', title: 'Stable'});
|
||||
expectFilteredResult('status: stable', item => item.stability === 'stable');
|
||||
});
|
||||
|
||||
it('item.show should be true for items with "security-risk" status when selected', () => {
|
||||
component.setStatus({value: 'security-risk', title: 'Security Risk'});
|
||||
expectFilteredResult('status: security-risk', item => item.securityRisk);
|
||||
});
|
||||
|
||||
it('item.show should be true for items of selected type', () => {
|
||||
component.setType({value: 'class', title: 'Class'});
|
||||
expectFilteredResult('type: class', item => item.docType === 'class');
|
||||
});
|
||||
|
||||
it('should have no sections and no items when no match', () => {
|
||||
component.setQuery('fizbuzz');
|
||||
component.filteredSections.subscribe(filtered => {
|
||||
expect(filtered.length).toBe(0, 'expected no sections');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initial critera from location', () => {
|
||||
let locationService: TestLocationService;
|
||||
|
||||
beforeEach(() => {
|
||||
locationService = <any> fixture.componentRef.injector.get(LocationService);
|
||||
});
|
||||
|
||||
function expectOneItem(name: string, section: string, type: string, stability: string) {
|
||||
fixture.detectChanges();
|
||||
|
||||
component.filteredSections.subscribe(filtered => {
|
||||
expect(filtered.length).toBe(1, 'sections');
|
||||
expect(filtered[0].name).toBe(section, 'section name');
|
||||
const items = filtered[0].items.filter(item => item.show);
|
||||
expect(items.length).toBe(1, 'items');
|
||||
|
||||
const item = items[0];
|
||||
const badItem = 'Wrong item: ' + JSON.stringify(item, null, 2);
|
||||
|
||||
expect(item.docType).toBe(type, badItem);
|
||||
expect(item.stability).toBe(stability, badItem);
|
||||
expect(item.name).toBe(name, badItem);
|
||||
});
|
||||
}
|
||||
|
||||
it('should filter as expected for ?query', () => {
|
||||
locationService.query = {query: '_3'};
|
||||
expectOneItem('class_3', 'core', 'class', 'experimental');
|
||||
});
|
||||
|
||||
it('should filter as expected for ?status', () => {
|
||||
locationService.query = {status: 'deprecated'};
|
||||
expectOneItem('function_1', 'core', 'function', 'deprecated');
|
||||
});
|
||||
|
||||
it('should filter as expected when status is security-risk', () => {
|
||||
locationService.query = {status: 'security-risk'};
|
||||
fixture.detectChanges();
|
||||
expectFilteredResult('security-risk', item => item.securityRisk);
|
||||
});
|
||||
|
||||
it('should filter as expected for ?type', () => {
|
||||
locationService.query = {type: 'pipe'};
|
||||
expectOneItem('pipe_1', 'common', 'pipe', 'stable');
|
||||
});
|
||||
|
||||
it('should filter as expected for ?query&status&type', () => {
|
||||
locationService.query = {
|
||||
query: 's_1',
|
||||
status: 'experimental',
|
||||
type: 'class'
|
||||
};
|
||||
fixture.detectChanges();
|
||||
expectOneItem('class_1', 'common', 'class', 'experimental');
|
||||
});
|
||||
|
||||
it('should ignore case for ?query&status&type', () => {
|
||||
locationService.query = {
|
||||
query: 'S_1',
|
||||
status: 'ExperiMental',
|
||||
type: 'CLASS'
|
||||
};
|
||||
fixture.detectChanges();
|
||||
expectOneItem('class_1', 'common', 'class', 'experimental');
|
||||
});
|
||||
});
|
||||
|
||||
describe('location path after criteria change', () => {
|
||||
let locationService: TestLocationService;
|
||||
|
||||
beforeEach(() => {
|
||||
locationService = <any> fixture.componentRef.injector.get(LocationService);
|
||||
});
|
||||
|
||||
it('should have query', () => {
|
||||
component.setQuery('foo');
|
||||
|
||||
// `setSearch` 2nd param is a query/search params object
|
||||
const search = locationService.setSearch.calls.mostRecent().args[1];
|
||||
expect(search.query).toBe('foo');
|
||||
});
|
||||
|
||||
it('should keep last of multiple query settings (in lowercase)', () => {
|
||||
component.setQuery('foo');
|
||||
component.setQuery('fooBar');
|
||||
|
||||
const search = locationService.setSearch.calls.mostRecent().args[1];
|
||||
expect(search.query).toBe('foobar');
|
||||
});
|
||||
|
||||
it('should have query, status, and type', () => {
|
||||
component.setQuery('foo');
|
||||
component.setStatus({value: 'stable', title: 'Stable'});
|
||||
component.setType({value: 'class', title: 'Class'});
|
||||
|
||||
const search = locationService.setSearch.calls.mostRecent().args[1];
|
||||
expect(search.query).toBe('foo');
|
||||
expect(search.status).toBe('stable');
|
||||
expect(search.type).toBe('class');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
////// Helpers ////////
|
||||
|
||||
class TestLocationService {
|
||||
query: {[index: string]: string } = {};
|
||||
setSearch = jasmine.createSpy('setSearch');
|
||||
search() { return this.query; }
|
||||
}
|
||||
|
||||
class TestApiService {
|
||||
sectionsSubject = new BehaviorSubject(getApiSections());
|
||||
sections = this.sectionsSubject.asObservable();
|
||||
}
|
||||
|
||||
// tslint:disable:quotemark
|
||||
const apiSections: ApiSection[] = [
|
||||
{
|
||||
"name": "common",
|
||||
"title": "common",
|
||||
"items": [
|
||||
{
|
||||
"name": "class_1",
|
||||
"title": "Class 1",
|
||||
"path": "api/common/class_1",
|
||||
"docType": "class",
|
||||
"stability": "experimental",
|
||||
"securityRisk": false
|
||||
},
|
||||
{
|
||||
"name": "class_2",
|
||||
"title": "Class 2",
|
||||
"path": "api/common/class_2",
|
||||
"docType": "class",
|
||||
"stability": "stable",
|
||||
"securityRisk": false
|
||||
},
|
||||
{
|
||||
"name": "directive_1",
|
||||
"title": "Directive 1",
|
||||
"path": "api/common/directive_1",
|
||||
"docType": "directive",
|
||||
"stability": "stable",
|
||||
"securityRisk": true
|
||||
},
|
||||
{
|
||||
"name": "pipe_1",
|
||||
"title": "Pipe 1",
|
||||
"path": "api/common/pipe_1",
|
||||
"docType": "pipe",
|
||||
"stability": "stable",
|
||||
"securityRisk": true
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "core",
|
||||
"title": "core",
|
||||
"items": [
|
||||
{
|
||||
"name": "class_3",
|
||||
"title": "Class 3",
|
||||
"path": "api/core/class_3",
|
||||
"docType": "class",
|
||||
"stability": "experimental",
|
||||
"securityRisk": false
|
||||
},
|
||||
{
|
||||
"name": "function_1",
|
||||
"title": "Function 1",
|
||||
"path": "api/core/function 1",
|
||||
"docType": "function",
|
||||
"stability": "deprecated",
|
||||
"securityRisk": true
|
||||
},
|
||||
{
|
||||
"name": "const_1",
|
||||
"title": "Const 1",
|
||||
"path": "api/core/const_1",
|
||||
"docType": "const",
|
||||
"stability": "stable",
|
||||
"securityRisk": false
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function getApiSections() { return apiSections; }
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* API List & Filter Component
|
||||
*
|
||||
* A page that displays a formatted list of the public Angular API entities.
|
||||
* Clicking on a list item triggers navigation to the corresponding API entity document.
|
||||
* Can add/remove API entity links based on filter settings.
|
||||
*/
|
||||
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import { combineLatest } from 'rxjs/observable/combineLatest';
|
||||
|
||||
import { LocationService } from 'app/shared/location.service';
|
||||
import { ApiSection, ApiService } from './api.service';
|
||||
|
||||
import { Option } from 'app/shared/select/select.component';
|
||||
|
||||
class SearchCriteria {
|
||||
query? = '';
|
||||
status? = 'all';
|
||||
type? = 'all';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aio-api-list',
|
||||
templateUrl: './api-list.component.html',
|
||||
})
|
||||
export class ApiListComponent implements OnInit {
|
||||
|
||||
filteredSections: Observable<ApiSection[]>;
|
||||
|
||||
showStatusMenu = false;
|
||||
showTypeMenu = false;
|
||||
|
||||
private criteriaSubject = new ReplaySubject<SearchCriteria>(1);
|
||||
private searchCriteria = new SearchCriteria();
|
||||
|
||||
status: Option;
|
||||
type: Option;
|
||||
|
||||
// API types
|
||||
types: Option[] = [
|
||||
{ value: 'all', title: 'All' },
|
||||
{ value: 'class', title: 'Class' },
|
||||
{ value: 'const', title: 'Const'},
|
||||
{ value: 'decorator', title: 'Decorator' },
|
||||
{ value: 'directive', title: 'Directive' },
|
||||
{ value: 'enum', title: 'Enum' },
|
||||
{ value: 'function', title: 'Function' },
|
||||
{ value: 'interface', title: 'Interface' },
|
||||
{ value: 'pipe', title: 'Pipe'},
|
||||
{ value: 'type-alias', title: 'Type Alias' }
|
||||
];
|
||||
|
||||
statuses: Option[] = [
|
||||
{ value: 'all', title: 'All' },
|
||||
{ value: 'stable', title: 'Stable' },
|
||||
{ value: 'deprecated', title: 'Deprecated' },
|
||||
{ value: 'experimental', title: 'Experimental' },
|
||||
{ value: 'security-risk', title: 'Security Risk' }
|
||||
];
|
||||
|
||||
@ViewChild('filter') queryEl: ElementRef;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private locationService: LocationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.filteredSections = combineLatest(
|
||||
this.apiService.sections,
|
||||
this.criteriaSubject,
|
||||
(sections, criteria) => {
|
||||
return sections.filter(section => this.filterSection(section, criteria));
|
||||
}
|
||||
);
|
||||
|
||||
this.initializeSearchCriteria();
|
||||
}
|
||||
|
||||
// Todo: may need to debounce as the original did
|
||||
// although there shouldn't be any perf consequences if we don't
|
||||
setQuery(query: string) {
|
||||
this.setSearchCriteria({query: (query || '').toLowerCase().trim() });
|
||||
}
|
||||
|
||||
setStatus(status: Option) {
|
||||
this.toggleStatusMenu();
|
||||
this.status = status;
|
||||
this.setSearchCriteria({status: status.value});
|
||||
}
|
||||
|
||||
setType(type: Option) {
|
||||
this.toggleTypeMenu();
|
||||
this.type = type;
|
||||
this.setSearchCriteria({type: type.value});
|
||||
}
|
||||
|
||||
toggleStatusMenu() {
|
||||
this.showStatusMenu = !this.showStatusMenu;
|
||||
}
|
||||
|
||||
toggleTypeMenu() {
|
||||
this.showTypeMenu = !this.showTypeMenu;
|
||||
}
|
||||
|
||||
//////// Private //////////
|
||||
|
||||
private filterSection(section: ApiSection, { query, status, type }: SearchCriteria) {
|
||||
let showSection = false;
|
||||
|
||||
section.items.forEach(item => {
|
||||
item.show = matchesType() && matchesStatus() && matchesQuery();
|
||||
|
||||
// show section if any of its items will be shown
|
||||
showSection = showSection || item.show;
|
||||
|
||||
function matchesQuery() {
|
||||
return !query ||
|
||||
section.name.indexOf(query) !== -1 ||
|
||||
item.name.indexOf(query) !== -1;
|
||||
}
|
||||
|
||||
function matchesStatus() {
|
||||
return status === 'all' ||
|
||||
status === item.stability ||
|
||||
(status === 'security-risk' && item.securityRisk);
|
||||
};
|
||||
|
||||
function matchesType() {
|
||||
return type === 'all' || type === item.docType;
|
||||
}
|
||||
});
|
||||
|
||||
return showSection;
|
||||
|
||||
}
|
||||
|
||||
// Get initial search criteria from URL search params
|
||||
private initializeSearchCriteria() {
|
||||
const {query, status, type} = this.locationService.search();
|
||||
|
||||
const q = (query || '').toLowerCase();
|
||||
// Hack: can't bind to query because input cursor always forced to end-of-line.
|
||||
this.queryEl.nativeElement.value = q;
|
||||
|
||||
this.status = this.statuses.find(x => x.value === status) || this.statuses[0];
|
||||
this.type = this.types.find(x => x.value === type) || this.types[0];
|
||||
|
||||
this.searchCriteria = {
|
||||
query: q,
|
||||
status: this.status.value,
|
||||
type: this.type.value
|
||||
};
|
||||
|
||||
this.criteriaSubject.next(this.searchCriteria);
|
||||
}
|
||||
|
||||
private setLocationSearch() {
|
||||
const {query, status, type} = this.searchCriteria;
|
||||
const params = {
|
||||
query: query ? query : undefined,
|
||||
status: status !== 'all' ? status : undefined,
|
||||
type: type !== 'all' ? type : undefined
|
||||
};
|
||||
|
||||
this.locationService.setSearch('API Search', params);
|
||||
}
|
||||
|
||||
private setSearchCriteria(criteria: SearchCriteria) {
|
||||
this.criteriaSubject.next(Object.assign(this.searchCriteria, criteria));
|
||||
this.setLocationSearch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { ApiListComponent } from './api-list.component';
|
||||
import { ApiService } from './api.service';
|
||||
import { WithCustomElementComponent } from '../element-registry';
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule, SharedModule, HttpClientModule ],
|
||||
declarations: [ ApiListComponent ],
|
||||
entryComponents: [ ApiListComponent ],
|
||||
providers: [ ApiService ]
|
||||
})
|
||||
export class ApiListModule implements WithCustomElementComponent {
|
||||
customElementComponent: Type<any> = ApiListComponent;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { Injector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
describe('ApiService', () => {
|
||||
|
||||
let injector: Injector;
|
||||
let service: ApiService;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
injector = TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
ApiService,
|
||||
{ provide: Logger, useClass: TestLogger }
|
||||
]
|
||||
});
|
||||
|
||||
service = injector.get(ApiService);
|
||||
httpMock = injector.get(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
|
||||
it('should not immediately connect to the server', () => {
|
||||
httpMock.expectNone({});
|
||||
});
|
||||
|
||||
it('subscribers should be completed/unsubscribed when service destroyed', () => {
|
||||
let completed = false;
|
||||
|
||||
service.sections.subscribe(
|
||||
undefined,
|
||||
undefined,
|
||||
() => completed = true
|
||||
);
|
||||
|
||||
service.ngOnDestroy();
|
||||
expect(completed).toBe(true);
|
||||
|
||||
// Stop `httpMock.verify()` from complaining.
|
||||
httpMock.expectOne({});
|
||||
});
|
||||
|
||||
describe('#sections', () => {
|
||||
|
||||
it('first subscriber should fetch sections', () => {
|
||||
const data = [{name: 'a', title: 'A', items: []}, {name: 'b', title: 'B', items: []}];
|
||||
|
||||
service.sections.subscribe(sections => {
|
||||
expect(sections).toEqual(data);
|
||||
});
|
||||
|
||||
httpMock.expectOne({}).flush(data);
|
||||
});
|
||||
|
||||
it('second subscriber should get previous sections and NOT trigger refetch', () => {
|
||||
const data = [{name: 'a', title: 'A', items: []}, {name: 'b', title: 'B', items: []}];
|
||||
let subscriptions = 0;
|
||||
|
||||
service.sections.subscribe(sections => {
|
||||
subscriptions++;
|
||||
expect(sections).toEqual(data);
|
||||
});
|
||||
|
||||
service.sections.subscribe(sections => {
|
||||
subscriptions++;
|
||||
expect(sections).toEqual(data);
|
||||
});
|
||||
|
||||
httpMock.expectOne({}).flush(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchSections', () => {
|
||||
|
||||
it('should connect to the server w/ expected URL', () => {
|
||||
service.fetchSections();
|
||||
httpMock.expectOne('generated/docs/api/api-list.json');
|
||||
});
|
||||
|
||||
it('should refresh the #sections observable w/ new content on second call', () => {
|
||||
|
||||
let call = 0;
|
||||
|
||||
let data = [{name: 'a', title: 'A', items: []}, {name: 'b', title: 'B', items: []}];
|
||||
|
||||
service.sections.subscribe(sections => {
|
||||
// called twice during this test
|
||||
// (1) during subscribe
|
||||
// (2) after refresh
|
||||
expect(sections).toEqual(data, 'call ' + call++);
|
||||
});
|
||||
|
||||
httpMock.expectOne({}).flush(data);
|
||||
|
||||
// refresh/refetch
|
||||
data = [{name: 'c', title: 'C', items: []}];
|
||||
service.fetchSections();
|
||||
httpMock.expectOne({}).flush(data);
|
||||
|
||||
expect(call).toBe(2, 'should be called twice');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestLogger {
|
||||
log = jasmine.createSpy('log');
|
||||
error = jasmine.createSpy('error');
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/takeUntil';
|
||||
|
||||
import { Logger } from 'app/shared/logger.service';
|
||||
import { DOC_CONTENT_URL_PREFIX } from 'app/documents/document.service';
|
||||
|
||||
export interface ApiItem {
|
||||
name: string;
|
||||
title: string;
|
||||
path: string;
|
||||
docType: string;
|
||||
stability: string;
|
||||
securityRisk: boolean;
|
||||
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiSection {
|
||||
name: string;
|
||||
title: string;
|
||||
items: ApiItem[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiService implements OnDestroy {
|
||||
|
||||
private apiBase = DOC_CONTENT_URL_PREFIX + 'api/';
|
||||
private apiListJsonDefault = 'api-list.json';
|
||||
private firstTime = true;
|
||||
private onDestroy = new Subject();
|
||||
private sectionsSubject = new ReplaySubject<ApiSection[]>(1);
|
||||
private _sections = this.sectionsSubject.takeUntil(this.onDestroy);
|
||||
|
||||
/**
|
||||
* Return a cached observable of API sections from a JSON file.
|
||||
* API sections is an array of Angular top modules and metadata about their API documents (items).
|
||||
*/
|
||||
get sections() {
|
||||
|
||||
if (this.firstTime) {
|
||||
this.firstTime = false;
|
||||
this.fetchSections(); // TODO: get URL for fetchSections by configuration?
|
||||
|
||||
// makes sectionsSubject hot; subscribe ensures stays alive (always refCount > 0);
|
||||
this._sections.subscribe(sections => this.logger.log('ApiService got API sections') );
|
||||
}
|
||||
|
||||
return this._sections;
|
||||
};
|
||||
|
||||
constructor(private http: HttpClient, private logger: Logger) { }
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch API sections from a JSON file.
|
||||
* API sections is an array of Angular top modules and metadata about their API documents (items).
|
||||
* Updates `sections` observable
|
||||
*
|
||||
* @param {string} [src] - Name of the api list JSON file
|
||||
*/
|
||||
fetchSections(src?: string) {
|
||||
// TODO: get URL by configuration?
|
||||
const url = this.apiBase + (src || this.apiListJsonDefault);
|
||||
this.http.get<ApiSection[]>(url)
|
||||
.takeUntil(this.onDestroy)
|
||||
.do(() => this.logger.log(`Got API sections from ${url}`))
|
||||
.subscribe(
|
||||
sections => this.sectionsSubject.next(sections),
|
||||
(err: HttpErrorResponse) => {
|
||||
// Todo: handle error
|
||||
this.logger.error(err);
|
||||
throw err; // rethrow for now.
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user