From ba71dda1011b3b9308f93f5c75a9b9b83250e0ab Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Wed, 18 Jan 2017 23:48:55 +0200 Subject: [PATCH] Expanding nested schemas by absolute pointer --- demo/swagger.yaml | 7 +++ lib/components/JsonSchema/json-schema-lazy.ts | 9 ++- lib/components/JsonSchema/json-schema.html | 7 ++- lib/components/JsonSchema/json-schema.ts | 55 ++++++++++++++++--- lib/components/Redoc/redoc.ts | 2 +- .../ResponsesList/responses-list.html | 5 +- .../ResponsesList/responses-list.ts | 34 ++++++++++-- lib/components/Search/redoc-search.html | 1 + lib/components/Search/redoc-search.ts | 12 +++- lib/components/base.ts | 35 +++++++++++- lib/redoc.module.ts | 2 + lib/services/app-state.service.ts | 2 + lib/services/index.ts | 1 + lib/services/search.service.ts | 13 +++++ lib/shared/components/Zippy/zippy.html | 2 +- lib/shared/components/Zippy/zippy.ts | 23 ++++---- lib/utils/JsonPointer.ts | 14 +++++ lib/utils/index.ts | 2 + package.json | 2 +- 19 files changed, 191 insertions(+), 37 deletions(-) create mode 100644 lib/services/search.service.ts diff --git a/demo/swagger.yaml b/demo/swagger.yaml index 126d919c..d00d87a2 100644 --- a/demo/swagger.yaml +++ b/demo/swagger.yaml @@ -676,6 +676,13 @@ definitions: description: Category name type: string minLength: 1 + sub: + description: test sub Category + type: object + properties: + prop1: + type: string + description: The best prop1 xml: name: Category Dog: diff --git a/lib/components/JsonSchema/json-schema-lazy.ts b/lib/components/JsonSchema/json-schema-lazy.ts index a50aa65b..177d0fdc 100644 --- a/lib/components/JsonSchema/json-schema-lazy.ts +++ b/lib/components/JsonSchema/json-schema-lazy.ts @@ -1,6 +1,6 @@ 'use strict'; -import { Component, ElementRef, ViewContainerRef, OnDestroy, Input, +import { Component, ElementRef, ViewContainerRef, OnDestroy, OnInit, Input, AfterViewInit, ComponentFactoryResolver, Renderer } from '@angular/core'; import { JsonSchema } from './json-schema'; @@ -15,8 +15,9 @@ var cache = {}; template: '', styles: [':host { display:none }'] }) -export class JsonSchemaLazy implements OnDestroy, AfterViewInit { +export class JsonSchemaLazy implements OnDestroy, OnInit, AfterViewInit { @Input() pointer: string; + @Input() absolutePointer: string; @Input() auto: boolean; @Input() isRequestSchema: boolean; @Input() final: boolean = false; @@ -78,6 +79,10 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit { Object.assign(instance, this); } + ngOnInit() { + if (!this.absolutePointer) this.absolutePointer = this.pointer; + } + ngAfterViewInit() { if (!this.auto && !this.disableLazy) return; this.loadCached(); diff --git a/lib/components/JsonSchema/json-schema.html b/lib/components/JsonSchema/json-schema.html index 028077a4..dd6441a4 100644 --- a/lib/components/JsonSchema/json-schema.html +++ b/lib/components/JsonSchema/json-schema.html @@ -34,6 +34,7 @@
[{{idx}}]:
@@ -92,9 +93,9 @@ - - + + diff --git a/lib/components/JsonSchema/json-schema.ts b/lib/components/JsonSchema/json-schema.ts index 323cf8af..9e2058a4 100644 --- a/lib/components/JsonSchema/json-schema.ts +++ b/lib/components/JsonSchema/json-schema.ts @@ -1,9 +1,19 @@ 'use strict'; -import { Component, Input, Renderer, ElementRef, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { Component, + Input, + Renderer, + ElementRef, + OnInit, + ChangeDetectionStrategy, + ChangeDetectorRef +} from '@angular/core'; -import { BaseComponent, SpecManager } from '../base'; -import { SchemaNormalizer, SchemaHelper } from '../../services/index'; +import { BaseSearchableComponent, SpecManager } from '../base'; +import { SchemaNormalizer, SchemaHelper, AppStateService } from '../../services/'; +import { JsonPointer } from '../../utils/'; +import { Zippy } from '../../shared/components'; +import { JsonSchemaLazy } from './json-schema-lazy'; @Component({ selector: 'json-schema', @@ -11,8 +21,9 @@ import { SchemaNormalizer, SchemaHelper } from '../../services/index'; styleUrls: ['./json-schema.css'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class JsonSchema extends BaseComponent implements OnInit { +export class JsonSchema extends BaseSearchableComponent implements OnInit { @Input() pointer: string; + @Input() absolutePointer: string; @Input() final: boolean = false; @Input() nestOdd: boolean; @Input() childFor: string; @@ -25,11 +36,18 @@ export class JsonSchema extends BaseComponent implements OnInit { properties: any; _isArray: boolean; normalizer: SchemaNormalizer; - autoExpand = false; descendants: any; - constructor(specMgr:SpecManager, private _renderer: Renderer, private _elementRef: ElementRef) { - super(specMgr); + // @ViewChildren(Zippy) childZippies: QueryList; + // @ViewChildren(forwardRef(() => JsonSchemaLazy)) childSchemas: QueryList; + + constructor( + specMgr:SpecManager, + app: AppStateService, + private _renderer: Renderer, + private cdr: ChangeDetectorRef, + private _elementRef: ElementRef) { + super(specMgr, app); this.normalizer = new SchemaNormalizer(specMgr); } @@ -78,6 +96,8 @@ export class JsonSchema extends BaseComponent implements OnInit { init() { if (!this.pointer) return; + if (!this.absolutePointer) this.absolutePointer = this.pointer; + this.schema = this.componentSchema; if (!this.schema) { throw new Error(`Can't load component schema at ${this.pointer}`); @@ -88,6 +108,7 @@ export class JsonSchema extends BaseComponent implements OnInit { this.schema = this.normalizer.normalize(this.schema, this.normPointer, {resolved: true}); this.schema = SchemaHelper.unwrapArray(this.schema, this.normPointer); this._isArray = this.schema._isArray; + this.absolutePointer += (this._isArray ? '/items' : ''); this.initDescendants(); this.preprocessSchema(); } @@ -114,7 +135,9 @@ export class JsonSchema extends BaseComponent implements OnInit { return (propSchema && propSchema.type === 'object' && propSchema._pointer); }); - this.autoExpand = this.properties && this.properties.length === 1; + if (this.properties.length === 1) { + this.properties[0].expanded = true; + } } applyStyling() { @@ -127,6 +150,22 @@ export class JsonSchema extends BaseComponent implements OnInit { return item.name + (item._pointer || ''); } + ensureSearchIsShown(ptr: string) { + if (ptr.startsWith(this.absolutePointer)) { + let props = this.properties; + let relative = JsonPointer.relative(this.absolutePointer, ptr); + let propName; + if (relative.length > 1 && relative[0] === 'properties') { + propName = relative[1]; + } + let prop = props.find(p => p._name === propName); + if (!prop) return; + prop.expanded = true; + this.cdr.markForCheck(); + this.cdr.detectChanges(); + } + } + ngOnInit() { this.preinit(); } diff --git a/lib/components/Redoc/redoc.ts b/lib/components/Redoc/redoc.ts index 7bbefece..6e89080f 100644 --- a/lib/components/Redoc/redoc.ts +++ b/lib/components/Redoc/redoc.ts @@ -15,7 +15,7 @@ import { BaseComponent } from '../base'; import * as detectScollParent from 'scrollparent'; import { SpecManager } from '../../utils/spec-manager'; -import { OptionsService, Hash, AppStateService, SchemaHelper } from '../../services/index'; +import { SearchService, OptionsService, Hash, AppStateService, SchemaHelper } from '../../services/'; import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for'; @Component({ diff --git a/lib/components/ResponsesList/responses-list.html b/lib/components/ResponsesList/responses-list.html index 8bbd42cc..d046d43e 100644 --- a/lib/components/ResponsesList/responses-list.html +++ b/lib/components/ResponsesList/responses-list.html @@ -1,6 +1,6 @@

Responses

+ [type]="response.type" [(open)]="response.expanded" [empty]="response.empty" (openChange)="lazySchema.load()">
Headers @@ -20,6 +20,7 @@
Response Schema
- + diff --git a/lib/components/ResponsesList/responses-list.ts b/lib/components/ResponsesList/responses-list.ts index 83fe2e0e..78d4966e 100644 --- a/lib/components/ResponsesList/responses-list.ts +++ b/lib/components/ResponsesList/responses-list.ts @@ -1,10 +1,16 @@ 'use strict'; -import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { BaseComponent, SpecManager } from '../base'; +import { Component, + Input, + OnInit, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef + } from '@angular/core'; +import { BaseSearchableComponent, SpecManager } from '../base'; import JsonPointer from '../../utils/JsonPointer'; import { statusCodeType } from '../../utils/helpers'; -import { OptionsService } from '../../services/index'; +import { OptionsService, AppStateService } from '../../services/index'; import { SchemaHelper } from '../../services/schema-helper.service'; function isNumeric(n) { @@ -17,14 +23,18 @@ function isNumeric(n) { styleUrls: ['./responses-list.css'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ResponsesList extends BaseComponent implements OnInit { +export class ResponsesList extends BaseSearchableComponent implements OnInit { @Input() pointer:string; responses: Array; options: any; - constructor(specMgr:SpecManager, optionsMgr:OptionsService) { - super(specMgr); + constructor(specMgr:SpecManager, + optionsMgr:OptionsService, + app: AppStateService, + private cdr: ChangeDetectorRef + ) { + super(specMgr, app); this.options = optionsMgr.options; } @@ -50,6 +60,7 @@ export class ResponsesList extends BaseComponent implements OnInit { resp.code = respCode; resp.type = statusCodeType(resp.code); + resp.expanded = false; if (this.options.expandResponses) { if (this.options.expandResponses === 'all' || this.options.expandResponses.has(respCode.toString())) { resp.expanded = true; @@ -74,6 +85,17 @@ export class ResponsesList extends BaseComponent implements OnInit { return el.code; } + ensureSearchIsShown(ptr: string) { + if (ptr.startsWith(this.pointer)) { + let code = JsonPointer.relative(this.pointer, ptr)[0]; + if (code && this.componentSchema[code]) { + this.componentSchema[code].expanded = true; + this.cdr.markForCheck(); + this.cdr.detectChanges(); + } + } + } + ngOnInit() { this.preinit(); } diff --git a/lib/components/Search/redoc-search.html b/lib/components/Search/redoc-search.html index c74bdbf3..598afffc 100644 --- a/lib/components/Search/redoc-search.html +++ b/lib/components/Search/redoc-search.html @@ -1 +1,2 @@ + diff --git a/lib/components/Search/redoc-search.ts b/lib/components/Search/redoc-search.ts index 18f21b28..00db55f8 100644 --- a/lib/components/Search/redoc-search.ts +++ b/lib/components/Search/redoc-search.ts @@ -1,6 +1,6 @@ 'use strict'; import { Component, ChangeDetectionStrategy, OnInit, HostBinding } from '@angular/core'; -import { Marker } from '../../services/'; +import { Marker, SearchService } from '../../services/'; @Component({ selector: 'redoc-search', @@ -11,7 +11,7 @@ import { Marker } from '../../services/'; export class RedocSearch implements OnInit { logo:any = {}; - constructor(private marker: Marker) { + constructor(private marker: Marker, public search: SearchService) { } init() { @@ -22,6 +22,14 @@ export class RedocSearch implements OnInit { this.marker.mark(val); } + tmpSearch() { + this.search.ensureSearchVisible([ + '/paths/~1pet~1findByStatus/get/responses/200/schema/items/properties/category/properties/sub', + '/paths/~1pet~1findByStatus/get/responses/200/schema/items/properties/tags', + '/paths/~1pet/post/parameters/0/schema/properties/tags' + ]); + } + ngOnInit() { } diff --git a/lib/components/base.ts b/lib/components/base.ts index 9325e2b3..de8417ac 100644 --- a/lib/components/base.ts +++ b/lib/components/base.ts @@ -1,7 +1,8 @@ 'use strict'; import { OnInit, OnDestroy } from '@angular/core'; import { SpecManager } from '../utils/spec-manager'; - +import { AppStateService } from '../services/app-state.service'; +import { Subscription } from 'rxjs/Subscription'; export { SpecManager }; @@ -65,3 +66,35 @@ export class BaseComponent implements OnInit, OnDestroy { // emtpy } } + +export class BaseSearchableComponent extends BaseComponent { + searchSubscription: Subscription; + constructor(public specMgr: SpecManager, public app: AppStateService) { + super(specMgr); + } + + subscribeForSearch() { + this.searchSubscription = this.app.searchContainingPointers.subscribe(ptrs => { + for (let i = 0; i < ptrs.length; ++i) { + this.ensureSearchIsShown(ptrs[i]); + } + }); + } + + preinit() { + super.preinit(); + this.subscribeForSearch(); + } + + ngOnDestroy() { + this.searchSubscription.unsubscribe(); + } + + /** + + Used to destroy component + * @abstract + */ + ensureSearchIsShown(ptr: string) { + // empy + } +} diff --git a/lib/redoc.module.ts b/lib/redoc.module.ts index 4ba10894..942656b2 100644 --- a/lib/redoc.module.ts +++ b/lib/redoc.module.ts @@ -17,6 +17,7 @@ import { ComponentParser, ContentProjector, Marker, + SearchService, COMPONENT_PARSER_ALLOWED } from './services/'; import { SpecManager } from './utils/spec-manager'; @@ -35,6 +36,7 @@ import { SpecManager } from './utils/spec-manager'; AppStateService, ComponentParser, ContentProjector, + SearchService, LazyTasksService, Marker, { provide: APP_ID, useValue: 'redoc' }, diff --git a/lib/services/app-state.service.ts b/lib/services/app-state.service.ts index 8bd8f143..9daad6b1 100644 --- a/lib/services/app-state.service.ts +++ b/lib/services/app-state.service.ts @@ -11,6 +11,8 @@ export class AppStateService { loading = new Subject(); initialized = new BehaviorSubject(false); + searchContainingPointers = new BehaviorSubject([]); + startLoading() { this.loading.next(true); } diff --git a/lib/services/index.ts b/lib/services/index.ts index 7200d85a..7e26d9eb 100644 --- a/lib/services/index.ts +++ b/lib/services/index.ts @@ -8,6 +8,7 @@ export * from './hash.service'; export * from './schema-normalizer.service'; export * from './schema-helper.service'; export * from './warnings.service'; +export * from './search.service'; export * from './component-parser.service'; export * from './content-projector.service'; diff --git a/lib/services/search.service.ts b/lib/services/search.service.ts new file mode 100644 index 00000000..15813a89 --- /dev/null +++ b/lib/services/search.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { AppStateService } from './app-state.service'; + + +@Injectable() +export class SearchService { + constructor(private app: AppStateService) { + window['locator'] = this; + } + ensureSearchVisible(containingPointers: string[]) { + this.app.searchContainingPointers.next(containingPointers); + } +} diff --git a/lib/shared/components/Zippy/zippy.html b/lib/shared/components/Zippy/zippy.html index f6c92aec..80ada597 100644 --- a/lib/shared/components/Zippy/zippy.html +++ b/lib/shared/components/Zippy/zippy.html @@ -1,4 +1,4 @@ -
+
diff --git a/lib/shared/components/Zippy/zippy.ts b/lib/shared/components/Zippy/zippy.ts index 2819aa39..a2d851f2 100644 --- a/lib/shared/components/Zippy/zippy.ts +++ b/lib/shared/components/Zippy/zippy.ts @@ -1,26 +1,29 @@ 'use strict'; -import { Component, EventEmitter, Output, Input } from '@angular/core'; +import { Component, EventEmitter, Output, Input, OnChanges } from '@angular/core'; @Component({ selector: 'zippy', templateUrl: './zippy.html', styleUrls: ['./zippy.css'] }) -export class Zippy { +export class Zippy implements OnChanges { @Input() type = 'general'; - @Input() visible = false; @Input() empty = false; @Input() title; @Input() headless: boolean = false; - @Output() open = new EventEmitter(); - @Output() close = new EventEmitter(); + @Input() open = false; + @Output() openChange = new EventEmitter(); + + toggle() { - this.visible = !this.visible; + this.open = !this.open; if (this.empty) return; - if (this.visible) { - this.open.next({}); - } else { - this.close.next({}); + this.openChange.emit(this.open); + } + + ngOnChanges(ch) { + if (ch.open.currentValue === true) { + this.openChange.emit(ch.open.currentValue); } } } diff --git a/lib/utils/JsonPointer.ts b/lib/utils/JsonPointer.ts index 220f2983..80009c08 100644 --- a/lib/utils/JsonPointer.ts +++ b/lib/utils/JsonPointer.ts @@ -35,6 +35,20 @@ export class JsonPointer { return JsonPointerLib.compile(tokens.slice(0, tokens.length - level)); } + /** + * returns relative path tokens + * @example + * // returns ['subpath'] + * JsonPointerHelper.relative('/path/0', '/path/0/subpath') + * // returns ['foo', 'subpath'] + * JsonPointerHelper.relative('/path', '/path/foo/subpath') + */ + static relative(from, to):string[] { + let fromTokens = JsonPointer.parse(from); + let toTokens = JsonPointer.parse(to); + return toTokens.slice(fromTokens.length); + } + /** * overridden JsonPointer original parse to take care of prefixing '#' symbol * that is not valid JsonPointer diff --git a/lib/utils/index.ts b/lib/utils/index.ts index cd933708..79d57372 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1,3 +1,5 @@ export * from './custom-error-handler'; export * from './helpers'; export * from './md-renderer'; + +export { default as JsonPointer } from './JsonPointer'; diff --git a/package.json b/package.json index 33179891..dd0c9b3d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "deploy": "node ./build/prepare_deploy.js && deploy-to-gh-pages --update demo", "ngc": "ngc -p .", "clean:dist": "npm run rimraf -- dist/", - "clean:aot": "npm run rimraf -- .tmp lib/**/*.css", + "clean:aot": "npm run rimraf -- .tmp compiled lib/**/*.css", "rimraf": "rimraf", "webpack:prod": "webpack --config build/webpack.prod.js --profile --bail", "build:sass": "node-sass -q -o lib lib",