Skip to content

Commit 589ecd6

Browse files
committed
refactor(files): updated files tree and related logic
1 parent 45244d4 commit 589ecd6

70 files changed

Lines changed: 2358 additions & 1403 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
[innerHTML]="'files.dialogs.moveFile.message' | translate: { dropNodeName: currentFolder.name, dragNodeName }"
44
></div>
55
<div class="flex justify-content-end gap-2 mt-4">
6-
<p-button severity="info" [label]="'common.buttons.cancel' | translate" (onClick)="dialogRef.close()"></p-button>
6+
<p-button
7+
severity="info"
8+
[disabled]="isLoading()"
9+
[label]="'common.buttons.cancel' | translate"
10+
(onClick)="dialogRef.close()"
11+
></p-button>
712

8-
<p-button [label]="'common.buttons.move' | translate" (onClick)="moveFiles()"></p-button>
9-
<p-button [label]="'common.buttons.copy' | translate" (onClick)="copyFiles()"></p-button>
13+
<p-button [disabled]="isLoading()" [label]="'common.buttons.move' | translate" (onClick)="moveFiles()"></p-button>
14+
<p-button [disabled]="isLoading()" [label]="'common.buttons.copy' | translate" (onClick)="copyFiles()"></p-button>
1015
</div>
1116
</div>
Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,147 @@
11
import { MockProvider } from 'ng-mocks';
22

3-
import { DynamicDialogConfig } from 'primeng/dynamicdialog';
3+
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
4+
5+
import { Subject } from 'rxjs';
46

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

7-
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
8-
import { FilesService } from '@osf/shared/services/files.service';
9-
import { ToastService } from '@osf/shared/services/toast.service';
9+
import { FileModel } from '@osf/shared/models/files/file.model';
1010

11+
import { FileModelMock } from '@testing/mocks/file.model.mock';
1112
import { provideOSFCore } from '@testing/osf.testing.provider';
12-
import { CustomConfirmationServiceMock } from '@testing/providers/custom-confirmation-provider.mock';
1313
import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock';
14-
import { ToastServiceMock } from '@testing/providers/toast-provider.mock';
14+
import {
15+
FilesMoveCopyServiceMock,
16+
FilesMoveCopyServiceMockType,
17+
} from '@testing/providers/files-move-copy-service.mock';
18+
19+
import { MoveCopyAction } from '../../enums/move-copy-action.enum';
20+
import { ConfirmMoveFilesOptions } from '../../models/files-actions-options.model';
21+
import { FilesMoveCopyService } from '../../services/files-move-copy.service';
1522

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

