diff --git a/aio/.gitignore b/aio/.gitignore
index 0a503cdc1e..2f721086ca 100644
--- a/aio/.gitignore
+++ b/aio/.gitignore
@@ -8,5 +8,5 @@ yarn-error.log
# Ignore generated content
/dist
/tmp
-/src/content/docs
+/src/content
/.sass-cache
diff --git a/aio/src/content/navigation.json b/aio/content/navigation.json
similarity index 100%
rename from aio/src/content/navigation.json
rename to aio/content/navigation.json
diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html
index b65a14ec00..9f17aa2949 100644
--- a/aio/src/app/app.component.html
+++ b/aio/src/app/app.component.html
@@ -8,6 +8,7 @@
+
@@ -16,5 +17,4 @@
-
\ No newline at end of file
diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts
index 688d7a6cee..8e63ac7cf9 100644
--- a/aio/src/app/app.component.ts
+++ b/aio/src/app/app.component.ts
@@ -5,7 +5,7 @@ import { GaService } from 'app/shared/ga.service';
import { LocationService } from 'app/shared/location.service';
import { DocumentService, DocumentContents } from 'app/documents/document.service';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
-import { NavigationService, NavigationViews, NavigationNode } from 'app/navigation/navigation.service';
+import { NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
import { SearchService } from 'app/search/search.service';
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
import { AutoScrollService } from 'app/shared/auto-scroll.service';
@@ -24,6 +24,7 @@ export class AppComponent implements OnInit {
currentDocument: Observable;
navigationViews: Observable;
selectedNodes: Observable;
+ versionInfo: Observable;
@ViewChildren('searchBox, searchResults', { read: ElementRef })
searchElements: QueryList;
@@ -45,6 +46,7 @@ export class AppComponent implements OnInit {
locationService.currentUrl.subscribe(url => gaService.locationChanged(url));
this.navigationViews = navigationService.navigationViews;
this.selectedNodes = navigationService.selectedNodes;
+ this.versionInfo = navigationService.versionInfo;
}
ngOnInit() {
diff --git a/aio/src/app/navigation/navigation.service.spec.ts b/aio/src/app/navigation/navigation.service.spec.ts
index e2aae81f9f..104a4fe821 100644
--- a/aio/src/app/navigation/navigation.service.spec.ts
+++ b/aio/src/app/navigation/navigation.service.spec.ts
@@ -1,7 +1,7 @@
import { ReflectiveInjector } from '@angular/core';
import { Http, ConnectionBackend, RequestOptions, BaseRequestOptions, Response, ResponseOptions } from '@angular/http';
import { MockBackend } from '@angular/http/testing';
-import { NavigationService, NavigationViews, NavigationNode } from 'app/navigation/navigation.service';
+import { NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';
import { Logger } from 'app/shared/logger.service';
@@ -125,4 +125,23 @@ describe('NavigationService', () => {
expect(currentNodes).toEqual([]);
});
});
+
+ describe('versionInfo', () => {
+ let service: NavigationService, versionInfo: VersionInfo;
+
+ beforeEach(() => {
+ service = injector.get(NavigationService);
+ service.versionInfo.subscribe(info => versionInfo = info);
+
+ const backend = injector.get(ConnectionBackend);
+ backend.connectionsArray[0].mockRespond(createResponse({
+ ['__versionInfo']: { raw: '4.0.0' }
+ }));
+ });
+
+ it('should extract the version info', () => {
+ const backend = injector.get(ConnectionBackend);
+ expect(versionInfo).toEqual({ raw: '4.0.0' });
+ });
+ });
});
diff --git a/aio/src/app/navigation/navigation.service.ts b/aio/src/app/navigation/navigation.service.ts
index de5d4a500a..3b7db011af 100644
--- a/aio/src/app/navigation/navigation.service.ts
+++ b/aio/src/app/navigation/navigation.service.ts
@@ -12,28 +12,62 @@ import { LocationService } from 'app/shared/location.service';
import { NavigationNode } from './navigation-node';
export { NavigationNode } from './navigation-node';
+export type NavigationResponse = {'__versionInfo': VersionInfo } & { [name: string]: NavigationNode[] };
+
export interface NavigationViews {
[name: string]: NavigationNode[];
}
+export interface NavigationMap {
+ [url: string]: NavigationNode;
+}
+
+export interface VersionInfo {
+ raw: string;
+ major: number;
+ minor: number;
+ patch: number;
+ prerelease: string[];
+ build: string;
+ version: string;
+ codeName: string;
+ isSnapshot: boolean;
+ full: string;
+ branch: string;
+ commitSHA: string;
+}
+
const navigationPath = 'content/navigation.json';
+
@Injectable()
export class NavigationService {
/**
* An observable collection of NavigationNode trees, which can be used to render navigational menus
*/
- navigationViews = this.fetchNavigationViews();
+ navigationViews: Observable;
+
+ /**
+ * The current version of doc-app that we are running
+ */
+ versionInfo: Observable;
+
/**
* An observable array of nodes that indicate which nodes in the `navigationViews` match the current URL location
*/
- selectedNodes = this.getSelectedNodes();
+ selectedNodes: Observable;
- constructor(private http: Http, private location: LocationService, private logger: Logger) { }
+ constructor(private http: Http, private location: LocationService, private logger: Logger) {
+ const navigationInfo = this.fetchNavigationInfo();
+ // The version information is packaged inside the navigation response to save us an extra request.
+ this.versionInfo = this.getVersionInfo(navigationInfo);
+ this.navigationViews = this.getNavigationViews(navigationInfo);
+ this.selectedNodes = this.getSelectedNodes(this.navigationViews);
+ }
/**
- * Get an observable that fetches the `NavigationViews` from the server.
+ * Get an observable that fetches the `NavigationResponse` from the server.
* We create an observable by calling `http.get` but then publish it to share the result
* among multiple subscribers, without triggering new requests.
* We use `publishLast` because once the http request is complete the request observable completes.
@@ -43,10 +77,22 @@ export class NavigationService {
* another request to the server.
* We are not storing the subscription from connecting as we do not expect this service to be destroyed.
*/
- private fetchNavigationViews(): Observable {
- const navigationViews = this.http.get(navigationPath)
- .map(res => res.json() as NavigationViews)
+ private fetchNavigationInfo(): Observable {
+ const navigationInfo = this.http.get(navigationPath)
+ .map(res => res.json() as NavigationResponse)
.publishLast();
+ navigationInfo.connect();
+ return navigationInfo;
+ }
+
+ private getVersionInfo(navigationInfo: Observable) {
+ const versionInfo = navigationInfo.map(response => response['__versionInfo']).publishReplay(1);
+ versionInfo.connect();
+ return versionInfo;
+ }
+
+ private getNavigationViews(navigationInfo: Observable) {
+ const navigationViews = navigationInfo.map(response => unpluck(response, '__versionInfo')).publishReplay(1);
navigationViews.connect();
return navigationViews;
}
@@ -57,9 +103,9 @@ export class NavigationService {
* URL change before they receive an emission.
* See above for discussion of using `connect`.
*/
- private getSelectedNodes() {
+ private getSelectedNodes(navigationViews: Observable) {
const selectedNodes = combineLatest(
- this.navigationViews.map(this.computeUrlToNodesMap),
+ navigationViews.map(this.computeUrlToNodesMap),
this.location.currentUrl,
(navMap, url) => navMap[url] || [])
.publishReplay(1);
@@ -74,7 +120,7 @@ export class NavigationService {
* @param navigation A collection of navigation nodes that are to be mapped
*/
private computeUrlToNodesMap(navigation: NavigationViews) {
- const navMap = {};
+ const navMap: NavigationMap = {};
Object.keys(navigation).forEach(key => navigation[key].forEach(node => walkNodes(node)));
return navMap;
@@ -90,3 +136,9 @@ export class NavigationService {
}
}
}
+
+function unpluck(obj: any, property: string) {
+ const result = Object.assign({}, obj);
+ delete result[property];
+ return result;
+}
diff --git a/aio/src/styles/1-layouts/_sidenav.scss b/aio/src/styles/1-layouts/_sidenav.scss
index 7bdb1654d7..26637827b1 100644
--- a/aio/src/styles/1-layouts/_sidenav.scss
+++ b/aio/src/styles/1-layouts/_sidenav.scss
@@ -162,3 +162,7 @@
.level-1:not(.expanded) .material-icons, .level-2:not(.expanded) .material-icons {
@include rotate(0deg);
}
+
+.version-info {
+ border: 3px $blue solid;
+}
\ No newline at end of file
diff --git a/aio/transforms/angular.io-package/index.js b/aio/transforms/angular.io-package/index.js
index da97986215..757ebbfe36 100644
--- a/aio/transforms/angular.io-package/index.js
+++ b/aio/transforms/angular.io-package/index.js
@@ -50,10 +50,13 @@ module.exports =
.processor(require('./processors/filterPrivateDocs'))
.processor(require('./processors/filterIgnoredDocs'))
.processor(require('./processors/fixInternalDocumentLinks'))
+ .processor(require('./processors/processNavigationMap'))
// overrides base packageInfo and returns the one for the 'angular/angular' repo.
.factory('packageInfo', function() { return require(path.resolve(PROJECT_ROOT, 'package.json')); })
+ .factory(require('./readers/navigation'))
+
.config(function(checkAnchorLinksProcessor, log) {
// TODO: re-enable
checkAnchorLinksProcessor.$enabled = false;
@@ -61,12 +64,13 @@ module.exports =
// Where do we get the source files?
.config(function(
- readTypeScriptModules, readFilesProcessor, collectExamples, generateKeywordsProcessor) {
+ readTypeScriptModules, readFilesProcessor, collectExamples, generateKeywordsProcessor, navigationFileReader) {
// API files are typescript
readTypeScriptModules.basePath = API_SOURCE_PATH;
readTypeScriptModules.ignoreExportsMatching = [/^_/];
readTypeScriptModules.hidePrivateMembers = true;
+ readFilesProcessor.fileReaders.push(navigationFileReader)
readTypeScriptModules.sourceFiles = [
'common/index.ts',
'common/testing/index.ts',
@@ -117,6 +121,11 @@ module.exports =
include: CONTENTS_PATH + '/examples/**/*',
fileReader: 'exampleFileReader'
},
+ {
+ basePath: CONTENTS_PATH,
+ include: CONTENTS_PATH + '/navigation.json',
+ fileReader: 'navigationFileReader'
+ },
];
collectExamples.exampleFolders = ['examples', 'examples'];
@@ -242,7 +251,8 @@ module.exports =
outputPathTemplate: '${path}'
},
{docTypes: ['example-region'], getOutputPath: function() {}},
- {docTypes: ['content'], pathTemplate: '${id}', outputPathTemplate: '${path}.json'}
+ {docTypes: ['content'], pathTemplate: '${id}', outputPathTemplate: '${path}.json'},
+ {docTypes: ['navigation-map'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'}
];
})
diff --git a/aio/transforms/angular.io-package/processors/processNavigationMap.js b/aio/transforms/angular.io-package/processors/processNavigationMap.js
new file mode 100644
index 0000000000..e625f9d683
--- /dev/null
+++ b/aio/transforms/angular.io-package/processors/processNavigationMap.js
@@ -0,0 +1,50 @@
+module.exports = function processNavigationMap(versionInfo, log) {
+ return {
+ $runAfter: ['paths-computed'],
+ $runBefore: ['rendering-docs'],
+ $process: function(docs) {
+
+ const navigationDoc = docs.find(doc => doc.docType === 'navigation-map');
+
+ if (!navigationDoc) {
+ throw new Error(
+ 'Missing navigation map document (docType="navigation-map").' +
+ 'Did you forget to add it to the readFileProcessor?');
+ }
+
+ // Verify that all the navigation paths are to valid docs
+ const pathMap = {};
+ docs.forEach(doc => pathMap[doc.path] = true);
+ const errors = walk(navigationDoc.data, pathMap, []);
+
+ if (errors.length) {
+ log.error(`Navigation doc: ${navigationDoc.fileInfo.relativePath} contains invalid urls`);
+ console.log(errors);
+ // TODO(petebd): fail if there are errors: throw new Error('processNavigationMap failed');
+ }
+
+ // Add in the version data in a "secret" field to be extracted in the docs app
+ navigationDoc.data['__versionInfo'] = versionInfo.currentVersion;
+ }
+ }
+};
+
+function walk(node, map, path) {
+ let errors = [];
+ for(const key in node) {
+ const child = node[key];
+ if (key === 'url') {
+ const url = child.replace(/#.*$/, ''); // strip hash
+ if (isRelative(url) && !map[url]) {
+ errors.push({ path: path.join('.'), url });
+ }
+ } else if (typeof child !== 'string') {
+ errors = errors.concat(walk(child, map, path.concat([key])));
+ }
+ }
+ return errors;
+}
+
+function isRelative(url) {
+ return !/^(https?:)?\/\//.test(url);
+}
\ No newline at end of file
diff --git a/aio/transforms/angular.io-package/readers/navigation.js b/aio/transforms/angular.io-package/readers/navigation.js
new file mode 100644
index 0000000000..43a82f19bb
--- /dev/null
+++ b/aio/transforms/angular.io-package/readers/navigation.js
@@ -0,0 +1,19 @@
+/**
+ * Read in the navigation JSON
+ */
+module.exports = function navigationFileReader() {
+ return {
+ name: 'navigationFileReader',
+ getDocs: function(fileInfo) {
+
+ // We return a single element array because content files only contain one document
+ return [{
+ docType: 'navigation-map',
+ data: JSON.parse(fileInfo.content),
+ template: 'json-doc.template.json',
+ id: 'navigation',
+ aliases: ['navigation', 'navigation.json']
+ }];
+ }
+ };
+};
\ No newline at end of file