[ACA-4729] Add infinite scroll to version list (#9248)

* [ACA-4729] Add infinite scroll to version list

* [ACA-4729] CR fixes

* [ACA-4729] CR fixes

* [ACA-4729] Items count fix for infinite scroll datasource
This commit is contained in:
MichalKinas
2024-01-25 09:23:03 +01:00
committed by GitHub
parent 2c0ad7137a
commit a7e7934505
13 changed files with 489 additions and 152 deletions
@@ -1,55 +1,53 @@
<mat-list class="adf-version-list" *ngIf="!isLoading; else loading_template">
<mat-list-item *ngFor="let version of versions; let idx = index; let latestVersion = first">
<mat-icon mat-list-icon>insert_drive_file</mat-icon>
<p mat-line class="adf-version-list-item-name" [id]="'adf-version-list-item-name-' + version.entry.id" >{{version.entry.name}}</p>
<p mat-line>
<span class="adf-version-list-item-version" [id]="'adf-version-list-item-version-' + version.entry.id" >{{version.entry.id}}</span> -
<span class="adf-version-list-item-date" [id]="'adf-version-list-item-date-' + version.entry.id" >{{version.entry.modifiedAt | date}}</span>
</p>
<p mat-line [id]="'adf-version-list-item-comment-'+ version.entry.id" class="adf-version-list-item-comment"
*ngIf="showComments">{{version.entry.versionComment}}</p>
<mat-progress-bar *ngIf="isLoading" data-automation-id="version-history-loading-bar" mode="indeterminate" color="accent"></mat-progress-bar>
<mat-list class="adf-version-list" [hidden]="isLoading">
<cdk-virtual-scroll-viewport #viewport itemSize="88" class="adf-version-list-viewport">
<mat-list-item *cdkVirtualFor="let version of versionsDataSource; let idx = index; let latestVersion = first">
<mat-icon mat-list-icon>insert_drive_file</mat-icon>
<p mat-line class="adf-version-list-item-name" [id]="'adf-version-list-item-name-' + version.entry.id" >{{version.entry.name}}</p>
<p mat-line>
<span class="adf-version-list-item-version" [id]="'adf-version-list-item-version-' + version.entry.id" >{{version.entry.id}}</span> -
<span class="adf-version-list-item-date" [id]="'adf-version-list-item-date-' + version.entry.id" >{{version.entry.modifiedAt | date}}</span>
</p>
<p mat-line [id]="'adf-version-list-item-comment-'+ version.entry.id" class="adf-version-list-item-comment"
*ngIf="showComments">{{version.entry.versionComment}}</p>
<div *ngIf="showActions">
<mat-menu [id]="'adf-version-list-action-menu-'+version.entry.id"
#versionMenu="matMenu" yPosition="below" xPosition="before">
<ng-container *adf-acs-version="'7'">
<button *ngIf="allowViewVersions"
[id]="'adf-version-list-action-view-'+version.entry.id"
mat-menu-item
(click)="onViewVersion(version.entry.id)">
{{ 'ADF_VERSION_LIST.ACTIONS.VIEW' | translate }}
</button>
</ng-container>
<button
[id]="'adf-version-list-action-restore-'+version.entry.id"
[disabled]="!canUpdate() || latestVersion"
mat-menu-item
(click)="restore(version.entry.id)">
{{ 'ADF_VERSION_LIST.ACTIONS.RESTORE' | translate }}
</button>
<button *ngIf="allowDownload"
[id]="'adf-version-list-action-download-'+version.entry.id"
<div *ngIf="showActions">
<mat-menu [id]="'adf-version-list-action-menu-'+version.entry.id"
#versionMenu="matMenu" yPosition="below" xPosition="before">
<ng-container *adf-acs-version="'7'">
<button *ngIf="allowViewVersions"
[id]="'adf-version-list-action-view-'+version.entry.id"
mat-menu-item
(click)="onViewVersion(version.entry.id)">
{{ 'ADF_VERSION_LIST.ACTIONS.VIEW' | translate }}
</button>
</ng-container>
<button
[id]="'adf-version-list-action-restore-'+version.entry.id"
[disabled]="!canUpdate() || latestVersion"
mat-menu-item
(click)="downloadVersion(version.entry.id)">
{{ 'ADF_VERSION_LIST.ACTIONS.DOWNLOAD' | translate }}
</button>
<button
[disabled]="!canDelete()"
[id]="'adf-version-list-action-delete-'+version.entry.id"
(click)="deleteVersion(version.entry.id)"
mat-menu-item>
{{ 'ADF_VERSION_LIST.ACTIONS.DELETE' | translate }}
</button>
</mat-menu>
(click)="restore(version.entry.id)">
{{ 'ADF_VERSION_LIST.ACTIONS.RESTORE' | translate }}
</button>
<button *ngIf="allowDownload"
[id]="'adf-version-list-action-download-'+version.entry.id"
mat-menu-item
(click)="downloadVersion(version.entry.id)">
{{ 'ADF_VERSION_LIST.ACTIONS.DOWNLOAD' | translate }}
</button>
<button
[disabled]="!canDelete()"
[id]="'adf-version-list-action-delete-'+version.entry.id"
(click)="deleteVersion(version.entry.id)"
mat-menu-item>
{{ 'ADF_VERSION_LIST.ACTIONS.DELETE' | translate }}
</button>
</mat-menu>
<button mat-icon-button [matMenuTriggerFor]="versionMenu" [id]="'adf-version-list-action-menu-button-'+version.entry.id" title="{{ 'ADF_VERSION_LIST.MANAGE_VERSION_OPTIONS' | translate }}">
<mat-icon>more_vert</mat-icon>
</button>
</div>
</mat-list-item>
<button mat-icon-button [matMenuTriggerFor]="versionMenu" [id]="'adf-version-list-action-menu-button-'+version.entry.id" title="{{ 'ADF_VERSION_LIST.MANAGE_VERSION_OPTIONS' | translate }}">
<mat-icon>more_vert</mat-icon>
</button>
</div>
</mat-list-item>
</cdk-virtual-scroll-viewport>
</mat-list>
<ng-template #loading_template>
<mat-progress-bar data-automation-id="version-history-loading-bar" mode="indeterminate"
color="accent"></mat-progress-bar>
</ng-template>
@@ -1,4 +1,8 @@
.adf-version-list {
&-viewport {
height: 100%;
}
.mat-list-item-content {
border-bottom: 1px solid #d8d8d8;
}
@@ -18,13 +18,14 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { VersionListComponent } from './version-list.component';
import { VersionListComponent, VersionListDataSource } from './version-list.component';
import { MatDialog } from '@angular/material/dialog';
import { of } from 'rxjs';
import { Node, VersionPaging, NodeEntry, VersionEntry, Version } from '@alfresco/js-api';
import { Node, NodeEntry, VersionEntry, Version } from '@alfresco/js-api';
import { ContentTestingModule } from '../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { ContentVersionService } from './content-version.service';
import { take } from 'rxjs/operators';
describe('VersionListComponent', () => {
let component: VersionListComponent;
@@ -56,14 +57,16 @@ describe('VersionListComponent', () => {
component = fixture.componentInstance;
component.node = { id: nodeId, allowableOperations: ['update'] } as Node;
component.isLoading = false;
spyOn(component, 'downloadContent').and.stub();
spyOn(component.nodesApi, 'getNode').and.returnValue(Promise.resolve(new NodeEntry({ entry: new Node({ id: 'nodeInfoId' }) })));
spyOn(VersionListDataSource.prototype, 'getNextBatch').and.callFake(() => of(versionTest));
spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() => Promise.resolve({ list: { entries: versionTest } }));
});
it('should raise confirmation dialog on delete', () => {
fixture.detectChanges();
component.versions = versionTest;
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(false)
@@ -74,15 +77,13 @@ describe('VersionListComponent', () => {
expect(dialog.open).toHaveBeenCalled();
});
it('should delete the version if user confirms', () => {
fixture.detectChanges();
component.versions = versionTest;
it('should delete the version if user confirms', async () => {
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(true)
} as any);
spyOn(component.versionsApi, 'deleteVersion').and.returnValue(Promise.resolve());
fixture.detectChanges();
component.deleteVersion(versionId);
expect(dialog.open).toHaveBeenCalled();
@@ -90,14 +91,12 @@ describe('VersionListComponent', () => {
});
it('should not delete version if user rejects', () => {
component.versions = versionTest;
spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(false)
} as any);
spyOn(component.versionsApi, 'deleteVersion').and.returnValue(Promise.resolve());
fixture.detectChanges();
component.deleteVersion(versionId);
expect(dialog.open).toHaveBeenCalled();
@@ -115,40 +114,35 @@ describe('VersionListComponent', () => {
});
describe('Version history fetching', () => {
it('should use loading bar', () => {
spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() => Promise.resolve({ list: { entries: versionTest } }));
it('should use loading bar', (done) => {
fixture.detectChanges();
let loadingProgressBar = fixture.debugElement.query(By.css('[data-automation-id="version-history-loading-bar"]'));
expect(loadingProgressBar).toBeNull();
component.ngOnChanges();
fixture.detectChanges();
component.versionsDataSource.isLoading.pipe(take(1)).subscribe(() => {
fixture.detectChanges();
loadingProgressBar = fixture.debugElement.query(By.css('[data-automation-id="version-history-loading-bar"]'));
expect(loadingProgressBar).not.toBeNull();
done();
});
loadingProgressBar = fixture.debugElement.query(By.css('[data-automation-id="version-history-loading-bar"]'));
expect(loadingProgressBar).not.toBeNull();
component.ngOnChanges();
});
it('should load the versions for a given id', () => {
spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() => Promise.resolve({ list: { entries: versionTest } }));
fixture.detectChanges();
spyOn(component.versionsDataSource, 'reset');
component.ngOnChanges();
fixture.detectChanges();
expect(component.versionsApi.listVersionHistory).toHaveBeenCalledWith(nodeId);
expect(component.versionsDataSource.reset).toHaveBeenCalled();
expect(component.versionsDataSource.getNextBatch).toHaveBeenCalled();
});
it('should show the versions after loading', (done) => {
fixture.detectChanges();
spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() =>
Promise.resolve(
new VersionPaging({
list: {
entries: [versionTest[0]]
}
})
)
);
component.ngOnChanges();
fixture.whenStable().then(() => {
@@ -165,16 +159,6 @@ describe('VersionListComponent', () => {
});
it('should NOT show the versions comments if input property is set not to show them', (done) => {
spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() =>
Promise.resolve(
new VersionPaging({
list: {
entries: [versionTest[0]]
}
})
)
);
component.showComments = false;
fixture.detectChanges();
@@ -190,9 +174,6 @@ describe('VersionListComponent', () => {
});
it('should be able to download a version', () => {
spyOn(component.versionsApi, 'listVersionHistory').and.returnValue(
Promise.resolve(new VersionPaging({ list: { entries: [versionTest[0]] } }))
);
spyOn(contentVersionService.contentApi, 'getContentUrl').and.returnValue('the/download/url');
fixture.detectChanges();
@@ -209,9 +190,6 @@ describe('VersionListComponent', () => {
});
it('should NOT be able to download a version if configured so', () => {
spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() =>
Promise.resolve(new VersionPaging({ list: { entries: [versionTest[0]] } }))
);
const spyOnDownload = spyOn(component.contentApi, 'getContentUrl').and.stub();
component.allowDownload = false;
@@ -232,10 +210,7 @@ describe('VersionListComponent', () => {
it('should load the versions for a given id', () => {
fixture.detectChanges();
component.versions = versionTest;
const spyOnRevertVersion = spyOn(component.versionsApi, 'revertVersion').and.callFake(() => Promise.resolve(versionTest[0]));
component.restore(versionId);
expect(spyOnRevertVersion).toHaveBeenCalledWith(nodeId, versionId, { majorVersion: true, comment: '' });
@@ -243,8 +218,6 @@ describe('VersionListComponent', () => {
it('should get node info after restoring the node', fakeAsync(() => {
fixture.detectChanges();
component.versions = versionTest;
spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() => Promise.resolve({ list: { entries: versionTest } }));
spyOn(component.versionsApi, 'revertVersion').and.callFake(() => Promise.resolve(versionTest[0]));
@@ -257,8 +230,6 @@ describe('VersionListComponent', () => {
it('should emit with node info data', fakeAsync(() => {
fixture.detectChanges();
component.versions = versionTest;
spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() => Promise.resolve({ list: { entries: versionTest } }));
spyOn(component.versionsApi, 'revertVersion').and.callFake(() => Promise.resolve(versionTest[0]));
@@ -273,18 +244,16 @@ describe('VersionListComponent', () => {
it('should reload the version list after a version restore', fakeAsync(() => {
fixture.detectChanges();
component.versions = versionTest;
const spyOnListVersionHistory = spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() =>
Promise.resolve({ list: { entries: versionTest } })
);
spyOn(component.versionsApi, 'revertVersion').and.callFake(() => Promise.resolve(null));
spyOn(component.versionsDataSource, 'reset');
component.restore(versionId);
fixture.detectChanges();
tick();
expect(spyOnListVersionHistory).toHaveBeenCalledTimes(1);
expect(component.versionsDataSource.reset).toHaveBeenCalled();
expect(component.versionsDataSource.getNextBatch).toHaveBeenCalled();
}));
});
@@ -302,15 +271,6 @@ describe('VersionListComponent', () => {
beforeEach(() => {
fixture.detectChanges();
versionTest[1].entry.id = '1.1';
spyOn(component.versionsApi, 'listVersionHistory').and.callFake(() =>
Promise.resolve(
new VersionPaging({
list: {
entries: versionTest
}
})
)
);
});
describe('showActions', () => {
@@ -320,8 +280,6 @@ describe('VersionListComponent', () => {
});
it('should show Actions if showActions is true', (done) => {
component.versions = versionTest;
component.showActions = true;
fixture.detectChanges();
@@ -16,12 +16,29 @@
*/
import { AlfrescoApiService } from '@alfresco/adf-core';
import { Component, Input, OnChanges, ViewEncapsulation, EventEmitter, Output } from '@angular/core';
import { VersionsApi, Node, VersionEntry, VersionPaging, NodesApi, NodeEntry, ContentApi } from '@alfresco/js-api';
import { Component, Input, OnChanges, ViewEncapsulation, EventEmitter, Output, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { VersionsApi, Node, VersionEntry, NodesApi, NodeEntry, ContentApi, ContentPagingQuery } from '@alfresco/js-api';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from '../dialogs/confirm.dialog';
import { ContentVersionService } from './content-version.service';
import { ContentService } from '../common/services/content.service';
import { InfiniteScrollDatasource } from '../infinite-scroll-datasource';
import { from, Observable, Subject } from 'rxjs';
import { map, take, takeUntil } from 'rxjs/operators';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
export class VersionListDataSource extends InfiniteScrollDatasource<VersionEntry> {
constructor(private versionsApi: VersionsApi, private node: Node) {
super();
}
getNextBatch(pagingOptions: ContentPagingQuery): Observable<VersionEntry[]> {
return from(this.versionsApi.listVersionHistory(this.node.id, pagingOptions)).pipe(
take(1),
map((versionPaging) => versionPaging.list.entries)
);
}
}
@Component({
selector: 'adf-version-list',
@@ -30,8 +47,8 @@ import { ContentService } from '../common/services/content.service';
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-version-list' }
})
export class VersionListComponent implements OnChanges {
export class VersionListComponent implements OnChanges, OnInit, OnDestroy {
private onDestroy$ = new Subject<void>();
private _contentApi: ContentApi;
get contentApi(): ContentApi {
this._contentApi = this._contentApi ?? new ContentApi(this.alfrescoApi.getInstance());
@@ -50,7 +67,8 @@ export class VersionListComponent implements OnChanges {
return this._nodesApi;
}
versions: VersionEntry[] = [];
versionsDataSource: VersionListDataSource;
latestVersion: VersionEntry;
isLoading = true;
/** The target node. */
@@ -85,34 +103,48 @@ export class VersionListComponent implements OnChanges {
@Output()
viewVersion = new EventEmitter<string>();
constructor(private alfrescoApi: AlfrescoApiService,
private contentService: ContentService,
private contentVersionService: ContentVersionService,
private dialog: MatDialog) {
@ViewChild('viewport')
viewport: CdkVirtualScrollViewport;
constructor(
private alfrescoApi: AlfrescoApiService,
private contentService: ContentService,
private contentVersionService: ContentVersionService,
private dialog: MatDialog
) {}
ngOnInit() {
this.versionsDataSource = new VersionListDataSource(this.versionsApi, this.node);
this.versionsDataSource.isLoading.pipe(takeUntil(this.onDestroy$)).subscribe((isLoading) => {
this.isLoading = isLoading;
this.latestVersion = this.versionsDataSource.firstItem;
});
}
ngOnChanges() {
this.loadVersionHistory();
if (this.versionsDataSource) {
this.loadVersionHistory();
}
}
ngOnDestroy() {
this.onDestroy$.next();
this.onDestroy$.complete();
}
canUpdate(): boolean {
return this.contentService.hasAllowableOperations(this.node, 'update') && this.versions.length > 1;
return this.contentService.hasAllowableOperations(this.node, 'update') && this.versionsDataSource.itemsCount > 1;
}
canDelete(): boolean {
return this.contentService.hasAllowableOperations(this.node, 'delete') && this.versions.length > 1;
return this.contentService.hasAllowableOperations(this.node, 'delete') && this.versionsDataSource.itemsCount > 1;
}
restore(versionId: string) {
if (this.canUpdate()) {
this.versionsApi
.revertVersion(this.node.id, versionId, { majorVersion: true, comment: '' })
.then(() =>
this.nodesApi.getNode(
this.node.id,
{ include: ['permissions', 'path', 'isFavorite', 'allowableOperations'] }
)
)
.then(() => this.nodesApi.getNode(this.node.id, { include: ['permissions', 'path', 'isFavorite', 'allowableOperations'] }))
.then((node) => this.onVersionRestored(node));
}
}
@@ -122,18 +154,16 @@ export class VersionListComponent implements OnChanges {
}
loadVersionHistory() {
this.isLoading = true;
this.versionsApi.listVersionHistory(this.node.id).then((versionPaging: VersionPaging) => {
this.versions = versionPaging.list.entries;
this.isLoading = false;
});
this.viewport.scrollToIndex(0);
this.versionsDataSource.reset();
}
downloadVersion(versionId: string) {
if (this.allowDownload) {
this.contentVersionService
.getVersionContentUrl(this.node.id, versionId, true)
.subscribe(versionDownloadUrl => this.downloadContent(versionDownloadUrl));
.pipe(takeUntil(this.onDestroy$))
.subscribe((versionDownloadUrl) => this.downloadContent(versionDownloadUrl));
}
}
@@ -149,13 +179,14 @@ export class VersionListComponent implements OnChanges {
minWidth: '250px'
});
dialogRef.afterClosed().subscribe((result) => {
if (result === true) {
this.versionsApi
.deleteVersion(this.node.id, versionId)
.then(() => this.onVersionDeleted(this.node));
}
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.onDestroy$))
.subscribe((result) => {
if (result) {
this.versionsApi.deleteVersion(this.node.id, versionId).then(() => this.onVersionDeleted(this.node));
}
});
}
}
@@ -10,7 +10,7 @@
id="adf-version-upload-button"
[node]="node"
[newFileVersion]="newFileVersion"
[currentVersion]="versionList?.versions[0]?.entry"
[currentVersion]="versionList?.latestVersion?.entry"
(success)="onUploadSuccess($event)"
(cancel)="onUploadCancel()"
(error)="onUploadError($event)">
@@ -55,7 +55,7 @@ describe('VersionManagerComponent', () => {
it('should load the versions for a given node', () => {
fixture.detectChanges();
expect(spyOnListVersionHistory).toHaveBeenCalledWith(node.id);
expect(spyOnListVersionHistory).toHaveBeenCalledWith(node.id, { skipCount: 0, maxItems: 100 });
});
it('should toggle new version if given a new file as input', () => {
@@ -27,6 +27,7 @@ import { UploadModule } from '../upload/upload.module';
import { VersionCompatibilityModule } from '../version-compatibility/version-compatibility.module';
import { CoreModule } from '@alfresco/adf-core';
import { VersionComparisonComponent } from './version-comparison.component';
import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
imports: [
@@ -35,7 +36,8 @@ import { VersionComparisonComponent } from './version-comparison.component';
CoreModule,
UploadModule,
VersionCompatibilityModule,
FormsModule
FormsModule,
ScrollingModule
],
exports: [
VersionUploadComponent,