Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1616c38
fix(justification): updated init step validation to fix freeze issue
nsemets Apr 17, 2026
f51f412
fix(registry-update): updated logic with registry update and tests
nsemets Apr 20, 2026
a80a105
fix(resource-information): added resource information to project and …
nsemets Apr 28, 2026
2488f56
[ENG-10869] Project Contributors section content overflows on smaller…
nsemets Apr 30, 2026
c171867
[ENG-10866] Duplicate text displayed on the My Registrations page (#964)
nsemets Apr 30, 2026
012ec5b
[ENG-10770] "See more" button shows error when only one admin contrib…
nsemets Apr 30, 2026
cdbfda5
[ENG-10626] User unable to Update embargoed registration (#959)
nsemets Apr 30, 2026
100d23f
[ENG-9958] Files in draft registrations cannot be previewed (#957)
mkovalua Apr 30, 2026
a933d9d
[ENG-7471] Have a display of some form when the OSF is down for plann…
nsemets Apr 30, 2026
5480900
fix(contributors-table): fixed failed tests (#977)
nsemets May 1, 2026
d8f827c
[ENG-9957] Users unable to delete/remove files from draft registratio…
mkovalua May 1, 2026
b4e6dfb
Merge remote-tracking branch 'upstream/feature/pbs-26-9' into fix/ENG…
nsemets May 4, 2026
70dc5bd
Merge remote-tracking branch 'upstream/feature/pbs-26-9' into feat/EN…
nsemets May 5, 2026
21dae65
fix(registration-card): updated logic for updates button
nsemets May 5, 2026
b616ba7
fix(funders-list): added more tests
nsemets May 5, 2026
df398ac
fix(resource-information): fixed comments
nsemets May 5, 2026
0f0f6b9
Merge pull request #974 from nsemets/fix/ENG-10348
nsemets May 7, 2026
a758e12
Merge pull request #980 from nsemets/feat/ENG-10626
nsemets May 7, 2026
b5fa5fb
fix(update-preprint): removed delete option for rejected preprint (#995)
nsemets May 26, 2026
f4ea516
[ENG-10944] Files general improvement (#983)
nsemets May 26, 2026
44583e9
[ENG-11004] Account Setting: Code key is displayed instead of button …
mkovalua May 26, 2026
d1513b2
Merge remote-tracking branch 'upstream/develop' into fix/merge-confli…
nsemets May 26, 2026
2ea839d
fix(package-lock): updated version and some packages
nsemets May 26, 2026
68e47d2
Merge pull request #996 from nsemets/fix/merge-conflicts-pbs-26-9
brianjgeiger May 26, 2026
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
48 changes: 24 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () =>
Expand Down
7 changes: 7 additions & 0 deletions src/app/core/components/layout/layout.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@

<osf-footer></osf-footer>
</div>

@if (isMaintenanceMode()) {
<section class="maintenance-overlay font-bold text-xl flex flex-column align-items-center justify-content-center">
<p>{{ 'maintenance.title' | translate }}</p>
<p>{{ 'maintenance.message' | translate }}</p>
</section>
}
</main>

<p-confirm-dialog
Expand Down
7 changes: 7 additions & 0 deletions src/app/core/components/layout/layout.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,10 @@
}
}
}

.maintenance-overlay {
position: fixed;
inset: 0;
z-index: 2000;
background: var(--white);
}
5 changes: 3 additions & 2 deletions src/app/core/components/layout/layout.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { MockComponents, MockProvider } from 'ng-mocks';

import { ConfirmationService } from 'primeng/api';
import { ConfirmDialog } from 'primeng/confirmdialog';

import { BehaviorSubject } from 'rxjs';

import { ComponentFixture, TestBed } from '@angular/core/testing';

import { MaintenanceModeService } from '@core/services/maintenance-mode.service';
import { IS_MEDIUM, IS_WEB } from '@osf/shared/helpers/breakpoints.tokens';

import { provideOSFCore } from '@testing/osf.testing.provider';
import { MaintenanceModeServiceMock } from '@testing/providers/maintenance-mode.service.mock';

import { BreadcrumbComponent } from '../breadcrumb/breadcrumb.component';
import { FooterComponent } from '../footer/footer.component';
Expand Down Expand Up @@ -47,7 +48,7 @@ describe('LayoutComponent', () => {
provideOSFCore(),
MockProvider(IS_WEB, isWebSubject),
MockProvider(IS_MEDIUM, isMediumSubject),
MockProvider(ConfirmationService),
MockProvider(MaintenanceModeService, MaintenanceModeServiceMock.simple()),
],
});

Expand Down
4 changes: 4 additions & 0 deletions src/app/core/components/layout/layout.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { RouterOutlet } from '@angular/router';

import { MaintenanceModeService } from '@core/services/maintenance-mode.service';
import { ScrollTopOnRouteChangeDirective } from '@osf/shared/directives/scroll-top.directive';
import { IS_MEDIUM, IS_WEB } from '@osf/shared/helpers/breakpoints.tokens';

Expand Down Expand Up @@ -35,6 +36,9 @@ import { TopnavComponent } from '../topnav/topnav.component';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LayoutComponent {
private readonly maintenanceModeService = inject(MaintenanceModeService);

isWeb = toSignal(inject(IS_WEB));
isMedium = toSignal(inject(IS_MEDIUM));
isMaintenanceMode = this.maintenanceModeService.isActive;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
styleClass="w-full"
icon="pi pi-info-circle"
[severity]="maintenance()?.severity"
[text]="maintenance()?.message"
[closable]="true"
(onClose)="dismiss()"
>
{{ maintenance()?.message }}
</p-message>
}
25 changes: 25 additions & 0 deletions src/app/core/interceptors/error.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ import { Router } from '@angular/router';

import { SENTRY_TOKEN } from '@core/provider/sentry.provider';
import { AuthService } from '@core/services/auth.service';
import { MaintenanceModeService } from '@core/services/maintenance-mode.service';
import { UserSelectors } from '@core/store/user';
import { ToastService } from '@osf/shared/services/toast.service';
import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service';

import { provideOSFCore } from '@testing/osf.testing.provider';
import { AuthServiceMock, AuthServiceMockType } from '@testing/providers/auth-service.mock';
import { LoaderServiceMock, provideLoaderServiceMock } from '@testing/providers/loader-service.mock';
import {
MaintenanceModeServiceMock,
MaintenanceModeServiceMockType,
} from '@testing/providers/maintenance-mode.service.mock';
import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
import { SentryMock, SentryMockType } from '@testing/providers/sentry-provider.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';
Expand All @@ -30,6 +35,7 @@ describe('errorInterceptor', () => {
let toastServiceMock: ToastServiceMockType;
let loaderServiceMock: LoaderServiceMock;
let authServiceMock: AuthServiceMockType;
let maintenanceModeServiceMock: MaintenanceModeServiceMockType;
let viewOnlyHelperMock: ViewOnlyLinkHelperMockType;
let sentryMock: SentryMockType;

Expand All @@ -43,6 +49,7 @@ describe('errorInterceptor', () => {
toastServiceMock = ToastServiceMock.simple();
loaderServiceMock = new LoaderServiceMock();
authServiceMock = AuthServiceMock.simple();
maintenanceModeServiceMock = MaintenanceModeServiceMock.simple();
viewOnlyHelperMock = ViewOnlyLinkHelperMock.simple(viewOnly);
sentryMock = SentryMock.simple();

Expand All @@ -56,6 +63,7 @@ describe('errorInterceptor', () => {
MockProvider(Router, router),
MockProvider(ToastService, toastServiceMock),
MockProvider(AuthService, authServiceMock),
MockProvider(MaintenanceModeService, maintenanceModeServiceMock),
MockProvider(ViewOnlyLinkHelperService, viewOnlyHelperMock),
MockProvider(PLATFORM_ID, platformId),
{ provide: SENTRY_TOKEN, useValue: sentryMock },
Expand Down Expand Up @@ -181,4 +189,21 @@ describe('errorInterceptor', () => {
expect(loaderServiceMock.hide).toHaveBeenCalled();
expect(toastServiceMock.showError).not.toHaveBeenCalled();
});

it('should activate maintenance mode on 503 maintenance response', async () => {
setup('browser', false);
const request = createRequest('/api/v2/');
const error = new HttpErrorResponse({
status: 503,
error: { meta: { maintenance_mode: true } },
url: request.url,
});

const caught = await runInterceptor(request, error);

expect(caught?.status).toBe(503);
expect(maintenanceModeServiceMock.activate).toHaveBeenCalled();
expect(loaderServiceMock.hide).toHaveBeenCalled();
expect(toastServiceMock.showError).not.toHaveBeenCalled();
});
});
14 changes: 14 additions & 0 deletions src/app/core/interceptors/error.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { inject, PLATFORM_ID } from '@angular/core';
import { Router } from '@angular/router';

import { ERROR_MESSAGES } from '@core/constants/error-messages';
import { MaintenanceResponse } from '@core/models/maintenance-response.model';
import { SENTRY_TOKEN } from '@core/provider/sentry.provider';
import { AuthService } from '@core/services/auth.service';
import { MaintenanceModeService } from '@core/services/maintenance-mode.service';
import { UserSelectors } from '@core/store/user';
import { LoaderService } from '@osf/shared/services/loader.service';
import { ToastService } from '@osf/shared/services/toast.service';
Expand All @@ -23,6 +25,7 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const loaderService = inject(LoaderService);
const router = inject(Router);
const authService = inject(AuthService);
const maintenanceModeService = inject(MaintenanceModeService);
const sentry = inject(SENTRY_TOKEN);
const platformId = inject(PLATFORM_ID);
const viewOnlyHelper = inject(ViewOnlyLinkHelperService);
Expand All @@ -47,6 +50,17 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => {
}

const serverErrorRegex = /5\d{2}/;
const maintenanceResponse = error.error as MaintenanceResponse | null;

const maintenanceMode = error.status === 503 && maintenanceResponse?.meta?.maintenance_mode === true;

if (maintenanceMode) {
loaderService.hide();
if (isPlatformBrowser(platformId)) {
maintenanceModeService.activate();
}
return throwError(() => error);
}

if (serverErrorRegex.test(error.status.toString())) {
errorMessage = error.error.message || 'common.errorMessages.serverError';
Expand Down
5 changes: 5 additions & 0 deletions src/app/core/models/maintenance-response.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface MaintenanceResponse {
meta?: {
maintenance_mode?: boolean;
};
}
66 changes: 66 additions & 0 deletions src/app/core/services/maintenance-mode.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { catchError, map, Observable, of, Subscription, switchMap, timer } from 'rxjs';

import { HttpClient, HttpContext } from '@angular/common/http';
import { inject, Injectable, OnDestroy, signal } from '@angular/core';

import { MaintenanceResponse } from '@core/models/maintenance-response.model';
import { ENVIRONMENT } from '@core/provider/environment.provider';

import { BYPASS_ERROR_INTERCEPTOR } from '../interceptors/error-interceptor.tokens';

@Injectable({
providedIn: 'root',
})
export class MaintenanceModeService implements OnDestroy {
private readonly http = inject(HttpClient);
private readonly environment = inject(ENVIRONMENT);

private readonly POLL_INTERVAL_MS = 5 * 60 * 1_000;
private readonly _isActive = signal(false);
private readonly bypassContext = new HttpContext().set(BYPASS_ERROR_INTERCEPTOR, true);

private pollingSubscription: Subscription | null = null;

readonly isActive = this._isActive.asReadonly();

activate(): void {
this._isActive.set(true);
if (this.pollingSubscription) {
return;
}
this.startPolling();
}

deactivate(): void {
this._isActive.set(false);
this.stopPolling();
}

ngOnDestroy(): void {
this.stopPolling();
}

private startPolling(): void {
this.pollingSubscription = timer(0, this.POLL_INTERVAL_MS)
.pipe(switchMap(() => this.checkMaintenanceStatus()))
.subscribe((isMaintenance) => {
if (!isMaintenance) {
this.deactivate();
}
});
}

private stopPolling(): void {
this.pollingSubscription?.unsubscribe();
this.pollingSubscription = null;
}

private checkMaintenanceStatus(): Observable<boolean> {
return this.http
.get<MaintenanceResponse>(`${this.environment.apiDomainUrl}/v2/`, { context: this.bypassContext })
.pipe(
map((response) => response.meta?.maintenance_mode === true),
catchError(() => of(true))
);
}
}
Loading
Loading