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
fa646b2
Show Osf introduction video and Collections,Institutions, Registries,…
mkovalua Aug 27, 2025
deb6fda
1. add translations
mkovalua Aug 27, 2025
817fcde
align footer content left
mkovalua Aug 27, 2025
4ea197b
use angular routerLink approach for <a>
mkovalua Aug 27, 2025
8b08eb2
add margin bottom for Visit button to look it better on resizing
mkovalua Aug 27, 2025
9518a69
update footer formatting
mkovalua Aug 27, 2025
ae3d58c
fix(dashboard): Fix [WARNING] NG8107
mkovalua Aug 27, 2025
78d9a3f
fix(dashboard): remove not used variable
mkovalua Aug 27, 2025
5b507ee
fix(dashboard): fix code format by running "npm run lint:fix && npm r…
mkovalua Aug 27, 2025
b4e4b36
fix(dashboard): resolve merge conflicts with main and npm run lint:fi…
mkovalua Aug 27, 2025
d11d726
fix(dashboard): use .png names without guid
mkovalua Aug 28, 2025
4a41ac8
resolve merge conflicts with main
mkovalua Aug 29, 2025
cdc4148
implement unit testing for dashboard when there is a project and ther…
mkovalua Aug 29, 2025
a2bf142
remove redundant footer RPCB and RCP links
mkovalua Aug 29, 2025
7026035
move dashboard images alt text to en.json
mkovalua Aug 29, 2025
78b2b23
add test for openInfoLink()
mkovalua Aug 29, 2025
90957d3
update(dashboard): use dashboard.data for mocking
mkovalua Aug 29, 2025
d191424
update(dashboard): add test to show that products footer images no ex…
mkovalua Aug 29, 2025
dc756f2
fix(dashboard): npm run lint:fix && npm run format
mkovalua Aug 29, 2025
3da021a
resolve merge conflicts with Main
mkovalua Sep 3, 2025
33235f5
add missing code for dashboard
mkovalua Sep 3, 2025
e5bdb8f
remove redundant imports for dashboard test
mkovalua Sep 3, 2025
928c19f
use providers -> useValue mock approach for dashboard
mkovalua Sep 3, 2025
c8c4e9b
resolve CR comments
mkovalua Sep 3, 2025
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/core/components/footer/footer.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<footer class="footer flex flex-column">
<div class="footer-nav flex flex-column-reverse xl:flex-row xl:justify-content-between">
<div class="footer-links flex flex-wrap justify-content-center align-items-center">
<div class="flex justify-content-center">
<div class="footer-links flex flex-wrap justify-content-left align-items-center">
<div class="flex justify-content-left">
<a href="https://cos.io/">{{ 'footer.links.centerForOpenScience' | translate }}</a>
</div>
</div>
Expand Down
190 changes: 135 additions & 55 deletions src/app/features/home/pages/dashboard/dashboard.component.html
Original file line number Diff line number Diff line change
@@ -1,61 +1,141 @@
<section class="home-container xl:mt-6">
<osf-sub-header
[showButton]="true"
[title]="'home.loggedIn.dashboard.title' | translate"
[icon]="'fas fa-home'"
[buttonLabel]="'home.loggedIn.dashboard.createProject' | translate"
(buttonClick)="createProject()"
/>

<div class="quick-search-container py-4 px-3 md:px-4">
<p class="text-center mb-4 xl:mb-6">
{{ 'home.loggedIn.dashboard.quickSearch.goTo' | translate }}
<a routerLink="/my-projects">
{{ 'home.loggedIn.dashboard.quickSearch.myProjects' | translate }}
<section class="home-container xl:mt-6" [class.medium]="isMedium()">
@if (areProjectsLoading()) {
<osf-loading-spinner />
} @else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this @else could likely be a @esle if

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if workflow goes to else it is needed to render highlighted code

image

if we use @if (isProjectsLoading())-> @else if (existsProjects() || !!searchControl.value?.length) -> else

it will be needed to duplicate the code for else if and else

