Skip to content
Closed
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
5 changes: 3 additions & 2 deletions src/app/features/analytics/analytics.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
[isLoading]="isRelatedCountsLoading()"
[title]="'project.analytics.kpi.forks'"
[value]="relatedCounts()?.forksCount"
[showButton]="true"
[showButton]="(relatedCounts()?.forksCount ?? 0) > 0"
[buttonLabel]="'project.analytics.kpi.viewForks'"
(buttonClick)="navigateToDuplicates()"
></osf-analytics-kpi>
Expand All @@ -100,8 +100,9 @@
[isLoading]="isRelatedCountsLoading()"
[title]="'project.analytics.kpi.linksToThisProject'"
[value]="relatedCounts()?.linksToCount"
[showButton]="false"
[showButton]="(relatedCounts()?.linksToCount ?? 0) > 0"
[buttonLabel]="'project.analytics.kpi.viewLinks'"
(buttonClick)="navigateToLinkedProjects()"
></osf-analytics-kpi>

<osf-analytics-kpi
Expand Down
8 changes: 6 additions & 2 deletions src/app/features/analytics/analytics.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { DatasetInput } from '@osf/shared/models';

import { AnalyticsKpiComponent } from './components';
import { DATE_RANGE_OPTIONS } from './constants';
import { DateRange } from './enums';
import { DateRange, RelatedItemsType } from './enums';
import { AnalyticsSelectors, ClearAnalytics, GetMetrics, GetRelatedCounts } from './store';

