Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { STATES } from '@core/constants';
import { provideTranslation } from '@core/helpers';

import { GlobalErrorHandler } from './core/handlers';
import { authInterceptor, errorInterceptor } from './core/interceptors';
import { authInterceptor, errorInterceptor, viewOnlyInterceptor } from './core/interceptors';
import CustomPreset from './core/theme/custom-preset';
import { routes } from './app.routes';

Expand All @@ -37,7 +37,7 @@ export const appConfig: ApplicationConfig = {
},
}),
provideAnimations(),
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
provideHttpClient(withInterceptors([authInterceptor, viewOnlyInterceptor, errorInterceptor])),
importProvidersFrom(TranslateModule.forRoot(provideTranslation())),
ConfirmationService,
MessageService,
Expand Down
1 change: 1 addition & 0 deletions src/app/core/components/nav-menu/nav-menu.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<ng-template #item let-item>
<a
[routerLink]="item.routerLink ? item.routerLink : null"
[queryParams]="item.queryParams"
routerLinkActive="active"
[routerLinkActiveOptions]="item.routerLinkActiveOptions"
class="nav-link flex align-items-center"
Expand Down
6 changes: 6 additions & 0 deletions src/app/core/components/nav-menu/nav-menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { AuthService } from '@osf/core/services';
import { UserSelectors } from '@osf/core/store/user';
import { IconComponent } from '@osf/shared/components';
import { CurrentResourceType } from '@osf/shared/enums';
import { getViewOnlyParam } from '@osf/shared/helpers';
import { WrapFnPipe } from '@osf/shared/pipes';
import { CurrentResourceSelectors } from '@osf/shared/stores';

Expand Down Expand Up @@ -54,6 +55,7 @@ export class NavMenuComponent {
preprintReviewsPageVisible: this.canUserViewReviews(),
isCollections: this.isCollectionsRoute() || false,
currentUrl: this.router.url,
isViewOnly: this.isViewOnlyMode(),
Comment thread
rnastyuk marked this conversation as resolved.
Outdated
};

