diff --git a/src/app/features/analytics/analytics.component.html b/src/app/features/analytics/analytics.component.html index 8cb527800..3ed2771cd 100644 --- a/src/app/features/analytics/analytics.component.html +++ b/src/app/features/analytics/analytics.component.html @@ -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()" > @@ -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()" >
- @if (!isDuplicatesLoading() && currentResource()) { - @if (!duplicates().length) { -

{{ 'project.overview.dialog.fork.noForksMessage' | translate }}

+ @if (!isLoading() && currentResource()) { + @if (!items().length) { +

{{ emptyScreenMessage() }}

} @else { -

{{ 'project.overview.dialog.fork.forksMessage' | translate }}

+

{{ message() }}

- @for (duplicate of duplicates(); track duplicate.id) { + @for (relatedItem of items(); track relatedItem.id) {

- - {{ duplicate.title }} + + {{ relatedItem.title }}

- @if (showMoreOptions(duplicate)) { + @if (showMoreOptions(relatedItem)) {
> - +
{{ 'common.labels.forked' | translate }}: -

{{ duplicate.dateCreated | date: 'MMM d, y, h:mm a' }}

+

{{ relatedItem.dateCreated | date: 'MMM d, y, h:mm a' }}

{{ 'common.labels.lastUpdated' | translate }}: -

{{ duplicate.dateModified | date: 'MMM d, y, h:mm a' }}

+

{{ relatedItem.dateModified | date: 'MMM d, y, h:mm a' }}

{{ 'common.labels.contributors' | translate }}: - +
- @if (duplicate.description) { + @if (relatedItem.description) { }
} diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.scss b/src/app/features/analytics/components/view-related-projects/view-related-projects.component.scss similarity index 100% rename from src/app/features/analytics/components/view-duplicates/view-duplicates.component.scss rename to src/app/features/analytics/components/view-related-projects/view-related-projects.component.scss diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts b/src/app/features/analytics/components/view-related-projects/view-related-projects.component.spec.ts similarity index 86% rename from src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts rename to src/app/features/analytics/components/view-related-projects/view-related-projects.component.spec.ts index 336aaacbb..80fe9a26a 100644 --- a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts +++ b/src/app/features/analytics/components/view-related-projects/view-related-projects.component.spec.ts @@ -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, @@ -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'; @@ -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; + let component: ViewRelatedProjectsComponent; + let fixture: ComponentFixture; let routerMock: ReturnType; let activatedRouteMock: ReturnType; let mockCustomDialogService: ReturnType; @@ -47,7 +48,7 @@ describe('Component: View Duplicates', () => { await TestBed.configureTestingModule({ imports: [ - ViewDuplicatesComponent, + ViewRelatedProjectsComponent, OSFTestingModule, ...MockComponents( SubHeaderComponent, @@ -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 }, @@ -76,7 +77,7 @@ describe('Component: View Duplicates', () => { ], }).compileComponents(); - fixture = TestBed.createComponent(ViewDuplicatesComponent); + fixture = TestBed.createComponent(ViewRelatedProjectsComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts b/src/app/features/analytics/components/view-related-projects/view-related-projects.component.ts similarity index 65% rename from src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts rename to src/app/features/analytics/components/view-related-projects/view-related-projects.component.ts index 595a71d56..d040442be 100644 --- a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts +++ b/src/app/features/analytics/components/view-related-projects/view-related-projects.component.ts @@ -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'; @@ -16,6 +16,7 @@ import { DestroyRef, effect, inject, + OnInit, Signal, signal, } from '@angular/core'; @@ -23,7 +24,12 @@ 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, @@ -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, @@ -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(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; @@ -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(); @@ -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; } }); @@ -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': @@ -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(); diff --git a/src/app/features/analytics/enums/index.ts b/src/app/features/analytics/enums/index.ts index c984e6081..342d2ca33 100644 --- a/src/app/features/analytics/enums/index.ts +++ b/src/app/features/analytics/enums/index.ts @@ -1 +1,2 @@ export { DateRange } from './date-range.enum'; +export { RelatedItemsType } from './related-items-type'; diff --git a/src/app/features/analytics/enums/related-items-type.ts b/src/app/features/analytics/enums/related-items-type.ts new file mode 100644 index 000000000..c4afb9e4c --- /dev/null +++ b/src/app/features/analytics/enums/related-items-type.ts @@ -0,0 +1,4 @@ +export const enum RelatedItemsType { + Duplicates = 'forks', + Linked = 'linked_by_nodes', +} diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 8c34fdce1..59d47c324 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -9,8 +9,8 @@ import { CitationsState, CollectionsState, ContributorsState, - DuplicatesState, NodeLinksState, + RelatedState, SubjectsState, ViewOnlyLinkState, } from '@osf/shared/stores'; @@ -96,13 +96,13 @@ export const projectRoutes: Routes = [ providers: [provideStates([AnalyticsState])], }, { - path: 'analytics/duplicates', + path: 'analytics/related-projects/:type', data: { resourceType: ResourceType.Project }, loadComponent: () => - import('../analytics/components/view-duplicates/view-duplicates.component').then( - (mod) => mod.ViewDuplicatesComponent + import('@osf/features/analytics/components/view-related-projects/view-related-projects.component').then( + (mod) => mod.ViewRelatedProjectsComponent ), - providers: [provideStates([DuplicatesState])], + providers: [provideStates([RelatedState])], }, { path: 'wiki', diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index f26006bbe..6ac716565 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -5,13 +5,7 @@ import { Routes } from '@angular/router'; import { viewOnlyGuard } from '@osf/core/guards'; import { ResourceType } from '@osf/shared/enums'; import { LicensesService } from '@osf/shared/services'; -import { - CitationsState, - ContributorsState, - DuplicatesState, - SubjectsState, - ViewOnlyLinkState, -} from '@osf/shared/stores'; +import { CitationsState, ContributorsState, RelatedState, SubjectsState, ViewOnlyLinkState } from '@osf/shared/stores'; import { ActivityLogsState } from '@shared/stores/activity-logs'; import { AnalyticsState } from '../analytics/store'; @@ -77,13 +71,13 @@ export const registryRoutes: Routes = [ providers: [provideStates([AnalyticsState])], }, { - path: 'analytics/duplicates', + path: 'analytics/related-projects/:type', data: { resourceType: ResourceType.Registration }, loadComponent: () => - import('../analytics/components/view-duplicates/view-duplicates.component').then( - (mod) => mod.ViewDuplicatesComponent + import('../analytics/components/view-related-projects/view-related-projects.component').then( + (mod) => mod.ViewRelatedProjectsComponent ), - providers: [provideStates([DuplicatesState])], + providers: [provideStates([RelatedState])], }, { path: 'files', diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 0f52b0dd9..dee9a9b4c 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -7,7 +7,6 @@ export { CollectionsService } from './collections.service'; export { ContributorsService } from './contributors.service'; export { CustomConfirmationService } from './custom-confirmation.service'; export { CustomDialogService } from './custom-dialog.service'; -export { DuplicatesService } from './duplicates.service'; export { FilesService } from './files.service'; export { GlobalSearchService } from './global-search.service'; export { GoogleFilePickerDownloadService } from './google-file-picker.download.service'; @@ -23,6 +22,7 @@ export { NodeLinksService } from './node-links.service'; export { ProjectRedirectDialogService } from './project-redirect-dialog.service'; export { RegionsService } from './regions.service'; export { RegistrationProviderService } from './registration-provider.service'; +export { RelatedService } from './related.service'; export { RequestAccessService } from './request-access.service'; export { ResourceGuidService } from './resource.service'; export { ResourceCardService } from './resource-card.service'; diff --git a/src/app/shared/services/duplicates.service.ts b/src/app/shared/services/related.service.ts similarity index 65% rename from src/app/shared/services/duplicates.service.ts rename to src/app/shared/services/related.service.ts index ff89ed899..c9d5bed8c 100644 --- a/src/app/shared/services/duplicates.service.ts +++ b/src/app/shared/services/related.service.ts @@ -4,6 +4,7 @@ import { map } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { RelatedItemsType } from '@osf/features/analytics/enums'; import { DuplicatesMapper } from '../mappers'; import { ResponseJsonApi } from '../models'; @@ -14,7 +15,7 @@ import { JsonApiService } from './json-api.service'; @Injectable({ providedIn: 'root', }) -export class DuplicatesService { +export class RelatedService { private jsonApiService = inject(JsonApiService); private readonly environment = inject(ENVIRONMENT); @@ -22,11 +23,25 @@ export class DuplicatesService { return `${this.environment.apiDomainUrl}/v2`; } + fetchAllLinkedNodes(resourceId: string, resourceType: string, pageNumber?: number, pageSize?: number) { + return this.fetchAllRelated(resourceId, resourceType, RelatedItemsType.Linked, pageNumber, pageSize); + } + fetchAllDuplicates( resourceId: string, resourceType: string, pageNumber?: number, pageSize?: number + ): Observable { + return this.fetchAllRelated(resourceId, resourceType, RelatedItemsType.Duplicates, pageNumber, pageSize); + } + + private fetchAllRelated( + resourceId: string, + resourceType: string, + relatedType: RelatedItemsType, + pageNumber?: number, + pageSize?: number ): Observable { const params: Record = { embed: 'bibliographic_contributors', @@ -42,7 +57,7 @@ export class DuplicatesService { } return this.jsonApiService - .get>(`${this.apiUrl}/${resourceType}/${resourceId}/forks/`, params) + .get>(`${this.apiUrl}/${resourceType}/${resourceId}/${relatedType}/`, params) .pipe(map((res) => DuplicatesMapper.fromDuplicatesJsonApiResponse(res))); } } diff --git a/src/app/shared/stores/duplicates/duplicates.selectors.ts b/src/app/shared/stores/duplicates/duplicates.selectors.ts deleted file mode 100644 index 67446a676..000000000 --- a/src/app/shared/stores/duplicates/duplicates.selectors.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Selector } from '@ngxs/store'; - -import { DuplicatesStateModel } from './duplicates.model'; -import { DuplicatesState } from './duplicates.state'; - -export class DuplicatesSelectors { - @Selector([DuplicatesState]) - static getDuplicates(state: DuplicatesStateModel) { - return state.duplicates.data; - } - - @Selector([DuplicatesState]) - static getDuplicatesLoading(state: DuplicatesStateModel) { - return state.duplicates.isLoading; - } - - @Selector([DuplicatesState]) - static getDuplicatesTotalCount(state: DuplicatesStateModel) { - return state.duplicates.totalCount; - } -} diff --git a/src/app/shared/stores/duplicates/duplicates.state.ts b/src/app/shared/stores/duplicates/duplicates.state.ts deleted file mode 100644 index e34f64f09..000000000 --- a/src/app/shared/stores/duplicates/duplicates.state.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Action, State, StateContext } from '@ngxs/store'; - -import { catchError, tap } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { handleSectionError } from '@shared/helpers'; -import { DuplicatesService } from '@shared/services/duplicates.service'; - -import { ClearDuplicates, GetAllDuplicates } from './duplicates.actions'; -import { DUPLICATES_DEFAULTS, DuplicatesStateModel } from './duplicates.model'; - -@State({ - name: 'duplicates', - defaults: DUPLICATES_DEFAULTS, -}) -@Injectable() -export class DuplicatesState { - duplicatesService = inject(DuplicatesService); - - @Action(GetAllDuplicates) - getDuplicates(ctx: StateContext, action: GetAllDuplicates) { - const state = ctx.getState(); - ctx.patchState({ - duplicates: { - ...state.duplicates, - isLoading: true, - }, - }); - - return this.duplicatesService - .fetchAllDuplicates(action.resourceId, action.resourceType, action.page, action.pageSize) - .pipe( - tap((response) => { - ctx.patchState({ - duplicates: { - data: response.data, - isLoading: false, - error: null, - totalCount: response.totalCount, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'duplicates', error)) - ); - } - - @Action(ClearDuplicates) - clearDuplicates(ctx: StateContext) { - ctx.patchState(DUPLICATES_DEFAULTS); - } -} diff --git a/src/app/shared/stores/duplicates/index.ts b/src/app/shared/stores/duplicates/index.ts index 7d2822569..c8c529bd6 100644 --- a/src/app/shared/stores/duplicates/index.ts +++ b/src/app/shared/stores/duplicates/index.ts @@ -1,4 +1,4 @@ -export * from './duplicates.actions'; -export * from './duplicates.model'; -export * from './duplicates.selectors'; -export * from './duplicates.state'; +export * from './related.actions'; +export * from './related.model'; +export * from './related.selectors'; +export * from './related.state.service'; diff --git a/src/app/shared/stores/duplicates/duplicates.actions.ts b/src/app/shared/stores/duplicates/related.actions.ts similarity index 56% rename from src/app/shared/stores/duplicates/duplicates.actions.ts rename to src/app/shared/stores/duplicates/related.actions.ts index 4bcf560da..a042cc3f4 100644 --- a/src/app/shared/stores/duplicates/duplicates.actions.ts +++ b/src/app/shared/stores/duplicates/related.actions.ts @@ -9,6 +9,17 @@ export class GetAllDuplicates { ) {} } +export class GetAllLinkedProjects { + static readonly type = '[Forks] Get All Linked Projects'; + + constructor( + public resourceId: string, + public resourceType: string, + public page: number, + public pageSize: number + ) {} +} + export class ClearDuplicates { static readonly type = '[Forks] Clear Duplicates'; } diff --git a/src/app/shared/stores/duplicates/duplicates.model.ts b/src/app/shared/stores/duplicates/related.model.ts similarity index 82% rename from src/app/shared/stores/duplicates/duplicates.model.ts rename to src/app/shared/stores/duplicates/related.model.ts index ae0951a5b..f21ce3292 100644 --- a/src/app/shared/stores/duplicates/duplicates.model.ts +++ b/src/app/shared/stores/duplicates/related.model.ts @@ -3,11 +3,11 @@ import { AsyncStateWithTotalCount } from '@osf/shared/models'; import { Duplicate } from 'src/app/shared/models/duplicates'; export interface DuplicatesStateModel { - duplicates: AsyncStateWithTotalCount; + related: AsyncStateWithTotalCount; } export const DUPLICATES_DEFAULTS: DuplicatesStateModel = { - duplicates: { + related: { data: [], isLoading: false, isSubmitting: false, diff --git a/src/app/shared/stores/duplicates/related.selectors.ts b/src/app/shared/stores/duplicates/related.selectors.ts new file mode 100644 index 000000000..e0f4e7c8c --- /dev/null +++ b/src/app/shared/stores/duplicates/related.selectors.ts @@ -0,0 +1,21 @@ +import { Selector } from '@ngxs/store'; + +import { DuplicatesStateModel } from './related.model'; +import { RelatedState } from './related.state.service'; + +export class RelatedSelectors { + @Selector([RelatedState]) + static getRelated(state: DuplicatesStateModel) { + return state.related.data; + } + + @Selector([RelatedState]) + static getRelatedLoading(state: DuplicatesStateModel) { + return state.related.isLoading; + } + + @Selector([RelatedState]) + static getRelatedTotalCount(state: DuplicatesStateModel) { + return state.related.totalCount; + } +} diff --git a/src/app/shared/stores/duplicates/related.state.service.ts b/src/app/shared/stores/duplicates/related.state.service.ts new file mode 100644 index 000000000..ecb9213ff --- /dev/null +++ b/src/app/shared/stores/duplicates/related.state.service.ts @@ -0,0 +1,79 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { catchError, tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { handleSectionError } from '@shared/helpers'; +import { RelatedService } from '@shared/services/related.service'; + +import { ClearDuplicates, GetAllDuplicates, GetAllLinkedProjects } from './related.actions'; +import { DUPLICATES_DEFAULTS, DuplicatesStateModel } from './related.model'; + +@State({ + name: 'duplicates', + defaults: DUPLICATES_DEFAULTS, +}) +@Injectable() +export class RelatedState { + relatedService = inject(RelatedService); + + @Action(GetAllDuplicates) + getDuplicates(ctx: StateContext, action: GetAllDuplicates) { + const state = ctx.getState(); + ctx.patchState({ + related: { + ...state.related, + isLoading: true, + }, + }); + + return this.relatedService + .fetchAllDuplicates(action.resourceId, action.resourceType, action.page, action.pageSize) + .pipe( + tap((response) => { + ctx.patchState({ + related: { + data: response.data, + isLoading: false, + error: null, + totalCount: response.totalCount, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'related', error)) + ); + } + + @Action(GetAllLinkedProjects) + getLinkedProjects(ctx: StateContext, action: GetAllDuplicates) { + const state = ctx.getState(); + ctx.patchState({ + related: { + ...state.related, + isLoading: true, + }, + }); + + return this.relatedService + .fetchAllLinkedNodes(action.resourceId, action.resourceType, action.page, action.pageSize) + .pipe( + tap((response) => { + ctx.patchState({ + related: { + data: response.data, + isLoading: false, + error: null, + totalCount: response.totalCount, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'related', error)) + ); + } + + @Action(ClearDuplicates) + clearDuplicates(ctx: StateContext) { + ctx.patchState(DUPLICATES_DEFAULTS); + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 89943c1b4..de5fecbf4 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -539,6 +539,11 @@ "viewForks": "View duplicates", "viewLinks": "View links" }, + "viewRelated": { + "linkedProjectsTitle": "Linked Projects", + "linkedProjectsMessage": "Linked Projects you have permission to view are shown here.", + "noLinkedProjectsMessage": "No Linked Projects found." + }, "charts": { "showAnalytics": "Show analytics for date range:", "pastWeek": "Past week",