Skip to content

Commit c3b39ca

Browse files
authored
fix(ANG-475): P50 - NIR: COS: [UI] “View” button should open list of linked items (#661)
* fix(analytics): implemented linked projects * chore(analytics): clean up models and components * chore(analytics): consolidated node models * fix(linked-projects): removed unused stuff
1 parent 1775dba commit c3b39ca

32 files changed

+561
-117
lines changed

src/app/core/constants/ngxs-states.constant.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { GlobalSearchState } from '@shared/stores/global-search';
1212
import { InstitutionsState } from '@shared/stores/institutions';
1313
import { InstitutionsSearchState } from '@shared/stores/institutions-search';
1414
import { LicensesState } from '@shared/stores/licenses';
15+
import { LinkedProjectsState } from '@shared/stores/linked-projects';
1516
import { MyResourcesState } from '@shared/stores/my-resources';
1617
import { RegionsState } from '@shared/stores/regions';
1718

@@ -34,4 +35,5 @@ export const STATES = [
3435
CurrentResourceState,
3536
GlobalSearchState,
3637
BannersState,
38+
LinkedProjectsState,
3739
];

src/app/features/analytics/analytics.component.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
[isLoading]="isRelatedCountsLoading()"
9292
[title]="'project.analytics.kpi.forks'"
9393
[value]="relatedCounts()?.forksCount"
94-
[showButton]="true"
94+
[showButton]="(relatedCounts()?.forksCount ?? 0) > 0"
9595
[buttonLabel]="'project.analytics.kpi.viewForks'"
9696
(buttonClick)="navigateToDuplicates()"
9797
></osf-analytics-kpi>
@@ -100,8 +100,9 @@
100100
[isLoading]="isRelatedCountsLoading()"
101101
[title]="'project.analytics.kpi.linksToThisProject'"
102102
[value]="relatedCounts()?.linksToCount"
103-
[showButton]="false"
103+
[showButton]="(relatedCounts()?.linksToCount ?? 0) > 0"
104104
[buttonLabel]="'project.analytics.kpi.viewLinks'"
105+
(buttonClick)="navigateToLinkedProjects()"
105106
></osf-analytics-kpi>
106107

107108
<osf-analytics-kpi

src/app/features/analytics/analytics.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ export class AnalyticsComponent implements OnInit {
137137
this.router.navigate(['duplicates'], { relativeTo: this.route });
138138
}
139139

140+
navigateToLinkedProjects() {
141+
this.router.navigate(['linked-projects'], { relativeTo: this.route });
142+
}
140143
private setData() {
141144
const analytics = this.analytics();
142145

src/app/features/analytics/components/view-duplicates/view-duplicates.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<div class="duplicate-wrapper flex flex-column gap-3 p-3 sm:p-4">
1717
<div class="flex justify-content-between align-items-center">
1818
<h2 class="flex align-items-center gap-2">
19-
<osf-icon [iconClass]="duplicate.public ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
19+
<osf-icon [iconClass]="duplicate.isPublic ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
2020
{{ duplicate.title }}
2121
</h2>
2222

@@ -62,7 +62,7 @@ <h2 class="flex align-items-center gap-2">
6262
<div class="flex flex-wrap align-items-center gap-1">
6363
<span class="font-bold">{{ 'common.labels.contributors' | translate }}:</span>
6464

65-
<osf-contributors-list [contributors]="duplicate.contributors"></osf-contributors-list>
65+
<osf-contributors-list [contributors]="duplicate.bibliographicContributors ?? []"></osf-contributors-list>
6666
</div>
6767

6868
@if (duplicate.description) {

src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ import {
3939
TruncatedTextComponent,
4040
} from '@osf/shared/components';
4141
import { ResourceType, UserPermissions } from '@osf/shared/enums';
42-
import { ToolbarResource } from '@osf/shared/models';
43-
import { Duplicate } from '@osf/shared/models/duplicates';
42+
import { BaseNodeModel, ToolbarResource } from '@osf/shared/models';
4443
import { CustomDialogService, LoaderService } from '@osf/shared/services';
4544
import { ClearDuplicates, DuplicatesSelectors, GetAllDuplicates, GetResourceWithChildren } from '@osf/shared/stores';
4645

@@ -58,6 +57,7 @@ import { ClearDuplicates, DuplicatesSelectors, GetAllDuplicates, GetResourceWith
5857
CustomPaginatorComponent,
5958
IconComponent,
6059
ContributorsListComponent,
60+
DatePipe,
6161
],
6262
templateUrl: './view-duplicates.component.html',
6363
styleUrl: './view-duplicates.component.scss',
@@ -171,7 +171,7 @@ export class ViewDuplicatesComponent {
171171
return null;
172172
});
173173

174-
showMoreOptions(duplicate: Duplicate) {
174+
showMoreOptions(duplicate: BaseNodeModel) {
175175
return (
176176
duplicate.currentUserPermissions.includes(UserPermissions.Admin) ||
177177
duplicate.currentUserPermissions.includes(UserPermissions.Write)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<osf-sub-header [title]="'project.analytics.viewRelated.linkedProjectsTitle' | translate" [showButton]="false" />
2+
3+
<div class="flex flex-column flex-1 bg-white gap-5 p-3 sm:p-4">
4+
@if (!isLoading() && currentResource()) {
5+
@if (!linkedProjects().length) {
6+
<p class="mt-5 text-center">{{ 'project.analytics.viewRelated.noLinkedProjectsMessage' | translate }}</p>
7+
} @else {
8+
<p>{{ 'project.analytics.viewRelated.linkedProjectsMessage' | translate }}</p>
9+
10+
@for (duplicate of linkedProjects(); track duplicate.id) {
11+
<div class="duplicate-wrapper flex flex-column gap-3 p-3 sm:p-4">
12+
<div class="flex justify-content-between align-items-center">
13+
<h2 class="flex align-items-center gap-2">
14+
<osf-icon [iconClass]="duplicate.isPublic ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
15+
{{ duplicate.title }}
16+
</h2>
17+
</div>
18+
19+
<div class="flex flex-column gap-2">
20+
<div class="flex flex-wrap align-items-center gap-1">
21+
<span class="font-bold">{{ 'common.labels.forked' | translate }}:</span>
22+
<p>{{ duplicate.dateCreated | date: 'MMM d, y, h:mm a' }}</p>
23+
</div>
24+
25+
<div class="flex flex-wrap align-items-center gap-1">
26+
<span class="font-bold">{{ 'common.labels.lastUpdated' | translate }}:</span>
27+
<p>{{ duplicate.dateModified | date: 'MMM d, y, h:mm a' }}</p>
28+
</div>
29+
30+
<div class="flex flex-wrap align-items-center gap-1">
31+
<span class="font-bold">{{ 'common.labels.contributors' | translate }}:</span>
32+
33+
<osf-contributors-list [contributors]="duplicate.bibliographicContributors ?? []"></osf-contributors-list>
34+
</div>
35+
36+
@if (duplicate.description) {
37+
<osf-truncated-text
38+
[text]="('resourceCard.labels.descriptionBold' | translate) + duplicate.description"
39+
/>
40+
}
41+
</div>
42+
<p-button
43+
[label]="'common.buttons.view' | translate"
44+
severity="secondary"
45+
[routerLink]="['/', duplicate.id]"
46+
/>
47+
</div>
48+
}
49+
50+
@if (totalLinkedProjects() > pageSize) {
51+
<osf-custom-paginator
52+
[totalCount]="totalLinkedProjects()"
53+
[rows]="pageSize"
54+
[first]="firstIndex()"
55+
(pageChanged)="onPageChange($event)"
56+
/>
57+
}
58+
}
59+
} @else {
60+
<osf-loading-spinner></osf-loading-spinner>
61+
}
62+
</div>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
:host {
2+
display: flex;
3+
flex-direction: column;
4+
flex: 1;
5+
}
6+
7+
.duplicate-wrapper {
8+
border: 1px solid var(--grey-2);
9+
border-radius: 0.75rem;
10+
color: var(--dark-blue-1);
11+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { MockComponents, MockProvider } from 'ng-mocks';
2+
3+
import { PaginatorState } from 'primeng/paginator';
4+
5+
import { ComponentFixture, TestBed } from '@angular/core/testing';
6+
import { ActivatedRoute, Router } from '@angular/router';
7+
8+
import { ProjectOverviewSelectors } from '@osf/features/project/overview/store';
9+
import { RegistryOverviewSelectors } from '@osf/features/registry/store/registry-overview';
10+
import { ResourceType } from '@osf/shared/enums';
11+
import { DuplicatesSelectors } from '@osf/shared/stores';
12+
import {
13+
ContributorsListComponent,
14+
CustomPaginatorComponent,
15+
IconComponent,
16+
LoadingSpinnerComponent,
17+
SubHeaderComponent,
18+
TruncatedTextComponent,
19+
} from '@shared/components';
20+
import { MOCK_PROJECT_OVERVIEW } from '@shared/mocks';
21+
import { CustomDialogService } from '@shared/services';
22+
23+
import { ViewLinkedProjectsComponent } from './view-linked-projects.component';
24+
25+
import { OSFTestingModule } from '@testing/osf.testing.module';
26+
import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock';
27+
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
28+
import { RouterMockBuilder } from '@testing/providers/router-provider.mock';
29+
import { provideMockStore } from '@testing/providers/store-provider.mock';
30+
31+
describe('Component: View Duplicates', () => {
32+
let component: ViewLinkedProjectsComponent;
33+
let fixture: ComponentFixture<ViewLinkedProjectsComponent>;
34+
let routerMock: ReturnType<RouterMockBuilder['build']>;
35+
let activatedRouteMock: ReturnType<ActivatedRouteMockBuilder['build']>;
36+
let mockCustomDialogService: ReturnType<CustomDialogServiceMockBuilder['build']>;
37+
38+
beforeEach(async () => {
39+
mockCustomDialogService = CustomDialogServiceMockBuilder.create().build();
40+
routerMock = RouterMockBuilder.create().build();
41+
activatedRouteMock = ActivatedRouteMockBuilder.create()
42+
.withParams({ id: 'rid' })
43+
.withData({ resourceType: ResourceType.Project })
44+
.build();
45+
46+
await TestBed.configureTestingModule({
47+
imports: [
48+
ViewLinkedProjectsComponent,
49+
OSFTestingModule,
50+
...MockComponents(
51+
SubHeaderComponent,
52+
TruncatedTextComponent,
53+
LoadingSpinnerComponent,
54+
CustomPaginatorComponent,
55+
IconComponent,
56+
ContributorsListComponent
57+
),
58+
],
59+
providers: [
60+
provideMockStore({
61+
signals: [
62+
{ selector: DuplicatesSelectors.getDuplicates, value: [] },
63+
{ selector: DuplicatesSelectors.getDuplicatesLoading, value: false },
64+
{ selector: DuplicatesSelectors.getDuplicatesTotalCount, value: 0 },
65+
{ selector: ProjectOverviewSelectors.getProject, value: MOCK_PROJECT_OVERVIEW },
66+
{ selector: ProjectOverviewSelectors.isProjectAnonymous, value: false },
67+
{ selector: RegistryOverviewSelectors.getRegistry, value: undefined },
68+
{ selector: RegistryOverviewSelectors.isRegistryAnonymous, value: false },
69+
],
70+
}),
71+
MockProvider(CustomDialogService, mockCustomDialogService),
72+
MockProvider(Router, routerMock),
73+
MockProvider(ActivatedRoute, activatedRouteMock),
74+
],
75+
}).compileComponents();
76+
77+
fixture = TestBed.createComponent(ViewLinkedProjectsComponent);
78+
component = fixture.componentInstance;
79+
80+
fixture.detectChanges();
81+
});
82+
83+
it('should create', () => {
84+
expect(component).toBeTruthy();
85+
});
86+
87+
it('should update currentPage when page is defined', () => {
88+
const event: PaginatorState = { page: 1 } as PaginatorState;
89+
component.onPageChange(event);
90+
expect(component.currentPage()).toBe('2');
91+
});
92+
93+
it('should not update currentPage when page is undefined', () => {
94+
component.currentPage.set('5');
95+
const event: PaginatorState = { page: undefined } as PaginatorState;
96+
component.onPageChange(event);
97+
expect(component.currentPage()).toBe('5');
98+
});
99+
});

0 commit comments

Comments
 (0)