Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<osf-scheduled-banner></osf-scheduled-banner>
<osf-maintenance-banner></osf-maintenance-banner>
<osf-cookie-consent-banner></osf-cookie-consent-banner>
<osf-tos-consent-banner></osf-tos-consent-banner>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';

import { CookieConsentBannerComponent } from './cookie-consent-banner/cookie-consent-banner.component';
import { ScheduledBannerComponent } from './scheduled-banner/scheduled-banner.component';
import { TosConsentBannerComponent } from './tos-consent-banner/tos-consent-banner.component';
import { OSFBannerComponent } from './osf-banner.component';

import { OSFTestingModule } from '@testing/osf.testing.module';
Expand All @@ -23,6 +24,7 @@ describe('Component: OSF Banner', () => {
MockComponentWithSignal('osf-maintenance-banner'),
MockComponent(ScheduledBannerComponent),
MockComponent(CookieConsentBannerComponent),
MockComponent(TosConsentBannerComponent),
],
}).compileComponents();

Expand Down
8 changes: 7 additions & 1 deletion src/app/core/components/osf-banners/osf-banner.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CookieConsentBannerComponent } from './cookie-consent-banner/cookie-consent-banner.component';
import { MaintenanceBannerComponent } from './maintenance-banner/maintenance-banner.component';
import { ScheduledBannerComponent } from './scheduled-banner/scheduled-banner.component';
import { TosConsentBannerComponent } from './tos-consent-banner/tos-consent-banner.component';

