Skip to content

Commit 9f1b4a9

Browse files
committed
feat(files): updated download logic
1 parent 5972d66 commit 9f1b4a9

11 files changed

Lines changed: 348 additions & 78 deletions

File tree

src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.spec.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@ import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum';
1313
import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum';
1414
import { FileFolderModel } from '@osf/shared/models/files/file-folder.model';
1515
import { FileLabelModel } from '@osf/shared/models/files/file-label.model';
16-
import { DataciteService } from '@osf/shared/services/datacite/datacite.service';
17-
import { FilesService } from '@osf/shared/services/files.service';
16+
import { FileDownloadService } from '@osf/shared/services/file-download.service';
1817
import { FilesShareEmbedService } from '@osf/shared/services/files-share-embed.service';
1918
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';
2019

2120
import { FileModelMock } from '@testing/mocks/file.model.mock';
2221
import { provideOSFCore } from '@testing/osf.testing.provider';
23-
import { DataciteServiceMock, DataciteServiceMockType } from '@testing/providers/datacite.service.mock';
24-
import { FilesServiceMock, FilesServiceMockType } from '@testing/providers/files-service.mock';
22+
import { FileDownloadServiceMock, FileDownloadServiceMockType } from '@testing/providers/file-download-service.mock';
2523
import {
2624
FilesShareEmbedServiceMock,
2725
FilesShareEmbedServiceMockType,
@@ -37,8 +35,7 @@ describe('FilesTreeExplorerComponent', () => {
3735
let component: FilesTreeExplorerComponent;
3836
let fixture: ComponentFixture<FilesTreeExplorerComponent>;
3937
let routerMock: RouterMockType;
40-
let filesService: FilesServiceMockType;
41-
let dataciteService: DataciteServiceMockType;
38+
let fileDownloadService: FileDownloadServiceMockType;
4239
let filesShareEmbedService: FilesShareEmbedServiceMockType;
4340
let viewOnlyHelper: ViewOnlyLinkHelperMockType;
4441

@@ -62,8 +59,7 @@ describe('FilesTreeExplorerComponent', () => {
6259

6360
function setup() {
6461
routerMock = RouterMockBuilder.create().withUrl('/node-1/files').build();
65-
filesService = FilesServiceMock.simple();
66-
dataciteService = DataciteServiceMock.simple();
62+
fileDownloadService = FileDownloadServiceMock.simple();
6763
viewOnlyHelper = ViewOnlyLinkHelperMock.simple(false);
6864
filesShareEmbedService = FilesShareEmbedServiceMock.simple();
6965

@@ -75,8 +71,7 @@ describe('FilesTreeExplorerComponent', () => {
7571
providers: [
7672
provideOSFCore(),
7773
MockProvider(Router, routerMock),
78-
MockProvider(FilesService, filesService),
79-
MockProvider(DataciteService, dataciteService),
74+
MockProvider(FileDownloadService, fileDownloadService),
8075
MockProvider(FilesShareEmbedService, filesShareEmbedService),
8176
MockProvider(ViewOnlyLinkHelperService, viewOnlyHelper),
8277
],
@@ -244,14 +239,20 @@ describe('FilesTreeExplorerComponent', () => {
244239
expect(emitSpy).toHaveBeenNthCalledWith(2, { file, action: MoveCopyAction.Copy });
245240
});
246241

247-
it('should download folder with resolved zip link', () => {
242+
it('should download folder from file menu action', () => {
248243
setup();
249-
const openSpy = vi.spyOn(window, 'open').mockReturnValue({ focus: vi.fn() } as unknown as Window);
244+
const file = FileModelMock.simple({
245+
kind: FileKind.Folder,
246+
links: { ...FileModelMock.simple().links, upload: '/upload-folder' },
247+
});
250248

251-
component.downloadFolder('/upload-folder');
249+
component.onFileMenuAction({ value: FileMenuType.Download }, file);
252250

253-
expect(filesService.getFolderDownloadLink).toHaveBeenCalledWith('/upload-folder');
254-
expect(openSpy).toHaveBeenCalledWith('/upload-folder?zip=', '_blank');
251+
expect(fileDownloadService.downloadFileOrFolder).toHaveBeenCalledWith({
252+
resourceId: 'node-1',
253+
resourceType: CurrentResourceType.Projects,
254+
file,
255+
});
255256
});
256257

257258
it('should open share link in new tab for non self target', () => {

src/app/features/files/components/files-tree-explorer/files-tree-explorer.component.ts

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
ChangeDetectionStrategy,
99
Component,
1010
computed,
11-
DestroyRef,
1211
effect,
1312
inject,
1413
input,
@@ -17,7 +16,6 @@ import {
1716
PLATFORM_ID,
1817
signal,
1918
} from '@angular/core';
20-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2119
import { Router } from '@angular/router';
2220

2321
import { FileMenuComponent } from '@osf/shared/components/file-menu/file-menu.component';
@@ -34,8 +32,7 @@ import { FileFolderModel } from '@osf/shared/models/files/file-folder.model';
3432
import { FileLabelModel } from '@osf/shared/models/files/file-label.model';
3533
import { FileMenuAction, FileMenuFlags } from '@osf/shared/models/files/file-menu-action.model';
3634
import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model';
37-
import { DataciteService } from '@osf/shared/services/datacite/datacite.service';
38-
import { FilesService } from '@osf/shared/services/files.service';
35+
import { FileDownloadService } from '@osf/shared/services/file-download.service';
3936
import { FilesShareEmbedService } from '@osf/shared/services/files-share-embed.service';
4037
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';
4138

@@ -60,10 +57,8 @@ import { MenuMoveCopyPayload } from '../../models/menu-move-copy.model';
6057
changeDetection: ChangeDetectionStrategy.OnPush,
6158
})
6259
export class FilesTreeExplorerComponent {
63-
private readonly destroyRef = inject(DestroyRef);
6460
private readonly router = inject(Router);
65-
private readonly filesService = inject(FilesService);
66-
private readonly dataciteService = inject(DataciteService);
61+
private readonly fileDownloadService = inject(FileDownloadService);
6762
private readonly filesShareEmbedService = inject(FilesShareEmbedService);
6863
private readonly viewOnlyService = inject(ViewOnlyLinkHelperService);
6964
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
@@ -189,29 +184,11 @@ export class FilesTreeExplorerComponent {
189184
}
190185

191186
downloadFileOrFolder(file: FileModel) {
192-
this.dataciteService
193-
.logFileDownload(this.resourceId(), this.resourceType())
194-
.pipe(takeUntilDestroyed(this.destroyRef))
195-
.subscribe();
196-
197-
if (file.kind === FileKind.File) {
198-
this.downloadFile(file.links.download);
199-
} else {
200-
this.downloadFolder(file.links.upload);
201-
}
202-
}
203-
204-
downloadFile(link: string): void {
205-
if (this.isBrowser) {
206-
window.open(link)?.focus();
207-
}
208-
}
209-
210-
downloadFolder(downloadLink: string): void {
211-
if (downloadLink) {
212-
const link = this.filesService.getFolderDownloadLink(downloadLink);
213-
window.open(link, '_blank')?.focus();
214-
}
187+
this.fileDownloadService.downloadFileOrFolder({
188+
resourceId: this.resourceId(),
189+
resourceType: this.resourceType(),
190+
file,
191+
});
215192
}
216193

217194
onLazyLoad(event: TreeLazyLoadEvent) {

src/app/features/files/pages/files/files.component.spec.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@ import { CurrentResource } from '@osf/shared/models/current-resource.model';
2323
import { FileFolderModel } from '@osf/shared/models/files/file-folder.model';
2424
import { FileLabelModel } from '@osf/shared/models/files/file-label.model';
2525
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
26+
import { FileDownloadService } from '@osf/shared/services/file-download.service';
2627
import { FilesService } from '@osf/shared/services/files.service';
2728
import { FilesTreeActionsService } from '@osf/shared/services/files-tree-actions.service';
2829
import { ToastService } from '@osf/shared/services/toast.service';
2930
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';
3031
import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores/current-resource';
31-
import { DataciteService } from '@shared/services/datacite/datacite.service';
3232

3333
import { MOCK_CONFIGURED_ADDON } from '@testing/mocks/configured-addon.mock';
3434
import { FileModelMock } from '@testing/mocks/file.model.mock';
3535
import { OSF_FILE_MOCK } from '@testing/mocks/osf-file.mock';
3636
import { provideOSFCore } from '@testing/osf.testing.provider';
3737
import { CustomDialogServiceMock, CustomDialogServiceMockType } from '@testing/providers/custom-dialog-provider.mock';
38-
import { DataciteServiceMock, DataciteServiceMockType } from '@testing/providers/datacite.service.mock';
38+
import { FileDownloadServiceMock, FileDownloadServiceMockType } from '@testing/providers/file-download-service.mock';
3939
import { FilesServiceMock, FilesServiceMockType } from '@testing/providers/files-service.mock';
4040
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
4141
import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
@@ -97,7 +97,7 @@ describe('FilesComponent', () => {
9797
uploadFiles: Mock;
9898
};
9999
let customDialogService: CustomDialogServiceMockType;
100-
let dataciteService: DataciteServiceMockType;
100+
let fileDownloadService: FileDownloadServiceMockType;
101101

102102
const currentFolder: FileFolderModel = {
103103
...OSF_FILE_MOCK,
@@ -121,7 +121,7 @@ describe('FilesComponent', () => {
121121
toastService = ToastServiceMock.simple();
122122
viewOnlyHelper = ViewOnlyLinkHelperMock.simple(false);
123123
customDialogService = CustomDialogServiceMock.simple();
124-
dataciteService = DataciteServiceMock.simple();
124+
fileDownloadService = FileDownloadServiceMock.simple();
125125

126126
filesActionsService = {
127127
deleteSelected: vi.fn(),
@@ -195,7 +195,7 @@ describe('FilesComponent', () => {
195195
MockProvider(ToastService, toastService),
196196
MockProvider(ViewOnlyLinkHelperService, viewOnlyHelper),
197197
MockProvider(CustomDialogService, customDialogService),
198-
MockProvider(DataciteService, dataciteService),
198+
MockProvider(FileDownloadService, fileDownloadService),
199199
MockProvider(FilesActionsService, filesActionsService),
200200
MockProvider(FilesTreeActionsService, filesTreeActionsService),
201201
MockProvider(FilesUploadService, filesUploadService),
@@ -468,28 +468,28 @@ describe('FilesComponent', () => {
468468
expect(store.dispatch).toHaveBeenCalledWith(new GetFiles('/files-link', 1));
469469
});
470470

471-
it('should log download and open folder zip link', () => {
471+
it('should download current folder as zip', () => {
472472
setup();
473-
(store.dispatch as Mock).mockClear();
474-
const openSpy = vi.spyOn(window, 'open').mockReturnValue({ focus: vi.fn() } as unknown as Window);
475473

476474
component.downloadFolder();
477475

478-
expect(dataciteService.logFileDownload).toHaveBeenCalledWith('node-1', 'nodes');
479-
expect(filesService.getFolderDownloadLink).toHaveBeenCalledWith('/v2/files/file-123/download/');
480-
expect(openSpy).toHaveBeenCalledWith('/v2/files/file-123/download/?zip=', '_blank');
481-
openSpy.mockRestore();
476+
expect(fileDownloadService.downloadFolderAsZip).toHaveBeenCalledWith({
477+
resourceId: 'node-1',
478+
resourceType: 'nodes',
479+
downloadLink: '/v2/files/file-123/download/',
480+
});
482481
});
483482

484483
it('should skip download when resource id is missing', () => {
485484
setup({ resourceId: '' });
486-
const openSpy = vi.spyOn(window, 'open');
487485

488486
component.downloadFolder();
489487

490-
expect(dataciteService.logFileDownload).not.toHaveBeenCalled();
491-
expect(openSpy).not.toHaveBeenCalled();
492-
openSpy.mockRestore();
488+
expect(fileDownloadService.downloadFolderAsZip).toHaveBeenCalledWith({
489+
resourceId: '',
490+
resourceType: 'nodes',
491+
downloadLink: '/v2/files/file-123/download/',
492+
});
493493
});
494494

495495
it('should open files browser info dialog', () => {

src/app/features/files/pages/files/files.component.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { CurrentResourceType, ResourceType } from '@osf/shared/enums/resource-ty
3939
import { mapRootFoldersToStorageLabels } from '@osf/shared/helpers/storage-addon-options.helper';
4040
import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model';
4141
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
42+
import { FileDownloadService } from '@osf/shared/services/file-download.service';
4243
import { FilesService } from '@osf/shared/services/files.service';
4344
import { FilesTreeActionsService } from '@osf/shared/services/files-tree-actions.service';
4445
import { ToastService } from '@osf/shared/services/toast.service';
@@ -48,7 +49,6 @@ import { StorageItem } from '@shared/models/addons/storage-item.model';
4849
import { FileModel } from '@shared/models/files/file.model';
4950
import { FileFolderModel } from '@shared/models/files/file-folder.model';
5051
import { FileLabelModel } from '@shared/models/files/file-label.model';
51-
import { DataciteService } from '@shared/services/datacite/datacite.service';
5252

5353
import { FileBrowserInfoComponent } from '../../components/file-browser-info/file-browser-info.component';
5454
import { FilesSelectionActionsComponent } from '../../components/files-selection-actions/files-selection-actions.component';
@@ -107,7 +107,7 @@ export class FilesComponent {
107107
private readonly customDialogService = inject(CustomDialogService);
108108
private readonly translateService = inject(TranslateService);
109109
private readonly router = inject(Router);
110-
private readonly dataciteService = inject(DataciteService);
110+
private readonly fileDownloadService = inject(FileDownloadService);
111111
private readonly filesActionsService = inject(FilesActionsService);
112112
private readonly filesTreeActionsService = inject(FilesTreeActionsService);
113113
private readonly filesUploadService = inject(FilesUploadService);
@@ -506,17 +506,11 @@ export class FilesComponent {
506506
}
507507

508508
downloadFolder(): void {
509-
const resourceId = this.resourceId();
510-
const resourcePath = this.resourceMetadata()?.type ?? 'nodes';
511-
const downloadLink = this.currentFolder()?.links.download ?? '';
512-
if (resourceId && downloadLink) {
513-
this.dataciteService
514-
.logFileDownload(resourceId, resourcePath)
515-
.pipe(takeUntilDestroyed(this.destroyRef))
516-
.subscribe();
517-
const link = this.filesService.getFolderDownloadLink(downloadLink);
518-
window.open(link, '_blank')?.focus();
519-
}
509+
this.fileDownloadService.downloadFolderAsZip({
510+
resourceId: this.resourceId() ?? '',
511+
resourceType: this.resourceMetadata()?.type ?? 'nodes',
512+
downloadLink: this.currentFolder()?.links.download ?? '',
513+
});
520514
}
521515

522516
showInfoDialog() {

src/app/features/project/overview/components/files-widget/files-widget.component.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ <h2>{{ 'project.overview.files.filesPreview' | translate }}</h2>
1515
<p-skeleton width="6rem" height="2.5rem" />
1616
</div>
1717
} @else {
18-
<p-tabs [value]="currentRootFolder()?.folder?.id || ''" (valueChange)="onStorageChange($event)" scrollable>
18+
<p-tabs [value]="currentRootFolder()?.folder?.id" (valueChange)="onStorageChange($event)" scrollable>
1919
<p-tablist>
2020
@for (option of storageAddons(); track option.folder.id) {
2121
<p-tab class="addon-tab p-2" [value]="option.folder.id" [disabled]="isFilesLoading()">
@@ -30,6 +30,17 @@ <h2>{{ 'project.overview.files.filesPreview' | translate }}</h2>
3030
}
3131
</p-tablist>
3232
</p-tabs>
33+
34+
<div class="flex">
35+
<p-button
36+
raised
37+
outlined
38+
[icon]="'fas fa-download'"
39+
[label]="'files.actions.downloadAsZip' | translate"
40+
[disabled]="isButtonDisabled()"
41+
(onClick)="downloadFolder()"
42+
/>
43+
</div>
3344
}
3445

3546
<osf-files-tree

src/app/features/project/overview/components/files-widget/files-widget.component.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,20 @@ import {
1818
SetFilesCurrentFolder,
1919
} from '@osf/features/files/store';
2020
import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enum';
21-
import { ResourceType } from '@osf/shared/enums/resource-type.enum';
21+
import { CurrentResourceType, ResourceType } from '@osf/shared/enums/resource-type.enum';
2222
import { FileFolderModel } from '@osf/shared/models/files/file-folder.model';
2323
import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model';
2424
import { NodeShortInfoModel } from '@osf/shared/models/nodes/node-with-children.model';
2525
import { SelectOption } from '@osf/shared/models/select-option.model';
26+
import { FileDownloadService } from '@osf/shared/services/file-download.service';
2627
import { FilesService } from '@osf/shared/services/files.service';
2728
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';
2829

2930
import { MOCK_CONFIGURED_ADDON } from '@testing/mocks/configured-addon.mock';
3031
import { FileModelMock } from '@testing/mocks/file.model.mock';
3132
import { OSF_FILE_MOCK } from '@testing/mocks/osf-file.mock';
3233
import { provideOSFCore } from '@testing/osf.testing.provider';
34+
import { FileDownloadServiceMock, FileDownloadServiceMockType } from '@testing/providers/file-download-service.mock';
3335
import { FilesServiceMock, FilesServiceMockType } from '@testing/providers/files-service.mock';
3436
import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
3537
import {
@@ -55,6 +57,7 @@ describe('FilesWidgetComponent', () => {
5557
let store: Store;
5658
let routerMock: RouterMockType;
5759
let filesService: FilesServiceMockType;
60+
let fileDownloadService: FileDownloadServiceMockType;
5861
let viewOnlyHelper: ViewOnlyLinkHelperMockType;
5962

6063
const rootFolder: FileFolderModel = {
@@ -78,6 +81,7 @@ describe('FilesWidgetComponent', () => {
7881
.withSerializeUrl(vi.fn().mockReturnValue('/serialized'))
7982
.build();
8083
filesService = FilesServiceMock.simple();
84+
fileDownloadService = FileDownloadServiceMock.simple();
8185
viewOnlyHelper = ViewOnlyLinkHelperMock.simple(overrides.hasViewOnly ?? false);
8286
viewOnlyHelper.getViewOnlyParamFromUrl.mockReturnValue('token');
8387

@@ -105,6 +109,7 @@ describe('FilesWidgetComponent', () => {
105109
provideOSFCore(),
106110
MockProvider(Router, routerMock),
107111
MockProvider(FilesService, filesService),
112+
MockProvider(FileDownloadService, fileDownloadService),
108113
MockProvider(ViewOnlyLinkHelperService, viewOnlyHelper),
109114
provideMockStore({ signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides) }),
110115
],
@@ -179,6 +184,34 @@ describe('FilesWidgetComponent', () => {
179184
expect(store.dispatch).toHaveBeenCalledWith(new SetFilesCurrentFolder(rootFolder));
180185
});
181186

187+
it('should disable download button when folder has no files', () => {
188+
setup();
189+
190+
expect(component.isButtonDisabled()).toBe(true);
191+
});
192+
193+
it('should enable download button when folder has files', () => {
194+
setup({
195+
selectorOverrides: [{ selector: FilesSelectors.getFilesTotalCount, value: 2 }],
196+
});
197+
198+
expect(component.isButtonDisabled()).toBe(false);
199+
});
200+
201+
it('should download current folder as zip', () => {
202+
setup({
203+
selectorOverrides: [{ selector: FilesSelectors.getFilesTotalCount, value: 1 }],
204+
});
205+
206+
component.downloadFolder();
207+
208+
expect(fileDownloadService.downloadFolderAsZip).toHaveBeenCalledWith({
209+
resourceId: 'project-1',
210+
resourceType: CurrentResourceType.Projects,
211+
downloadLink: '/v2/files/file-123/download/',
212+
});
213+
});
214+
182215
it('should open file directly when guid exists', () => {
183216
setup();
184217
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);

0 commit comments

Comments
 (0)