[ACA-45] Drag and Drop a New Version (#5710)

* added a new input: file

* unit test - having singleFile option and a file as input, the type of the input for uploading should be button instead of file

* added a click event for the upload button and also handle if having a file as input, the type of the input should be 'button' instead of 'file'

* handling allowed for upload also for 'update' permissions over a dropped file. also emitting a new event for updating file version if i'm dropping a single file over another file.

* unit tests for handling dropping a file over another

* added a new input (file type)

* passing a file to adf-version-upload component

* new input as file and toggle new version if having that as input + unit test

* added new input as file for new version

* added new input to allow dropping a file over another to update it's version

* added a new variable for handling dropping a file over another one and also handle a new event when we update the file version

* pass a new dropped file to the dialog

* new message

* new method to allow isDropTarget for a file instead only to folders.

* new emitter for updating a file's version

* allows updating a file's version by dropping another file over it.

* refactor allowDropFiles

* update docs for drag&drop file into another file

* update for drag&drop a file over another file functionality

* made the allowDropFiles checking optional for isDropTarget property, only checking if the value is passed to the share-data-row

Co-authored-by: Eugenio Romano <eugenio.romano@alfresco.com>
This commit is contained in:
Urse Daniel
2020-05-20 13:57:17 +03:00
committed by GitHub
parent 531d1d7345
commit 4cb3a876d9
28 changed files with 185 additions and 53 deletions

View File

@@ -5,7 +5,6 @@
[actions]="contentActions"
[actionsPosition]="contentActionsPosition"
[multiselect]="multiselect"
[allowDropFiles]="allowDropFiles"
[contextMenu]="contextMenuActions"
[rowStyle]="rowStyle"
[rowStyleClass]="rowStyleClass"

View File

@@ -65,7 +65,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./document-list.component.scss'],
templateUrl: './document-list.component.html',
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-document-list' }
host: {class: 'adf-document-list'}
})
export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, AfterContentInit, PaginatedComponent, NavigableComponentInterface {
@@ -163,7 +163,8 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
/**
* When true, this enables you to drop files directly into subfolders shown
* as items in the list. When false, the dropped file will be added to the
* as items in the list or into another file to trigger updating it's version.
* When false, the dropped file will be added to the
* current folder (ie, the one containing all the items shown in the list).
* See the Upload directive for further details about how the file drop is
* handled.
@@ -380,7 +381,8 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
ngOnInit() {
this.rowMenuCache = {};
this.loadLayoutPresets();
this.data = new ShareDataTableAdapter(this.thumbnailService, this.contentService, null, this.getDefaultSorting(), this.sortingMode);
this.data = new ShareDataTableAdapter(this.thumbnailService, this.contentService, null, this.getDefaultSorting(),
this.sortingMode, this.allowDropFiles);
this.data.thumbnails = this.thumbnails;
this.data.permissionsStyle = this.permissionsStyle;
@@ -555,14 +557,14 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
if (typeof node === 'string') {
this.resetNewFolderPagination();
this._currentFolderId = node;
this.folderChange.emit(new NodeEntryEvent(<Node> { id: node }));
this.folderChange.emit(new NodeEntryEvent(<Node> {id: node}));
this.reload();
return true;
} else {
if (this.canNavigateFolder(node)) {
this.resetNewFolderPagination();
this._currentFolderId = this.getNodeFolderDestinationId(node);
this.folderChange.emit(new NodeEntryEvent(<Node> { id: this._currentFolderId }));
this.folderChange.emit(new NodeEntryEvent(<Node> {id: this._currentFolderId}));
this.reload();
return true;
}
@@ -651,7 +653,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
onPageLoaded(nodePaging: NodePaging) {
if (nodePaging) {
this.data.loadPage(nodePaging, this._pagination.merge);
this.data.loadPage(nodePaging, this._pagination.merge, this.allowDropFiles);
this.setLoadingState(false);
this.onDataReady(nodePaging);
}

View File

@@ -40,18 +40,22 @@ export class ShareDataRow implements DataRow {
constructor(private obj: NodeEntry,
private contentService: ContentService,
private permissionsStyle: PermissionStyleModel[],
private thumbnailService?: ThumbnailService) {
private thumbnailService?: ThumbnailService,
private allowDropFiles?: boolean) {
if (!obj) {
throw new Error(ShareDataRow.ERR_OBJECT_NOT_FOUND);
}
this.isDropTarget = this.isFolderAndHasPermissionToUpload(obj);
this.isDropTarget = allowDropFiles !== undefined ? this.allowDropFiles && this.checkNodeTypeAndPermissions(obj) : this.checkNodeTypeAndPermissions(obj);
if (permissionsStyle) {
this.cssClass = this.getPermissionClass(obj);
}
}
checkNodeTypeAndPermissions(nodeEntry: NodeEntry) {
return this.isFolderAndHasPermissionToUpload(nodeEntry) || this.isFileAndHasParentFolderPermissionToUpload(nodeEntry);
}
getPermissionClass(nodeEntity: NodeEntry): string {
let permissionsClasses = '';
@@ -81,6 +85,14 @@ export class ShareDataRow implements DataRow {
return this.isFolder(nodeEntry) && this.contentService.hasAllowableOperations(nodeEntry.entry, 'create');
}
isFileAndHasParentFolderPermissionToUpload(nodeEntry: NodeEntry): boolean {
return this.isFile(nodeEntry) && this.contentService.hasAllowableOperations(nodeEntry.entry, 'update');
}
isFile(nodeEntry: NodeEntry): boolean {
return nodeEntry.entry && nodeEntry.entry.isFile;
}
isFolder(nodeEntry: NodeEntry): boolean {
return nodeEntry.entry && nodeEntry.entry.isFolder;
}

View File

@@ -44,6 +44,7 @@ export class ShareDataTableAdapter implements DataTableAdapter {
thumbnails: boolean = false;
permissionsStyle: PermissionStyleModel[];
selectedRow: DataRow;
allowDropFiles: boolean;
set sortingMode(value: string) {
let newValue = (value || 'client').toLowerCase();
@@ -61,11 +62,13 @@ export class ShareDataTableAdapter implements DataTableAdapter {
private contentService: ContentService,
schema: DataColumn[] = [],
sorting?: DataSorting,
sortingMode: string = 'client') {
sortingMode: string = 'client',
allowDropFiles: boolean = false) {
this.rows = [];
this.columns = schema || [];
this.sorting = sorting;
this.sortingMode = sortingMode;
this.allowDropFiles = allowDropFiles;
}
getRows(): Array<DataRow> {
@@ -242,13 +245,16 @@ export class ShareDataTableAdapter implements DataTableAdapter {
}
}
public loadPage(nodePaging: NodePaging, merge: boolean = false) {
public loadPage(nodePaging: NodePaging, merge: boolean = false, allowDropFiles?: boolean) {
let shareDataRows: ShareDataRow[] = [];
if (allowDropFiles !== undefined) {
this.allowDropFiles = allowDropFiles;
}
if (nodePaging && nodePaging.list) {
const nodeEntries: NodeEntry[] = nodePaging.list.entries;
if (nodeEntries && nodeEntries.length > 0) {
shareDataRows = nodeEntries.map((item) => new ShareDataRow(item, this.contentService, this.permissionsStyle, this.thumbnailService));
shareDataRows = nodeEntries.map((item) => new ShareDataRow(item, this.contentService, this.permissionsStyle,
this.thumbnailService, this.allowDropFiles));
if (this.filter) {
shareDataRows = shareDataRows.filter(this.filter);

View File

@@ -71,6 +71,10 @@ export abstract class UploadBase implements OnInit, OnDestroy {
@Output()
beginUpload = new EventEmitter<UploadFilesEvent>();
/** Emitted when dropping a file over another file to update the version. */
@Output()
updateFileVersion = new EventEmitter<CustomEvent>();
protected onDestroy$ = new Subject<boolean>();
constructor(protected uploadService: UploadService,

View File

@@ -15,12 +15,13 @@
<input #uploadSingleFile
id="upload-single-file"
data-automation-id="upload-single-file"
type="file"
[type]="file ? 'button' : 'file'"
name="uploadFiles"
accept="{{acceptedFilesType}}"
[attr.disabled]="isButtonDisabled()"
[title]="tooltip"
(change)="onFilesAdded($event)">
(change)="onFilesAdded($event)"
(click)="onClickUploadButton()">
</button>
<!--Multiple Files Upload-->

View File

@@ -98,6 +98,19 @@ describe('UploadButtonComponent', () => {
expect(compiled.querySelector('#uploadFolder')).toBeDefined();
});
it('should have input type as button if receiving a file as input', () => {
component.multipleFiles = false;
component.file = new File([], 'Fake file name');
const compiled = fixture.debugElement.nativeElement;
fixture.detectChanges();
const inputButton = compiled.querySelector('#upload-single-file');
expect(inputButton.type).toBe('button');
component.file = undefined;
fixture.detectChanges();
expect(inputButton.type).toBe('file');
});
it('should disable uploadFolder button if disabled is true', () => {
component.disabled = true;
component.uploadFolders = true;

View File

@@ -55,6 +55,10 @@ export class UploadButtonComponent extends UploadBase implements OnInit, OnChang
@Input()
tooltip: string = null;
/** Custom added file. The upload button type will be 'button' instead of 'file' */
@Input()
file: File;
/** Emitted when create permission is missing. */
@Output()
permissionEvent: EventEmitter<PermissionModel> = new EventEmitter<PermissionModel>();
@@ -100,6 +104,16 @@ export class UploadButtonComponent extends UploadBase implements OnInit, OnChang
$event.target.value = '';
}
onClickUploadButton(): void {
const files: File[] = [this.file];
if (this.hasAllowableOperations) {
this.uploadFiles(files);
} else {
this.permissionEvent.emit(new PermissionModel({ type: 'content', action: 'upload', permission: 'create' }));
}
}
onDirectoryAdded($event: any): void {
if (this.hasAllowableOperations) {
const files: File[] = FileUtils.toFileArray($event.currentTarget.files);

View File

@@ -264,6 +264,34 @@ describe('UploadDragAreaComponent', () => {
});
}));
it('should NOT upload the file if it is dropped on another file', () => {
const fakeItem = {
fullPath: '/folder-fake/file-fake.png',
isDirectory: false,
isFile: true,
name: 'file-fake.png',
relativeFolder: '/',
file: (callbackFile) => {
const fileFake = new File(['fakefake'], 'file-fake.png', { type: 'image/png' });
callbackFile(fileFake);
}
};
addToQueueSpy.and.callFake((fileList) => {
expect(fileList.name).toBe('file');
expect(fileList.options.path).toBe('pippo/');
});
const fakeCustomEvent: CustomEvent = new CustomEvent('CustomEvent', {
detail: {
data: getFakeShareDataRow(),
files: [fakeItem]
}
});
component.onUploadFiles(fakeCustomEvent);
});
it('should not upload a file if fileType is not in acceptedFilesType', async(() => {
component.success = null;
component.acceptedFilesType = '.pdf';
@@ -367,8 +395,8 @@ describe('UploadDragAreaComponent', () => {
component.onUploadFiles(fakeCustomEvent);
}));
it('should upload the file in the current folder when the target is file', async(() => {
it('should trigger updating the file version when we drop a file over another file', async(() => {
spyOn(component.updateFileVersion, 'emit');
const fakeItem = {
fullPath: '/folder-fake/file-fake.png',
isDirectory: false,
@@ -394,6 +422,7 @@ describe('UploadDragAreaComponent', () => {
});
component.onUploadFiles(fakeCustomEvent);
expect(component.updateFileVersion.emit).toHaveBeenCalledWith(fakeCustomEvent);
}));
});

View File

@@ -26,9 +26,9 @@ import { UploadBase } from './base-upload/upload-base';
selector: 'adf-upload-drag-area',
templateUrl: './upload-drag-area.component.html',
styleUrls: ['./upload-drag-area.component.scss'],
host: { 'class': 'adf-upload-drag-area' },
host: {'class': 'adf-upload-drag-area'},
viewProviders: [
{ provide: EXTENDIBLE_COMPONENT, useExisting: forwardRef(() => UploadDragAreaComponent) }
{provide: EXTENDIBLE_COMPONENT, useExisting: forwardRef(() => UploadDragAreaComponent)}
],
encapsulation: ViewEncapsulation.None
})
@@ -94,15 +94,21 @@ export class UploadDragAreaComponent extends UploadBase implements NodeAllowable
onUploadFiles(event: CustomEvent) {
event.stopPropagation();
event.preventDefault();
const isAllowed: boolean = this.contentService.hasAllowableOperations(event.detail.data.obj.entry, AllowableOperationsEnum.CREATE);
const isAllowed: boolean = this.isTargetNodeFolder(event) ?
this.contentService.hasAllowableOperations(event.detail.data.obj.entry, AllowableOperationsEnum.CREATE)
: this.contentService.hasAllowableOperations(event.detail.data.obj.entry, AllowableOperationsEnum.UPDATE);
if (isAllowed) {
const fileInfo: FileInfo[] = event.detail.files;
if (this.isTargetNodeFolder(event)) {
const destinationFolderName = event.detail.data.obj.entry.name;
fileInfo.map((file) => file.relativeFolder = destinationFolderName ? destinationFolderName.concat(file.relativeFolder) : file.relativeFolder);
}
if (fileInfo && fileInfo.length > 0) {
this.uploadFilesInfo(fileInfo);
if (!this.isTargetNodeFolder(event) && event.detail.files.length === 1) {
this.updateFileVersion.emit(event);
} else {
const fileInfo: FileInfo[] = event.detail.files;
if (this.isTargetNodeFolder(event)) {
const destinationFolderName = event.detail.data.obj.entry.name;
fileInfo.map((file) => file.relativeFolder = destinationFolderName ? destinationFolderName.concat(file.relativeFolder) : file.relativeFolder);
}
if (fileInfo && fileInfo.length > 0) {
this.uploadFilesInfo(fileInfo);
}
}
}
}

View File

@@ -6,6 +6,7 @@
<adf-version-upload
id="adf-version-upload-button"
[node]="node"
[newFileVersion]="newFileVersion"
(success)="onUploadSuccess($event)"
(cancel)="onUploadCancel()"
(error)="onUploadError($event)">

View File

@@ -74,6 +74,12 @@ describe('VersionManagerComponent', () => {
expect(spyOnListVersionHistory).toHaveBeenCalledWith(node.id);
});
it('should toggle new version if given a new file as input', () => {
component.newFileVersion = new File([], 'New file version');
fixture.detectChanges();
expect(component.uploadState).toBe('open');
});
it('should display comments for versions when not configured otherwise', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, Input, ViewEncapsulation, ViewChild, Output, EventEmitter } from '@angular/core';
import { Component, Input, ViewEncapsulation, ViewChild, Output, EventEmitter, OnInit } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { VersionListComponent } from './version-list.component';
import { ContentService, AlfrescoApiService } from '@alfresco/adf-core';
@@ -27,26 +27,30 @@ import { trigger, state, style, animate, transition } from '@angular/animations'
styleUrls: ['./version-manager.component.scss'],
animations: [
trigger('uploadToggle', [
state('open', style({ height: '175px', opacity: 1, visibility: 'visible' })),
state('close', style({ height: '0%', opacity: 0, visibility: 'hidden' })),
state('open', style({height: '175px', opacity: 1, visibility: 'visible'})),
state('close', style({height: '0%', opacity: 0, visibility: 'hidden'})),
transition('open => close', [
style({ visibility: 'hidden' }),
style({visibility: 'hidden'}),
animate('0.4s cubic-bezier(0.25, 0.8, 0.25, 1)')
]),
transition('close => open', [
style({ visibility: 'visible' }),
style({visibility: 'visible'}),
animate('0.4s cubic-bezier(0.25, 0.8, 0.25, 1)')
])
])
],
encapsulation: ViewEncapsulation.None
})
export class VersionManagerComponent {
export class VersionManagerComponent implements OnInit {
/** Target node to manage version history. */
@Input()
node: Node;
/** New file for updating current version. */
@Input()
newFileVersion: File;
/** Toggles showing/hiding of comments. */
@Input()
showComments = true;
@@ -72,6 +76,12 @@ export class VersionManagerComponent {
private alfrescoApiService: AlfrescoApiService) {
}
ngOnInit() {
if (this.newFileVersion) {
this.toggleNewVersion();
}
}
refresh(node: Node) {
this.alfrescoApiService.nodeUpdated.next(node);
this.versionListComponent.loadVersionHistory();
@@ -80,6 +90,7 @@ export class VersionManagerComponent {
}
onUploadSuccess(event: any) {
this.newFileVersion = null;
this.alfrescoApiService.nodeUpdated.next(event.value.entry);
this.versionListComponent.loadVersionHistory();
this.uploadSuccess.emit(event.value.entry);

View File

@@ -25,6 +25,7 @@
tooltip="{{ 'ADF_VERSION_LIST.ACTIONS.UPLOAD.TOOLTIP' | translate }}"
[comment]="comment"
[versioning]="true"
[file]="newFileVersion"
[majorVersion]="isMajorVersion()"
(success)="success.emit($event)"
(error)="error.emit($event)">

View File

@@ -35,6 +35,9 @@ export class VersionUploadComponent {
@Input()
node: Node;
@Input()
newFileVersion: File;
@Output()
success = new EventEmitter();