/**
* Wrapper component responsible for rendering all global or conditional banners.
Expand All @@ -21,7 +22,12 @@ import { ScheduledBannerComponent } from './scheduled-banner/scheduled-banner.co
*/
@Component({
selector: 'osf-banner-component',
imports: [MaintenanceBannerComponent, ScheduledBannerComponent, CookieConsentBannerComponent],
imports: [
MaintenanceBannerComponent,
ScheduledBannerComponent,
CookieConsentBannerComponent,
TosConsentBannerComponent,
],
templateUrl: './osf-banner.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@if (!acceptedTermsOfServiceChange()) {
<div class="p-4 bg-white">
<div class="p-3">
<p-message styleClass="w-full" severity="warn">
<ng-template #container>
<div class="flex flex-column w-full">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { Store } from '@ngxs/store';

import { MockComponent } from 'ng-mocks';

import { of } from 'rxjs';

import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

Expand All @@ -15,32 +14,26 @@ import { IconComponent } from '@shared/components';
import { TosConsentBannerComponent } from './tos-consent-banner.component';

import { TranslationServiceMock } from '@testing/mocks/translation.service.mock';
import { OSFTestingModule, OSFTestingStoreModule } from '@testing/osf.testing.module';
import { OSFTestingStoreModule } from '@testing/osf.testing.module';
import { provideMockStore } from '@testing/providers/store-provider.mock';
import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock';

describe('TosConsentBannerComponent', () => {
let component: TosConsentBannerComponent;
describe('Component: Tos Consent Banner', () => {
let fixture: ComponentFixture<TosConsentBannerComponent>;
let store: jest.Mocked<Store>;
let toastServiceMock: ReturnType<ToastServiceMockBuilder['build']>;
let store: Store;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TosConsentBannerComponent, OSFTestingStoreModule, OSFTestingModule, MockComponent(IconComponent)],
imports: [OSFTestingStoreModule, TosConsentBannerComponent, MockComponent(IconComponent)],
providers: [
provideMockStore({
signals: [{ selector: UserSelectors.getCurrentUser, value: MOCK_USER }],
signals: [{ selector: UserSelectors.getCurrentUser, value: signal(MOCK_USER) }],
}),
TranslationServiceMock,
],
}).compileComponents();

fixture = TestBed.createComponent(TosConsentBannerComponent);
store = TestBed.inject(Store) as jest.Mocked<Store>;
component = fixture.componentInstance;
store.dispatch = jest.fn().mockReturnValue(of(undefined));
toastServiceMock = ToastServiceMockBuilder.create().build();
store = TestBed.inject(Store);
fixture.detectChanges();
});

Expand All @@ -58,6 +51,7 @@ describe('TosConsentBannerComponent', () => {
});

it('should dispatch AcceptTermsOfServiceByUser action when "Continue" is clicked', () => {
jest.spyOn(store, 'dispatch');
const checkboxInput = fixture.debugElement.query(By.css('p-checkbox input')).nativeElement;
checkboxInput.click();
fixture.detectChanges();
Expand All @@ -67,15 +61,5 @@ describe('TosConsentBannerComponent', () => {
fixture.detectChanges();

expect(store.dispatch).toHaveBeenCalledWith(new AcceptTermsOfServiceByUser());
expect(toastServiceMock.showError).not.toHaveBeenCalled();
});

it('should show toast banner if acceptedTermsOfService is false and "Continue" is clicked', () => {
component.acceptedTermsOfService.set(false);
const continueButton = fixture.debugElement.query(By.css('p-button button')).nativeElement;
continueButton.disabled = false;
continueButton.click();
fixture.detectChanges();
expect(component.errorMessage).toEqual('toast.tos-consent.error-message');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createDispatchMap, select } from '@ngxs/store';

import { TranslatePipe } from '@ngx-translate/core';

import { Button } from 'primeng/button';
import { Checkbox } from 'primeng/checkbox';
import { Message } from 'primeng/message';

import { Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';

import { AcceptTermsOfServiceByUser, UserSelectors } from '@core/store/user';
import { IconComponent } from '@osf/shared/components';

/**
* TosConsentBannerComponent displays a Terms of Service (ToS) consent banner for users who haven't accepted yet.
* It includes a checkbox, error handling, and dispatches an action to accept the ToS upon confirmation.
*
* This component integrates:
* - PrimeNG UI elements (`Checkbox`, `Button`, `Message`)
* - i18n translation support
* - NGXS store selectors and actions
* - Signal-based reactivity and computed values
* - Toast notifications for user feedback
*
* @component
* @example
* <osf-tos-consent-banner></osf-tos-consent-banner>
*/
@Component({
selector: 'osf-tos-consent-banner',
imports: [FormsModule, Checkbox, Button, Message, TranslatePipe, IconComponent, RouterLink],
templateUrl: './tos-consent-banner.component.html',
})
export class TosConsentBannerComponent {
/**
* NGXS dispatch map for the AcceptTermsOfServiceByUser action.
*/
readonly actions = createDispatchMap({ acceptTermsOfServiceByUser: AcceptTermsOfServiceByUser });

/**
* Signal of the current user from NGXS UserSelectors.
*/
readonly currentUser = select(UserSelectors.getCurrentUser);

/**
* Local signal tracking whether the user has accepted the Terms of Service via checkbox.
*/
acceptedTermsOfService = signal(false);

/**
* Computed signal indicating whether the user has already accepted the Terms of Service.
*/
readonly acceptedTermsOfServiceChange = computed(() => {
const user = this.currentUser();
return user?.acceptedTermsOfService ?? false;
});

/**
* Triggered when the user clicks the Continue button.
* - Shows an error toast if checkbox is not checked.
* - Dispatches `AcceptTermsOfServiceByUser` action otherwise.
*/
onContinue() {
this.actions.acceptTermsOfServiceByUser();
}
}
18 changes: 7 additions & 11 deletions src/app/features/analytics/analytics.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MockComponents, 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';

Expand All @@ -24,7 +25,7 @@ import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.moc
import { RouterMockBuilder } from '@testing/providers/router-provider.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';

describe.skip('AnalyticsComponent', () => {
describe('Component: Analytics', () => {
let component: AnalyticsComponent;
let fixture: ComponentFixture<AnalyticsComponent>;
let routerMock: ReturnType<RouterMockBuilder['build']>;
Expand Down Expand Up @@ -69,11 +70,11 @@ describe.skip('AnalyticsComponent', () => {
{ selector: AnalyticsSelectors.isMetricsError, value: false },
],
signals: [
{ selector: metricsSelector, value: metrics },
{ selector: relatedCountsSelector, value: relatedCounts },
{ selector: AnalyticsSelectors.isMetricsLoading, value: false },
{ selector: AnalyticsSelectors.isRelatedCountsLoading, value: false },
{ selector: AnalyticsSelectors.isMetricsError, value: false },
{ selector: metricsSelector, value: signal(metrics) },
{ selector: relatedCountsSelector, value: signal(relatedCounts) },
{ selector: AnalyticsSelectors.isMetricsLoading, value: signal(false) },
{ selector: AnalyticsSelectors.isRelatedCountsLoading, value: signal(false) },
{ selector: AnalyticsSelectors.isMetricsError, value: signal(false) },
],
}),
{ provide: IS_WEB, useValue: of(true) },
Expand All @@ -86,11 +87,6 @@ describe.skip('AnalyticsComponent', () => {
component = fixture.componentInstance;
});

it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});

it('should set selectedRange via onRangeChange', () => {
fixture.detectChanges();
component.onRangeChange('month');
Expand Down
2 changes: 1 addition & 1 deletion src/app/features/home/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { TosConsentBannerComponent } from './tos-consent-banner/tos-consent-banner.component';
export { TosConsentBannerComponent } from '../../../core/components/osf-banners/tos-consent-banner/tos-consent-banner.component';

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
[buttonLabel]="'home.loggedIn.dashboard.createProject' | translate"
(buttonClick)="createProject()"
/>
<osf-tos-consent-banner></osf-tos-consent-banner>
<div>
<div class="quick-search-container py-4 px-3 md:px-4">
<p class="text-center mb-4 xl:mb-6">
Expand Down Expand Up @@ -76,7 +75,6 @@ <h1>{{ 'home.loggedIn.hosting.title' | translate }}</h1>
[buttonLabel]="'home.loggedIn.dashboard.createProject' | translate"
(buttonClick)="createProject()"
/>
<osf-tos-consent-banner></osf-tos-consent-banner>
<div class="flex items-center justify-center min-h-screen bg-white pt-4">
<div class="text-center max-w-2xl px-6 w-full">
<p class="mb-4">{{ 'home.loggedIn.dashboard.noCreatedProject' | translate }}</p>
Expand Down
4 changes: 0 additions & 4 deletions src/app/features/home/pages/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import { MyResourcesItem, MyResourcesSearchFilters, TableParameters } from '@osf
import { ProjectRedirectDialogService } from '@osf/shared/services';
import { ClearMyResources, GetMyProjects, MyResourcesSelectors } from '@osf/shared/stores';

import { TosConsentBannerComponent } from '../../components';

@Component({
selector: 'osf-dashboard',
imports: [
Expand All @@ -40,8 +38,6 @@ import { TosConsentBannerComponent } from '../../components';
IconComponent,
TranslatePipe,
LoadingSpinnerComponent,
TosConsentBannerComponent,
LoadingSpinnerComponent,
],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss',
Expand Down
Loading