Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div class="flex flex-column gap-2">
<div
[innerHTML]="'files.dialogs.moveFile.message' | translate: { dropNodeName: currentFolder.name, dragNodeName }"
></div>
<div class="flex justify-content-end gap-2 mt-4">
<p-button severity="info" [label]="'common.buttons.cancel' | translate" (onClick)="dialogRef.close()"></p-button>

<p-button [label]="'common.buttons.move' | translate" (onClick)="moveFiles()"></p-button>
<p-button [label]="'common.buttons.copy' | translate" (onClick)="copyFiles()"></p-button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { TranslatePipe } from '@ngx-translate/core';
import { MockComponents, MockPipe } from 'ng-mocks';

import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';

import { ComponentFixture, TestBed } from '@angular/core/testing';

import { FileSelectDestinationComponent } from '@osf/shared/components/file-select-destination/file-select-destination.component';
import { IconComponent } from '@osf/shared/components/icon/icon.component';
import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component';
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
import { FilesService } from '@osf/shared/services/files.service';
import { ToastService } from '@osf/shared/services/toast.service';

import { FilesSelectors } from '../../store';

import { ConfirmMoveFileDialogComponent } from './confirm-move-file-dialog.component';

import { OSFTestingModule } from '@testing/osf.testing.module';
import { CustomConfirmationServiceMock } from '@testing/providers/custom-confirmation-provider.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';
import { ToastServiceMock } from '@testing/providers/toast-provider.mock';