@Component({
Expand Down Expand Up @@ -134,7 +134,11 @@ export class AnalyticsComponent implements OnInit {
}

navigateToDuplicates() {
this.router.navigate(['duplicates'], { relativeTo: this.route });
this.router.navigate(['related-projects', RelatedItemsType.Duplicates], { relativeTo: this.route });
}

navigateToLinkedProjects() {
this.router.navigate(['related-projects', RelatedItemsType.Linked], { relativeTo: this.route });
}

private setData() {
Expand Down
2 changes: 1 addition & 1 deletion src/app/features/analytics/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { AnalyticsKpiComponent } from './analytics-kpi/analytics-kpi.component';
export { ViewDuplicatesComponent } from './view-duplicates/view-duplicates.component';
export { ViewRelatedProjectsComponent } from '@osf/features/analytics/components/view-related-projects/view-related-projects.component';
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
<osf-sub-header
[title]="'project.analytics.kpi.forks' | translate"
[title]="title()"
[showButton]="isAuthenticated()"
[buttonLabel]="'project.overview.actions.forkProjectLabel' | translate"
(buttonClick)="handleForkResource()"
[buttonLabel]="mainActionMessage()"
(buttonClick)="handleMainAction()"
/>

<div class="flex flex-column flex-1 bg-white gap-5 p-3 sm:p-4">
@if (!isDuplicatesLoading() && currentResource()) {
@if (!duplicates().length) {
<p class="mt-5 text-center">{{ 'project.overview.dialog.fork.noForksMessage' | translate }}</p>
@if (!isLoading() && currentResource()) {
@if (!items().length) {
<p class="mt-5 text-center">{{ emptyScreenMessage() }}</p>
} @else {
<p>{{ 'project.overview.dialog.fork.forksMessage' | translate }}</p>
<p>{{ message() }}</p>

@for (duplicate of duplicates(); track duplicate.id) {
@for (relatedItem of items(); track relatedItem.id) {
<div class="duplicate-wrapper flex flex-column gap-3 p-3 sm:p-4">
<div class="flex justify-content-between align-items-center">
<h2 class="flex align-items-center gap-2">
<osf-icon [iconClass]="duplicate.public ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
{{ duplicate.title }}
<osf-icon [iconClass]="relatedItem.public ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
{{ relatedItem.title }}
</h2>

@if (showMoreOptions(duplicate)) {
@if (showMoreOptions(relatedItem)) {
<div>
<p-button
severity="contrast"
Expand All @@ -32,7 +32,7 @@ <h2 class="flex align-items-center gap-2">
>
</p-button>

<p-menu appendTo="body" [model]="forkActionItems(duplicate.id)" popup #menu>
<p-menu appendTo="body" [model]="forkActionItems(relatedItem.id)" popup #menu>
<ng-template #item let-item>
<a
class="p-menu-item-link"
Expand All @@ -51,30 +51,30 @@ <h2 class="flex align-items-center gap-2">
<div class="flex flex-column gap-2">
<div class="flex flex-wrap align-items-center gap-1">
<span class="font-bold">{{ 'common.labels.forked' | translate }}:</span>
<p>{{ duplicate.dateCreated | date: 'MMM d, y, h:mm a' }}</p>
<p>{{ relatedItem.dateCreated | date: 'MMM d, y, h:mm a' }}</p>
</div>

<div class="flex flex-wrap align-items-center gap-1">
<span class="font-bold">{{ 'common.labels.lastUpdated' | translate }}:</span>
<p>{{ duplicate.dateModified | date: 'MMM d, y, h:mm a' }}</p>
<p>{{ relatedItem.dateModified | date: 'MMM d, y, h:mm a' }}</p>
</div>

<div class="flex flex-wrap align-items-center gap-1">
<span class="font-bold">{{ 'common.labels.contributors' | translate }}:</span>

<osf-contributors-list [contributors]="duplicate.contributors"></osf-contributors-list>
<osf-contributors-list [contributors]="relatedItem.contributors"></osf-contributors-list>
</div>

@if (duplicate.description) {
@if (relatedItem.description) {
<osf-truncated-text
[text]="('resourceCard.labels.descriptionBold' | translate) + duplicate.description"
[text]="('resourceCard.labels.descriptionBold' | translate) + relatedItem.description"
/>
}
</div>
<p-button
[label]="'common.buttons.view' | translate"
severity="secondary"
[routerLink]="['/', duplicate.id]"
[routerLink]="['/', relatedItem.id]"
/>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { ActivatedRoute, Router } from '@angular/router';
import { ProjectOverviewSelectors } from '@osf/features/project/overview/store';
import { RegistryOverviewSelectors } from '@osf/features/registry/store/registry-overview';
import { ResourceType } from '@osf/shared/enums';
import { DuplicatesSelectors } from '@osf/shared/stores';
import { IS_SMALL } from '@osf/shared/helpers';
import { RelatedSelectors } from '@osf/shared/stores';
import {
ContributorsListComponent,
CustomPaginatorComponent,
Expand All @@ -22,7 +23,7 @@ import {
import { MOCK_PROJECT_OVERVIEW } from '@shared/mocks';
import { CustomDialogService } from '@shared/services';

import { ViewDuplicatesComponent } from './view-duplicates.component';
import { ViewRelatedProjectsComponent } from './view-related-projects.component';

import { OSFTestingModule } from '@testing/osf.testing.module';
import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock';
Expand All @@ -31,8 +32,8 @@ import { RouterMockBuilder } from '@testing/providers/router-provider.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';

describe('Component: View Duplicates', () => {
let component: ViewDuplicatesComponent;
let fixture: ComponentFixture<ViewDuplicatesComponent>;
let component: ViewRelatedProjectsComponent;
let fixture: ComponentFixture<ViewRelatedProjectsComponent>;
let routerMock: ReturnType<RouterMockBuilder['build']>;
let activatedRouteMock: ReturnType<ActivatedRouteMockBuilder['build']>;
let mockCustomDialogService: ReturnType<CustomDialogServiceMockBuilder['build']>;
Expand All @@ -47,7 +48,7 @@ describe('Component: View Duplicates', () => {

await TestBed.configureTestingModule({
imports: [
ViewDuplicatesComponent,
ViewRelatedProjectsComponent,
OSFTestingModule,
...MockComponents(
SubHeaderComponent,
Expand All @@ -61,9 +62,9 @@ describe('Component: View Duplicates', () => {
providers: [
provideMockStore({
signals: [
{ selector: DuplicatesSelectors.getDuplicates, value: [] },
{ selector: DuplicatesSelectors.getDuplicatesLoading, value: false },
{ selector: DuplicatesSelectors.getDuplicatesTotalCount, value: 0 },
{ selector: RelatedSelectors.getRelated, value: [] },
{ selector: RelatedSelectors.getRelatedLoading, value: false },
{ selector: RelatedSelectors.getRelatedTotalCount, value: 0 },
{ selector: ProjectOverviewSelectors.getProject, value: MOCK_PROJECT_OVERVIEW },
{ selector: ProjectOverviewSelectors.isProjectAnonymous, value: false },
{ selector: RegistryOverviewSelectors.getRegistry, value: undefined },
Expand All @@ -76,7 +77,7 @@ describe('Component: View Duplicates', () => {
],
}).compileComponents();

fixture = TestBed.createComponent(ViewDuplicatesComponent);
fixture = TestBed.createComponent(ViewRelatedProjectsComponent);
component = fixture.componentInstance;

fixture.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createDispatchMap, select } from '@ngxs/store';

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

import { Button } from 'primeng/button';
import { Menu } from 'primeng/menu';
Expand All @@ -16,14 +16,20 @@ import {
DestroyRef,
effect,
inject,
OnInit,
Signal,
signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';

import { UserSelectors } from '@core/store/user';
import { DeleteComponentDialogComponent, ForkDialogComponent } from '@osf/features/project/overview/components';
import { RelatedItemsType } from '@osf/features/analytics/enums/related-items-type';
import {
DeleteComponentDialogComponent,
ForkDialogComponent,
LinkResourceDialogComponent,
} from '@osf/features/project/overview/components';
import { ClearProjectOverview, GetProjectById, ProjectOverviewSelectors } from '@osf/features/project/overview/store';
import {
ClearRegistryOverview,
Expand All @@ -39,13 +45,14 @@ import {
TruncatedTextComponent,
} from '@osf/shared/components';
import { ResourceType, UserPermissions } from '@osf/shared/enums';
import { IS_MEDIUM } from '@osf/shared/helpers';
import { ToolbarResource } from '@osf/shared/models';
import { Duplicate } from '@osf/shared/models/duplicates';
import { CustomDialogService, LoaderService } from '@osf/shared/services';
import { ClearDuplicates, DuplicatesSelectors, GetAllDuplicates, GetResourceWithChildren } from '@osf/shared/stores';
import { CustomDialogService } from '@osf/shared/services';
import { ClearDuplicates, GetAllDuplicates, GetAllLinkedProjects, RelatedSelectors } from '@osf/shared/stores';

@Component({
selector: 'osf-view-duplicates',
selector: 'osf-view-related-projects',
imports: [
SubHeaderComponent,
TranslatePipe,
Expand All @@ -59,25 +66,64 @@ import { ClearDuplicates, DuplicatesSelectors, GetAllDuplicates, GetResourceWith
IconComponent,
ContributorsListComponent,
],
templateUrl: './view-duplicates.component.html',
styleUrl: './view-duplicates.component.scss',
templateUrl: './view-related-projects.component.html',
styleUrl: './view-related-projects.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ViewDuplicatesComponent {
export class ViewRelatedProjectsComponent implements OnInit {
private customDialogService = inject(CustomDialogService);
private loaderService = inject(LoaderService);
private route = inject(ActivatedRoute);
private router = inject(Router);
private destroyRef = inject(DestroyRef);
private translateService = inject(TranslateService);
private project = select(ProjectOverviewSelectors.getProject);
private registration = select(RegistryOverviewSelectors.getRegistry);
private isProjectAnonymous = select(ProjectOverviewSelectors.isProjectAnonymous);
private isRegistryAnonymous = select(RegistryOverviewSelectors.isRegistryAnonymous);
private itemType = signal<RelatedItemsType | null>(null);

duplicates = select(DuplicatesSelectors.getDuplicates);
isDuplicatesLoading = select(DuplicatesSelectors.getDuplicatesLoading);
totalDuplicates = select(DuplicatesSelectors.getDuplicatesTotalCount);
items = select(RelatedSelectors.getRelated);
isLoading = select(RelatedSelectors.getRelatedLoading);
totalDuplicates = select(RelatedSelectors.getRelatedTotalCount);
isAuthenticated = select(UserSelectors.isAuthenticated);
isMedium = toSignal(inject(IS_MEDIUM));

title = computed(() => {
switch (this.itemType()) {
case RelatedItemsType.Duplicates:
return this.translateService.instant('project.analytics.kpi.forks');
case RelatedItemsType.Linked:
return this.translateService.instant('project.analytics.viewRelated.linkedProjectsTitle');
}
});

emptyScreenMessage = computed(() => {
switch (this.itemType()) {
case RelatedItemsType.Duplicates:
return this.translateService.instant('project.overview.dialog.fork.noForksMessage');
case RelatedItemsType.Linked:
return this.translateService.instant('project.analytics.viewRelated.noLinkedProjectsMessage');
}
});

message = computed(() => {
switch (this.itemType()) {
case RelatedItemsType.Duplicates:
return this.translateService.instant('project.overview.dialog.fork.forksMessage');
case RelatedItemsType.Linked:
return this.translateService.instant('project.analytics.viewRelated.linkedProjectsMessage');
}
});

mainActionMessage = computed(() => {
switch (this.itemType()) {
case RelatedItemsType.Duplicates:
return this.translateService.instant('project.overview.actions.forkProjectLabel');
case RelatedItemsType.Linked:
return this.translateService.instant('project.overview.components.linkProjectsButton');
}
});

readonly pageSize = 10;
readonly UserPermissions = UserPermissions;
Expand Down Expand Up @@ -123,12 +169,19 @@ export class ViewDuplicatesComponent {
getProject: GetProjectById,
getRegistration: GetRegistryById,
getDuplicates: GetAllDuplicates,
getLinkedProjects: GetAllLinkedProjects,
clearDuplicates: ClearDuplicates,
clearProject: ClearProjectOverview,
clearRegistration: ClearRegistryOverview,
getComponentsTree: GetResourceWithChildren,
});

ngOnInit() {
this.route.paramMap.subscribe((params) => {
this.itemType.set((params.get('type') as RelatedItemsType) ?? RelatedItemsType.Duplicates);
});
}

constructor() {
effect(() => {
const resourceId = this.resourceId();
Expand All @@ -142,9 +195,16 @@ export class ViewDuplicatesComponent {

effect(() => {
const resource = this.currentResource();

if (resource) {
this.actions.getDuplicates(resource.id, resource.type, parseInt(this.currentPage()), this.pageSize);
if (!resource) {
return;
}
switch (this.itemType()) {
case RelatedItemsType.Duplicates:
this.actions.getDuplicates(resource.id, resource.type, parseInt(this.currentPage()), this.pageSize);
break;
case RelatedItemsType.Linked:
this.actions.getLinkedProjects(resource.id, resource.type, parseInt(this.currentPage()), this.pageSize);
break;
}
});

Expand Down Expand Up @@ -173,11 +233,16 @@ export class ViewDuplicatesComponent {

showMoreOptions(duplicate: Duplicate) {
return (
duplicate.currentUserPermissions.includes(UserPermissions.Admin) ||
duplicate.currentUserPermissions.includes(UserPermissions.Write)
this.isShowingDuplicates() &&
(duplicate.currentUserPermissions.includes(UserPermissions.Admin) ||
duplicate.currentUserPermissions.includes(UserPermissions.Write))
);
}

isShowingDuplicates() {
return this.itemType() == RelatedItemsType.Duplicates;
}

handleMenuAction(action: string, resourceId: string): void {
switch (action) {
case 'manageContributors':
Expand All @@ -191,6 +256,26 @@ export class ViewDuplicatesComponent {
break;
}
}
handleMainAction() {
switch (this.itemType()) {
case RelatedItemsType.Duplicates:
this.handleForkResource();
return;

case RelatedItemsType.Linked:
this.openLinkProjectModal();
return;
}
}

openLinkProjectModal() {
const dialogWidth = this.isMedium() ? '850px' : '95vw';

this.customDialogService.open(LinkResourceDialogComponent, {
header: 'project.overview.dialog.linkProject.header',
width: dialogWidth,
});
}

handleForkResource(): void {
const toolbarResource = this.toolbarResource();
Expand Down
1 change: 1 addition & 0 deletions src/app/features/analytics/enums/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { DateRange } from './date-range.enum';
export { RelatedItemsType } from './related-items-type';
Loading
Loading