diff --git a/src/app/features/files/pages/files/files.component.html b/src/app/features/files/pages/files/files.component.html index 43d076f6b..10a2f47bb 100644 --- a/src/app/features/files/pages/files/files.component.html +++ b/src/app/features/files/pages/files/files.component.html @@ -144,8 +144,8 @@ (selectFile)="onFileTreeSelected($event)" (unselectFile)="onFileTreeUnselected($event)" (clearSelection)="onClearSelection()" - (entryFileClicked)="navigateToFile($event)" (deleteEntryAction)="deleteEntry($event)" + (entryFileClicked)="navigateToFile($event)" (renameEntryAction)="renameEntry($event)" (uploadFilesConfirmed)="uploadFiles($event)" (loadFiles)="onLoadFiles($event)" diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index a264e749a..00ab2a2ef 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -619,8 +619,8 @@ export class FilesComponent { this.actions.setMoveDialogCurrentFolder(folder); } - deleteEntry(link: string) { - this.actions.deleteEntry(link).subscribe(() => { + deleteEntry(file: FileModel): void { + this.actions.deleteEntry(file?.links.delete).subscribe(() => { this.toastService.showSuccess('files.dialogs.deleteFile.success'); this.updateFilesList(); }); diff --git a/src/app/features/registries/components/custom-step/custom-step.component.html b/src/app/features/registries/components/custom-step/custom-step.component.html index 6bdc77c6e..e3d0540a6 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.html +++ b/src/app/features/registries/components/custom-step/custom-step.component.html @@ -168,7 +168,7 @@

{{ 'files.actions.uploadFile' | translate }}

[label]="file.name" severity="info" removable="true" - (onRemove)="removeFromAttachedFiles(file, q.responseKey!)" + (onRemove)="removeFromAttachedFiles(file.file_id, q.responseKey!)" /> } @@ -181,6 +181,7 @@

{{ 'files.actions.uploadFile' | translate }}

(attachFile)="onAttachFile($event, q.responseKey!)" (openFile)="onOpenFile($event)" [filesViewOnly]="filesViewOnly()" + (removeFromAttachedFiles)="removeFromAttachedFiles($event, q.responseKey!)" > } diff --git a/src/app/features/registries/components/custom-step/custom-step.component.spec.ts b/src/app/features/registries/components/custom-step/custom-step.component.spec.ts index b17f69381..68cca32a7 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.spec.ts +++ b/src/app/features/registries/components/custom-step/custom-step.component.spec.ts @@ -213,7 +213,7 @@ describe('CustomStepComponent', () => { { file_id: 'f2', name: 'b' }, ]; - component.removeFromAttachedFiles({ file_id: 'f1', name: 'a' }, 'field1'); + component.removeFromAttachedFiles('f1', 'field1'); expect(component.attachedFiles['field1'].length).toBe(1); expect(component.attachedFiles['field1'][0].file_id).toBe('f2'); @@ -223,7 +223,7 @@ describe('CustomStepComponent', () => { it('should skip non-existent questionKey', () => { const { component } = setup(); const emitSpy = vi.spyOn(component.updateAction, 'emit'); - component.removeFromAttachedFiles({ file_id: 'f1' }, 'nonexistent'); + component.removeFromAttachedFiles('f1', 'nonexistent'); expect(emitSpy).not.toHaveBeenCalled(); }); diff --git a/src/app/features/registries/components/custom-step/custom-step.component.ts b/src/app/features/registries/components/custom-step/custom-step.component.ts index 7b5f066e6..484b5e4a9 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.ts +++ b/src/app/features/registries/components/custom-step/custom-step.component.ts @@ -143,12 +143,12 @@ export class CustomStepComponent implements OnDestroy { } } - removeFromAttachedFiles(file: AttachedFile, questionKey: string): void { + removeFromAttachedFiles(fileId: string | undefined, questionKey: string): void { if (!this.attachedFiles[questionKey]) { return; } - this.attachedFiles[questionKey] = this.attachedFiles[questionKey].filter((f) => f.file_id !== file.file_id); + this.attachedFiles[questionKey] = this.attachedFiles[questionKey].filter((f) => f.file_id !== fileId); this.stepForm.patchValue({ [questionKey]: this.attachedFiles[questionKey] }); this.updateAction.emit({ [questionKey]: this.mapFilesToPayload(this.attachedFiles[questionKey]), diff --git a/src/app/features/registries/components/files-control/files-control.component.html b/src/app/features/registries/components/files-control/files-control.component.html index 78c18d21c..951fcac20 100644 --- a/src/app/features/registries/components/files-control/files-control.component.html +++ b/src/app/features/registries/components/files-control/files-control.component.html @@ -51,6 +51,8 @@ [resourceId]="projectId()" [provider]="provider()" [selectedFiles]="filesSelection" + [isDraftResource]="true" + (deleteEntryAction)="deleteEntry($event)" (selectFile)="onFileTreeSelected($event)" (entryFileClicked)="onEntryFileClicked($event)" (uploadFilesConfirmed)="uploadFiles($event)" diff --git a/src/app/features/registries/components/files-control/files-control.component.spec.ts b/src/app/features/registries/components/files-control/files-control.component.spec.ts index 874ea26d7..1557f575a 100644 --- a/src/app/features/registries/components/files-control/files-control.component.spec.ts +++ b/src/app/features/registries/components/files-control/files-control.component.spec.ts @@ -228,4 +228,19 @@ describe('FilesControlComponent', () => { expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(SetFilesIsLoading)); expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(GetFiles)); }); + + it('should delete entry, show success toast, refresh files, and emit removal', () => { + const file = { id: 'file-1', links: { delete: '/delete-link' } } as FileModel; + const deleteSpy = vi.spyOn(component['actions'], 'deleteDraftRegistrationFiles').mockReturnValue(of(void 0)); + const refreshSpy = vi.spyOn(component as any, 'refreshFilesList'); + const emitSpy = vi.spyOn(component.removeFromAttachedFiles, 'emit'); + const toastSpy = vi.spyOn(toastService, 'showSuccess'); + + component.deleteEntry(file); + + expect(deleteSpy).toHaveBeenCalledWith('/delete-link'); + expect(toastSpy).toHaveBeenCalledWith('files.dialogs.deleteFile.success'); + expect(refreshSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith('file-1'); + }); }); diff --git a/src/app/features/registries/components/files-control/files-control.component.ts b/src/app/features/registries/components/files-control/files-control.component.ts index 604141db5..fd367abbc 100644 --- a/src/app/features/registries/components/files-control/files-control.component.ts +++ b/src/app/features/registries/components/files-control/files-control.component.ts @@ -25,6 +25,7 @@ import { FileFolderModel } from '@shared/models/files/file-folder.model'; import { CreateFolder, + DeleteDraftRegistrationFiles, GetFiles, GetRootFolders, RegistriesSelectors, @@ -54,6 +55,7 @@ export class FilesControlComponent { provider = input.required(); filesViewOnly = input(false); attachFile = output(); + removeFromAttachedFiles = output(); openFile = output(); private readonly filesService = inject(FilesService); @@ -79,6 +81,7 @@ export class FilesControlComponent { setFilesIsLoading: SetFilesIsLoading, setCurrentFolder: SetRegistriesCurrentFolder, getRootFolders: GetRootFolders, + deleteDraftRegistrationFiles: DeleteDraftRegistrationFiles, }); constructor() { @@ -86,6 +89,14 @@ export class FilesControlComponent { this.setupCurrentFolderWatcher(); } + deleteEntry(file: FileModel): void { + this.actions.deleteDraftRegistrationFiles(file?.links.delete).subscribe(() => { + this.toastService.showSuccess('files.dialogs.deleteFile.success'); + this.refreshFilesList(); + this.removeFromAttachedFiles.emit(file.id); + }); + } + onFileSelected(event: Event): void { const input = event.target as HTMLInputElement; const file = input.files?.[0]; diff --git a/src/app/features/registries/store/handlers/files.handlers.ts b/src/app/features/registries/store/handlers/files.handlers.ts index e6b5d4f38..615386a72 100644 --- a/src/app/features/registries/store/handlers/files.handlers.ts +++ b/src/app/features/registries/store/handlers/files.handlers.ts @@ -7,7 +7,7 @@ import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; import { FilesService } from '@osf/shared/services/files.service'; -import { CreateFolder, GetFiles, GetRootFolders } from '../registries.actions'; +import { CreateFolder, DeleteDraftRegistrationFiles, GetFiles, GetRootFolders } from '../registries.actions'; import { RegistriesStateModel } from '../registries.model'; @Injectable() @@ -70,4 +70,13 @@ export class FilesHandlers { .createFolder(action.newFolderLink, action.folderName) .pipe(finalize(() => ctx.patchState({ files: { ...state.files, isLoading: false, error: null } }))); } + + deleteDraftRegistrationFiles(ctx: StateContext, action: DeleteDraftRegistrationFiles) { + const state = ctx.getState(); + ctx.patchState({ files: { ...state.files, isLoading: true, error: null } }); + + return this.filesService + .deleteEntry(action.link) + .pipe(finalize(() => ctx.patchState({ files: { ...state.files, isLoading: false, error: null } }))); + } } diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index 45db0f8f9..25a553773 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -135,6 +135,12 @@ export class GetFiles { ) {} } +export class DeleteDraftRegistrationFiles { + static readonly type = '[Registries] Delete Draft Registration Files'; + + constructor(public link: string) {} +} + export class SetFilesIsLoading { static readonly type = '[Registries] Set Files Loading'; diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index d24602bbf..c2373159c 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -23,6 +23,7 @@ import { CreateFolder, CreateSchemaResponse, DeleteDraft, + DeleteDraftRegistrationFiles, DeleteSchemaResponse, FetchAllSchemaResponses, FetchDraft, @@ -351,6 +352,11 @@ export class RegistriesState { return this.filesHandlers.getProjectFiles(ctx, { filesLink, page }); } + @Action(DeleteDraftRegistrationFiles) + deleteDraftRegistrationFiles(ctx: StateContext, action: DeleteDraftRegistrationFiles) { + return this.filesHandlers.deleteDraftRegistrationFiles(ctx, action); + } + @Action(GetRootFolders) getRootFolders(ctx: StateContext, action: GetRootFolders) { return this.filesHandlers.getRootFolders(ctx, action); diff --git a/src/app/shared/components/files-tree/files-tree.component.html b/src/app/shared/components/files-tree/files-tree.component.html index fa2954b12..ba61a2c22 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -95,6 +95,18 @@ } + @if (isDraftResource()) { + + } } diff --git a/src/app/shared/components/files-tree/files-tree.component.ts b/src/app/shared/components/files-tree/files-tree.component.ts index f08df941e..efe638512 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -3,6 +3,8 @@ import { select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { PrimeTemplate, TreeNode } from 'primeng/api'; +import { Button } from 'primeng/button'; +import { Tooltip } from 'primeng/tooltip'; import { Tree, TreeLazyLoadEvent, TreeNodeDropEvent, TreeNodeSelectEvent } from 'primeng/tree'; import { Clipboard } from '@angular/cdk/clipboard'; @@ -66,6 +68,8 @@ type FileTreeNode = FileModel & TreeNode; LoadingSpinnerComponent, FileMenuComponent, StopPropagationDirective, + Button, + Tooltip, ], templateUrl: './files-tree.component.html', styleUrl: './files-tree.component.scss', @@ -101,12 +105,13 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { selectedFiles = input([]); scrollHeight = input('300px'); selectionMode = input<'multiple' | null>('multiple'); + isDraftResource = input(false); entryFileClicked = output(); uploadFilesConfirmed = output(); setCurrentFolder = output(); setMoveDialogCurrentFolder = output(); - deleteEntryAction = output(); + deleteEntryAction = output(); renameEntryAction = output<{ newName: string; link: string }>(); loadFiles = output<{ link: string; page: number }>(); selectFile = output(); @@ -344,12 +349,12 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { messageKey: file.kind === FileKind.Folder ? 'files.dialogs.deleteFolder.message' : 'files.dialogs.deleteFile.message', acceptLabelKey: 'common.buttons.remove', - onConfirm: () => this.confirmDeleteEntry(file.links.delete), + onConfirm: () => this.confirmDeleteEntry(file), }); } - confirmDeleteEntry(link: string): void { - this.deleteEntryAction.emit(link); + confirmDeleteEntry(file: FileModel): void { + this.deleteEntryAction.emit(file); } confirmRename(file: FileModel): void {