From fc7f300146e07c1bd9547e919e13d3fbe8090d6d Mon Sep 17 00:00:00 2001 From: mkovalua Date: Fri, 17 Apr 2026 14:32:23 +0300 Subject: [PATCH 01/15] open preview url on draft registry file click --- .../components/custom-step/custom-step.component.html | 1 + .../components/custom-step/custom-step.component.ts | 1 + .../components/files-control/files-control.component.ts | 8 ++++++++ 3 files changed, 10 insertions(+) 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 3424906dd..25bed2c5e 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 @@ -178,6 +178,7 @@

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

[filesLink]="filesLink()" [projectId]="projectId()" [provider]="provider()" + [draftId]="draftId()" (attachFile)="onAttachFile($event, q.responseKey!)" [filesViewOnly]="filesViewOnly()" > 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 357bc71b5..4c8736206 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 @@ -98,6 +98,7 @@ export class CustomStepComponent implements OnDestroy { readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; step = signal(this.route.snapshot.params['step']); + draftId = signal(this.route.snapshot.params['id']); currentPage = computed(() => this.pages()[this.step() - 1]); stepForm: FormGroup = this.fb.group({}); 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 423a65d45..d6dee6616 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 @@ -10,6 +10,7 @@ import { filter, finalize, switchMap, take } from 'rxjs'; import { HttpEventType } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, DestroyRef, inject, input, output, signal } from '@angular/core'; import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { Router } from '@angular/router'; import { CreateFolderDialogComponent } from '@osf/features/files/components'; import { FileUploadDialogComponent } from '@osf/shared/components/file-upload-dialog/file-upload-dialog.component'; @@ -53,6 +54,7 @@ export class FilesControlComponent { projectId = input.required(); provider = input.required(); filesViewOnly = input(false); + draftId = input.required(); attachFile = output(); private readonly filesService = inject(FilesService); @@ -153,9 +155,15 @@ export class FilesControlComponent { }); } + private readonly router = inject(Router); + selectFile(file: FileModel): void { if (this.filesViewOnly()) return; this.attachFile.emit(file); + if (this.draftId() && file.guid) { + const url = this.router.serializeUrl(this.router.createUrlTree([this.draftId(), '/', 'files', file.guid])); + window.open(url, '_blank'); + } } onFileTreeSelected(file: FileModel): void { From 68ceae9726f6c9de200f85299cfc1712a1d84208 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Fri, 17 Apr 2026 16:43:44 +0300 Subject: [PATCH 02/15] add rendering page for draft registry files --- src/app/app.routes.ts | 7 ++ .../draft-file-detail.component.html | 25 ++++++ .../draft-file-detail.component.scss | 0 .../draft-file-detail.component.spec.ts | 22 +++++ .../draft-file-detail.component.ts | 80 +++++++++++++++++++ .../files-control/files-control.component.ts | 6 +- 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/app/features/files/pages/draft-file-detail/draft-file-detail.component.html create mode 100644 src/app/features/files/pages/draft-file-detail/draft-file-detail.component.scss create mode 100644 src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts create mode 100644 src/app/features/files/pages/draft-file-detail/draft-file-detail.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 711ff4387..6c108a66e 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -205,6 +205,13 @@ export const routes: Routes = [ loadComponent: () => import('./features/files/pages/file-redirect/file-redirect.component').then((m) => m.FileRedirectComponent), }, + { + path: 'registries/drafts/:id/files/:fileGuid', + loadComponent: () => + import('./features/files/pages/draft-file-detail/draft-file-detail.component').then( + (m) => m.DraftFileDetailComponent + ), + }, { path: ':id', canMatch: [isFileGuard], diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.html b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.html new file mode 100644 index 000000000..f678c6d28 --- /dev/null +++ b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.html @@ -0,0 +1,25 @@ + + +
+
+
+ @if (safeLink) { + + } + @if (isIframeLoading) { + + } +
+
+
diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.scss b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts new file mode 100644 index 000000000..c8c3c0115 --- /dev/null +++ b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DraftFileDetailComponent } from './draft-file-detail.component'; + +describe('DraftFileDetailComponent', () => { + let component: DraftFileDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DraftFileDetailComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DraftFileDetailComponent); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.ts b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.ts new file mode 100644 index 000000000..9dc15fcf0 --- /dev/null +++ b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.ts @@ -0,0 +1,80 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { switchMap } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { FilesSelectors, GetFile } from '@osf/features/files/store'; +import { LoadingSpinnerComponent } from '@shared/components/loading-spinner/loading-spinner.component'; +import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; +import { ViewOnlyLinkHelperService } from '@shared/services/view-only-link-helper.service'; + +@Component({ + selector: 'osf-draft-file-detail.component', + imports: [SubHeaderComponent, LoadingSpinnerComponent], + templateUrl: './draft-file-detail.component.html', + styleUrl: './draft-file-detail.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DraftFileDetailComponent { + isFileLoading = select(FilesSelectors.isOpenedFileLoading); + file = select(FilesSelectors.getOpenedFile); + readonly router = inject(Router); + readonly route = inject(ActivatedRoute); + readonly sanitizer = inject(DomSanitizer); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); + isIframeLoading = true; + safeLink: SafeResourceUrl | null = null; + readonly destroyRef = inject(DestroyRef); + fileGuid = ''; + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); + + private readonly actions = createDispatchMap({ + getFile: GetFile, + }); + + constructor() { + this.route.params + .pipe( + takeUntilDestroyed(this.destroyRef), + switchMap((params) => { + this.fileGuid = params['fileGuid']; + return this.actions.getFile(this.fileGuid); + }) + ) + .subscribe(() => { + this.getIframeLink(''); + }); + } + + getIframeLink(version: string) { + const url = this.getMfrUrlWithVersion(version); + if (url) { + this.safeLink = this.sanitizer.bypassSecurityTrustResourceUrl(url); + } + } + + getMfrUrlWithVersion(version?: string): string | null { + const mfrUrl = this.file()?.links.render; + if (!mfrUrl) return null; + const mfrUrlObj = new URL(mfrUrl); + const encodedDownloadUrl = mfrUrlObj.searchParams.get('url'); + if (!encodedDownloadUrl) return mfrUrl; + + const downloadUrlObj = new URL(decodeURIComponent(encodedDownloadUrl)); + + if (version) downloadUrlObj.searchParams.set('version', version); + + if (this.hasViewOnly()) { + const viewOnlyParam = this.viewOnlyService.getViewOnlyParam(); + if (viewOnlyParam) downloadUrlObj.searchParams.set('view_only', viewOnlyParam); + } + + mfrUrlObj.searchParams.set('url', downloadUrlObj.toString()); + + return mfrUrlObj.toString(); + } +} 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 d6dee6616..4625c0b15 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 @@ -54,7 +54,7 @@ export class FilesControlComponent { projectId = input.required(); provider = input.required(); filesViewOnly = input(false); - draftId = input.required(); + draftId = input(''); attachFile = output(); private readonly filesService = inject(FilesService); @@ -161,7 +161,9 @@ export class FilesControlComponent { if (this.filesViewOnly()) return; this.attachFile.emit(file); if (this.draftId() && file.guid) { - const url = this.router.serializeUrl(this.router.createUrlTree([this.draftId(), '/', 'files', file.guid])); + const url = this.router.serializeUrl( + this.router.createUrlTree(['registries', 'drafts', this.draftId(), 'files', file.guid]) + ); window.open(url, '_blank'); } } From cdfdebb94f27445707803422c59d59ac629bf615 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Fri, 17 Apr 2026 17:31:06 +0300 Subject: [PATCH 03/15] update tests --- .../draft-file-detail.component.spec.ts | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts index c8c3c0115..a6916ca17 100644 --- a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts +++ b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts @@ -1,4 +1,19 @@ +import { MockComponent, MockProvider } from 'ng-mocks'; + +import { of } from 'rxjs'; + +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; +import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; + +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +import { FilesSelectors } from '../../store'; import { DraftFileDetailComponent } from './draft-file-detail.component'; @@ -7,10 +22,29 @@ describe('DraftFileDetailComponent', () => { let fixture: ComponentFixture; beforeEach(async () => { + const mockRoute: Partial = { + params: of({ fileGuid: 'test-file-guid' }), + queryParams: of({}), + }; + await TestBed.configureTestingModule({ - imports: [DraftFileDetailComponent], + imports: [DraftFileDetailComponent, MockComponent(SubHeaderComponent), MockComponent(LoadingSpinnerComponent)], + providers: [ + provideOSFCore(), + { provide: ActivatedRoute, useValue: mockRoute }, + MockProvider(Router, { url: '' }), + MockProvider(ViewOnlyLinkHelperService, { + hasViewOnlyParam: () => false, + getViewOnlyParam: () => null, + }), + provideMockStore({ + signals: [ + { selector: FilesSelectors.isOpenedFileLoading, value: false }, + { selector: FilesSelectors.getOpenedFile, value: null }, + ], + }), + ], }).compileComponents(); - fixture = TestBed.createComponent(DraftFileDetailComponent); component = fixture.componentInstance; await fixture.whenStable(); @@ -19,4 +53,23 @@ describe('DraftFileDetailComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set safeLink when getIframeLink is called with a valid render link', () => { + const mfrUrl = 'https://mfr.osf.io/render?url=https%3A%2F%2Fosf.io%2Fdownload%2Fch7jz%2F'; + component.file = signal({ links: { render: mfrUrl } }) as any; + component.getIframeLink(''); + expect(component.safeLink).not.toBeNull(); + }); + + it('should not set safeLink when file has no render link', () => { + component.file = signal({ links: {} }) as any; + component.safeLink = null; + component.getIframeLink(''); + expect(component.safeLink).toBeNull(); + }); + + it('should return null from getMfrUrlWithVersion when file has no render link', () => { + component.file = signal({ links: {} }) as any; + expect(component.getMfrUrlWithVersion('1')).toBeNull(); + }); }); From ea1d9a323bb4524bf40d3bc756e12078086e1039 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 20 Apr 2026 16:50:03 +0300 Subject: [PATCH 04/15] resolve CR --- .../draft-file-detail.component.spec.ts | 2 +- .../custom-step/custom-step.component.html | 2 +- .../custom-step/custom-step.component.ts | 9 +++++++++ .../files-control/files-control.component.html | 2 +- .../files-control/files-control.component.ts | 14 +++++--------- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts index a6916ca17..14b5a9e6c 100644 --- a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts +++ b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts @@ -44,7 +44,7 @@ describe('DraftFileDetailComponent', () => { ], }), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(DraftFileDetailComponent); component = fixture.componentInstance; await fixture.whenStable(); 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 25bed2c5e..6bdc77c6e 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 @@ -178,8 +178,8 @@

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

[filesLink]="filesLink()" [projectId]="projectId()" [provider]="provider()" - [draftId]="draftId()" (attachFile)="onAttachFile($event, q.responseKey!)" + (openFile)="onOpenFile($event)" [filesViewOnly]="filesViewOnly()" > 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 4c8736206..8eb487380 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 @@ -136,6 +136,15 @@ export class CustomStepComponent implements OnDestroy { }); } + onOpenFile(file: FileModel): void { + if (this.draftId() && file.guid) { + const url = this.router.serializeUrl( + this.router.createUrlTree(['registries', 'drafts', this.draftId(), 'files', file.guid]) + ); + window.open(url, '_blank'); + } + } + removeFromAttachedFiles(file: AttachedFile, questionKey: string): void { if (!this.attachedFiles[questionKey]) { return; 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 8d3350ae2..78c18d21c 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 @@ -52,7 +52,7 @@ [provider]="provider()" [selectedFiles]="filesSelection" (selectFile)="onFileTreeSelected($event)" - (entryFileClicked)="selectFile($event)" + (entryFileClicked)="onEntryFileClicked($event)" (uploadFilesConfirmed)="uploadFiles($event)" (loadFiles)="onLoadFiles($event)" (setCurrentFolder)="setCurrentFolder($event)" 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 4625c0b15..604141db5 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 @@ -10,7 +10,6 @@ import { filter, finalize, switchMap, take } from 'rxjs'; import { HttpEventType } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, DestroyRef, inject, input, output, signal } from '@angular/core'; import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; -import { Router } from '@angular/router'; import { CreateFolderDialogComponent } from '@osf/features/files/components'; import { FileUploadDialogComponent } from '@osf/shared/components/file-upload-dialog/file-upload-dialog.component'; @@ -54,8 +53,8 @@ export class FilesControlComponent { projectId = input.required(); provider = input.required(); filesViewOnly = input(false); - draftId = input(''); attachFile = output(); + openFile = output(); private readonly filesService = inject(FilesService); private readonly customDialogService = inject(CustomDialogService); @@ -155,17 +154,14 @@ export class FilesControlComponent { }); } - private readonly router = inject(Router); + onEntryFileClicked(file: FileModel): void { + this.selectFile(file); + this.openFile.emit(file); + } selectFile(file: FileModel): void { if (this.filesViewOnly()) return; this.attachFile.emit(file); - if (this.draftId() && file.guid) { - const url = this.router.serializeUrl( - this.router.createUrlTree(['registries', 'drafts', this.draftId(), 'files', file.guid]) - ); - window.open(url, '_blank'); - } } onFileTreeSelected(file: FileModel): void { From 00a33724d8a0b9f733867d16d9f72e9e9f914de0 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 20 Apr 2026 21:20:57 +0300 Subject: [PATCH 05/15] resolve CR and update UI --- src/app/app.routes.ts | 12 +- .../draft-file-detail.component.scss | 0 .../draft-file-detail.component.spec.ts | 75 -------- .../file-preview/file-preview..component.scss | 10 ++ .../file-preview.component.html} | 8 +- .../file-preview.component.spec.ts | 166 ++++++++++++++++++ .../file-preview.component.ts} | 14 +- .../custom-step/custom-step.component.ts | 4 +- src/assets/i18n/en.json | 3 +- 9 files changed, 200 insertions(+), 92 deletions(-) delete mode 100644 src/app/features/files/pages/draft-file-detail/draft-file-detail.component.scss delete mode 100644 src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts create mode 100644 src/app/features/files/pages/file-preview/file-preview..component.scss rename src/app/features/files/pages/{draft-file-detail/draft-file-detail.component.html => file-preview/file-preview.component.html} (67%) create mode 100644 src/app/features/files/pages/file-preview/file-preview.component.spec.ts rename src/app/features/files/pages/{draft-file-detail/draft-file-detail.component.ts => file-preview/file-preview.component.ts} (87%) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 6c108a66e..a227f8e5a 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -182,6 +182,11 @@ export const routes: Routes = [ import('./core/components/page-not-found/page-not-found.component').then((mod) => mod.PageNotFoundComponent), data: { skipBreadcrumbs: true }, }, + { + path: ':id/files/:fileGuid/preview', + loadComponent: () => + import('./features/files/pages/file-preview/file-preview.component').then((m) => m.FilePreviewComponent), + }, { path: 'spam-content', loadComponent: () => @@ -205,13 +210,6 @@ export const routes: Routes = [ loadComponent: () => import('./features/files/pages/file-redirect/file-redirect.component').then((m) => m.FileRedirectComponent), }, - { - path: 'registries/drafts/:id/files/:fileGuid', - loadComponent: () => - import('./features/files/pages/draft-file-detail/draft-file-detail.component').then( - (m) => m.DraftFileDetailComponent - ), - }, { path: ':id', canMatch: [isFileGuard], diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.scss b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts b/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts deleted file mode 100644 index 14b5a9e6c..000000000 --- a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { MockComponent, MockProvider } from 'ng-mocks'; - -import { of } from 'rxjs'; - -import { signal } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, Router } from '@angular/router'; - -import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; -import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; -import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; - -import { provideOSFCore } from '@testing/osf.testing.provider'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; - -import { FilesSelectors } from '../../store'; - -import { DraftFileDetailComponent } from './draft-file-detail.component'; - -describe('DraftFileDetailComponent', () => { - let component: DraftFileDetailComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - const mockRoute: Partial = { - params: of({ fileGuid: 'test-file-guid' }), - queryParams: of({}), - }; - - await TestBed.configureTestingModule({ - imports: [DraftFileDetailComponent, MockComponent(SubHeaderComponent), MockComponent(LoadingSpinnerComponent)], - providers: [ - provideOSFCore(), - { provide: ActivatedRoute, useValue: mockRoute }, - MockProvider(Router, { url: '' }), - MockProvider(ViewOnlyLinkHelperService, { - hasViewOnlyParam: () => false, - getViewOnlyParam: () => null, - }), - provideMockStore({ - signals: [ - { selector: FilesSelectors.isOpenedFileLoading, value: false }, - { selector: FilesSelectors.getOpenedFile, value: null }, - ], - }), - ], - }); - fixture = TestBed.createComponent(DraftFileDetailComponent); - component = fixture.componentInstance; - await fixture.whenStable(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should set safeLink when getIframeLink is called with a valid render link', () => { - const mfrUrl = 'https://mfr.osf.io/render?url=https%3A%2F%2Fosf.io%2Fdownload%2Fch7jz%2F'; - component.file = signal({ links: { render: mfrUrl } }) as any; - component.getIframeLink(''); - expect(component.safeLink).not.toBeNull(); - }); - - it('should not set safeLink when file has no render link', () => { - component.file = signal({ links: {} }) as any; - component.safeLink = null; - component.getIframeLink(''); - expect(component.safeLink).toBeNull(); - }); - - it('should return null from getMfrUrlWithVersion when file has no render link', () => { - component.file = signal({ links: {} }) as any; - expect(component.getMfrUrlWithVersion('1')).toBeNull(); - }); -}); diff --git a/src/app/features/files/pages/file-preview/file-preview..component.scss b/src/app/features/files/pages/file-preview/file-preview..component.scss new file mode 100644 index 000000000..2ecbf1c4e --- /dev/null +++ b/src/app/features/files/pages/file-preview/file-preview..component.scss @@ -0,0 +1,10 @@ +.metadata { + color: var(--dark-blue-1); + border: 1px solid var(--grey-2); + border-radius: 12px; +} + +.full-image { + min-height: 100vh; + min-width: 100%; +} diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.html b/src/app/features/files/pages/file-preview/file-preview.component.html similarity index 67% rename from src/app/features/files/pages/draft-file-detail/draft-file-detail.component.html rename to src/app/features/files/pages/file-preview/file-preview.component.html index f678c6d28..2402dacfe 100644 --- a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.html +++ b/src/app/features/files/pages/file-preview/file-preview.component.html @@ -1,5 +1,4 @@ -
@@ -21,5 +20,12 @@ }
+ +
+ +
diff --git a/src/app/features/files/pages/file-preview/file-preview.component.spec.ts b/src/app/features/files/pages/file-preview/file-preview.component.spec.ts new file mode 100644 index 000000000..2ef809426 --- /dev/null +++ b/src/app/features/files/pages/file-preview/file-preview.component.spec.ts @@ -0,0 +1,166 @@ +import { Store } from '@ngxs/store'; + +import { MockProvider } from 'ng-mocks'; + +import { Mock } from 'vitest'; + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { FileKind } from '@osf/shared/enums/file-kind.enum'; +import { FileDetailsModel } from '@osf/shared/models/files/file.model'; +import { BaseNodeModel } from '@osf/shared/models/nodes/base-node.model'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; + +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ViewOnlyLinkHelperMock, ViewOnlyLinkHelperMockType } from '@testing/providers/view-only-link-helper.mock'; + +import { FilesSelectors, GetFile } from '../../store'; + +import { FilePreviewComponent } from './file-preview.component'; + +interface SetupOverrides extends BaseSetupOverrides { + hasViewOnlyParam?: boolean; + viewOnlyParam?: string | null; + renderLink?: string; +} + +describe('FilePreviewComponent', () => { + let component: FilePreviewComponent; + let fixture: ComponentFixture; + let store: Store; + let mockRouter: RouterMockType; + let viewOnlyService: ViewOnlyLinkHelperMockType; + + const encodedDownloadUrl = 'https://files.osf.io/v1/resources/abc/providers/osfstorage/file.txt'; + const defaultRenderLink = `https://mfr.osf.io/render?url=${encodeURIComponent(encodedDownloadUrl)}`; + + function buildFileDetailsModel(renderLink: string): FileDetailsModel { + return { + id: 'file-1', + guid: 'file-guid-1', + name: 'file.txt', + kind: FileKind.File, + path: '/file.txt', + size: 128, + materializedPath: '/file.txt', + dateModified: '2026-01-01T00:00:00.000Z', + dateCreated: '2026-01-01T00:00:00.000Z', + lastTouched: null, + tags: [], + currentVersion: 1, + showAsUnviewed: false, + extra: { + hashes: { + md5: 'md5', + sha256: 'sha256', + }, + downloads: 1, + }, + links: { + info: '', + move: '', + upload: '', + delete: '', + download: '', + render: renderLink, + html: '', + self: '', + }, + target: {} as unknown as BaseNodeModel, + }; + } + + const defaultSignals: SignalOverride[] = [ + { selector: FilesSelectors.isOpenedFileLoading, value: false }, + { selector: FilesSelectors.getOpenedFile, value: buildFileDetailsModel(defaultRenderLink) }, + ]; + + function setup(overrides: SetupOverrides = {}) { + const route = ActivatedRouteMockBuilder.create() + .withParams(overrides.routeParams ?? { fileGuid: 'file-1' }) + .build(); + mockRouter = RouterMockBuilder.create().withUrl('/files/file-1/preview').build(); + viewOnlyService = ViewOnlyLinkHelperMock.simple(overrides.hasViewOnlyParam ?? false); + viewOnlyService.getViewOnlyParam = vi.fn().mockReturnValue(overrides.viewOnlyParam ?? null); + + const signals = mergeSignalOverrides(defaultSignals, [ + { + selector: FilesSelectors.getOpenedFile, + value: buildFileDetailsModel(overrides.renderLink ?? defaultRenderLink), + }, + ...(overrides.selectorOverrides ?? []), + ]); + + TestBed.configureTestingModule({ + imports: [FilePreviewComponent], + providers: [ + provideOSFCore(), + MockProvider(ActivatedRoute, route), + MockProvider(Router, mockRouter), + MockProvider(ViewOnlyLinkHelperService, viewOnlyService), + provideMockStore({ signals }), + ], + }); + + store = TestBed.inject(Store); + fixture = TestBed.createComponent(FilePreviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + } + + it('should create', () => { + setup(); + + expect(component).toBeTruthy(); + }); + + it('should dispatch get file action with route file guid on init', () => { + setup(); + + expect(store.dispatch).toHaveBeenCalledWith(new GetFile('file-1')); + }); + + it('should keep mfr url unchanged when render link has no nested url param', () => { + setup({ renderLink: 'https://mfr.osf.io/render' }); + (store.dispatch as Mock).mockClear(); + + const result = component.getMfrUrlWithVersion('2'); + + expect(result).toBe('https://mfr.osf.io/render'); + expect(store.dispatch).not.toHaveBeenCalled(); + }); + + it('should append version param to nested download url', () => { + setup(); + + const result = component.getMfrUrlWithVersion('3'); + + expect(result).toContain('https://mfr.osf.io/render?'); + expect(result).toContain(encodeURIComponent('version=3')); + }); + + it('should append view only param when present', () => { + setup({ hasViewOnlyParam: true, viewOnlyParam: 'view-token-1' }); + + const result = component.getMfrUrlWithVersion(); + + expect(result).toContain(encodeURIComponent('view_only=view-token-1')); + }); + + it('should return null for empty render link', () => { + setup({ renderLink: '' }); + + const result = component.getMfrUrlWithVersion(); + + expect(result).toBeNull(); + }); +}); diff --git a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.ts b/src/app/features/files/pages/file-preview/file-preview.component.ts similarity index 87% rename from src/app/features/files/pages/draft-file-detail/draft-file-detail.component.ts rename to src/app/features/files/pages/file-preview/file-preview.component.ts index 9dc15fcf0..352a9fbd7 100644 --- a/src/app/features/files/pages/draft-file-detail/draft-file-detail.component.ts +++ b/src/app/features/files/pages/file-preview/file-preview.component.ts @@ -1,8 +1,10 @@ import { createDispatchMap, select } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { switchMap } from 'rxjs'; -import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, DestroyRef, HostBinding, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; @@ -14,12 +16,14 @@ import { ViewOnlyLinkHelperService } from '@shared/services/view-only-link-helpe @Component({ selector: 'osf-draft-file-detail.component', - imports: [SubHeaderComponent, LoadingSpinnerComponent], - templateUrl: './draft-file-detail.component.html', - styleUrl: './draft-file-detail.component.scss', + imports: [SubHeaderComponent, LoadingSpinnerComponent, TranslatePipe], + templateUrl: './file-preview.component.html', + styleUrl: './file-preview..component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DraftFileDetailComponent { +export class FilePreviewComponent { + @HostBinding('class') classes = 'flex flex-column flex-1 w-full h-full'; + isFileLoading = select(FilesSelectors.isOpenedFileLoading); file = select(FilesSelectors.getOpenedFile); readonly router = inject(Router); 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 8eb487380..7b5f066e6 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 @@ -138,9 +138,7 @@ export class CustomStepComponent implements OnDestroy { onOpenFile(file: FileModel): void { if (this.draftId() && file.guid) { - const url = this.router.serializeUrl( - this.router.createUrlTree(['registries', 'drafts', this.draftId(), 'files', file.guid]) - ); + const url = this.router.serializeUrl(this.router.createUrlTree([this.draftId(), 'files', file.guid, 'preview'])); window.open(url, '_blank'); } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 2887a045d..bcec6b3a1 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -615,7 +615,8 @@ "resourceLanguage": "Resource Language", "resourceType": "Resource Type" }, - "title": "File Metadata" + "title": "File Metadata", + "previewNotAvailable": "File or Registration metadata not available in preview mode." }, "keywords": { "title": "Keywords" From 29c9361ffe0befd95db30dc6b49f0ed54b7d8dc1 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Tue, 21 Apr 2026 13:28:35 +0300 Subject: [PATCH 06/15] resolve CR comments --- .../files/pages/file-preview/file-preview..component.scss | 3 +-- .../files/pages/file-preview/file-preview.component.html | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/features/files/pages/file-preview/file-preview..component.scss b/src/app/features/files/pages/file-preview/file-preview..component.scss index 2ecbf1c4e..4bd4ee39e 100644 --- a/src/app/features/files/pages/file-preview/file-preview..component.scss +++ b/src/app/features/files/pages/file-preview/file-preview..component.scss @@ -1,7 +1,6 @@ .metadata { - color: var(--dark-blue-1); border: 1px solid var(--grey-2); - border-radius: 12px; + border-radius: 0.75rem; } .full-image { diff --git a/src/app/features/files/pages/file-preview/file-preview.component.html b/src/app/features/files/pages/file-preview/file-preview.component.html index 2402dacfe..2548a7650 100644 --- a/src/app/features/files/pages/file-preview/file-preview.component.html +++ b/src/app/features/files/pages/file-preview/file-preview.component.html @@ -23,8 +23,8 @@
From 2573a8151b5df7ab06875e8bdb19d3fb70293e84 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Tue, 21 Apr 2026 14:05:43 +0300 Subject: [PATCH 07/15] update code --- ...onent.scss => file-preview.component.scss} | 0 .../file-preview/file-preview.component.ts | 41 ++++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) rename src/app/features/files/pages/file-preview/{file-preview..component.scss => file-preview.component.scss} (100%) diff --git a/src/app/features/files/pages/file-preview/file-preview..component.scss b/src/app/features/files/pages/file-preview/file-preview.component.scss similarity index 100% rename from src/app/features/files/pages/file-preview/file-preview..component.scss rename to src/app/features/files/pages/file-preview/file-preview.component.scss diff --git a/src/app/features/files/pages/file-preview/file-preview.component.ts b/src/app/features/files/pages/file-preview/file-preview.component.ts index 352a9fbd7..ab9bbd904 100644 --- a/src/app/features/files/pages/file-preview/file-preview.component.ts +++ b/src/app/features/files/pages/file-preview/file-preview.component.ts @@ -10,48 +10,43 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { FilesSelectors, GetFile } from '@osf/features/files/store'; -import { LoadingSpinnerComponent } from '@shared/components/loading-spinner/loading-spinner.component'; -import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; -import { ViewOnlyLinkHelperService } from '@shared/services/view-only-link-helper.service'; +import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; +import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; @Component({ - selector: 'osf-draft-file-detail.component', + selector: 'osf-draft-file-detail', imports: [SubHeaderComponent, LoadingSpinnerComponent, TranslatePipe], templateUrl: './file-preview.component.html', - styleUrl: './file-preview..component.scss', + styleUrl: './file-preview.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class FilePreviewComponent { @HostBinding('class') classes = 'flex flex-column flex-1 w-full h-full'; - isFileLoading = select(FilesSelectors.isOpenedFileLoading); - file = select(FilesSelectors.getOpenedFile); - readonly router = inject(Router); - readonly route = inject(ActivatedRoute); - readonly sanitizer = inject(DomSanitizer); + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + private readonly sanitizer = inject(DomSanitizer); + private readonly destroyRef = inject(DestroyRef); private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); + + private readonly actions = createDispatchMap({ getFile: GetFile }); + + file = select(FilesSelectors.getOpenedFile); + isFileLoading = select(FilesSelectors.isOpenedFileLoading); + isIframeLoading = true; safeLink: SafeResourceUrl | null = null; - readonly destroyRef = inject(DestroyRef); - fileGuid = ''; - hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); - private readonly actions = createDispatchMap({ - getFile: GetFile, - }); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); constructor() { this.route.params .pipe( takeUntilDestroyed(this.destroyRef), - switchMap((params) => { - this.fileGuid = params['fileGuid']; - return this.actions.getFile(this.fileGuid); - }) + switchMap((params) => this.actions.getFile(params['fileGuid'])) ) - .subscribe(() => { - this.getIframeLink(''); - }); + .subscribe(() => this.getIframeLink('')); } getIframeLink(version: string) { From abd255bcbf36e21ff6a02c08cd7e0b1fd74e4c91 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Tue, 28 Apr 2026 16:39:25 +0300 Subject: [PATCH 08/15] delete files from draft registrations --- .../files/pages/files/files.component.ts | 4 +- .../custom-step/custom-step.component.html | 2 + .../files-control.component.html | 3 ++ .../files-control/files-control.component.ts | 37 ++++++++++++++++++- .../files-tree/files-tree.component.html | 12 ++++++ .../files-tree/files-tree.component.ts | 18 +++++++-- 6 files changed, 69 insertions(+), 7 deletions(-) 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..5774d84a1 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 @@ -181,6 +181,8 @@

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

(attachFile)="onAttachFile($event, q.responseKey!)" (openFile)="onOpenFile($event)" [filesViewOnly]="filesViewOnly()" + [isDraftResource]="true" + (removeFromAttachedFiles)="removeFromAttachedFiles($event, 'uploader')" > } 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..22f57ff84 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,9 @@ [resourceId]="projectId()" [provider]="provider()" [selectedFiles]="filesSelection" + [isDraftResource]="isDraftResource()" + (deleteEntryAction)="deleteEntry($event)" + (removeFromAttachedFiles)="onRemoveFromAttachedFiles($event)" (selectFile)="onFileTreeSelected($event)" (entryFileClicked)="onEntryFileClicked($event)" (uploadFilesConfirmed)="uploadFiles($event)" 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..cf8302235 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 @@ -8,10 +8,11 @@ import { Button } from 'primeng/button'; import { filter, finalize, switchMap, take } from 'rxjs'; import { HttpEventType } from '@angular/common/http'; -import { ChangeDetectionStrategy, Component, DestroyRef, inject, input, output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, output, signal } from '@angular/core'; import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { CreateFolderDialogComponent } from '@osf/features/files/components'; +import { DeleteEntry } from '@osf/features/files/store'; import { FileUploadDialogComponent } from '@osf/shared/components/file-upload-dialog/file-upload-dialog.component'; import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; @@ -53,7 +54,9 @@ export class FilesControlComponent { projectId = input.required(); provider = input.required(); filesViewOnly = input(false); + isDraftResource = input(false); attachFile = output(); + removeFromAttachedFiles = output(); openFile = output(); private readonly filesService = inject(FilesService); @@ -69,6 +72,7 @@ export class FilesControlComponent { readonly progress = signal(0); readonly fileName = signal(''); readonly dataLoaded = signal(false); + pageNumber = signal(1); fileIsUploading = signal(false); filesSelection: FileModel[] = []; @@ -79,11 +83,38 @@ export class FilesControlComponent { setFilesIsLoading: SetFilesIsLoading, setCurrentFolder: SetRegistriesCurrentFolder, getRootFolders: GetRootFolders, + deleteEntry: DeleteEntry, }); constructor() { this.setupRootFoldersLoader(); this.setupCurrentFolderWatcher(); + + effect(() => { + const currentFolder = this.currentFolder(); + if (currentFolder) { + this.pageNumber.set(1); + this.updateFilesList(); + } + }); + } + + updateFilesList = (): void => { + const currentFolder = this.currentFolder(); + const filesLink = currentFolder?.links.filesLink; + if (filesLink) { + this.actions.getFiles(filesLink, this.pageNumber()); + } + }; + + deleteEntry(file: FileModel): void { + this.actions.deleteEntry(file?.links.delete).subscribe(() => { + this.toastService.showSuccess('files.dialogs.deleteFile.success'); + this.updateFilesList(); + if (this.isDraftResource()) { + this.removeFromAttachedFiles.emit(file); + } + }); } onFileSelected(event: Event): void { @@ -169,6 +200,10 @@ export class FilesControlComponent { this.filesSelection = [...new Set(this.filesSelection)]; } + onRemoveFromAttachedFiles(file: FileModel) { + this.removeFromAttachedFiles.emit(file); + } + onLoadFiles(event: { link: string; page: number }) { this.actions.getFiles(event.link, event.page); } 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..c67c6d15b 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..d159baf24 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 { TooltipModule } 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, + TooltipModule, ], templateUrl: './files-tree.component.html', styleUrl: './files-tree.component.scss', @@ -101,12 +105,14 @@ 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(); + removeFromAttachedFiles = output(); renameEntryAction = output<{ newName: string; link: string }>(); loadFiles = output<{ link: string; page: number }>(); selectFile = output(); @@ -264,6 +270,10 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { this.clearSelection.emit(); } + deleteFile(file: FileModel) { + this.deleteEntry(file); + } + onFileMenuAction(action: FileMenuAction, file: FileModel): void { const { value, data } = action; @@ -344,12 +354,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 { From c3611feba5689d95e146732d2638095829a3d105 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Wed, 29 Apr 2026 15:22:23 +0300 Subject: [PATCH 09/15] resolve CR comments --- .../files/pages/files/files.component.html | 1 - .../custom-step/custom-step.component.html | 5 ++- .../custom-step/custom-step.component.ts | 4 +-- .../files-control.component.html | 3 -- .../files-control/files-control.component.ts | 32 +++---------------- .../files-tree/files-tree.component.html | 4 +-- .../files-tree/files-tree.component.ts | 8 ++--- 7 files changed, 12 insertions(+), 45 deletions(-) diff --git a/src/app/features/files/pages/files/files.component.html b/src/app/features/files/pages/files/files.component.html index 43d076f6b..ecc89b872 100644 --- a/src/app/features/files/pages/files/files.component.html +++ b/src/app/features/files/pages/files/files.component.html @@ -145,7 +145,6 @@ (unselectFile)="onFileTreeUnselected($event)" (clearSelection)="onClearSelection()" (entryFileClicked)="navigateToFile($event)" - (deleteEntryAction)="deleteEntry($event)" (renameEntryAction)="renameEntry($event)" (uploadFilesConfirmed)="uploadFiles($event)" (loadFiles)="onLoadFiles($event)" 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 5774d84a1..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,8 +181,7 @@

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

(attachFile)="onAttachFile($event, q.responseKey!)" (openFile)="onOpenFile($event)" [filesViewOnly]="filesViewOnly()" - [isDraftResource]="true" - (removeFromAttachedFiles)="removeFromAttachedFiles($event, 'uploader')" + (removeFromAttachedFiles)="removeFromAttachedFiles($event, q.responseKey!)" > } 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 22f57ff84..78c18d21c 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,9 +51,6 @@ [resourceId]="projectId()" [provider]="provider()" [selectedFiles]="filesSelection" - [isDraftResource]="isDraftResource()" - (deleteEntryAction)="deleteEntry($event)" - (removeFromAttachedFiles)="onRemoveFromAttachedFiles($event)" (selectFile)="onFileTreeSelected($event)" (entryFileClicked)="onEntryFileClicked($event)" (uploadFilesConfirmed)="uploadFiles($event)" 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 cf8302235..74470234c 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 @@ -8,7 +8,7 @@ import { Button } from 'primeng/button'; import { filter, finalize, switchMap, take } from 'rxjs'; import { HttpEventType } from '@angular/common/http'; -import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, DestroyRef, inject, input, output, signal } from '@angular/core'; import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { CreateFolderDialogComponent } from '@osf/features/files/components'; @@ -54,9 +54,8 @@ export class FilesControlComponent { projectId = input.required(); provider = input.required(); filesViewOnly = input(false); - isDraftResource = input(false); attachFile = output(); - removeFromAttachedFiles = output(); + removeFromAttachedFiles = output(); openFile = output(); private readonly filesService = inject(FilesService); @@ -72,7 +71,6 @@ export class FilesControlComponent { readonly progress = signal(0); readonly fileName = signal(''); readonly dataLoaded = signal(false); - pageNumber = signal(1); fileIsUploading = signal(false); filesSelection: FileModel[] = []; @@ -89,31 +87,13 @@ export class FilesControlComponent { constructor() { this.setupRootFoldersLoader(); this.setupCurrentFolderWatcher(); - - effect(() => { - const currentFolder = this.currentFolder(); - if (currentFolder) { - this.pageNumber.set(1); - this.updateFilesList(); - } - }); } - updateFilesList = (): void => { - const currentFolder = this.currentFolder(); - const filesLink = currentFolder?.links.filesLink; - if (filesLink) { - this.actions.getFiles(filesLink, this.pageNumber()); - } - }; - deleteEntry(file: FileModel): void { this.actions.deleteEntry(file?.links.delete).subscribe(() => { this.toastService.showSuccess('files.dialogs.deleteFile.success'); - this.updateFilesList(); - if (this.isDraftResource()) { - this.removeFromAttachedFiles.emit(file); - } + this.refreshFilesList(); + this.removeFromAttachedFiles.emit(file.id); }); } @@ -200,10 +180,6 @@ export class FilesControlComponent { this.filesSelection = [...new Set(this.filesSelection)]; } - onRemoveFromAttachedFiles(file: FileModel) { - this.removeFromAttachedFiles.emit(file); - } - onLoadFiles(event: { link: string; page: number }) { this.actions.getFiles(event.link, event.page); } 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 c67c6d15b..9e1861367 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -101,9 +101,9 @@ severity="danger" text rounded - [pTooltip]="'Delete'" + [pTooltip]="'common.buttons.cancel' | translate" tooltipPosition="top" - (onClick)="deleteFile(file)" + (onClick)="deleteEntry(file)" osfStopPropagation /> } 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 d159baf24..341fe4a97 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -4,7 +4,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { PrimeTemplate, TreeNode } from 'primeng/api'; import { Button } from 'primeng/button'; -import { TooltipModule } from 'primeng/tooltip'; +import { Tooltip } from 'primeng/tooltip'; import { Tree, TreeLazyLoadEvent, TreeNodeDropEvent, TreeNodeSelectEvent } from 'primeng/tree'; import { Clipboard } from '@angular/cdk/clipboard'; @@ -69,7 +69,7 @@ type FileTreeNode = FileModel & TreeNode; FileMenuComponent, StopPropagationDirective, Button, - TooltipModule, + Tooltip, ], templateUrl: './files-tree.component.html', styleUrl: './files-tree.component.scss', @@ -270,10 +270,6 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { this.clearSelection.emit(); } - deleteFile(file: FileModel) { - this.deleteEntry(file); - } - onFileMenuAction(action: FileMenuAction, file: FileModel): void { const { value, data } = action; From 960ce79813de59bd9821a6c9b1f4a1f3f1655f0d Mon Sep 17 00:00:00 2001 From: mkovalua Date: Wed, 29 Apr 2026 16:03:09 +0300 Subject: [PATCH 10/15] use some of CR approaches & keep some existing approaches to show the delete button --- src/app/features/files/pages/files/files.component.html | 1 + .../components/custom-step/custom-step.component.html | 1 + .../components/files-control/files-control.component.html | 2 ++ .../components/files-control/files-control.component.ts | 1 + 4 files changed, 5 insertions(+) diff --git a/src/app/features/files/pages/files/files.component.html b/src/app/features/files/pages/files/files.component.html index ecc89b872..10a2f47bb 100644 --- a/src/app/features/files/pages/files/files.component.html +++ b/src/app/features/files/pages/files/files.component.html @@ -144,6 +144,7 @@ (selectFile)="onFileTreeSelected($event)" (unselectFile)="onFileTreeUnselected($event)" (clearSelection)="onClearSelection()" + (deleteEntryAction)="deleteEntry($event)" (entryFileClicked)="navigateToFile($event)" (renameEntryAction)="renameEntry($event)" (uploadFilesConfirmed)="uploadFiles($event)" 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 e3d0540a6..9701f2101 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 @@ -181,6 +181,7 @@

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

(attachFile)="onAttachFile($event, q.responseKey!)" (openFile)="onOpenFile($event)" [filesViewOnly]="filesViewOnly()" + [isDraftResource]="true" (removeFromAttachedFiles)="removeFromAttachedFiles($event, q.responseKey!)" > 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..2a42187c3 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]="isDraftResource()" + (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.ts b/src/app/features/registries/components/files-control/files-control.component.ts index 74470234c..d3d219286 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 @@ -54,6 +54,7 @@ export class FilesControlComponent { projectId = input.required(); provider = input.required(); filesViewOnly = input(false); + isDraftResource = input(false); attachFile = output(); removeFromAttachedFiles = output(); openFile = output(); From 81d76ddef0de3cf39b558880b43b6dfba457d1f4 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Wed, 29 Apr 2026 16:23:52 +0300 Subject: [PATCH 11/15] update tests and code --- .../components/custom-step/custom-step.component.spec.ts | 4 ++-- .../shared/components/files-tree/files-tree.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/shared/components/files-tree/files-tree.component.html b/src/app/shared/components/files-tree/files-tree.component.html index 9e1861367..ba61a2c22 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -101,7 +101,7 @@ severity="danger" text rounded - [pTooltip]="'common.buttons.cancel' | translate" + [pTooltip]="'common.buttons.delete' | translate" tooltipPosition="top" (onClick)="deleteEntry(file)" osfStopPropagation From c1a66e1c35d5af637985123bbaa50d642f10ef3f Mon Sep 17 00:00:00 2001 From: mkovalua Date: Wed, 29 Apr 2026 18:43:23 +0300 Subject: [PATCH 12/15] resolve CR comments --- .../components/custom-step/custom-step.component.html | 1 - .../files-control/files-control.component.html | 2 +- .../files-control/files-control.component.ts | 7 +++---- .../registries/store/handlers/files.handlers.ts | 11 ++++++++++- .../features/registries/store/registries.actions.ts | 6 ++++++ src/app/features/registries/store/registries.state.ts | 6 ++++++ 6 files changed, 26 insertions(+), 7 deletions(-) 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 9701f2101..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 @@ -181,7 +181,6 @@

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

(attachFile)="onAttachFile($event, q.responseKey!)" (openFile)="onOpenFile($event)" [filesViewOnly]="filesViewOnly()" - [isDraftResource]="true" (removeFromAttachedFiles)="removeFromAttachedFiles($event, q.responseKey!)" > 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 2a42187c3..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,7 +51,7 @@ [resourceId]="projectId()" [provider]="provider()" [selectedFiles]="filesSelection" - [isDraftResource]="isDraftResource()" + [isDraftResource]="true" (deleteEntryAction)="deleteEntry($event)" (selectFile)="onFileTreeSelected($event)" (entryFileClicked)="onEntryFileClicked($event)" 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 d3d219286..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 @@ -12,7 +12,6 @@ import { ChangeDetectionStrategy, Component, DestroyRef, inject, input, output, import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; import { CreateFolderDialogComponent } from '@osf/features/files/components'; -import { DeleteEntry } from '@osf/features/files/store'; import { FileUploadDialogComponent } from '@osf/shared/components/file-upload-dialog/file-upload-dialog.component'; import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; @@ -26,6 +25,7 @@ import { FileFolderModel } from '@shared/models/files/file-folder.model'; import { CreateFolder, + DeleteDraftRegistrationFiles, GetFiles, GetRootFolders, RegistriesSelectors, @@ -54,7 +54,6 @@ export class FilesControlComponent { projectId = input.required(); provider = input.required(); filesViewOnly = input(false); - isDraftResource = input(false); attachFile = output(); removeFromAttachedFiles = output(); openFile = output(); @@ -82,7 +81,7 @@ export class FilesControlComponent { setFilesIsLoading: SetFilesIsLoading, setCurrentFolder: SetRegistriesCurrentFolder, getRootFolders: GetRootFolders, - deleteEntry: DeleteEntry, + deleteDraftRegistrationFiles: DeleteDraftRegistrationFiles, }); constructor() { @@ -91,7 +90,7 @@ export class FilesControlComponent { } deleteEntry(file: FileModel): void { - this.actions.deleteEntry(file?.links.delete).subscribe(() => { + this.actions.deleteDraftRegistrationFiles(file?.links.delete).subscribe(() => { this.toastService.showSuccess('files.dialogs.deleteFile.success'); this.refreshFilesList(); this.removeFromAttachedFiles.emit(file.id); 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); From f174f0af7bd6fa586239155d7e6d7ad9b1c8fd79 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Wed, 29 Apr 2026 19:06:14 +0300 Subject: [PATCH 13/15] update unittests --- .../files-control/files-control.component.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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'); + }); }); From bba5bfb9bb6fac35157e411e985fc8fcfde8b6bf Mon Sep 17 00:00:00 2001 From: mkovalua Date: Thu, 30 Apr 2026 12:44:52 +0300 Subject: [PATCH 14/15] remove not used attribute --- src/app/shared/components/files-tree/files-tree.component.ts | 1 - 1 file changed, 1 deletion(-) 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 341fe4a97..efe638512 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -112,7 +112,6 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { setCurrentFolder = output(); setMoveDialogCurrentFolder = output(); deleteEntryAction = output(); - removeFromAttachedFiles = output(); renameEntryAction = output<{ newName: string; link: string }>(); loadFiles = output<{ link: string; page: number }>(); selectFile = output(); From 835b490778f26c662a05e574baac0acd26165f91 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Thu, 30 Apr 2026 22:18:36 +0300 Subject: [PATCH 15/15] Update allowedHosts in angular.json for security --- angular.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/angular.json b/angular.json index 47e749c53..8a4e753dc 100644 --- a/angular.json +++ b/angular.json @@ -69,7 +69,7 @@ "entry": "src/server.ts" }, "security": { - "allowedHosts": ["localhost", "127.0.0.1"] + "allowedHosts": ["localhost", "127.0.0.1", "10.*", "*.osf.io", "osf.io"] } }, "configurations": {