@if (existsProjects()) {
<osf-sub-header
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The html for the else if and else conditions if fairly large. I will let you make the decision and there's a fine line between leaving it as 1 component or splitting it into two components.

What do you think?

Copy link
Contributor Author

@mkovalua mkovalua Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agree that it is not compact,

I prefer to keep 1 component, because I am not confident what we may split into 2 components.

I suppose it may be the following if it may be used in other places too not only for dashboard, what is your opinion?

image

[showButton]="true"
[title]="'home.loggedIn.dashboard.title' | translate"
[icon]="'home'"
[buttonLabel]="'home.loggedIn.dashboard.createProject' | translate"
(buttonClick)="createProject()"
/>
<div>
<div class="quick-search-container py-4 px-3 md:px-4">
<p class="text-center mb-4 xl:mb-6">
{{ 'home.loggedIn.dashboard.quickSearch.goTo' | translate }}
<a routerLink="/my-projects">
{{ 'home.loggedIn.dashboard.quickSearch.myProjects' | translate }}
</a>
{{ 'home.loggedIn.dashboard.quickSearch.toOrganize' | translate }}
<a routerLink="/search">
{{ 'home.loggedIn.dashboard.quickSearch.search' | translate }}
</a>
{{ 'home.loggedIn.dashboard.quickSearch.osf' | translate }}
</p>

<osf-my-projects-table
[items]="filteredProjects()"
[tableParams]="tableParams()"
[searchControl]="searchControl"
[sortColumn]="sortColumn()"
[sortOrder]="sortOrder()"
[isLoading]="areProjectsLoading()"
[searchPlaceholder]="'home.loggedIn.dashboard.quickSearch.searchPlaceholder' | translate"
(pageChange)="onPageChange($event)"
(sort)="onSort($event)"
(itemClick)="navigateToProject($event)"
/>
</div>

<div class="public-projects-container flex align-items-center flex-row pt-6 pb-4 px-3 md:px-4 xl:py-6">
<osf-icon class="mr-2" iconClass="fas fa-2xl fa-magnifying-glass"></osf-icon>
<h1>{{ 'home.loggedIn.publicProjects.title' | translate }}</h1>
</div>

<div
class="latest-research-container flex flex-column gap-4 py-6 px-3 md:px-4 xl:flex-row xl:justify-content-between"
>
<div>
<h1>{{ 'home.loggedIn.latestResearch.title' | translate }}</h1>
<p class="m-t-12">{{ 'home.loggedIn.latestResearch.subtitle' | translate }}</p>
</div>

<p-button
routerLink="/preprints"
[label]="'home.loggedIn.latestResearch.button' | translate"
severity="success"
/>
</div>

<div class="hosting-container flex flex-column gap-4 py-6 px-3 md:px-4 xl:flex-row xl:justify-content-between">
<div class="text-container">
<h1>{{ 'home.loggedIn.hosting.title' | translate }}</h1>
<p class="m-t-12">{{ 'home.loggedIn.hosting.subtitle' | translate }}</p>
</div>

<p-button routerLink="/meetings" [label]="'home.loggedIn.hosting.button' | translate" severity="success" />
</div>
</div>
} @else {
<osf-sub-header
[showButton]="true"
[title]="'home.loggedIn.dashboard.welcome' | translate"
[icon]="'home'"
[buttonLabel]="'home.loggedIn.dashboard.createProject' | translate"
(buttonClick)="createProject()"
/>
<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>

<p class="mb-6">{{ 'home.loggedIn.dashboard.watchVideoBelow' | translate }}</p>

<div class="mb-6 flex flex-column align-items-center w-full">
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/X07mBq2tnMg"
title="Introduction to OSF"
frameborder="0"
allowfullscreen
class="rounded-xl shadow-md"
></iframe>
</div>

<div class="flex justify-content-center mt-4 mb-4">
<p-button
[label]="'home.loggedIn.dashboard.getStartedHelp' | translate"
severity="secondary"
(onClick)="openInfoLink()"
/>
</div>
</div>
</div>
}
<div class="flex flex-column align-items-left w-full mt-4 ml-4 mb-4">
<a
href="https://www.cos.io/products/osf-collections"
target="_blank"
rel="noopener noreferrer"
data-test-products-collections
>
<img
src="assets/images/dashboard/products/osf-collections.png"
[alt]="'home.loggedIn.dashboard.images.osfCollectionsImageAltText' | translate"
/>
</a>
{{ 'home.loggedIn.dashboard.quickSearch.toOrganize' | translate }}
<a routerLink="/search">
{{ 'home.loggedIn.dashboard.quickSearch.search' | translate }}