18-
describe('ConfirmConfirmMoveFileDialogComponent', () => {
25+
describe('ConfirmMoveFileDialogComponent', () => {
1926
let component: ConfirmMoveFileDialogComponent;
2027
let fixture: ComponentFixture<ConfirmMoveFileDialogComponent>;
28+
let dialogRefMock: DynamicDialogRef;
29+
let dialogConfigMock: DynamicDialogConfig & { data: ConfirmMoveFilesOptions };
30+
let filesMoveCopyService: FilesMoveCopyServiceMockType;
31+
32+
interface SetupOverrides {
33+
files?: FileModel[];
34+
destination?: FileModel;
35+
}
2136

22-
beforeEach(() => {
23-
const dialogConfigMock = {
24-
data: { files: [], destination: { name: 'files' } },
37+
function setup(overrides: SetupOverrides = {}) {
38+
const defaultFile = FileModelMock.simple({ name: 'a.txt' });
39+
const defaultDestination = FileModelMock.simple({ name: 'folder' });
40+
const files = overrides.files ?? [defaultFile];
41+
const destination = overrides.destination ?? defaultDestination;
42+
43+
const data: ConfirmMoveFilesOptions = {
44+
files,
45+
destination,
46+
resourceId: 'resource-1',
47+
storageProvider: 'osfstorage',
2548
};
2649

50+
filesMoveCopyService = FilesMoveCopyServiceMock.simple();
51+
dialogConfigMock = { header: 'files.dialogs.moveFile.title', data };
52+
2753
TestBed.configureTestingModule({
2854
imports: [ConfirmMoveFileDialogComponent],
2955
providers: [
3056
provideOSFCore(),
3157
provideDynamicDialogRefMock(),
3258
MockProvider(DynamicDialogConfig, dialogConfigMock),
33-
MockProvider(FilesService),
34-
MockProvider(ToastService, ToastServiceMock.simple()),
35-
MockProvider(CustomConfirmationService, CustomConfirmationServiceMock.simple()),
59+
MockProvider(FilesMoveCopyService, filesMoveCopyService),
3660
],
3761
});
3862

3963
fixture = TestBed.createComponent(ConfirmMoveFileDialogComponent);
4064
component = fixture.componentInstance;
65+
dialogRefMock = TestBed.inject(DynamicDialogRef);
4166
fixture.detectChanges();
42-
});
67+
}
4368

4469
it('should create', () => {
70+
setup();
4571
expect(component).toBeTruthy();
4672
});
4773

48-
it('should initialize with correct properties', () => {
49-
expect(component.config).toBeDefined();
50-
expect(component.dialogRef).toBeDefined();
74+
it('should read currentFolder from dialog data', () => {
75+
const dest = FileModelMock.simple({ name: 'target' });
76+
setup({ destination: dest });
77+
expect(component.currentFolder).toBe(dest);
78+
});
79+
80+
it('should set dragNodeName to the file name when one file is selected', () => {
81+
setup({ files: [FileModelMock.simple({ name: 'readme.md' })] });
82+
expect(component.dragNodeName).toBe('readme.md');
83+
});
84+
85+
it('should close without a result when cancel is clicked', () => {
86+
setup();
87+
const buttons = fixture.nativeElement.querySelectorAll('button');
88+
buttons[0].click();
89+
expect(dialogRefMock.close).toHaveBeenCalledWith();
90+
});
91+
92+
it('should call move copy service with move action and close with true on success', () => {
93+
const file = FileModelMock.simple({ name: 'a.txt' });
94+
const dest = FileModelMock.simple({ name: 'folder' });
95+
setup({ files: [file], destination: dest });
96+
component.moveFiles();
97+
expect(filesMoveCopyService.execute).toHaveBeenCalledWith({
98+
files: [file],
99+
destination: dest,
100+
resourceId: 'resource-1',
101+
storageProvider: 'osfstorage',
102+
action: MoveCopyAction.Move,
103+
});
104+
expect(dialogRefMock.close).toHaveBeenCalledWith(true);
105+
expect(component.isLoading()).toBe(false);
106+
});
107+
108+
it('should call move copy service with copy action and close with true on success', () => {
109+
const file = FileModelMock.simple({ name: 'a.txt' });
110+
const dest = FileModelMock.simple({ name: 'folder' });
111+
setup({ files: [file], destination: dest });
112+
component.copyFiles();
113+
expect(filesMoveCopyService.execute).toHaveBeenCalledWith({
114+
files: [file],
115+
destination: dest,
116+
resourceId: 'resource-1',
117+
storageProvider: 'osfstorage',
118+
action: MoveCopyAction.Copy,
119+
});
120+
expect(dialogRefMock.close).toHaveBeenCalledWith(true);
121+
expect(component.isLoading()).toBe(false);
122+
});
123+
124+
it('should ignore a second move while the first is in progress', () => {
125+
setup();
126+
const pending = new Subject<boolean>();
127+
filesMoveCopyService.execute.mockReturnValue(pending.asObservable());
128+
component.moveFiles();
129+
component.moveFiles();
130+
expect(filesMoveCopyService.execute).toHaveBeenCalledTimes(1);
131+
pending.next(true);
132+
pending.complete();
133+
fixture.detectChanges();
134+
});
135+
136+
it('should keep loading true until move finishes', () => {
137+
setup();
138+
const pending = new Subject<boolean>();
139+
filesMoveCopyService.execute.mockReturnValue(pending.asObservable());
140+
component.moveFiles();
141+
expect(component.isLoading()).toBe(true);
142+
pending.next(true);
143+
pending.complete();
144+
fixture.detectChanges();
145+
expect(component.isLoading()).toBe(false);
51146
});
52147
});

src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.ts

Lines changed: 39 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core';
33
import { Button } from 'primeng/button';
44
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
55

6-
import { finalize, forkJoin, of } from 'rxjs';
7-
import { catchError } from 'rxjs/operators';
6+
import { finalize, tap } from 'rxjs';
87

9-
import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core';
8+
import { ChangeDetectionStrategy, Component, DestroyRef, inject, signal } from '@angular/core';
109
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1110

12-
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
13-
import { FilesService } from '@osf/shared/services/files.service';
14-
import { ToastService } from '@osf/shared/services/toast.service';
15-
import { FileMenuType } from '@shared/enums/file-menu-type.enum';
16-
import { FileModel } from '@shared/models/files/file.model';
11+
import { FileModel } from '@osf/shared/models/files/file.model';
12+
13+
import { MoveCopyAction } from '../../enums/move-copy-action.enum';
14+
import { FilesMoveCopyService } from '../../services/files-move-copy.service';
1715

1816
@Component({
1917
selector: 'osf-confirm-move-file-dialog',
@@ -26,124 +24,54 @@ export class ConfirmMoveFileDialogComponent {
2624
readonly config = inject(DynamicDialogConfig);
2725
readonly dialogRef = inject(DynamicDialogRef);
2826

29-
private readonly filesService = inject(FilesService);
3027
private readonly destroyRef = inject(DestroyRef);
3128
private readonly translateService = inject(TranslateService);
32-
private readonly toastService = inject(ToastService);
33-
private readonly customConfirmationService = inject(CustomConfirmationService);
29+
private readonly filesMoveCopyService = inject(FilesMoveCopyService);
3430

35-
readonly provider = this.config.data.storageProvider;
31+
readonly currentFolder = this.config.data.destination as FileModel;
3632

37-
private fileProjectId = this.config.data.resourceId;
38-
protected currentFolder = this.config.data.destination;
33+
readonly dragNodeName =
34+
this.config.data.files.length > 1
35+
? this.translateService.instant('files.dialogs.moveFile.multipleFiles', { count: this.config.data.files.length })
36+
: (this.config.data.files[0]?.name ?? '');
3937

40-
get dragNodeName() {
41-
const filesCount = this.config.data.files.length;
42-
if (filesCount > 1) {
43-
return this.translateService.instant('files.dialogs.moveFile.multipleFiles', { count: filesCount });
44-
} else {
45-
return this.config.data.files[0]?.name;
46-
}
47-
}
38+
readonly isLoading = signal(false);
4839

4940
copyFiles(): void {
50-
return this.copyOrMoveFiles(FileMenuType.Copy);
41+
this.copyOrMoveFiles(MoveCopyAction.Copy);
5142
}
5243

5344
moveFiles(): void {
54-
return this.copyOrMoveFiles(FileMenuType.Move);
45+
this.copyOrMoveFiles(MoveCopyAction.Move);
5546
}
5647

57-
private copyOrMoveFiles(action: FileMenuType): void {
58-
const path = this.currentFolder.path;
59-
if (!path) {
60-
throw new Error(this.translateService.instant('files.dialogs.moveFile.pathError'));
48+
private copyOrMoveFiles(action: MoveCopyAction): void {
49+
if (this.isLoading()) {
50+
return;
6151
}
62-
const isMoveAction = action === FileMenuType.Move;
6352

64-
const headerKey = isMoveAction ? 'files.dialogs.moveFile.movingHeader' : 'files.dialogs.moveFile.copingHeader';
65-
this.config.header = this.translateService.instant(headerKey);
66-
const files: FileModel[] = this.config.data.files;
67-
const totalFiles = files.length;
68-
let completed = 0;
69-
const conflictFiles: { file: FileModel; link: string }[] = [];
70-
71-
files.forEach((file) => {
72-
const link = file.links.move;
73-
this.filesService
74-
.moveFile(link, path, this.fileProjectId, this.provider, action)
75-
.pipe(
76-
takeUntilDestroyed(this.destroyRef),
77-
catchError((error) => {
78-
if (error.status === 409) {
79-
conflictFiles.push({ file, link });
80-
} else {
81-
this.showErrorToast(action, error.error?.message);
82-
}
83-
return of(null);
84-
}),
85-
finalize(() => {
86-
completed++;
87-
if (completed === totalFiles) {
88-
if (conflictFiles.length > 0) {
89-
this.openReplaceMoveDialog(conflictFiles, path, action);
90-
} else {
91-
this.showSuccessToast(action);
92-
this.config.header = this.translateService.instant('files.dialogs.moveFile.title');
93-
this.completeMove();
94-
}
95-
}
96-
})
97-
)
98-
.subscribe();
99-
});
100-
}
53+
this.isLoading.set(true);
10154

102-
private openReplaceMoveDialog(
103-
conflictFiles: { file: FileModel; link: string }[],
104-
path: string,
105-
action: string
106-
): void {
107-
this.customConfirmationService.confirmDelete({
108-
headerKey: conflictFiles.length > 1 ? 'files.dialogs.replaceFile.multiple' : 'files.dialogs.replaceFile.single',
109-
messageKey: 'files.dialogs.replaceFile.message',
110-
messageParams: { name: conflictFiles.map((c) => c.file.name).join(', ') },
111-
acceptLabelKey: 'common.buttons.replace',
112-
onConfirm: () => {
113-
const replaceRequests$ = conflictFiles.map(({ link }) =>
114-
this.filesService.moveFile(link, path, this.fileProjectId, this.provider, action, true).pipe(
115-
takeUntilDestroyed(this.destroyRef),
116-
catchError(() => of(null))
117-
)
118-
);
119-
forkJoin(replaceRequests$).subscribe({
120-
next: () => {
121-
this.showSuccessToast(action);
122-
this.completeMove();
123-
},
124-
});
125-
},
126-
onReject: () => {
127-
const totalFiles = this.config.data.files.length;
128-
if (totalFiles > conflictFiles.length) {
129-
this.showErrorToast(action);
130-
}
131-
this.completeMove();
132-
},
133-
});
134-
}
135-
136-
private showSuccessToast(action: string) {
137-
const messageType = action === 'move' ? 'moveFile' : 'copyFile';
138-
this.toastService.showSuccess(`files.dialogs.${messageType}.success`);
139-
}
140-
141-
private showErrorToast(action: string, errorMessage?: string) {
142-
const messageType = action === 'move' ? 'moveFile' : 'copyFile';
143-
this.toastService.showError(errorMessage ?? `files.dialogs.${messageType}.error`);
144-
}
55+
const headerKey =
56+
action === MoveCopyAction.Move ? 'files.dialogs.moveFile.movingHeader' : 'files.dialogs.moveFile.copingHeader';
57+
this.config.header = this.translateService.instant(headerKey);
14558

146-
private completeMove(): void {
147-
this.dialogRef.close(true);
59+
this.filesMoveCopyService
60+
.execute({
61+
files: this.config.data.files,
62+
destination: this.currentFolder,
63+
resourceId: this.config.data.resourceId,
64+
storageProvider: this.config.data.storageProvider,
65+
action,
66+
})
67+
.pipe(
68+
tap(() => this.dialogRef.close(true)),
69+
finalize(() => {
70+
this.isLoading.set(false);
71+
this.config.header = this.translateService.instant('files.dialogs.moveFile.title');
72+
}),
73+
takeUntilDestroyed(this.destroyRef)
74+
)
75+
.subscribe();
14876
}
14977
}

src/app/features/files/components/file-metadata/file-metadata.component.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
@let metadata = fileMetadata();
2+
13
<div class="p-4 flex flex-column gap-3">
24
<div class="flex justify-content-between">
35
<h2>{{ 'files.detail.fileMetadata.title' | translate }}</h2>
46

5-
@if (!hasViewOnly()) {
7+
@if (!hasViewOnly) {
68
<div class="flex gap-2">
79
<p-button
810
severity="secondary"
@@ -29,11 +31,11 @@ <h2>{{ 'files.detail.fileMetadata.title' | translate }}</h2>
2931
<p-skeleton width="100%" height="4rem" />
3032
} @else {
3133
@for (field of metadataFields; track field.key) {
32-
@if (fileMetadata()?.[field.key]) {
34+
@if (metadata?.[field.key]) {
3335
<div class="flex flex-column gap-2">
3436
<h4>{{ field.label | translate }}</h4>
3537
<p>
36-
{{ field.key === 'language' ? getLanguageName(fileMetadata()?.[field.key]!) : fileMetadata()?.[field.key] }}
38+
{{ field.key === 'language' ? (metadata?.[field.key] | languageLabel) : metadata?.[field.key] }}
3739
</p>
3840
</div>
3941
}

0 commit comments

Comments
 (0)