- Notifications
You must be signed in to change notification settings - Fork 22
feat(ENG-9544): implement in tree drag and drop support #750
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
brianjgeiger merged 5 commits into CenterForOpenScience:feature/pbs-25.04 from opaduchak:feat/ENG-9544 Nov 11, 2025
Merged
Changes from all commits
Commits
Show all changes
5 commits Select commit Hold shift + click to select a range
22926a1 feat(files-tree): implement in tree drag and drop support
opaduchak 730b77d fix(translations): fixed typo
opaduchak 2f2a3d6 test(confirm-move-file-dialog): fixed tests
opaduchak af2650e fix(move-file-dialog): fixed toasts
opaduchak ce0ac16 fix(confirm-move-dialog): fixed pr comments
opaduchak 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
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
11 changes: 11 additions & 0 deletions 11 ...eatures/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
| 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> | ||
Empty file.
82 changes: 82 additions & 0 deletions 82 ...ures/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
| 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([]); | ||
| }); | ||
| }); |
154 changes: 154 additions & 0 deletions 154 .../features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.