mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
@@ -5,7 +5,6 @@
|
||||
[actions]="contentActions"
|
||||
[actionsPosition]="contentActionsPosition"
|
||||
[multiselect]="multiselect"
|
||||
[allowDropFiles]="allowDropFiles"
|
||||
[contextMenu]="contextMenuActions"
|
||||
[rowStyle]="rowStyle"
|
||||
[rowStyleClass]="rowStyleClass"
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -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-->
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}));
|
||||
});
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@
|
||||
<adf-version-upload
|
||||
id="adf-version-upload-button"
|
||||
[node]="node"
|
||||
[newFileVersion]="newFileVersion"
|
||||
(success)="onUploadSuccess($event)"
|
||||
(cancel)="onUploadCancel()"
|
||||
(error)="onUploadError($event)">
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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);
|
||||
|
@@ -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)">
|
||||
|
@@ -35,6 +35,9 @@ export class VersionUploadComponent {
|
||||
@Input()
|
||||
node: Node;
|
||||
|
||||
@Input()
|
||||
newFileVersion: File;
|
||||
|
||||
@Output()
|
||||
success = new EventEmitter();
|
||||
|
||||
|
Reference in New Issue
Block a user