<a routerLink="/institutions">
<img
src="assets/images/dashboard/products/osf-institutions.png"
[alt]="'home.loggedIn.dashboard.images.osfInstitutionsImageAltText' | translate"
/>
</a>
{{ 'home.loggedIn.dashboard.quickSearch.osf' | translate }}
</p>

<osf-my-projects-table
[items]="filteredProjects()"
[tableParams]="tableParams()"
[searchControl]="searchControl"
[sortColumn]="sortColumn()"
[sortOrder]="sortOrder()"
[isLoading]="areProjectsLoading()"
[searchPlaceholder]="'home.loggedIn.dashboard.quickSearch.searchPlaceholder' | translate"
(pageChange)="onPageChange($event)"
(sort)="onSort($event)"
(itemClick)="navigateToProject($event)"
/>
</div>

<div class="public-projects-container flex align-items-center flex-row pt-6 pb-4 px-3 md:px-4 xl:py-6">
<osf-icon class="mr-2" iconClass="fas fa-2xl fa-magnifying-glass"></osf-icon>
<h1>{{ 'home.loggedIn.publicProjects.title' | translate }}</h1>
</div>

<div
class="latest-research-container flex flex-column gap-4 py-6 px-3 md:px-4 xl:flex-row xl:justify-content-between"
>
<div>
<h1>{{ 'home.loggedIn.latestResearch.title' | translate }}</h1>
<p class="m-t-12">{{ 'home.loggedIn.latestResearch.subtitle' | translate }}</p>
</div>

<p-button routerLink="/preprints" [label]="'home.loggedIn.latestResearch.button' | translate" severity="success" />
</div>
<a routerLink="/registries">
<img
src="assets/images/dashboard/products/osf-registries.png"
[alt]="'home.loggedIn.dashboard.images.osfRegistriesImageAltTest' | translate"
/>
</a>

<div class="hosting-container flex flex-column gap-4 py-6 px-3 md:px-4 xl:flex-row xl:justify-content-between">
<div class="text-container">
<h1>{{ 'home.loggedIn.hosting.title' | translate }}</h1>
<p class="m-t-12">{{ 'home.loggedIn.hosting.subtitle' | translate }}</p>
<a routerLink="/preprints">
<img
src="assets/images/dashboard/products/osf-preprints.png"
[alt]="'home.loggedIn.dashboard.images.osfPreprintsImageAltTest' | translate"
/>
</a>
</div>

<p-button routerLink="/meetings" [label]="'home.loggedIn.hosting.button' | translate" severity="success" />
</div>
}
</section>
119 changes: 115 additions & 4 deletions src/app/features/home/pages/dashboard/dashboard.component.spec.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should any text (translations string) or links be verified after the spinners are finished loading?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added test to verify product images

d191424

Original file line number Diff line number Diff line change
@@ -1,22 +1,133 @@
import { Store } from '@ngxs/store';

import { MockComponents } from 'ng-mocks';

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

import { LoadingSpinnerComponent, MyProjectsTableComponent, SubHeaderComponent } from '@shared/components';
import { MyResourcesSelectors } from '@shared/stores';

import { DashboardComponent } from './dashboard.component';