describe('ConfirmConfirmMoveFileDialogComponent', () => {
let component: ConfirmMoveFileDialogComponent;
let fixture: ComponentFixture<ConfirmMoveFileDialogComponent>;

const mockFilesService = {
moveFiles: jest.fn(),
getMoveDialogFiles: jest.fn(),
};

beforeEach(async () => {
const dialogRefMock = {
close: jest.fn(),
};

const dialogConfigMock = {
data: { files: [], destination: { name: 'files' } },
};

await TestBed.configureTestingModule({
imports: [
ConfirmMoveFileDialogComponent,
OSFTestingModule,
...MockComponents(IconComponent, LoadingSpinnerComponent, FileSelectDestinationComponent),
MockPipe(TranslatePipe),
],
providers: [
{ provide: DynamicDialogRef, useValue: dialogRefMock },
{ provide: DynamicDialogConfig, useValue: dialogConfigMock },
{ provide: FilesService, useValue: mockFilesService },
{ provide: ToastService, useValue: ToastServiceMock.simple() },
{ provide: CustomConfirmationService, useValue: CustomConfirmationServiceMock.simple() },
provideMockStore({
signals: [
{ selector: FilesSelectors.getMoveDialogFiles, value: [] },
{ selector: FilesSelectors.getProvider, value: null },
],
}),
],
}).compileComponents();

fixture = TestBed.createComponent(ConfirmMoveFileDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should initialize with correct properties', () => {
expect(component.config).toBeDefined();
expect(component.dialogRef).toBeDefined();
expect(component.files).toBeDefined();
});

it('should get files from store', () => {
expect(component.files()).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { select } from '@ngxs/store';

import { TranslatePipe, TranslateService } from '@ngx-translate/core';

import { Button } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';

import { finalize, forkJoin, of } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

import { FilesSelectors } from '@osf/features/files/store';
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
import { FilesService } from '@osf/shared/services/files.service';
import { ToastService } from '@osf/shared/services/toast.service';
import { FileMenuType } from '@shared/enums/file-menu-type.enum';
import { FileModel } from '@shared/models/files/file.model';

@Component({
selector: 'osf-move-file-dialog',
imports: [Button, TranslatePipe],
templateUrl: './confirm-move-file-dialog.component.html',
styleUrl: './confirm-move-file-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmMoveFileDialogComponent {
readonly config = inject(DynamicDialogConfig);
readonly dialogRef = inject(DynamicDialogRef);
private readonly filesService = inject(FilesService);
private readonly destroyRef = inject(DestroyRef);
private readonly translateService = inject(TranslateService);
private readonly toastService = inject(ToastService);
private readonly customConfirmationService = inject(CustomConfirmationService);

readonly files = select(FilesSelectors.getMoveDialogFiles);
readonly provider = this.config.data.storageProvider;

private fileProjectId = this.config.data.resourceId;
protected currentFolder = this.config.data.destination;

get dragNodeName() {
const filesCount = this.config.data.files.length;
if (filesCount > 1) {
return this.translateService.instant('files.dialogs.moveFile.multipleFiles', { count: filesCount });
} else {
return this.config.data.files[0]?.name;
}
}

copyFiles(): void {
return this.copyOrMoveFiles(FileMenuType.Copy);
}

moveFiles(): void {
return this.copyOrMoveFiles(FileMenuType.Move);
}

private copyOrMoveFiles(action: FileMenuType): void {
const path = this.currentFolder.path;
if (!path) {
throw new Error(this.translateService.instant('files.dialogs.moveFile.pathError'));
}
const isMoveAction = action === FileMenuType.Move;

const headerKey = isMoveAction ? 'files.dialogs.moveFile.movingHeader' : 'files.dialogs.moveFile.copingHeader';
this.config.header = this.translateService.instant(headerKey);
const files: FileModel[] = this.config.data.files;
const totalFiles = files.length;
let completed = 0;
const conflictFiles: { file: FileModel; link: string }[] = [];

files.forEach((file) => {
const link = file.links.move;
this.filesService
.moveFile(link, path, this.fileProjectId, this.provider(), action)
.pipe(
takeUntilDestroyed(this.destroyRef),
catchError((error) => {
if (error.status === 409) {
conflictFiles.push({ file, link });
} else {
this.showErrorToast(action, error.error?.message);
}
return of(null);
}),
finalize(() => {
completed++;
if (completed === totalFiles) {
if (conflictFiles.length > 0) {
this.openReplaceMoveDialog(conflictFiles, path, action);
} else {
this.showSuccessToast(action);
this.config.header = this.translateService.instant('files.dialogs.moveFile.title');
this.completeMove();
}
}
})
)
.subscribe();
});
}

private openReplaceMoveDialog(
conflictFiles: { file: FileModel; link: string }[],
path: string,
action: string
): void {
this.customConfirmationService.confirmDelete({
headerKey: conflictFiles.length > 1 ? 'files.dialogs.replaceFile.multiple' : 'files.dialogs.replaceFile.single',
messageKey: 'files.dialogs.replaceFile.message',
messageParams: {
name: conflictFiles.map((c) => c.file.name).join(', '),
},
acceptLabelKey: 'common.buttons.replace',
onConfirm: () => {
const replaceRequests$ = conflictFiles.map(({ link }) =>
this.filesService.moveFile(link, path, this.fileProjectId, this.provider(), action, true).pipe(
takeUntilDestroyed(this.destroyRef),
catchError(() => of(null))
)
);
forkJoin(replaceRequests$).subscribe({
next: () => {
this.showSuccessToast(action);
this.completeMove();
},
});
},
onReject: () => {
const totalFiles = this.config.data.files.length;
if (totalFiles > conflictFiles.length) {
this.showErrorToast(action);
}
this.completeMove();
},
});
}

private showSuccessToast(action: string) {
const messageType = action === 'move' ? 'moveFile' : 'copyFile';
this.toastService.showSuccess(`files.dialogs.${messageType}.success`);
}

private showErrorToast(action: string, errorMessage?: string) {
const messageType = action === 'move' ? 'moveFile' : 'copyFile';
this.toastService.showError(errorMessage ?? `files.dialogs.${messageType}.error`);
}

private completeMove(): void {
this.dialogRef.close(true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { MockComponents, MockPipe } from 'ng-mocks';

import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';

import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { FileSelectDestinationComponent } from '@osf/shared/components/file-select-destination/file-select-destination.component';
Expand Down Expand Up @@ -56,14 +55,14 @@ describe('MoveFileDialogComponent', () => {
{ provide: CustomConfirmationService, useValue: CustomConfirmationServiceMock.simple() },
provideMockStore({
signals: [
{ selector: FilesSelectors.getMoveDialogFiles, value: signal([]) },
{ selector: FilesSelectors.getMoveDialogFilesTotalCount, value: signal(0) },
{ selector: FilesSelectors.isMoveDialogFilesLoading, value: signal(false) },
{ selector: FilesSelectors.getMoveDialogCurrentFolder, value: signal(null) },
{ selector: CurrentResourceSelectors.getCurrentResource, value: signal(null) },
{ selector: CurrentResourceSelectors.getResourceWithChildren, value: signal([]) },
{ selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: signal(false) },
{ selector: FilesSelectors.isMoveDialogConfiguredStorageAddonsLoading, value: signal(false) },
{ selector: FilesSelectors.getMoveDialogFiles, value: [] },
{ selector: FilesSelectors.getMoveDialogFilesTotalCount, value: 0 },
{ selector: FilesSelectors.isMoveDialogFilesLoading, value: false },
{ selector: FilesSelectors.getMoveDialogCurrentFolder, value: null },
{ selector: CurrentResourceSelectors.getCurrentResource, value: null },
{ selector: CurrentResourceSelectors.getResourceWithChildren, value: [] },
{ selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false },
{ selector: FilesSelectors.isMoveDialogConfiguredStorageAddonsLoading, value: false },
],
}),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export class MoveFileDialogComponent {
if (error.status === 409) {
conflictFiles.push({ file, link });
} else {
this.toastService.showError(error.error?.message ?? 'Error');
this.showErrorToast(action, error.error?.message ?? 'Error');
}
return of(null);
}),
Expand All @@ -221,7 +221,7 @@ export class MoveFileDialogComponent {
if (conflictFiles.length > 0) {
this.openReplaceMoveDialog(conflictFiles, path, action);
} else {
this.showToast(action);
this.showSuccessToast(action);
this.config.header = this.translateService.instant('files.dialogs.moveFile.title');
this.completeMove();
}
Expand Down Expand Up @@ -254,26 +254,31 @@ export class MoveFileDialogComponent {

forkJoin(replaceRequests$).subscribe({
next: () => {
this.showToast(action);
this.showSuccessToast(action);
this.completeMove();
},
});
},
onReject: () => {
const totalFiles = this.config.data.files.length;
if (totalFiles > conflictFiles.length) {
this.showToast(action);
this.showErrorToast(action);
}
this.completeMove();
},
});
}

private showToast(action: string): void {
private showSuccessToast(action: string) {
const messageType = action === 'move' ? 'moveFile' : 'copyFile';
this.toastService.showSuccess(`files.dialogs.${messageType}.success`);
}

private showErrorToast(action: string, errorMessage?: string) {
const messageType = action === 'move' ? 'moveFile' : 'copyFile';
this.toastService.showError(errorMessage ?? `files.dialogs.${messageType}.error`);
}

private completeMove(): void {
this.isFilesUpdating.set(false);
this.actions.setCurrentFolder(this.initialFolder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
<div class="files-table flex">
<p-tree
[value]="nodes()"
[draggableNodes]="true"
[droppableNodes]="true"
(onScrollIndexChange)="onScrollIndexChange($event)"
[scrollHeight]="scrollHeight()"
[virtualScroll]="true"
Expand All @@ -36,6 +38,7 @@
[selection]="selectedFiles()"
(onNodeSelect)="onNodeSelect($event)"
(onNodeUnselect)="onNodeUnselect($event)"
(onNodeDrop)="onNodeDrop($event)"
>
<ng-template let-file pTemplate="default">
@if (file.previousFolder) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MockComponents, MockProvider } from 'ng-mocks';

import { TreeDragDropService } from 'primeng/api';
import { DialogService } from 'primeng/dynamicdialog';

import { signal } from '@angular/core';
Expand Down Expand Up @@ -53,6 +54,7 @@ describe('FilesTreeComponent', () => {
MockProvider(ToastService),
MockProvider(CustomConfirmationService),
MockProvider(DialogService),
TreeDragDropService,
],
}).compileComponents();

Expand Down
Loading