feat(devtools): implement input/output/state preview functionality with the reworked property explorer

This commit is contained in:
AleksanderBodurri 2020-03-20 13:24:22 -04:00 committed by Minko Gechev
parent 39b65dd05a
commit 8ea84d5776
15 changed files with 289 additions and 48 deletions

View file

@ -86,7 +86,7 @@ describe('Search items in component tree', () => {
checkComponentName('app-todos');
// should display correct title for properties panel
cy.get('header').should('have.text', ' Properties of app-todos ');
cy.get('header span').should('have.text', ' Properties of app-todos ');
// should show correct component properties
cy.get('ng-property-view').find('mat-tree-node');

View file

@ -46,7 +46,6 @@ const sameDirectives = (a: IndexedNode, b: IndexedNode) => {
],
})
export class DirectiveExplorerComponent implements OnInit {
directivesData: DirectivesProperties | null = null;
currentSelectedElement: IndexedNode | null = null;
forest: DevToolsNode[];
highlightIDinTreeFromElement: ElementPosition | null = null;
@ -189,7 +188,7 @@ export class DirectiveExplorerComponent implements OnInit {
let data = {};
const controller = this._propResolver.getDirectiveController(directive);
if (controller) {
data = controller.getDirectiveProperties();
data = controller.directiveProperties;
}
if (!e.clipboardData) {
return;

View file

@ -6,6 +6,12 @@ import { getExpandedDirectiveProperties } from './property-expanded-directive-pr
import { Observable } from 'rxjs';
import { Property, FlatNode } from './element-property-resolver';
export enum PropertyViewFilterOptions {
INPUTS,
OUTPUTS,
STATE,
}
const expandable = (prop: Descriptor) => {
if (!prop) {
return false;
@ -60,10 +66,18 @@ export class DirectivePropertyResolver {
};
}
getDirectiveProperties(): { [name: string]: Descriptor } {
get directiveProperties(): { [name: string]: Descriptor } {
return this._props.props;
}
get directiveInputs(): string[] {
return Object.keys(this._props.inputs || {});
}
get directiveOutputs(): string[] {
return Object.keys(this._props.outputs || {});
}
getExpandedProperties(): NestedProp[] {
return getExpandedDirectiveProperties(this._dataSource.data);
}
@ -91,7 +105,10 @@ export class DirectivePropertyResolver {
private _getChildren(prop: Property): Property[] | undefined {
const descriptor = prop.descriptor;
if (descriptor.type === PropType.Object && !(descriptor.value instanceof Observable)) {
if (
(descriptor.type === PropType.Object || descriptor.type === PropType.Array) &&
!(descriptor.value instanceof Observable)
) {
return Object.keys(descriptor.value || {}).map(name => {
return {
name,
@ -99,14 +116,6 @@ export class DirectivePropertyResolver {
parent: prop,
};
});
} else if (descriptor.type === PropType.Array && !(descriptor.value instanceof Observable)) {
return (descriptor.value || []).map((el: Descriptor, idx: number) => {
return {
name: idx.toString(),
descriptor: el,
parent: prop,
};
});
} else {
console.error('Unexpected data type', descriptor, 'in property', prop);
}

View file

@ -7,6 +7,20 @@ import { map } from 'rxjs/operators';
import { DefaultIterableDiffer } from '@angular/core';
import { diff } from '../../diffing';
import { FlatNode, Property } from './element-property-resolver';
import { PropertyViewFilterOptions } from './directive-property-resolver';
const expandable = (prop: Descriptor, messageBus?: MessageBus<Events>) => {
if (!prop) {
return false;
}
if (!prop.value && !messageBus) {
return false;
}
if (!prop.expandable) {
return false;
}
return !(prop.type !== PropType.Object && prop.type !== PropType.Array);
};
const trackBy = (_: number, item: FlatNode) => {
return `#${item.prop.name}#${item.level}`;
@ -18,6 +32,8 @@ export class PropertyDataSource extends DataSource<FlatNode> {
private _expandedData = new BehaviorSubject<FlatNode[]>([]);
private _differ = new DefaultIterableDiffer(trackBy);
private readonly _originalData: FlatNode[];
constructor(
props: { [prop: string]: Descriptor },
private _treeFlattener: MatTreeFlattener<Property, FlatNode>,
@ -28,7 +44,8 @@ export class PropertyDataSource extends DataSource<FlatNode> {
private _onReceivedNestedProperties: () => void
) {
super();
this._data.next(this._treeFlattener.flattenNodes(this._arrayify(props)));
this._originalData = this._treeFlattener.flattenNodes(this._arrayify(props));
this._data.next(this._originalData);
}
get data(): FlatNode[] {
@ -110,25 +127,23 @@ export class PropertyDataSource extends DataSource<FlatNode> {
});
}
private _getChildren(prop: Property): Property[] {
const descriptor = prop.descriptor;
if (descriptor.type === PropType.Object && !(descriptor.value instanceof Observable)) {
return Object.keys(descriptor.value || {}).map(name => {
return {
name,
descriptor: descriptor.value ? descriptor.value[name] : null,
parent: prop,
};
});
} else if (descriptor.type === PropType.Array && !(descriptor.value instanceof Observable)) {
return (descriptor.value || []).map((el: Descriptor, idx: number) => {
return {
name: idx.toString(),
descriptor: el,
parent: prop,
};
});
filterDataSource(filter: string[] | null): void {
if (filter === null) {
return;
}
throw new Error('Unexpected data type');
let pushFlag = false;
const filteredData: FlatNode[] = [];
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < this._originalData.length; i++) {
const node = this._originalData[i];
if (node.level === 0) {
pushFlag = filter.includes(node.prop.name);
}
if (pushFlag) {
filteredData.push(node);
}
}
this._data.next(filteredData);
}
}

View file

@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { FlatTreeControl } from '@angular/cdk/tree';
import { PropertyDataSource } from '../../../../property-resolver/property-data-source';
import { FlatNode } from '../../../../property-resolver/element-property-resolver';
@ -9,13 +9,22 @@ import { DirectivePropertyResolver } from '../../../../property-resolver/directi
templateUrl: './property-view-body.component.html',
styleUrls: ['./property-view-body.component.css'],
})
export class PropertyViewBodyComponent {
export class PropertyViewBodyComponent implements OnChanges {
@Input() dataSource: PropertyDataSource;
@Input() treeControl: FlatTreeControl<FlatNode>;
@Input() controller: DirectivePropertyResolver;
@Input() filterList: string[] | null = null;
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
ngOnChanges(): void {
this.filterTreeNodes();
}
filterTreeNodes(): void {
this.dataSource.filterDataSource(this.filterList);
}
toggle(node: FlatNode): void {
if (this.treeControl.isExpanded(node)) {
this.treeControl.collapse(node);

View file

@ -0,0 +1,50 @@
:host {
width: 325px;
display: block;
overflow: auto;
height: calc(100% - 24px);
}
:host mat-tree {
display: table;
}
.name {
margin-left: -9px;
}
.non-expandable {
margin-left: 13px;
}
.property-list {
margin: 5px;
margin-left: 15px;
}
.property-list mat-tree-node .name {
color: #b82519;
}
.property-list mat-tree-node.disabled .name,
.disabled {
color: #333;
}
.arrow {
font-family: monospace;
font-size: 7px;
color: #6e6e6e;
}
:host /deep/ .mat-tree-node .mat-icon {
font-size: 12px;
width: 16px;
height: 16px;
}
:host /deep/ .mat-tree-node {
min-height: 20px !important;
cursor: default;
font-family: Menlo, monospace;
border-radius: 5px;
}
:host /deep/ .mat-tree-node,
.mat-nested-tree-node {
font-size: 11px;
}

View file

@ -0,0 +1,32 @@
<mat-tree *ngIf="_treeControl" class="property-list" [dataSource]="_dataSource" [treeControl]="_treeControl">
<mat-tree-node matTreeNodePaddingIndent="14" *matTreeNodeDef="let node" matTreeNodePadding>
<span class="name non-expandable"> {{ node.prop.name }} </span>:&nbsp;
<ng-container *ngIf="!node.prop.descriptor.editable; else editable">
<span class="value">
{{ node.prop.descriptor.preview }}
</span>
</ng-container>
<ng-template #editable>
<ng-property-editor
[key]="node.prop.name"
[initialValue]="node.prop.descriptor.value || node.prop.descriptor.preview"
(updateValue)="handleUpdate(node, $event)"
>
</ng-property-editor>
</ng-template>
</mat-tree-node>
<mat-tree-node matTreeNodePaddingIndent="14" *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
LOL
<div (click)="toggle(node)">
<span class="arrow">
{{ _treeControl.isExpanded(node) ? '▼' : '►' }}
</span>
&nbsp;
<span class="name"> {{ node.prop.name }} </span>:&nbsp;
<span class="value">
{{ _treeControl.isExpanded(node) ? '' : node.prop.descriptor.preview }}
</span>
</div>
</mat-tree-node>
</mat-tree>

View file

@ -0,0 +1,49 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { PropertyDataSource } from '../../../../../property-resolver/property-data-source';
import { FlatNode } from '../../../../../property-resolver/element-property-resolver';
import { UpdateEvent } from '../property-view-body.component';
import { FlatTreeControl } from '@angular/cdk/tree';
@Component({
selector: 'ng-property-view-tree',
templateUrl: './property-view-tree.component.html',
styleUrls: ['./property-view-tree.component.css'],
})
export class PropertyViewTreeComponent {
@Input() set dataSource(dataSource: PropertyDataSource) {
this._dataSource = dataSource;
this._treeControl = dataSource.getTreeControl();
}
@Output() updateValue = new EventEmitter<UpdateEvent>();
_dataSource: PropertyDataSource;
_treeControl: FlatTreeControl<FlatNode>;
hasChild = (_: number, node: FlatNode): boolean => {
debugger;
return node.expandable;
};
toggle(node: FlatNode): void {
if (this._treeControl.isExpanded(node)) {
this._treeControl.collapse(node);
return;
}
this.expand(node);
}
expand(node: FlatNode): void {
const { prop } = node;
if (!prop.descriptor.expandable) {
return;
}
this._treeControl.expand(node);
}
handleUpdate(node: FlatNode, updatedValue: any): void {
this.updateValue.emit({
node,
updatedValue,
});
}
}

View file

@ -1,4 +1,4 @@
.explorer-panel header {
header {
background-color: #eee;
padding-left: 9px;
border-top: 1px solid #ccc;
@ -22,3 +22,7 @@
line-height: 15px;
margin-right: 7px;
}
:host /deep/ .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
line-height: 25px;
}

View file

@ -1,5 +1,16 @@
<header>
Properties of {{ directive }}
<span> Properties of {{ directive }} </span>
<mat-button-toggle-group [multiple]="true" (change)="filterProperties($event)">
<mat-button-toggle [value]="cmpFilterOptions.INPUTS" [checked]="true">
Inputs
</mat-button-toggle>
<mat-button-toggle [value]="cmpFilterOptions.OUTPUTS" [checked]="true">
Outputs
</mat-button-toggle>
<mat-button-toggle [value]="cmpFilterOptions.STATE" [checked]="true">
State
</mat-button-toggle>
</mat-button-toggle-group>
<button mat-icon-button (click)="copyPropData.emit(directive)">
<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -1,4 +1,6 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatButtonToggleChange } from '@angular/material/button-toggle';
import { PropertyViewFilterOptions } from '../../../../property-resolver/directive-property-resolver';
@Component({
selector: 'ng-property-view-header',
@ -7,5 +9,12 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
})
export class PropertyViewHeaderComponent {
@Input() directive: string;
@Output() filter = new EventEmitter<PropertyViewFilterOptions[]>();
@Output() copyPropData = new EventEmitter<string>();
cmpFilterOptions = PropertyViewFilterOptions;
filterProperties(event: MatButtonToggleChange): void {
this.filter.emit(event.value);
}
}

View file

@ -1,6 +1,11 @@
<ng-property-view-header [directive]="directive" (copyPropData)="copyPropData.emit($event)"></ng-property-view-header>
<ng-property-view-header
[directive]="directive"
(copyPropData)="copyPropData.emit($event)"
(filter)="filter($event)"
></ng-property-view-header>
<ng-property-view-body
[controller]="controller"
[dataSource]="dataSource"
[filterList]="allowedList"
[treeControl]="treeControl"
></ng-property-view-body>

View file

@ -1,8 +1,11 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DirectivePropertyResolver } from '../../../property-resolver/directive-property-resolver';
import { FlatNode, PropertyDataSource } from '../../../property-resolver/property-data-source';
import {
DirectivePropertyResolver,
PropertyViewFilterOptions,
} from '../../../property-resolver/directive-property-resolver';
import { PropertyDataSource } from '../../../property-resolver/property-data-source';
import { TreeControl } from '@angular/cdk/tree';
import { ElementPropertyResolver } from '../../../property-resolver/element-property-resolver';
import { ElementPropertyResolver, FlatNode } from '../../../property-resolver/element-property-resolver';
@Component({
selector: 'ng-property-view',
@ -13,17 +16,56 @@ export class PropertyViewComponent {
@Input() directive: string;
@Output() copyPropData = new EventEmitter<string>();
currentFilter: PropertyViewFilterOptions[] = [
PropertyViewFilterOptions.INPUTS,
PropertyViewFilterOptions.OUTPUTS,
PropertyViewFilterOptions.STATE,
];
allowedList: string[] | null = null;
constructor(private _nestedProps: ElementPropertyResolver) {}
get controller(): DirectivePropertyResolver {
get controller(): DirectivePropertyResolver | undefined {
return this._nestedProps.getDirectiveController(this.directive);
}
get dataSource(): PropertyDataSource {
get dataSource(): PropertyDataSource | void {
if (!this.controller) {
return;
}
return this.controller.getDirectiveControls().dataSource;
}
get treeControl(): TreeControl<FlatNode> {
get treeControl(): TreeControl<FlatNode> | void {
if (!this.controller) {
return;
}
return this.controller.getDirectiveControls().treeControl;
}
filter(evt: PropertyViewFilterOptions[]): void {
this.currentFilter = evt;
this.computeAllowedList();
}
computeAllowedList(): void {
if (!this.controller || !this.controller.directiveProperties) {
return;
}
const inputList = this.controller.directiveInputs;
const outputList = this.controller.directiveOutputs;
const stateList = Object.keys(this.controller.directiveProperties).filter(
prop => !inputList.includes(prop) && !outputList.includes(prop)
);
const propList = {
[PropertyViewFilterOptions.INPUTS]: inputList,
[PropertyViewFilterOptions.OUTPUTS]: outputList,
[PropertyViewFilterOptions.STATE]: stateList,
};
this.allowedList = [].concat.apply(
[],
this.currentFilter.map(filterOption => propList[filterOption])
);
}
}

View file

@ -5,10 +5,18 @@ import { MatTreeModule } from '@angular/material/tree';
import { PropertyEditorModule } from './property-view-body/property-editor/property-editor.module';
import { PropertyViewHeaderComponent } from './property-view-header/property-view-header.component';
import { PropertyViewComponent } from './property-view.component';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { PropertyViewTreeComponent } from './property-view-body/property-view-tree/property-view-tree.component';
@NgModule({
declarations: [PropertyViewBodyComponent, PropertyViewHeaderComponent, PropertyViewComponent],
imports: [MatTreeModule, CommonModule, PropertyEditorModule],
declarations: [
PropertyViewBodyComponent,
PropertyViewHeaderComponent,
PropertyViewComponent,
PropertyViewTreeComponent,
],
imports: [MatTreeModule, CommonModule, PropertyEditorModule, MatButtonModule, MatButtonToggleModule],
exports: [PropertyViewBodyComponent, PropertyViewHeaderComponent, PropertyViewComponent],
})
export class PropertyViewModule {}

View file

@ -1,6 +1,5 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { IndexedNode } from '../directive-forest/index-forest';
import { Descriptor, DirectivesProperties, Events, MessageBus } from 'protocol';
import { PropertyTabBodyComponent } from './property-tab-body/property-tab-body.component';
@Component({