describe.skip('DashboardComponent', () => {
import { getProjectsMockForComponent } from '@testing/data/dashboard/dasboard.data';
import { OSFTestingStoreModule } from '@testing/osf.testing.module';

describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;

let projectsSignal: WritableSignal<any[]>;
let totalProjectsSignal: WritableSignal<number>;
let areProjectsLoadingSignal: WritableSignal<boolean>;

beforeEach(async () => {
projectsSignal = signal(getProjectsMockForComponent());
totalProjectsSignal = signal(getProjectsMockForComponent().length);
areProjectsLoadingSignal = signal(false);

await TestBed.configureTestingModule({
imports: [DashboardComponent],
imports: [
DashboardComponent,
OSFTestingStoreModule,
...MockComponents(SubHeaderComponent, MyProjectsTableComponent, LoadingSpinnerComponent),
],
providers: [
{
provide: Store,
useValue: {
selectSignal: (selector: any) => {
if (selector === MyResourcesSelectors.getProjects) return projectsSignal;
if (selector === MyResourcesSelectors.getTotalProjects) return totalProjectsSignal;
if (selector === MyResourcesSelectors.getProjectsLoading) return areProjectsLoadingSignal;
return signal(null);
},
dispatch: jest.fn(),
},
},
],
}).compileComponents();

fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
});

it('should show loading s pinner when projects are loading', () => {
areProjectsLoadingSignal.set(true);
fixture.detectChanges();

const spinner = fixture.debugElement.query(By.directive(LoadingSpinnerComponent));
expect(spinner).toBeTruthy();
});

it('should render projects table when projects exist', () => {
projectsSignal.set(getProjectsMockForComponent());
totalProjectsSignal.set(getProjectsMockForComponent().length);
areProjectsLoadingSignal.set(false);
fixture.detectChanges();

const table = fixture.debugElement.query(By.directive(MyProjectsTableComponent));
expect(table).toBeTruthy();
});

it('should render welcome video when no projects exist', () => {
projectsSignal.set([]);
totalProjectsSignal.set(0);
areProjectsLoadingSignal.set(false);
fixture.detectChanges();
const iframe = fixture.debugElement.query(By.css('iframe'));
expect(iframe).toBeTruthy();
expect(iframe.nativeElement.src).toContain('youtube.com');
});

it('should create', () => {
expect(component).toBeTruthy();
it('should render welcome screen when no projects exist', () => {
projectsSignal.set([]);
totalProjectsSignal.set(0);
areProjectsLoadingSignal.set(false);
fixture.detectChanges();

const welcomeText = fixture.debugElement.nativeElement.textContent;
expect(welcomeText).toContain('home.loggedIn.dashboard.noCreatedProject');
});

it('should open OSF help link in new tab when openInfoLink is called', () => {
const spy = jest.spyOn(window, 'open').mockImplementation(() => null);
component.openInfoLink();
expect(spy).toHaveBeenCalledWith('https://help.osf.io/', '_blank');
});

it('should render product images after loading spinner disappears', () => {
areProjectsLoadingSignal.set(true);
fixture.detectChanges();

let productImages = fixture.debugElement
.queryAll(By.css('img'))
.filter((img) => img.nativeElement.getAttribute('src')?.includes('assets/images/dashboard/products/'));

expect(productImages.length).toBe(0);

const spinner = fixture.debugElement.query(By.css('osf-loading-spinner'));
expect(spinner).toBeTruthy();

areProjectsLoadingSignal.set(false);
fixture.detectChanges();

productImages = fixture.debugElement
.queryAll(By.css('img'))
.filter((img) => img.nativeElement.getAttribute('src')?.includes('assets/images/dashboard/products/'));

expect(productImages.length).toBe(4);

const sources = productImages.map((img) => img.nativeElement.getAttribute('src'));

expect(sources).toEqual(
expect.arrayContaining([
'assets/images/dashboard/products/osf-collections.png',
'assets/images/dashboard/products/osf-institutions.png',
'assets/images/dashboard/products/osf-registries.png',
'assets/images/dashboard/products/osf-preprints.png',
])
);
});
});
Loading