const items = updateMenuItems(filtered, routeContext);
Expand All @@ -77,6 +79,10 @@ export class NavMenuComponent {
protected readonly isPreprintRoute = computed(() => this.currentRoute().isPreprintRoute);
protected readonly canUserViewReviews = select(UserSelectors.getCanViewReviews);

private isViewOnlyMode(): boolean {
return !!getViewOnlyParam(this.router);
}
Comment thread
rnastyuk marked this conversation as resolved.
Outdated

private getRouteInfo() {
const urlSegments = this.router.url.split('/').filter((segment) => segment);
const resourceFromQueryParams = this.route.snapshot.queryParams['resourceId'];
Expand Down
15 changes: 15 additions & 0 deletions src/app/core/constants/nav-items.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ export const AUTHENTICATED_MENU_ITEMS: string[] = [
'settings',
];

export const VIEW_ONLY_PROJECT_MENU_ITEMS: string[] = [
'project-overview',
'project-files',
'project-wiki',
'project-analytics',
];

export const VIEW_ONLY_REGISTRY_MENU_ITEMS: string[] = [
'registration-overview',
'registration-files',
'registration-wiki',
'registration-analytics',
'registration-components',
];

export const PROJECT_MENU_ITEMS: MenuItem[] = [
{
id: 'project-overview',
Expand Down
9 changes: 9 additions & 0 deletions src/app/core/constants/view-only-excluded-routes.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const VIEW_ONLY_EXCLUDED_ROUTES = [
'metadata',
'registrations',
'contributors',
'addons',
'settings',
'resources',
'links',
];
5 changes: 5 additions & 0 deletions src/app/core/guards/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';

import { GetCurrentUser, UserSelectors } from '@osf/core/store/user';
import { hasViewOnlyParam } from '@osf/shared/helpers';

export const authGuard: CanActivateFn = () => {
const store = inject(Store);
Expand All @@ -17,6 +18,10 @@ export const authGuard: CanActivateFn = () => {
return true;
}

if (hasViewOnlyParam(router)) {
return true;
}

return store.dispatch(GetCurrentUser).pipe(
switchMap(() => {
return store.select(UserSelectors.isAuthenticated).pipe(
Expand Down
1 change: 1 addition & 0 deletions src/app/core/guards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { authGuard } from './auth.guard';
export { isProjectGuard } from './is-project.guard';
export { isRegistryGuard } from './is-registry.guard';
export { redirectIfLoggedInGuard } from './redirect-if-logged-in.guard';
export { viewOnlyGuard } from './view-only.guard';
37 changes: 37 additions & 0 deletions src/app/core/guards/view-only.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';

import { VIEW_ONLY_EXCLUDED_ROUTES } from '@core/constants/view-only-excluded-routes.const';
import { hasViewOnlyParam } from '@osf/shared/helpers';

export const viewOnlyGuard: CanActivateFn = (route) => {
const router = inject(Router);

if (!hasViewOnlyParam(router)) {
return true;
}

const routePath = route.routeConfig?.path || '';

const isBlocked = VIEW_ONLY_EXCLUDED_ROUTES.some(
(blockedRoute) => routePath === blockedRoute || routePath.startsWith(`${blockedRoute}/`)
);

if (!isBlocked) {
return true;
}

const urlSegments = router.url.split('/');
const resourceId = urlSegments[1];
const viewOnlyParam = new URLSearchParams(window.location.search).get('view_only');

if (resourceId && viewOnlyParam) {
router.navigate([resourceId, 'overview'], {
queryParams: { view_only: viewOnlyParam },
});
} else {
router.navigate(['/']);
}

return false;
};
25 changes: 23 additions & 2 deletions src/app/core/helpers/nav-menu.helper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { MenuItem } from 'primeng/api';

import { getViewOnlyParamFromUrl } from '@osf/shared/helpers';

import {
AUTHENTICATED_MENU_ITEMS,
PREPRINT_MENU_ITEMS,
PROJECT_MENU_ITEMS,
REGISTRATION_MENU_ITEMS,
VIEW_ONLY_PROJECT_MENU_ITEMS,
VIEW_ONLY_REGISTRY_MENU_ITEMS,
} from '../constants';
import { RouteContext } from '../models';

Expand Down Expand Up @@ -82,13 +86,21 @@ function updateProjectMenuItem(item: MenuItem, ctx: RouteContext): MenuItem {
const items = (item.items || []).map((subItem) => {
if (subItem.id === 'project-details') {
if (hasProject) {
let menuItems = PROJECT_MENU_ITEMS;

if (ctx.isViewOnly) {
const allowedViewOnlyItems = VIEW_ONLY_PROJECT_MENU_ITEMS;
menuItems = PROJECT_MENU_ITEMS.filter((menuItem) => allowedViewOnlyItems.includes(menuItem.id || ''));
}

return {
...subItem,
visible: true,
expanded: true,
items: PROJECT_MENU_ITEMS.map((menuItem) => ({
items: menuItems.map((menuItem) => ({
...menuItem,
routerLink: [ctx.resourceId as string, menuItem.routerLink],
queryParams: ctx.isViewOnly ? { view_only: getViewOnlyParamFromUrl(ctx.currentUrl) } : undefined,
})),
};
}
Expand All @@ -105,13 +117,21 @@ function updateRegistryMenuItem(item: MenuItem, ctx: RouteContext): MenuItem {
const items = (item.items || []).map((subItem) => {
if (subItem.id === 'registry-details') {
if (hasRegistry) {
let menuItems = REGISTRATION_MENU_ITEMS;

if (ctx.isViewOnly) {
const allowedViewOnlyItems = VIEW_ONLY_REGISTRY_MENU_ITEMS;
menuItems = REGISTRATION_MENU_ITEMS.filter((menuItem) => allowedViewOnlyItems.includes(menuItem.id || ''));
}

return {
...subItem,
visible: true,
expanded: true,
items: REGISTRATION_MENU_ITEMS.map((menuItem) => ({
items: menuItems.map((menuItem) => ({
...menuItem,
routerLink: [ctx.resourceId as string, menuItem.routerLink],
queryParams: ctx.isViewOnly ? { view_only: getViewOnlyParamFromUrl(ctx.currentUrl) } : undefined,
})),
};
}
Expand All @@ -135,6 +155,7 @@ function updatePreprintMenuItem(item: MenuItem, ctx: RouteContext): MenuItem {
items: PREPRINT_MENU_ITEMS.map((menuItem) => ({
...menuItem,
routerLink: ['preprints', ctx.providerId, ctx.resourceId as string],
queryParams: ctx.isViewOnly ? { view_only: getViewOnlyParamFromUrl(ctx.currentUrl) } : undefined,
})),
};
}
Expand Down
5 changes: 4 additions & 1 deletion src/app/core/interceptors/error.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';

import { hasViewOnlyParam } from '@osf/shared/helpers';
import { LoaderService, ToastService } from '@osf/shared/services';

import { ERROR_MESSAGES } from '../constants';
Expand All @@ -31,7 +32,9 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => {
}

if (error.status === 401) {
authService.logout();
if (!hasViewOnlyParam(router)) {
authService.logout();
}
return throwError(() => error);
}

Expand Down
1 change: 1 addition & 0 deletions src/app/core/interceptors/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './auth.interceptor';
export * from './error.interceptor';
export * from './view-only.interceptor';
29 changes: 29 additions & 0 deletions src/app/core/interceptors/view-only.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Observable } from 'rxjs';

import { HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';

import { getViewOnlyParam } from '@osf/shared/helpers';

export const viewOnlyInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
const router = inject(Router);

const viewOnlyParam = getViewOnlyParam(router);

if (!req.url.includes('/api.crossref.org/funders') && viewOnlyParam) {
const separator = req.url.includes('?') ? '&' : '?';
const updatedUrl = `${req.url}${separator}view_only=${encodeURIComponent(viewOnlyParam)}`;

const viewOnlyReq = req.clone({
url: updatedUrl,
});

return next(viewOnlyReq);
} else {
return next(req);
}
};
1 change: 1 addition & 0 deletions src/app/core/models/route-context.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface RouteContext {
preprintReviewsPageVisible?: boolean;
isCollections: boolean;
currentUrl?: string;
isViewOnly?: boolean;
}
1 change: 1 addition & 0 deletions src/app/features/files/models/file-target.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export interface OsfFileTarget {
wikiEnabled: boolean;
public: boolean;
type: string;
isAnonymous?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
</div>

<div class="flex gap-3 ml-auto sm-round-padding-btn">
<p-button
severity="secondary"
[label]="'files.detail.actions.addCommunityMetadata' | translate"
[routerLink]="['/metadata']"
></p-button>
@if (!isAnonymous()) {
<p-button
severity="secondary"
[label]="'files.detail.actions.addCommunityMetadata' | translate"
[routerLink]="['/metadata']"
></p-button>
}

@if (file()?.links?.download) {
<p-button severity="secondary" (click)="downloadFile(file()?.links?.download!)">
Expand Down Expand Up @@ -55,7 +57,7 @@
</p-menu>
</div>
}
@if (file()) {
@if (file() && !isAnonymous()) {
<p-button severity="danger" (click)="confirmDelete(file()!)">
<i class="fas fa-trash p-1"></i>
</p-button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,16 @@ export class FileDetailComponent {

file = select(FilesSelectors.getOpenedFile);
isFileLoading = select(FilesSelectors.isOpenedFileLoading);
isAnonymous = select(FilesSelectors.isFilesAnonymous);
safeLink: SafeResourceUrl | null = null;
resourceId = '';
resourceType = '';

isIframeLoading = true;

protected readonly FileDetailTab = FileDetailTab;
readonly FileDetailTab = FileDetailTab;

protected selectedTab: FileDetailTab = FileDetailTab.Details;
selectedTab: FileDetailTab = FileDetailTab.Details;

fileGuid = '';

Expand Down Expand Up @@ -189,27 +190,27 @@ export class FileDetailComponent {
this.selectedTab = index;
}

protected handleEmailShare(): void {
handleEmailShare(): void {
const link = `mailto:?subject=${this.file()?.name ?? ''}&body=${this.file()?.links?.html ?? ''}`;
window.location.href = link;
}

protected handleXShare(): void {
handleXShare(): void {
const link = `https://x.com/intent/tweet?url=${this.file()?.links?.html ?? ''}&text=${this.file()?.name ?? ''}&via=OSFramework`;
window.open(link, '_blank', 'noopener,noreferrer');
}

protected handleFacebookShare(): void {
handleFacebookShare(): void {
const link = `https://www.facebook.com/dialog/share?app_id=1022273774556662&display=popup&href=${this.file()?.links?.html ?? ''}&redirect_uri=${this.file()?.links?.html ?? ''}`;
window.open(link, '_blank', 'noopener,noreferrer');
}

protected handleCopyDynamicEmbed(): void {
handleCopyDynamicEmbed(): void {
const data = embedDynamicJs.replace('ENCODED_URL', this.file()?.links?.render ?? '');
this.copyToClipboard(data);
}

protected handleCopyStaticEmbed(): void {
handleCopyStaticEmbed(): void {
const data = embedStaticHtml.replace('ENCODED_URL', this.file()?.links?.render ?? '');
this.copyToClipboard(data);
}
Expand Down
7 changes: 6 additions & 1 deletion src/app/features/files/pages/files/files.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<osf-loading-spinner></osf-loading-spinner>
} @else {
<div class="flex flex-column bg-white gap-4 h-full flex-1 p-4 p-dialog-align-center">
@if (hasViewOnly()) {
<osf-view-only-link-message />
}

<div class="flex flex-grow-0 w-full sm:w-30rem">
<p-floatlabel class="w-full md:w-56" variant="in">
<p-select
Expand Down Expand Up @@ -55,7 +59,7 @@

<p-button icon="fas fa-info-circle blue-icon" severity="secondary" text (onClick)="showInfoDialog()"></p-button>

@if (!isViewOnly()) {
@if (!isViewOnly() && !isAnonymous()) {
<p-button
[disabled]="fileIsUploading() || isFilesLoading()"
outlined
Expand Down Expand Up @@ -108,6 +112,7 @@
[viewOnlyDownloadable]="isViewOnlyDownloadable()"
[resourceId]="resourceId()"
[provider]="provider()"
[isAnonymous]="isAnonymous()"
(folderIsOpening)="folderIsOpening($event)"
(entryFileClicked)="navigateToFile($event)"
(uploadFileConfirmed)="uploadFile($event)"
Expand Down
Loading
Loading