[MNT-22606] Add properties viewer as form widget (#7278)

* [MNT-22606] Add properties viewer as form widget

* [MNT-22606] Add documentation and fix the params accessor

* [MNT-22606] Fix unit tests

* [MNT-22606] Fix lint

* [MNT-22606] Fix lint

* [MNT-22606] Fix lint
This commit is contained in:
Pablo Martinez Garcia
2021-10-06 18:12:14 +02:00
committed by GitHub
parent dc133643c9
commit 5a2d27393d
12 changed files with 445 additions and 10 deletions

View File

@@ -454,10 +454,7 @@ for more information about installing and using the source code.
| [Form cloud component](process-services-cloud/components/form-cloud.component.md) | Shows a form from Process Services. | [Source](../lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts) |
| [Form Definition Selector Cloud](process-services-cloud/components/form-definition-selector-cloud.component.md) | Allows one form to be selected from a dropdown list. For forms to be displayed in this component they will need to be compatible with standAlone tasks. | [Source](../lib/process-services-cloud/src/lib/form/components/form-definition-selector-cloud.component.ts) |
| [Group Cloud component](process-services-cloud/components/group-cloud.component.md) ![Experimental](docassets/images/ExperimentalIcon.png) | Searches Groups. | [Source](../lib/process-services-cloud/src/lib/group/components/group-cloud.component.ts) |
| [](process-services-cloud/components/people-cloud.component.md) | Title: People Cloud Component | |
Added: v3.0.0
Status: Experimental | [Source](<>) |
| [People Cloud Component](process-services-cloud/components/people-cloud.component.md) ![Experimental](docassets/images/ExperimentalIcon.png) | Added: v3.0.0 | [Source](<>) |
| [Process Filters Cloud Component](process-services-cloud/components/process-filters-cloud.component.md) ![Experimental](docassets/images/ExperimentalIcon.png) | Lists all available process filters and allows to select a filter. | [Source](../lib/process-services-cloud/src/lib/process/process-filters/components/process-filters-cloud.component.ts) |
| [Process Header Cloud Component](process-services-cloud/components/process-header-cloud.component.md) ![Experimental](docassets/images/ExperimentalIcon.png) | Shows all the information related to a process instance. | [Source](../lib/process-services-cloud/src/lib/process/process-header/components/process-header-cloud.component.ts) |
| [Process Instance List Cloud component](process-services-cloud/components/process-list-cloud.component.md) ![Experimental](docassets/images/ExperimentalIcon.png) | Renders a list containing all the process instances matched by the parameters specified. | [Source](../lib/process-services-cloud/src/lib/process/process-list/components/process-list-cloud.component.ts) |
@@ -504,6 +501,13 @@ Status: Experimental | [Source](<>) |
<!--process-services-cloud end-->
### Widgets
| Name | Description | Source link |
| ---- | ----------- | ----------- |
| [APA Properties Viewer Widget](process-services-cloud/widgets/properties-viewer.widget.md) | Display the metadata of the file selected in the linked attach widget. | [Source](../lib/process-services-cloud/src/lib/form/components/widgets/properties-viewer/properties-viewer.widget.ts) |
[(Back to Contents)](#contents)
## Extensions API

View File

@@ -0,0 +1,33 @@
---
Title: APA Properties Viewer Widget
Added: v4.7.0
Status: Active
---
# [APA Properties Viewer Widget](../../../lib/process-services-cloud/src/lib/form/components/widgets/properties-viewer/properties-viewer.widget.ts "Defined in properties-viewer.widget.ts")
It makes use of the [content metadata card](../../content-services/components/content-metadata-card.component.md "content-metadata-card") to display the properties of the selected file in an attach widget in a form.
## Basic Usage
This a form widget so it receives the [`FormFieldModel`](../../core/models/form-field.model.md) form the form renderer with the following meanings:
| Accessor | Type | Description |
| -------- | ---- | ----------- |
|`field.value`| `string` | The nodeId of the node which properties are going to be displayed |
|`field.params.propertiesViewerOptions` | `object` | An object containing all the [properties of the content metadata card](../../content-services/components/content-metadata-card.component.md#properties "content-metadata-card properties") |
## Class members
### Properties
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| field | `FormFieldModel` | | The field information |
### Events
| Name | Type | Description |
| ---- | ---- | ----------- |
| nodeContentLoaded | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<Node>` | Emitted when the properties to be displayed are retrieved from the content service. |
| fieldChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the field changes |

View File

@@ -402,7 +402,7 @@ export class FormModel {
setNodeIdValueForViewersLinkedToUploadWidget(linkedUploadWidgetContentSelected: UploadWidgetContentLinkModel) {
const subscribedViewers = this.getFormFields().filter(field =>
field.type === FormFieldTypes.FILE_VIEWER && linkedUploadWidgetContentSelected.uploadWidgetId === field.params['uploadWidget']
linkedUploadWidgetContentSelected.uploadWidgetId === field.params['uploadWidget']
);
subscribedViewers.forEach(viewer => {

View File

@@ -22,6 +22,7 @@ import { DropdownCloudWidgetComponent } from './widgets/dropdown/dropdown-cloud.
import { DateCloudWidgetComponent } from './widgets/date/date-cloud.widget';
import { PeopleCloudWidgetComponent } from './widgets/people/people-cloud.widget';
import { GroupCloudWidgetComponent } from './widgets/group/group-cloud.widget';
import { PropertiesViewerWidgetComponent } from './widgets/properties-viewer/properties-viewer.widget';
@Injectable({
providedIn: 'root'
@@ -35,7 +36,8 @@ export class CloudFormRenderingService extends FormRenderingService {
'dropdown': () => DropdownCloudWidgetComponent,
'date': () => DateCloudWidgetComponent,
'people': () => PeopleCloudWidgetComponent,
'functional-group': () => GroupCloudWidgetComponent
'functional-group': () => GroupCloudWidgetComponent,
'properties-viewer': () => PropertiesViewerWidgetComponent
}, true);
}
}

View File

@@ -0,0 +1,10 @@
<adf-content-metadata [displayDefaultProperties]="displayDefaultProperties" [expanded]="expanded" [node]="node"
[preset]="preset" [displayEmpty]="displayEmpty" [editable]="editable" [multi]="multi"
[displayAspect]="displayAspect" [copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty" *ngIf="!loading; else loadingTemplate">
</adf-content-metadata>
<ng-template #loadingTemplate>
<div class="adf-properties-viewer-wrapper-loading">
<mat-progress-spinner></mat-progress-spinner>
</div>
</ng-template>

View File

@@ -0,0 +1,100 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { NodesApiService } from '@alfresco/adf-core';
import { Node } from '@alfresco/js-api';
/* tslint:disable:component-selector */
@Component({
selector: 'adf-properties-viewer-wrapper',
templateUrl: './properties-viewer-wrapper.component.html',
encapsulation: ViewEncapsulation.None
})
export class PropertiesViewerWrapperComponent implements OnInit, OnChanges {
node: Node;
loading = true;
@Input()
nodeId: string;
/** Toggles whether the edit button should be shown */
@Input()
editable;
/** Toggles whether to display empty values in the card view */
@Input()
displayEmpty;
/** Toggles between expanded (ie, full information) and collapsed
* (ie, reduced information) in the display
*/
@Input()
expanded;
/** The multi parameter of the underlying material expansion panel, set to true to allow multi accordion to be expanded at the same time */
@Input()
multi;
/** Name of the metadata preset, which defines aspects and their properties */
@Input()
preset: string;
/** Toggles whether the metadata properties should be shown */
@Input()
displayDefaultProperties;
/** (optional) shows the given aspect in the expanded card */
@Input()
displayAspect: string = null;
/** Toggles whether or not to enable copy to clipboard action. */
@Input()
copyToClipboardAction;
/** Toggles whether or not to enable chips for multivalued properties. */
@Input()
useChipsForMultiValueProperty;
@Output()
nodeContentLoaded: EventEmitter<Node> = new EventEmitter();
constructor(private nodesApiService: NodesApiService) { }
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes['nodeId'] && changes['nodeId'].currentValue && !changes['nodeId'].isFirstChange()) {
this.getNode(changes['nodeId'].currentValue);
}
}
ngOnInit(): void {
if (this.nodeId) {
this.getNode(this.nodeId);
}
}
private getNode(nodeId: string) {
this.loading = true;
this.nodesApiService.getNode(nodeId).subscribe(retrievedNode => {
this.node = retrievedNode;
this.loading = false;
this.nodeContentLoaded.emit(retrievedNode);
});
}
}

View File

@@ -0,0 +1,90 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {
NodesApiService,
setupTestBed
} from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
import { PropertiesViewerWrapperComponent } from './properties-viewer-wrapper.component';
import { ProcessServiceCloudTestingModule } from 'process-services-cloud/src/lib/testing/process-service-cloud.testing.module';
import { of } from 'rxjs';
import { fakeNodeWithProperties } from 'process-services-cloud/src/lib/form/mocks/attach-file-cloud-widget.mock';
import { BasicPropertiesService } from 'content-services';
describe('PropertiesViewerWidgetComponent', () => {
let component: PropertiesViewerWrapperComponent;
let fixture: ComponentFixture<PropertiesViewerWrapperComponent>;
let nodesApiService: NodesApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ProcessServiceCloudTestingModule
],
providers: [
NodesApiService,
{ provide: BasicPropertiesService, useValue: { getProperties: () => [] } }
]
});
beforeEach(() => {
fixture = TestBed.createComponent(PropertiesViewerWrapperComponent);
component = fixture.componentInstance;
nodesApiService = TestBed.inject(NodesApiService);
});
afterEach(() => fixture.destroy());
it('should retrieve the node when initializing the component', async () => {
component.nodeId = '1234';
const nodesApiServiceSpy = spyOn(nodesApiService, 'getNode').and.returnValue(of(fakeNodeWithProperties));
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
expect(nodesApiServiceSpy).toHaveBeenCalledWith('1234');
});
it('should retrieve the node when the nodeId changes', async () => {
const nodesApiServiceSpy = spyOn(nodesApiService, 'getNode').and.returnValue(of(fakeNodeWithProperties));
component.ngOnChanges({ nodeId: { isFirstChange: () => false, currentValue: '1234', previousValue: undefined, firstChange: false } });
fixture.detectChanges();
await fixture.whenStable();
expect(nodesApiServiceSpy).toHaveBeenCalledWith('1234');
});
it('should emit the node when node is retrieved', async () => {
component.nodeId = '1234';
spyOn(nodesApiService, 'getNode').and.returnValue(of(fakeNodeWithProperties));
const nodeContentLoadedSpy = spyOn(component.nodeContentLoaded, 'emit');
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
expect(nodeContentLoadedSpy).toHaveBeenCalledWith(fakeNodeWithProperties);
});
});

View File

@@ -0,0 +1,20 @@
<div class="adf-file-viewer-widget {{field.className}}" [class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span
*ngIf="isRequired()">*</span></label>
<ng-template #properties [ngTemplateOutlet]="properties" let-properties="properties" [ngTemplateOutletContext]="{ properties: field.params?.propertiesViewerOptions }">
<adf-properties-viewer-wrapper *ngIf="field.value" [nodeId]="field.value"
[displayDefaultProperties]="properties?.displayDefaultProperties !== undefined ? properties?.displayDefaultProperties : true"
[expanded]="properties?.expanded !== undefined ? properties?.expanded : true"
[preset]="properties?.preset"
[displayEmpty]="properties?.displayEmpty !== undefined ? properties?.displayEmpty : false"
[editable]="properties?.editable !== undefined ? properties?.editable : false"
[multi]="properties?.multi !== undefined ? properties?.multi : false"
[displayAspect]="properties?.displayAspect !== undefined ? properties?.displayAspect : null"
[copyToClipboardAction]="properties?.copyToClipboardAction !== undefined ? properties?.copyToClipboardAction : true"
[useChipsForMultiValueProperty]="properties?.useChipsForMultiValueProperty !== undefined ? properties?.useChipsForMultiValueProperty : true"
(nodeContentLoaded)="onNodeContentLoaded($event)">
</adf-properties-viewer-wrapper>
</ng-template>
<error-widget [error]="field.validationSummary"></error-widget>
</div>

View File

@@ -0,0 +1,22 @@
adf-properties-viewer-widget {
height: 100%;
width: 100%;
adf-properties-viewer-wrapper {
display: block;
height: 100%;
width: 100%;
border: 1px solid var(--theme-border-color);
border-radius: 6px;
.adf-properties-viewer-wrapper-loading {
height: 100%;
width: 100%;
mat-progress-spinner {
display: block;
margin: auto;
}
}
}
}

View File

@@ -0,0 +1,95 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {
FormFieldModel,
FormModel,
NodesApiService,
setupTestBed
} from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
import { PropertiesViewerWidgetComponent } from './properties-viewer.widget';
import { ProcessServiceCloudTestingModule } from 'process-services-cloud/src/lib/testing/process-service-cloud.testing.module';
import { fakeNodeWithProperties } from '../../../mocks/attach-file-cloud-widget.mock';
import { PropertiesViewerWrapperComponent } from './properties-viewer-wrapper/properties-viewer-wrapper.component';
import { BasicPropertiesService } from 'content-services';
import { of } from 'rxjs';
describe('PropertiesViewerWidgetComponent', () => {
let widget: PropertiesViewerWidgetComponent;
let fixture: ComponentFixture<PropertiesViewerWidgetComponent>;
let element: HTMLElement;
let nodesApiService: NodesApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ProcessServiceCloudTestingModule
],
declarations: [PropertiesViewerWrapperComponent],
providers: [
NodesApiService,
{ provide: BasicPropertiesService, useValue: { getProperties: () => [] } }
]
});
beforeEach(() => {
fixture = TestBed.createComponent(PropertiesViewerWidgetComponent);
nodesApiService = TestBed.inject(NodesApiService);
widget = fixture.componentInstance;
element = fixture.nativeElement;
widget.field = new FormFieldModel(new FormModel());
spyOn(nodesApiService, 'getNode').and.returnValue(of(fakeNodeWithProperties));
});
afterEach(() => fixture.destroy());
it('should not display properties viewer when value is not set', async () => {
widget.field.value = undefined;
fixture.detectChanges();
await fixture.whenStable();
const propertiesViewer = element.querySelector('properties-viewer-wrapper');
expect(propertiesViewer).toBeNull();
});
it('should display properties viewer when value is set', async () => {
widget.field.value = '1234';
fixture.detectChanges();
await fixture.whenStable();
const propertiesViewer = element.querySelector('adf-properties-viewer-wrapper');
expect(propertiesViewer).not.toBeNull();
});
it('should emit the node when node content is loaded', async () => {
const nodeContentLoadedSpy = spyOn(widget.nodeContentLoaded, 'emit');
widget.onNodeContentLoaded(fakeNodeWithProperties);
fixture.detectChanges();
await fixture.whenStable();
expect(nodeContentLoadedSpy).toHaveBeenCalledWith(fakeNodeWithProperties);
});
});

View File

@@ -0,0 +1,53 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, EventEmitter, Output, ViewEncapsulation } from '@angular/core';
import { FormService, WidgetComponent } from '@alfresco/adf-core';
import { Node } from '@alfresco/js-api';
/* tslint:disable:component-selector */
@Component({
selector: 'adf-properties-viewer-widget',
templateUrl: './properties-viewer.widget.html',
styleUrls: ['./properties-viewer.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class PropertiesViewerWidgetComponent extends WidgetComponent {
@Output()
nodeContentLoaded: EventEmitter<Node> = new EventEmitter();
constructor(formService: FormService) {
super(formService);
}
onNodeContentLoaded(node: Node) {
this.nodeContentLoaded.emit(node);
}
}

View File

@@ -24,7 +24,7 @@ import { MaterialModule } from '../material.module';
import { FormCloudComponent } from './components/form-cloud.component';
import { FormDefinitionSelectorCloudComponent } from './components/form-definition-selector-cloud.component';
import { FormCustomOutcomesComponent } from './components/form-cloud-custom-outcomes.component';
import { ContentNodeSelectorModule } from '@alfresco/adf-content-services';
import { ContentMetadataModule, ContentNodeSelectorModule } from '@alfresco/adf-content-services';
import { DateCloudWidgetComponent } from './components/widgets/date/date-cloud.widget';
import { DropdownCloudWidgetComponent } from './components/widgets/dropdown/dropdown-cloud.widget';
@@ -35,6 +35,8 @@ import { AttachFileCloudWidgetComponent } from './components/widgets/attach-file
import { UploadCloudWidgetComponent } from './components/widgets/attach-file/upload-cloud.widget';
import { PeopleCloudModule } from '../people/people-cloud.module';
import { GroupCloudModule } from '../group/group-cloud.module';
import { PropertiesViewerWidgetComponent } from './components/widgets/properties-viewer/properties-viewer.widget';
import { PropertiesViewerWrapperComponent } from './components/widgets/properties-viewer/properties-viewer-wrapper/properties-viewer-wrapper.component';
@NgModule({
imports: [
@@ -46,7 +48,8 @@ import { GroupCloudModule } from '../group/group-cloud.module';
CoreModule,
ContentNodeSelectorModule,
PeopleCloudModule,
GroupCloudModule
GroupCloudModule,
ContentMetadataModule
],
declarations: [
FormCloudComponent,
@@ -57,7 +60,9 @@ import { GroupCloudModule } from '../group/group-cloud.module';
AttachFileCloudWidgetComponent,
DateCloudWidgetComponent,
PeopleCloudWidgetComponent,
GroupCloudWidgetComponent
GroupCloudWidgetComponent,
PropertiesViewerWrapperComponent,
PropertiesViewerWidgetComponent
],
exports: [
FormCloudComponent,
@@ -68,7 +73,8 @@ import { GroupCloudModule } from '../group/group-cloud.module';
AttachFileCloudWidgetComponent,
DateCloudWidgetComponent,
PeopleCloudWidgetComponent,
GroupCloudWidgetComponent
GroupCloudWidgetComponent,
PropertiesViewerWidgetComponent
]
})
export class FormCloudModule {