Expanding nested schemas by absolute pointer

This commit is contained in:
Roman Hotsiy 2017-01-18 23:48:55 +02:00
parent 46f6b29547
commit e164590fca
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
19 changed files with 191 additions and 37 deletions

View File

@ -676,6 +676,13 @@ definitions:
description: Category name description: Category name
type: string type: string
minLength: 1 minLength: 1
sub:
description: test sub Category
type: object
properties:
prop1:
type: string
description: The best prop1
xml: xml:
name: Category name: Category
Dog: Dog:

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { Component, ElementRef, ViewContainerRef, OnDestroy, Input, import { Component, ElementRef, ViewContainerRef, OnDestroy, OnInit, Input,
AfterViewInit, ComponentFactoryResolver, Renderer } from '@angular/core'; AfterViewInit, ComponentFactoryResolver, Renderer } from '@angular/core';
import { JsonSchema } from './json-schema'; import { JsonSchema } from './json-schema';
@ -15,8 +15,9 @@ var cache = {};
template: '', template: '',
styles: [':host { display:none }'] styles: [':host { display:none }']
}) })
export class JsonSchemaLazy implements OnDestroy, AfterViewInit { export class JsonSchemaLazy implements OnDestroy, OnInit, AfterViewInit {
@Input() pointer: string; @Input() pointer: string;
@Input() absolutePointer: string;
@Input() auto: boolean; @Input() auto: boolean;
@Input() isRequestSchema: boolean; @Input() isRequestSchema: boolean;
@Input() final: boolean = false; @Input() final: boolean = false;
@ -78,6 +79,10 @@ export class JsonSchemaLazy implements OnDestroy, AfterViewInit {
Object.assign(instance, this); Object.assign(instance, this);
} }
ngOnInit() {
if (!this.absolutePointer) this.absolutePointer = this.pointer;
}
ngAfterViewInit() { ngAfterViewInit() {
if (!this.auto && !this.disableLazy) return; if (!this.auto && !this.disableLazy) return;
this.loadCached(); this.loadCached();

View File

@ -34,6 +34,7 @@
<div class="tuple-item"> <div class="tuple-item">
<span class="tuple-item-index"> [{{idx}}]: </span> <span class="tuple-item-index"> [{{idx}}]: </span>
<json-schema class="nested-schema" [pointer]="item._pointer" <json-schema class="nested-schema" [pointer]="item._pointer"
[absolutePointer]="item._pointer"
[nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema"> [nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema">
</json-schema> </json-schema>
</div> </div>
@ -92,9 +93,9 @@
</tr> </tr>
<tr class="param-schema" [ngClass]="{'last': last}" [hidden]="!prop._pointer"> <tr class="param-schema" [ngClass]="{'last': last}" [hidden]="!prop._pointer">
<td colspan="2"> <td colspan="2">
<zippy #subSchema title="Expand" [headless]="true" (open)="lazySchema.load()" [visible]="autoExpand"> <zippy [attr.disabled]="prop._name" #subSchema title="Expand" [headless]="true" (openChange)="lazySchema.load()" [(open)]="prop.expanded">
<json-schema-lazy #lazySchema [auto]="autoExpand" class="nested-schema" [pointer]="prop._pointer" <json-schema-lazy #lazySchema [auto]="prop.expanded" class="nested-schema" [pointer]="prop._pointer"
[nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema"> [nestOdd]="!nestOdd" [isRequestSchema]="isRequestSchema" absolutePointer="{{absolutePointer}}/properties/{{prop._name}}">
</json-schema-lazy> </json-schema-lazy>
</zippy> </zippy>
</td> </td>

View File

@ -1,9 +1,19 @@
'use strict'; '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 { BaseSearchableComponent, SpecManager } from '../base';
import { SchemaNormalizer, SchemaHelper } from '../../services/index'; import { SchemaNormalizer, SchemaHelper, AppStateService } from '../../services/';
import { JsonPointer } from '../../utils/';
import { Zippy } from '../../shared/components';
import { JsonSchemaLazy } from './json-schema-lazy';
@Component({ @Component({
selector: 'json-schema', selector: 'json-schema',
@ -11,8 +21,9 @@ import { SchemaNormalizer, SchemaHelper } from '../../services/index';
styleUrls: ['./json-schema.css'], styleUrls: ['./json-schema.css'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class JsonSchema extends BaseComponent implements OnInit { export class JsonSchema extends BaseSearchableComponent implements OnInit {
@Input() pointer: string; @Input() pointer: string;
@Input() absolutePointer: string;
@Input() final: boolean = false; @Input() final: boolean = false;
@Input() nestOdd: boolean; @Input() nestOdd: boolean;
@Input() childFor: string; @Input() childFor: string;
@ -25,11 +36,18 @@ export class JsonSchema extends BaseComponent implements OnInit {
properties: any; properties: any;
_isArray: boolean; _isArray: boolean;
normalizer: SchemaNormalizer; normalizer: SchemaNormalizer;
autoExpand = false;
descendants: any; descendants: any;
constructor(specMgr:SpecManager, private _renderer: Renderer, private _elementRef: ElementRef) { // @ViewChildren(Zippy) childZippies: QueryList<Zippy>;
super(specMgr); // @ViewChildren(forwardRef(() => JsonSchemaLazy)) childSchemas: QueryList<JsonSchemaLazy>;
constructor(
specMgr:SpecManager,
app: AppStateService,
private _renderer: Renderer,
private cdr: ChangeDetectorRef,
private _elementRef: ElementRef) {
super(specMgr, app);
this.normalizer = new SchemaNormalizer(specMgr); this.normalizer = new SchemaNormalizer(specMgr);
} }
@ -78,6 +96,8 @@ export class JsonSchema extends BaseComponent implements OnInit {
init() { init() {
if (!this.pointer) return; if (!this.pointer) return;
if (!this.absolutePointer) this.absolutePointer = this.pointer;
this.schema = this.componentSchema; this.schema = this.componentSchema;
if (!this.schema) { if (!this.schema) {
throw new Error(`Can't load component schema at ${this.pointer}`); 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 = this.normalizer.normalize(this.schema, this.normPointer, {resolved: true});
this.schema = SchemaHelper.unwrapArray(this.schema, this.normPointer); this.schema = SchemaHelper.unwrapArray(this.schema, this.normPointer);
this._isArray = this.schema._isArray; this._isArray = this.schema._isArray;
this.absolutePointer += (this._isArray ? '/items' : '');
this.initDescendants(); this.initDescendants();
this.preprocessSchema(); this.preprocessSchema();
} }
@ -114,7 +135,9 @@ export class JsonSchema extends BaseComponent implements OnInit {
return (propSchema && propSchema.type === 'object' && propSchema._pointer); 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() { applyStyling() {
@ -127,6 +150,22 @@ export class JsonSchema extends BaseComponent implements OnInit {
return item.name + (item._pointer || ''); 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() { ngOnInit() {
this.preinit(); this.preinit();
} }

View File

@ -15,7 +15,7 @@ import { BaseComponent } from '../base';
import * as detectScollParent from 'scrollparent'; import * as detectScollParent from 'scrollparent';
import { SpecManager } from '../../utils/spec-manager'; 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'; import { LazyTasksService } from '../../shared/components/LazyFor/lazy-for';
@Component({ @Component({

View File

@ -1,6 +1,6 @@
<h2 class="responses-list-header" *ngIf="responses.length"> Responses </h2> <h2 class="responses-list-header" *ngIf="responses.length"> Responses </h2>
<zippy *ngFor="let response of responses;trackBy:trackByCode" [title]="response.code + ' ' + response.description | marked" <zippy *ngFor="let response of responses;trackBy:trackByCode" [title]="response.code + ' ' + response.description | marked"
[type]="response.type" [visible]="response.expanded" [empty]="response.empty" (open)="lazySchema.load()"> [type]="response.type" [(open)]="response.expanded" [empty]="response.empty" (openChange)="lazySchema.load()">
<div *ngIf="response.headers" class="response-headers"> <div *ngIf="response.headers" class="response-headers">
<header> <header>
Headers Headers
@ -20,6 +20,7 @@
<header *ngIf="response.schema"> <header *ngIf="response.schema">
Response Schema Response Schema
</header> </header>
<json-schema-lazy [auto]="response.expanded" #lazySchema pointer="{{response.schema ? response.pointer + '/schema' : null}}"> <json-schema-lazy [auto]="response.expanded" #lazySchema
pointer="{{response.schema ? response.pointer + '/schema' : null}}">
</json-schema-lazy> </json-schema-lazy>
</zippy> </zippy>

View File

@ -1,10 +1,16 @@
'use strict'; 'use strict';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component,
import { BaseComponent, SpecManager } from '../base'; Input,
OnInit,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { BaseSearchableComponent, SpecManager } from '../base';
import JsonPointer from '../../utils/JsonPointer'; import JsonPointer from '../../utils/JsonPointer';
import { statusCodeType } from '../../utils/helpers'; import { statusCodeType } from '../../utils/helpers';
import { OptionsService } from '../../services/index'; import { OptionsService, AppStateService } from '../../services/index';
import { SchemaHelper } from '../../services/schema-helper.service'; import { SchemaHelper } from '../../services/schema-helper.service';
function isNumeric(n) { function isNumeric(n) {
@ -17,14 +23,18 @@ function isNumeric(n) {
styleUrls: ['./responses-list.css'], styleUrls: ['./responses-list.css'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ResponsesList extends BaseComponent implements OnInit { export class ResponsesList extends BaseSearchableComponent implements OnInit {
@Input() pointer:string; @Input() pointer:string;
responses: Array<any>; responses: Array<any>;
options: any; options: any;
constructor(specMgr:SpecManager, optionsMgr:OptionsService) { constructor(specMgr:SpecManager,
super(specMgr); optionsMgr:OptionsService,
app: AppStateService,
private cdr: ChangeDetectorRef
) {
super(specMgr, app);
this.options = optionsMgr.options; this.options = optionsMgr.options;
} }
@ -50,6 +60,7 @@ export class ResponsesList extends BaseComponent implements OnInit {
resp.code = respCode; resp.code = respCode;
resp.type = statusCodeType(resp.code); resp.type = statusCodeType(resp.code);
resp.expanded = false;
if (this.options.expandResponses) { if (this.options.expandResponses) {
if (this.options.expandResponses === 'all' || this.options.expandResponses.has(respCode.toString())) { if (this.options.expandResponses === 'all' || this.options.expandResponses.has(respCode.toString())) {
resp.expanded = true; resp.expanded = true;
@ -74,6 +85,17 @@ export class ResponsesList extends BaseComponent implements OnInit {
return el.code; 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() { ngOnInit() {
this.preinit(); this.preinit();
} }

View File

@ -1 +1,2 @@
<input #search (keyup)="update(search.value)"> <input #search (keyup)="update(search.value)">
<button (click)="tmpSearch()"> Expand Search </button>

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { Component, ChangeDetectionStrategy, OnInit, HostBinding } from '@angular/core'; import { Component, ChangeDetectionStrategy, OnInit, HostBinding } from '@angular/core';
import { Marker } from '../../services/'; import { Marker, SearchService } from '../../services/';
@Component({ @Component({
selector: 'redoc-search', selector: 'redoc-search',
@ -11,7 +11,7 @@ import { Marker } from '../../services/';
export class RedocSearch implements OnInit { export class RedocSearch implements OnInit {
logo:any = {}; logo:any = {};
constructor(private marker: Marker) { constructor(private marker: Marker, public search: SearchService) {
} }
init() { init() {
@ -22,6 +22,14 @@ export class RedocSearch implements OnInit {
this.marker.mark(val); 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() { ngOnInit() {
} }

View File

@ -1,7 +1,8 @@
'use strict'; 'use strict';
import { OnInit, OnDestroy } from '@angular/core'; import { OnInit, OnDestroy } from '@angular/core';
import { SpecManager } from '../utils/spec-manager'; import { SpecManager } from '../utils/spec-manager';
import { AppStateService } from '../services/app-state.service';
import { Subscription } from 'rxjs/Subscription';
export { SpecManager }; export { SpecManager };
@ -65,3 +66,35 @@ export class BaseComponent implements OnInit, OnDestroy {
// emtpy // 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
}
}

View File

@ -17,6 +17,7 @@ import {
ComponentParser, ComponentParser,
ContentProjector, ContentProjector,
Marker, Marker,
SearchService,
COMPONENT_PARSER_ALLOWED } from './services/'; COMPONENT_PARSER_ALLOWED } from './services/';
import { SpecManager } from './utils/spec-manager'; import { SpecManager } from './utils/spec-manager';
@ -35,6 +36,7 @@ import { SpecManager } from './utils/spec-manager';
AppStateService, AppStateService,
ComponentParser, ComponentParser,
ContentProjector, ContentProjector,
SearchService,
LazyTasksService, LazyTasksService,
Marker, Marker,
{ provide: APP_ID, useValue: 'redoc' }, { provide: APP_ID, useValue: 'redoc' },

View File

@ -11,6 +11,8 @@ export class AppStateService {
loading = new Subject<boolean>(); loading = new Subject<boolean>();
initialized = new BehaviorSubject<any>(false); initialized = new BehaviorSubject<any>(false);
searchContainingPointers = new BehaviorSubject<string[]>([]);
startLoading() { startLoading() {
this.loading.next(true); this.loading.next(true);
} }

View File

@ -8,6 +8,7 @@ export * from './hash.service';
export * from './schema-normalizer.service'; export * from './schema-normalizer.service';
export * from './schema-helper.service'; export * from './schema-helper.service';
export * from './warnings.service'; export * from './warnings.service';
export * from './search.service';
export * from './component-parser.service'; export * from './component-parser.service';
export * from './content-projector.service'; export * from './content-projector.service';

View File

@ -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);
}
}

View File

@ -1,4 +1,4 @@
<div class="zippy zippy-{{type}}" [ngClass]="{'zippy-empty': empty, 'zippy-hidden': !visible}"> <div class="zippy zippy-{{type}}" [ngClass]="{'zippy-empty': empty, 'zippy-hidden': !open}">
<div *ngIf='!headless' class="zippy-title" (click)="toggle()"> <div *ngIf='!headless' class="zippy-title" (click)="toggle()">
<span class="zippy-indicator"> <span class="zippy-indicator">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0 24 24" xml:space="preserve">

View File

@ -1,26 +1,29 @@
'use strict'; 'use strict';
import { Component, EventEmitter, Output, Input } from '@angular/core'; import { Component, EventEmitter, Output, Input, OnChanges } from '@angular/core';
@Component({ @Component({
selector: 'zippy', selector: 'zippy',
templateUrl: './zippy.html', templateUrl: './zippy.html',
styleUrls: ['./zippy.css'] styleUrls: ['./zippy.css']
}) })
export class Zippy { export class Zippy implements OnChanges {
@Input() type = 'general'; @Input() type = 'general';
@Input() visible = false;
@Input() empty = false; @Input() empty = false;
@Input() title; @Input() title;
@Input() headless: boolean = false; @Input() headless: boolean = false;
@Output() open = new EventEmitter(); @Input() open = false;
@Output() close = new EventEmitter(); @Output() openChange = new EventEmitter();
toggle() { toggle() {
this.visible = !this.visible; this.open = !this.open;
if (this.empty) return; if (this.empty) return;
if (this.visible) { this.openChange.emit(this.open);
this.open.next({}); }
} else {
this.close.next({}); ngOnChanges(ch) {
if (ch.open.currentValue === true) {
this.openChange.emit(ch.open.currentValue);
} }
} }
} }

View File

@ -35,6 +35,20 @@ export class JsonPointer {
return JsonPointerLib.compile(tokens.slice(0, tokens.length - level)); 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 * overridden JsonPointer original parse to take care of prefixing '#' symbol
* that is not valid JsonPointer * that is not valid JsonPointer

View File

@ -1,3 +1,5 @@
export * from './custom-error-handler'; export * from './custom-error-handler';
export * from './helpers'; export * from './helpers';
export * from './md-renderer'; export * from './md-renderer';
export { default as JsonPointer } from './JsonPointer';

View File

@ -20,7 +20,7 @@
"deploy": "node ./build/prepare_deploy.js && deploy-to-gh-pages --update demo", "deploy": "node ./build/prepare_deploy.js && deploy-to-gh-pages --update demo",
"ngc": "ngc -p .", "ngc": "ngc -p .",
"clean:dist": "npm run rimraf -- dist/", "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", "rimraf": "rimraf",
"webpack:prod": "webpack --config build/webpack.prod.js --profile --bail", "webpack:prod": "webpack --config build/webpack.prod.js --profile --bail",
"build:sass": "node-sass -q -o lib lib", "build:sass": "node-sass -q -o lib lib",