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
type: string
minLength: 1
sub:
description: test sub Category
type: object
properties:
prop1:
type: string
description: The best prop1
xml:
name: Category
Dog:

View File

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

View File

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

View File

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

View File

@ -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({

View File

@ -1,6 +1,6 @@
<h2 class="responses-list-header" *ngIf="responses.length"> Responses </h2>
<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">
<header>
Headers
@ -20,6 +20,7 @@
<header *ngIf="response.schema">
Response Schema
</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>
</zippy>

View File

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

View File

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

View File

@ -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() {
}

View File

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

View File

@ -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' },

View File

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

View File

@ -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';

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()">
<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">

View File

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

View File

@ -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

View File

@ -1,3 +1,5 @@
export * from './custom-error-handler';
export * from './helpers';
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",
"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",