From 96794a618b12c1f6fb5c3229bd17dd96e52d2ff4 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Tue, 16 Sep 2025 15:27:47 +0300 Subject: [PATCH 1/3] fix(view-only-links): fixed view only links files access issues --- .../interceptors/view-only.interceptor.ts | 4 +++ .../file-keywords.component.html | 27 ++++++++++++------- .../file-keywords/file-keywords.component.ts | 7 +++-- .../file-metadata.component.html | 18 +++++++------ .../file-metadata/file-metadata.component.ts | 7 +++-- .../file-resource-metadata.component.html | 2 +- .../file-resource-metadata.component.ts | 9 +++++-- .../files/mappers/resource-metadata.mapper.ts | 25 +++++++++-------- .../file-detail/file-detail.component.html | 6 ++--- .../file-detail/file-detail.component.ts | 21 +++++++++++++-- .../files/pages/files/files.component.ts | 10 +++++-- .../files-widget/files-widget.component.ts | 10 +++++-- .../contributors/contributors.mapper.ts | 12 ++++----- src/app/shared/services/files.service.ts | 11 +++++--- 14 files changed, 115 insertions(+), 54 deletions(-) diff --git a/src/app/core/interceptors/view-only.interceptor.ts b/src/app/core/interceptors/view-only.interceptor.ts index 908913796..a0a4d91e1 100644 --- a/src/app/core/interceptors/view-only.interceptor.ts +++ b/src/app/core/interceptors/view-only.interceptor.ts @@ -15,6 +15,10 @@ export const viewOnlyInterceptor: HttpInterceptorFn = ( const viewOnlyParam = getViewOnlyParam(router); if (!req.url.includes('/api.crossref.org/funders') && viewOnlyParam) { + if (req.url.includes('view_only=')) { + return next(req); + } + const separator = req.url.includes('?') ? '&' : '?'; const updatedUrl = `${req.url}${separator}view_only=${encodeURIComponent(viewOnlyParam)}`; diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.html b/src/app/features/files/components/file-keywords/file-keywords.component.html index 495d7d9e5..123708174 100644 --- a/src/app/features/files/components/file-keywords/file-keywords.component.html +++ b/src/app/features/files/components/file-keywords/file-keywords.component.html @@ -1,21 +1,28 @@

{{ 'files.detail.keywords.title' | translate }}

-
- + @if (!hasViewOnly()) { +
+ - - -
+ + +
+ } @if (!isTagsLoading()) {
@for (tag of tags(); track $index) { - + }
} @else { diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.ts b/src/app/features/files/components/file-keywords/file-keywords.component.ts index 7991a5740..80e08b78d 100644 --- a/src/app/features/files/components/file-keywords/file-keywords.component.ts +++ b/src/app/features/files/components/file-keywords/file-keywords.component.ts @@ -7,11 +7,12 @@ import { Chip } from 'primeng/chip'; import { InputText } from 'primeng/inputtext'; import { Skeleton } from 'primeng/skeleton'; -import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; -import { CustomValidators } from '@osf/shared/helpers'; +import { CustomValidators, hasViewOnlyParam } from '@osf/shared/helpers'; import { InputLimits } from '@shared/constants'; import { FilesSelectors, UpdateTags } from '../../store'; @@ -26,10 +27,12 @@ import { FilesSelectors, UpdateTags } from '../../store'; export class FileKeywordsComponent { private readonly actions = createDispatchMap({ updateTags: UpdateTags }); private readonly destroyRef = inject(DestroyRef); + private readonly router = inject(Router); readonly tags = select(FilesSelectors.getFileTags); readonly isTagsLoading = select(FilesSelectors.isFileTagsLoading); readonly file = select(FilesSelectors.getOpenedFile); + readonly hasViewOnly = computed(() => hasViewOnlyParam(this.router)); keywordControl = new FormControl('', { nonNullable: true, diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.html b/src/app/features/files/components/file-metadata/file-metadata.component.html index 868f4fbd1..1f24fde19 100644 --- a/src/app/features/files/components/file-metadata/file-metadata.component.html +++ b/src/app/features/files/components/file-metadata/file-metadata.component.html @@ -2,15 +2,17 @@

{{ 'files.detail.fileMetadata.title' | translate }}

-
- + @if (!hasViewOnly()) { +
+ - -
+ +
+ }
@if (isLoading()) { diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.ts b/src/app/features/files/components/file-metadata/file-metadata.component.ts index cd7ec7d92..5c24dfa75 100644 --- a/src/app/features/files/components/file-metadata/file-metadata.component.ts +++ b/src/app/features/files/components/file-metadata/file-metadata.component.ts @@ -8,11 +8,12 @@ import { Skeleton } from 'primeng/skeleton'; import { filter, map, of } from 'rxjs'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { languageCodes } from '@osf/shared/constants'; +import { hasViewOnlyParam } from '@osf/shared/helpers'; import { LanguageCodeModel } from '@osf/shared/models'; import { FileMetadataFields } from '../../constants'; @@ -33,11 +34,13 @@ import { environment } from 'src/environments/environment'; export class FileMetadataComponent { private readonly actions = createDispatchMap({ setFileMetadata: SetFileMetadata }); private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); private readonly dialogService = inject(DialogService); private readonly translateService = inject(TranslateService); fileMetadata = select(FilesSelectors.getFileCustomMetadata); isLoading = select(FilesSelectors.isFileMetadataLoading); + hasViewOnly = computed(() => hasViewOnlyParam(this.router)); readonly languageCodes = languageCodes; diff --git a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html index 73a66aca3..06b9f50c5 100644 --- a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html +++ b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html @@ -98,7 +98,7 @@

{{ 'files.detail.resourceMetadata.fields.dateModified' | translate }}

@if (isResourceContributorsLoading()) { } @else { - @if (contributors()?.length) { + @if (contributors()?.length && !hasViewOnly()) {

{{ 'files.detail.resourceMetadata.fields.contributors' | translate }}

diff --git a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts index 3e962d0c2..adcaaab74 100644 --- a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts +++ b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts @@ -5,8 +5,10 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Skeleton } from 'primeng/skeleton'; import { DatePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; -import { RouterLink } from '@angular/router'; +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; +import { Router, RouterLink } from '@angular/router'; + +import { hasViewOnlyParam } from '@osf/shared/helpers'; import { FilesSelectors } from '../../store'; @@ -18,9 +20,12 @@ import { FilesSelectors } from '../../store'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class FileResourceMetadataComponent { + private readonly router = inject(Router); + resourceType = input('nodes'); resourceMetadata = select(FilesSelectors.getResourceMetadata); contributors = select(FilesSelectors.getContributors); isResourceMetadataLoading = select(FilesSelectors.isResourceMetadataLoading); isResourceContributorsLoading = select(FilesSelectors.isResourceContributorsLoading); + hasViewOnly = computed(() => hasViewOnlyParam(this.router)); } diff --git a/src/app/features/files/mappers/resource-metadata.mapper.ts b/src/app/features/files/mappers/resource-metadata.mapper.ts index b5a993af2..0f3890722 100644 --- a/src/app/features/files/mappers/resource-metadata.mapper.ts +++ b/src/app/features/files/mappers/resource-metadata.mapper.ts @@ -13,16 +13,19 @@ export function MapResourceMetadata( description: shortInfo.data.attributes.description, dateCreated: new Date(shortInfo.data.attributes.date_created), dateModified: new Date(shortInfo.data.attributes.date_modified), - funders: customMetadata.data.embeds.custom_metadata.data.attributes.funders.map((funder) => ({ - funderName: funder.funder_name, - funderIdentifier: funder.funder_identifier, - funderIdentifierType: funder.funder_identifier_type, - awardNumber: funder.award_number, - awardUri: funder.award_uri, - awardTitle: funder.award_title, - })), - identifiers: IdentifiersMapper.fromJsonApi(shortInfo.data.embeds.identifiers), - language: customMetadata.data.embeds.custom_metadata.data.attributes.language, - resourceTypeGeneral: customMetadata.data.embeds.custom_metadata.data.attributes.resource_type_general, + funders: + customMetadata.data.embeds?.custom_metadata?.data?.attributes?.funders?.map((funder) => ({ + funderName: funder.funder_name, + funderIdentifier: funder.funder_identifier, + funderIdentifierType: funder.funder_identifier_type, + awardNumber: funder.award_number, + awardUri: funder.award_uri, + awardTitle: funder.award_title, + })) || [], + identifiers: shortInfo.data.embeds?.identifiers?.data.length + ? IdentifiersMapper.fromJsonApi(shortInfo.data.embeds?.identifiers) + : [], + language: customMetadata.data.embeds?.custom_metadata?.data?.attributes?.language || '', + resourceTypeGeneral: customMetadata.data.embeds?.custom_metadata?.data?.attributes?.resource_type_general || '', }; } diff --git a/src/app/features/files/pages/file-detail/file-detail.component.html b/src/app/features/files/pages/file-detail/file-detail.component.html index 05c85e754..5e96f3629 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.html +++ b/src/app/features/files/pages/file-detail/file-detail.component.html @@ -12,13 +12,13 @@
- @if (!isAnonymous()) { + @if (!isAnonymous() && !hasViewOnly()) {
} - @if (file() && !isAnonymous()) { + @if (file() && !isAnonymous() && !hasViewOnly()) { hasViewOnlyParam(this.router)); + + get backNavigationQueryParams(): Record | null { + const viewOnlyParam = getViewOnlyParam(this.router); + return viewOnlyParam ? { view_only: viewOnlyParam } : null; + } + safeLink: SafeResourceUrl | null = null; resourceId = ''; resourceType = ''; @@ -221,7 +228,7 @@ export class FileDetailComponent { .subscribe(() => { const link = this.file()?.links.render; if (link) { - this.safeLink = this.sanitizer.bypassSecurityTrustResourceUrl(link); + this.safeLink = this.sanitizer.bypassSecurityTrustResourceUrl(this.addViewOnlyToUrl(link)); } this.resourceId = this.file()?.target.id || ''; this.resourceType = this.file()?.target.type || ''; @@ -393,4 +400,14 @@ export class FileDetailComponent { this.actions.getCedarTemplates(); } } + + private addViewOnlyToUrl(url: string): string { + if (!this.hasViewOnly()) return url; + + const viewOnlyParam = getViewOnlyParam(); + if (!viewOnlyParam) return url; + + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}view_only=${encodeURIComponent(viewOnlyParam)}`; + } } diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index eb6f6bdcd..d4647570d 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -46,7 +46,7 @@ import { } from '@osf/features/files/store'; import { ALL_SORT_OPTIONS } from '@osf/shared/constants'; import { ResourceType, UserPermissions } from '@osf/shared/enums'; -import { hasViewOnlyParam, IS_MEDIUM } from '@osf/shared/helpers'; +import { getViewOnlyParamFromUrl, hasViewOnlyParam, IS_MEDIUM } from '@osf/shared/helpers'; import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores'; import { FilesTreeComponent, @@ -390,7 +390,13 @@ export class FilesComponent { } navigateToFile(file: OsfFile) { - this.router.navigate([file.guid], { relativeTo: this.activeRoute }); + const viewOnlyParam = getViewOnlyParamFromUrl(this.router.url); + const queryParams = viewOnlyParam ? { view_only: viewOnlyParam } : null; + + this.router.navigate([file.guid], { + relativeTo: this.activeRoute, + queryParams, + }); } getAddonName(addons: ConfiguredAddonModel[], provider: string): string { diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.ts index 5007345db..9babb7252 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.ts +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.ts @@ -30,7 +30,7 @@ import { SetFilesIsLoading, } from '@osf/features/files/store'; import { FilesTreeComponent, SelectComponent } from '@osf/shared/components'; -import { Primitive } from '@osf/shared/helpers'; +import { getViewOnlyParamFromUrl, Primitive } from '@osf/shared/helpers'; import { ConfiguredAddonModel, FileLabelModel, @@ -218,7 +218,13 @@ export class FilesWidgetComponent { } navigateToFile(file: OsfFile) { - this.router.navigate(['files', file.guid], { relativeTo: this.activeRoute.parent }); + const viewOnlyParam = getViewOnlyParamFromUrl(this.router.url); + const queryParams = viewOnlyParam ? { view_only: viewOnlyParam } : null; + + this.router.navigate(['files', file.guid], { + relativeTo: this.activeRoute.parent, + queryParams, + }); } onFilesPageChange(page: number) { diff --git a/src/app/shared/mappers/contributors/contributors.mapper.ts b/src/app/shared/mappers/contributors/contributors.mapper.ts index 7a95fc4b7..d344f1e02 100644 --- a/src/app/shared/mappers/contributors/contributors.mapper.ts +++ b/src/app/shared/mappers/contributors/contributors.mapper.ts @@ -13,17 +13,17 @@ export class ContributorsMapper { static fromResponse(response: ContributorResponse[]): ContributorModel[] { return response.map((contributor) => ({ id: contributor.id, - userId: contributor.embeds.users.data.id, + userId: contributor.embeds?.users?.data?.id || '', type: contributor.type, isBibliographic: contributor.attributes.bibliographic, isUnregisteredContributor: !!contributor.attributes.unregistered_contributor, isCurator: contributor.attributes.is_curator, permission: contributor.attributes.permission, - fullName: contributor.embeds.users.data.attributes.full_name, - givenName: contributor.embeds.users.data.attributes.given_name, - familyName: contributor.embeds.users.data.attributes.family_name, - education: contributor.embeds.users.data.attributes.education, - employment: contributor.embeds.users.data.attributes.employment, + fullName: contributor.embeds?.users?.data?.attributes?.full_name || '', + givenName: contributor.embeds?.users?.data?.attributes?.given_name || '', + familyName: contributor.embeds?.users?.data?.attributes?.family_name || '', + education: contributor.embeds?.users?.data?.attributes?.education || '', + employment: contributor.embeds?.users?.data?.attributes?.employment || '', })); } diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 0a7232fed..0ec82d73e 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -166,10 +166,12 @@ export class FilesService { } getFolderDownloadLink(storageLink: string, folderId: string, isRootFolder: boolean): string { + const separator = storageLink.includes('?') ? '&' : '?'; + if (isRootFolder) { - return `${storageLink}?zip=`; + return `${storageLink}${separator}zip=`; } - return `${storageLink}${folderId}/?zip=`; + return `${storageLink}${folderId}/${separator}zip=`; } getFileTarget(fileGuid: string): Observable { @@ -250,8 +252,11 @@ export class FilesService { } getFileRevisions(link: string): Observable { + const separator = link.includes('?') ? '&' : '?'; + const urlWithRevisions = `${link}${separator}revisions=`; + return this.jsonApiService - .get(`${link}?revisions=`) + .get(urlWithRevisions) .pipe(map((response) => MapFileRevision(response.data))); } From 1375f2396c57b6b8258a2135a2603d747cbe24a7 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Tue, 16 Sep 2025 19:54:03 +0300 Subject: [PATCH 2/3] fix(view-only-links): fixed file guid absence while redirecting --- .../pages/file-detail/file-detail.component.html | 6 +++--- .../files/pages/file-detail/file-detail.component.ts | 5 ----- .../files-container/files-container.component.scss | 5 +++++ .../files-container/files-container.component.ts | 1 + .../features/files/pages/files/files.component.ts | 7 ++----- .../files-widget/files-widget.component.ts | 7 ++----- .../components/files-tree/files-tree.component.html | 6 +++--- .../components/files-tree/files-tree.component.scss | 3 +++ .../metadata-tabs/metadata-tabs.component.scss | 5 +++++ .../services/activity-logs/activity-logs.service.ts | 4 ++-- src/app/shared/services/files.service.ts | 12 +++++++++++- src/testing/data/activity-logs/activity-logs.data.ts | 2 +- 12 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 src/app/features/files/pages/files-container/files-container.component.scss diff --git a/src/app/features/files/pages/file-detail/file-detail.component.html b/src/app/features/files/pages/file-detail/file-detail.component.html index 5e96f3629..1e7d75fd7 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.html +++ b/src/app/features/files/pages/file-detail/file-detail.component.html @@ -1,6 +1,6 @@ - + {{ 'files.detail.tabs.details' | translate }} {{ 'files.detail.tabs.revisions' | translate }} @@ -12,7 +12,7 @@
@@ -108,7 +108,7 @@ } @else if (selectedTab === FileDetailTab.Keywords) { } @else { -