Aspect List - added spinner and refresh (#6693)

* Adding spinner for loading aspects

* Added metadata update and test
This commit is contained in:
Vito
2021-02-18 09:37:15 +00:00
committed by GitHub
parent f58550216c
commit 59a991dc66
12 changed files with 137 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
<div id="aspect-list-container" class="adf-aspect-list-container">
<div id="aspect-list-container" class="adf-aspect-list-container" *ngIf="aspects$ | async as aspects; else loading">
<mat-accordion class="adf-accordion-aspect-list">
<mat-expansion-panel *ngFor="let aspect of (aspects$ | async); let colIndex = index" [id]="'aspect-list-'+aspect?.entry?.title">
<mat-expansion-panel *ngFor="let aspect of aspects; let colIndex = index" [id]="'aspect-list-'+aspect?.entry?.title">
<mat-expansion-panel-header [id]="'aspect-list-'+aspect?.entry?.title+'header'">
<mat-panel-title>
<mat-checkbox class="adf-aspect-list-check-button" [id]="'aspect-list-'+colIndex+'-check'"
@@ -36,3 +36,9 @@
</mat-expansion-panel>
</mat-accordion>
</div>
<ng-template #loading>
<div class="adf-aspect-list-spinner">
<mat-spinner id="adf-aspect-spinner"></mat-spinner>
</div>
</ng-template>

View File

@@ -8,10 +8,17 @@
.adf {
&-aspect-list-spinner {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
}
&-aspect-list-container {
padding-top: 3px;
max-height: 400px;
height: 400px;
overflow: auto;
.adf-aspect-list-check-button {

View File

@@ -23,6 +23,7 @@ import { AspectListComponent } from './aspect-list.component';
import { AspectListService } from './aspect-list.service';
import { of } from 'rxjs';
import { AspectEntry } from '@alfresco/js-api';
import { delay } from 'rxjs/operators';
const aspectListMock: AspectEntry[] = [{
entry: {
@@ -80,6 +81,26 @@ describe('AspectListComponent', () => {
providers: [AspectListService]
});
describe('Loading', () => {
beforeEach(() => {
fixture = TestBed.createComponent(AspectListComponent);
component = fixture.componentInstance;
nodeService = TestBed.inject(NodesApiService);
aspectListService = TestBed.inject(AspectListService);
});
it('should show the loading spinner when result is loading', () => {
const delayReusult = of([]).pipe(delay(0));
spyOn(nodeService, 'getNode').and.returnValue(delayReusult);
spyOn(aspectListService, 'getAspects').and.returnValue(delayReusult);
fixture.detectChanges();
const spinner = fixture.nativeElement.querySelector('#adf-aspect-spinner');
expect(spinner).toBeDefined();
expect(spinner).not.toBeNull();
});
});
describe('When passing a node id', () => {
beforeEach(() => {

View File

@@ -27,6 +27,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { AspectListDialogComponent } from './aspect-list-dialog.component';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@NgModule({
imports: [
@@ -38,7 +39,8 @@ import { MatTooltipModule } from '@angular/material/tooltip';
TranslateModule,
MatDialogModule,
MatButtonModule,
MatTooltipModule
MatTooltipModule,
MatProgressSpinnerModule
],
exports: [
AspectListComponent,

View File

@@ -19,8 +19,8 @@ import { AspectEntry, AspectPaging } from '@alfresco/js-api';
import { async, TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService, AppConfigService, setupTestBed } from 'core';
import { of, Subject } from 'rxjs';
import { AlfrescoApiService, AppConfigService, LogService, setupTestBed } from 'core';
import { of, Subject, throwError } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { AspectListService } from './aspect-list.service';
@@ -146,11 +146,12 @@ describe('AspectListService', () => {
const aspectTypesApi = jasmine.createSpyObj('AspectsApi', ['listAspects']);
const apiService: AlfrescoApiService = new AlfrescoApiService(null, null);
const logService: LogService = new LogService(appConfigService);
beforeEach(() => {
spyOn(appConfigService, 'get').and.returnValue({ 'default': ['frs:AspectOne'] });
spyOnProperty(apiService, 'aspectsApi').and.returnValue(aspectTypesApi);
service = new AspectListService(apiService, appConfigService, null);
service = new AspectListService(apiService, appConfigService, null, logService);
});
it('should get the list of only available aspects', async(() => {
@@ -161,6 +162,26 @@ describe('AspectListService', () => {
expect(list[1].entry.id).toBe('frs:AspectCustom');
});
}));
it('should return a value when the standard aspect call fails', async(() => {
spyOn(logService, 'error').and.stub();
aspectTypesApi.listAspects.and.returnValues(throwError('Insert Coin'), of(customListAspectResp));
service.getAspects().subscribe((list) => {
expect(list.length).toBe(1);
expect(list[0].entry.id).toBe('frs:AspectCustom');
expect(logService.error).toHaveBeenCalled();
});
}));
it('should return a value when the custom aspect call fails', async(() => {
spyOn(logService, 'error').and.stub();
aspectTypesApi.listAspects.and.returnValues(of(listAspectResp), throwError('Insert Coin'));
service.getAspects().subscribe((list) => {
expect(list.length).toBe(1);
expect(list[0].entry.id).toBe('frs:AspectOne');
expect(logService.error).toHaveBeenCalled();
});
}));
});
});

View File

@@ -17,11 +17,11 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
import { from, Observable, Subject, zip } from 'rxjs';
import { AlfrescoApiService, AppConfigService, LogService } from '@alfresco/adf-core';
import { from, Observable, of, Subject, zip } from 'rxjs';
import { AspectListDialogComponentData } from './aspect-list-dialog-data.interface';
import { AspectListDialogComponent } from './aspect-list-dialog.component';
import { map } from 'rxjs/operators';
import { catchError, map } from 'rxjs/operators';
import { AspectEntry, AspectPaging } from '@alfresco/js-api';
@Injectable({
@@ -30,7 +30,9 @@ import { AspectEntry, AspectPaging } from '@alfresco/js-api';
export class AspectListService {
constructor(private alfrescoApiService: AlfrescoApiService,
private appConfigService: AppConfigService, private dialog: MatDialog) {
private appConfigService: AppConfigService,
private dialog: MatDialog,
private logService: LogService) {
}
getAspects(): Observable<AspectEntry[]> {
@@ -44,17 +46,25 @@ export class AspectListService {
getStandardAspects(whiteList: string[]): Observable<AspectEntry[]> {
const where = `(modelIds in ('cm:contentmodel', 'emailserver:emailserverModel', 'smf:smartFolder', 'app:applicationmodel' ))`;
return from(this.alfrescoApiService.aspectsApi.listAspects(where))
return from(this.alfrescoApiService.aspectsApi.listAspects({where}))
.pipe(
map((result: AspectPaging) => this.filterAspectByConfig(whiteList, result?.list?.entries))
map((result: AspectPaging) => this.filterAspectByConfig(whiteList, result?.list?.entries)),
catchError((error) => {
this.logService.error(error);
return of([]);
})
);
}
getCustomAspects(): Observable<AspectEntry[]> {
const where = `(not namespaceUri matches('http://www.alfresco.*')`;
return from(this.alfrescoApiService.aspectsApi.listAspects(where))
const where = `(not namespaceUri matches('http://www.alfresco.*'))`;
return from(this.alfrescoApiService.aspectsApi.listAspects({where}))
.pipe(
map((result: AspectPaging) => result?.list?.entries)
map((result: AspectPaging) => result?.list?.entries),
catchError((error) => {
this.logService.error(error);
return of([]);
})
);
}

View File

@@ -17,7 +17,7 @@
import { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService, NodesApiService, setupTestBed } from 'core';
import { AlfrescoApiService, CardViewUpdateService, NodesApiService, setupTestBed } from 'core';
import { of } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { AspectListService } from './aspect-list.service';
@@ -29,6 +29,7 @@ describe('NodeAspectService', () => {
let nodeAspectService: NodeAspectService;
let nodeApiService: NodesApiService;
let alfrescoApiService: AlfrescoApiService;
let cardViewUpdateService: CardViewUpdateService;
setupTestBed({
imports: [
@@ -42,6 +43,7 @@ describe('NodeAspectService', () => {
nodeAspectService = TestBed.inject(NodeAspectService);
nodeApiService = TestBed.inject(NodesApiService);
alfrescoApiService = TestBed.inject(AlfrescoApiService);
cardViewUpdateService = TestBed.inject(CardViewUpdateService);
});
it('should open the aspect list dialog', () => {
@@ -71,4 +73,16 @@ describe('NodeAspectService', () => {
nodeAspectService.updateNodeAspects('fake-node-id');
});
it('should send and update node aspect once the node has been updated', (done) => {
cardViewUpdateService.updatedAspect$.subscribe((nodeUpdated) => {
expect(nodeUpdated.id).toBe('fake-node-id');
expect(nodeUpdated.aspectNames).toEqual(['a', 'b', 'c']);
done();
});
const fakeNode = { id: 'fake-node-id', aspectNames: ['a', 'b', 'c'] };
spyOn(aspectListService, 'openAspectListDialog').and.returnValue(of(['a', 'b', 'c']));
spyOn(nodeApiService, 'updateNode').and.returnValue(of(fakeNode));
nodeAspectService.updateNodeAspects('fake-node-id');
});
});

View File

@@ -16,7 +16,7 @@
*/
import { Injectable } from '@angular/core';
import { AlfrescoApiService, NodesApiService } from '@alfresco/adf-core';
import { AlfrescoApiService, CardViewUpdateService, NodesApiService } from '@alfresco/adf-core';
import { AspectListService } from './aspect-list.service';
@Injectable({
@@ -26,13 +26,15 @@ export class NodeAspectService {
constructor(private alfrescoApiService: AlfrescoApiService,
private nodesApiService: NodesApiService,
private aspectListService: AspectListService) {
private aspectListService: AspectListService,
private cardViewUpdateService: CardViewUpdateService) {
}
updateNodeAspects(nodeId: string) {
this.aspectListService.openAspectListDialog(nodeId).subscribe((aspectList) => {
this.nodesApiService.updateNode(nodeId, { aspectNames: [...aspectList] }).subscribe((updatedNode) => {
this.alfrescoApiService.nodeUpdated.next(updatedNode);
this.cardViewUpdateService.updateNodeAspect(updatedNode);
});
});
}

View File

@@ -18,7 +18,7 @@
import { async, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { Node } from '@alfresco/js-api';
import { MinimalNode, Node } from '@alfresco/js-api';
import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataService } from '../../services/content-metadata.service';
import {
@@ -124,6 +124,17 @@ describe('ContentMetadataComponent', () => {
expect(component.changedProperties).toEqual({ properties: { 'property-key': 'updated-value' } });
}));
it('nodeAspectUpdate', fakeAsync(() => {
const fakeNode: MinimalNode = <MinimalNode> { id: 'fake-minimal-node', aspectNames: ['ft:a', 'ft:b', 'ft:c'], name: 'fake-node'};
spyOn(contentMetadataService, 'getGroupedProperties').and.stub();
spyOn(contentMetadataService, 'getBasicProperties').and.stub();
updateService.updateNodeAspect(fakeNode);
tick(600);
expect(contentMetadataService.getBasicProperties).toHaveBeenCalled();
expect(contentMetadataService.getGroupedProperties).toHaveBeenCalled();
}));
it('should save changedProperties on save click', fakeAsync(async () => {
component.editable = true;
const property = <CardViewBaseItemModel> { key: 'properties.property-key', value: 'original-value' };

View File

@@ -26,7 +26,8 @@ import {
AlfrescoApiService,
TranslationService,
AppConfigService,
CardViewBaseItemModel
CardViewBaseItemModel,
UpdateNotification
} from '@alfresco/adf-core';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { CardViewGroup } from '../../interfaces/content-metadata.interfaces';
@@ -115,13 +116,18 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
debounceTime(200),
takeUntil(this.onDestroy$))
.subscribe(
(updatedNode) => {
(updatedNode: UpdateNotification) => {
this.hasMetadataChanged = true;
this.targetProperty = updatedNode.target;
this.updateChanges(updatedNode.changed);
}
);
this.cardViewUpdateService.updatedAspect$.pipe(
debounceTime(200),
takeUntil(this.onDestroy$))
.subscribe((node) => this.loadProperties(node));
this.loadProperties(this.node);
}

View File

@@ -15,6 +15,7 @@
* limitations under the License.
*/
import { MinimalNode } from '@alfresco/js-api';
import { async, TestBed } from '@angular/core/testing';
import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
import { CardViewUpdateService, transformKeyToObject } from './card-view-update.service';
@@ -82,5 +83,13 @@ describe('CardViewUpdateService', () => {
);
cardViewUpdateService.clicked(property);
}));
it('should send updated node when aspect changed', async(() => {
const fakeNode: MinimalNode = <MinimalNode> { id: 'Bigfoot'};
cardViewUpdateService.updatedAspect$.subscribe((node: MinimalNode) => {
expect(node.id).toBe('Bigfoot');
});
cardViewUpdateService.updateNodeAspect(fakeNode);
}));
});
});

View File

@@ -15,6 +15,7 @@
* limitations under the License.
*/
import { MinimalNode } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
@@ -44,6 +45,7 @@ export class CardViewUpdateService {
itemUpdated$ = new Subject<UpdateNotification>();
itemClicked$ = new Subject<ClickNotification>();
updateItem$ = new Subject<CardViewBaseItemModel>();
updatedAspect$ = new Subject<MinimalNode>();
update(property: CardViewBaseItemModel, newValue: any) {
this.itemUpdated$.next({
@@ -66,4 +68,8 @@ export class CardViewUpdateService {
this.updateItem$.next(notification);
}
updateNodeAspect(node: MinimalNode) {
this.updatedAspect$.next(node);
}
}