diff --git a/src/app/features/registry/components/registration-links-card/registration-links-card.component.html b/src/app/features/registry/components/registration-links-card/registration-links-card.component.html index 42dcee062..95a2f6e72 100644 --- a/src/app/features/registry/components/registration-links-card/registration-links-card.component.html +++ b/src/app/features/registry/components/registration-links-card/registration-links-card.component.html @@ -13,6 +13,20 @@

+ @if (isComponentData() && componentsDataTyped()?.registrationSupplement) { +
+ {{ 'project.registrations.card.registrationTemplate' | translate }} + {{ componentsDataTyped()!.registrationSupplement }} +
+ } + + @if (isComponentData() && componentsDataTyped()?.registry) { +
+ {{ 'project.registrations.card.registry' | translate }} + {{ componentsDataTyped()!.registry }} +
+ } +
{{ 'project.registrations.card.registered' | translate }} {{ registrationData().dateCreated | date: 'medium' }} @@ -88,20 +102,18 @@

- @if (hasResources()) { -
-

{{ 'shared.resources.title' | translate }}

- -
- } +
+

{{ 'shared.resources.title' | translate }}

+ +
} diff --git a/src/app/features/registry/components/registration-links-card/registration-links-card.component.ts b/src/app/features/registry/components/registration-links-card/registration-links-card.component.ts index 08159b8cd..bc6556254 100644 --- a/src/app/features/registry/components/registration-links-card/registration-links-card.component.ts +++ b/src/app/features/registry/components/registration-links-card/registration-links-card.component.ts @@ -6,7 +6,7 @@ import { Card } from 'primeng/card'; import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; -import { LinkedNode, LinkedRegistration } from '@osf/features/registry/models'; +import { LinkedNode, LinkedRegistration, RegistryComponentModel } from '@osf/features/registry/models'; import { DataResourcesComponent, TruncatedTextComponent } from '@shared/components'; import { RevisionReviewStates } from '@shared/enums'; @@ -18,7 +18,8 @@ import { RevisionReviewStates } from '@shared/enums'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegistrationLinksCardComponent { - readonly registrationData = input.required(); + readonly registrationData = input.required(); + readonly updateEmitRegistrationData = output(); readonly reviewEmitRegistrationData = output(); @@ -29,16 +30,18 @@ export class RegistrationLinksCardComponent { return 'reviewsState' in data; }); + protected readonly isComponentData = computed(() => { + const data = this.registrationData(); + return 'registrationSupplement' in data; + }); + protected readonly registrationDataTyped = computed(() => { const data = this.registrationData(); return this.isRegistrationData() ? (data as LinkedRegistration) : null; }); - protected readonly hasResources = computed(() => { - const regData = this.registrationDataTyped(); - if (!regData) return false; - return ( - regData.hasData || regData.hasAnalyticCode || regData.hasMaterials || regData.hasPapers || regData.hasSupplements - ); + protected readonly componentsDataTyped = computed(() => { + const data = this.registrationData(); + return this.isComponentData() ? (data as RegistryComponentModel) : null; }); } diff --git a/src/app/features/registry/mappers/index.ts b/src/app/features/registry/mappers/index.ts index ab8e8924f..969eb4fa2 100644 --- a/src/app/features/registry/mappers/index.ts +++ b/src/app/features/registry/mappers/index.ts @@ -3,6 +3,7 @@ export * from './bibliographic-contributors.mapper'; export * from './cedar-form.mapper'; export * from './linked-nodes.mapper'; export * from './linked-registrations.mapper'; +export * from './registry-components.mapper'; export * from './registry-metadata.mapper'; export * from './registry-overview.mapper'; export * from './registry-resource.mapper'; diff --git a/src/app/features/registry/mappers/registry-components.mapper.ts b/src/app/features/registry/mappers/registry-components.mapper.ts new file mode 100644 index 000000000..3de6bb6e4 --- /dev/null +++ b/src/app/features/registry/mappers/registry-components.mapper.ts @@ -0,0 +1,23 @@ +import { RegistryComponentModel } from '../models/registry-components.models'; +import { RegistryComponentJsonApi } from '../models/registry-components-json-api.model'; + +export class RegistryComponentsMapper { + static fromApiResponse(apiComponent: RegistryComponentJsonApi): RegistryComponentModel { + return { + id: apiComponent.id, + title: apiComponent.attributes.title, + description: apiComponent.attributes.description, + category: apiComponent.attributes.category, + dateCreated: apiComponent.attributes.date_created, + dateModified: apiComponent.attributes.date_modified, + dateRegistered: apiComponent.attributes.date_registered, + registrationSupplement: apiComponent.attributes.registration_supplement, + tags: apiComponent.attributes.tags, + isPublic: apiComponent.attributes.public, + }; + } + + static fromApiResponseArray(apiComponents: RegistryComponentJsonApi[]): RegistryComponentModel[] { + return apiComponents.map(this.fromApiResponse); + } +} diff --git a/src/app/features/registry/mappers/registry-overview.mapper.ts b/src/app/features/registry/mappers/registry-overview.mapper.ts index 484556521..084276d6e 100644 --- a/src/app/features/registry/mappers/registry-overview.mapper.ts +++ b/src/app/features/registry/mappers/registry-overview.mapper.ts @@ -60,6 +60,7 @@ export function MapRegistryOverview(data: RegistryOverviewJsonApiData): Registry revisionResponses: schemaResponse.attributes?.revision_responses, updatedResponseKeys: schemaResponse.attributes?.updated_response_keys, })), + registry: data.embeds.provider.data.attributes.name, status: MapRegistryStatus(data.attributes), revisionStatus: data.attributes.revision_state, links: { diff --git a/src/app/features/registry/models/get-registry-overview-json-api.model.ts b/src/app/features/registry/models/get-registry-overview-json-api.model.ts index 7e96999ac..d0ccd9e0b 100644 --- a/src/app/features/registry/models/get-registry-overview-json-api.model.ts +++ b/src/app/features/registry/models/get-registry-overview-json-api.model.ts @@ -114,6 +114,13 @@ export interface RegistryOverviewJsonApiEmbed { }; }[]; }; + provider: { + data: { + attributes: { + name: string; + }; + }; + }; } export interface RegistryOverviewJsonApiRelationships { diff --git a/src/app/features/registry/models/index.ts b/src/app/features/registry/models/index.ts index 853f8b006..e303bc64c 100644 --- a/src/app/features/registry/models/index.ts +++ b/src/app/features/registry/models/index.ts @@ -7,6 +7,8 @@ export * from './linked-nodes.models'; export * from './linked-nodes-json-api.model'; export * from './linked-registrations-json-api.model'; export * from './linked-response.models'; +export * from './registry-components.models'; +export * from './registry-components-json-api.model'; export * from './registry-contributor-json-api.model'; export * from './registry-institution.model'; export * from './registry-institutions-json-api.model'; diff --git a/src/app/features/registry/models/registry-components-json-api.model.ts b/src/app/features/registry/models/registry-components-json-api.model.ts new file mode 100644 index 000000000..7e51f4f07 --- /dev/null +++ b/src/app/features/registry/models/registry-components-json-api.model.ts @@ -0,0 +1,30 @@ +import { RegistryComponentModel } from './registry-components.models'; + +export interface RegistryComponentJsonApi { + id: string; + type: string; + attributes: { + title: string; + description: string; + category: string; + date_created: string; + date_modified: string; + date_registered: string; + registration_supplement: string; + tags: string[]; + public: boolean; + }; +} + +export interface RegistryComponentsJsonApiResponse { + data: RegistryComponentJsonApi[]; + meta: { + total: number; + per_page: number; + }; +} + +export interface RegistryComponentsResponseJsonApi { + data: RegistryComponentModel[]; + meta: { total: number; per_page: number }; +} diff --git a/src/app/features/registry/models/registry-components.models.ts b/src/app/features/registry/models/registry-components.models.ts new file mode 100644 index 000000000..8fc63540f --- /dev/null +++ b/src/app/features/registry/models/registry-components.models.ts @@ -0,0 +1,17 @@ +import { NodeBibliographicContributor } from './bibliographic-contributors.models'; + +export interface RegistryComponentModel { + id: string; + title: string; + description: string; + category: string; + dateCreated: string; + dateModified: string; + dateRegistered: string; + registrationSupplement: string; + tags: string[]; + isPublic: boolean; + contributorsCount?: number; + contributors?: NodeBibliographicContributor[]; + registry?: string; +} diff --git a/src/app/features/registry/models/registry-overview.models.ts b/src/app/features/registry/models/registry-overview.models.ts index b9c34528a..8c4bb3d9e 100644 --- a/src/app/features/registry/models/registry-overview.models.ts +++ b/src/app/features/registry/models/registry-overview.models.ts @@ -16,6 +16,7 @@ export interface RegistryOverview { registrationType: string; doi: string; tags: string[]; + registry?: string; contributors: ProjectOverviewContributor[]; citation: string; category: string; diff --git a/src/app/features/registry/pages/registry-components/registry-components.component.html b/src/app/features/registry/pages/registry-components/registry-components.component.html new file mode 100644 index 000000000..3009631a3 --- /dev/null +++ b/src/app/features/registry/pages/registry-components/registry-components.component.html @@ -0,0 +1,26 @@ +
+ + +
+
+ @if (registryComponentsLoading()) { +
+ +
+ } @else if (components().length === 0) { +
+

{{ 'registry.components.noComponentsFound' | translate }}

+
+ } @else { +
+ @for (component of components(); track component.id) { + + } +
+ } +
+
+
diff --git a/src/app/features/registry/pages/registry-components/registry-components.component.scss b/src/app/features/registry/pages/registry-components/registry-components.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registry/pages/registry-components/registry-components.component.spec.ts b/src/app/features/registry/pages/registry-components/registry-components.component.spec.ts new file mode 100644 index 000000000..522af3339 --- /dev/null +++ b/src/app/features/registry/pages/registry-components/registry-components.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegistryComponentsComponent } from './registry-components.component'; + +describe('RegistryComponentsComponent', () => { + let component: RegistryComponentsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegistryComponentsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegistryComponentsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registry/pages/registry-components/registry-components.component.ts b/src/app/features/registry/pages/registry-components/registry-components.component.ts new file mode 100644 index 000000000..6d240eb94 --- /dev/null +++ b/src/app/features/registry/pages/registry-components/registry-components.component.ts @@ -0,0 +1,100 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { ChangeDetectionStrategy, Component, effect, inject, OnInit, signal } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { RegistrationLinksCardComponent } from '@osf/features/registry/components'; +import { RegistryComponentModel } from '@osf/features/registry/models'; +import { GetRegistryById, RegistryOverviewSelectors } from '@osf/features/registry/store/registry-overview'; +import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'; + +import { GetRegistryComponents, RegistryComponentsSelectors } from '../../store/registry-components'; +import { GetBibliographicContributorsForRegistration, RegistryLinksSelectors } from '../../store/registry-links'; + +@Component({ + selector: 'osf-registry-components', + imports: [SubHeaderComponent, TranslatePipe, LoadingSpinnerComponent, RegistrationLinksCardComponent], + templateUrl: './registry-components.component.html', + styleUrl: './registry-components.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegistryComponentsComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + + private registryId = signal(''); + + protected actions = createDispatchMap({ + getRegistryComponents: GetRegistryComponents, + getBibliographicContributorsForRegistration: GetBibliographicContributorsForRegistration, + getRegistryById: GetRegistryById, + }); + + components = signal([]); + + protected registryComponents = select(RegistryComponentsSelectors.getRegistryComponents); + protected registryComponentsLoading = select(RegistryComponentsSelectors.getRegistryComponentsLoading); + + protected bibliographicContributorsForRegistration = select( + RegistryLinksSelectors.getBibliographicContributorsForRegistration + ); + protected bibliographicContributorsForRegistrationId = select( + RegistryLinksSelectors.getBibliographicContributorsForRegistrationId + ); + + protected registry = select(RegistryOverviewSelectors.getRegistry); + + constructor() { + effect(() => { + const components = this.registryComponents(); + + if (components.length > 0) { + components.forEach((component) => { + this.fetchContributorsForComponent(component.id); + }); + + this.components.set(components); + } + }); + + effect(() => { + const bibliographicContributorsForRegistration = this.bibliographicContributorsForRegistration(); + const bibliographicContributorsForRegistrationId = this.bibliographicContributorsForRegistrationId(); + + if (bibliographicContributorsForRegistration && bibliographicContributorsForRegistrationId) { + this.components.set( + this.registryComponents().map((component) => { + if (component.id === bibliographicContributorsForRegistrationId) { + return { + ...component, + registry: this.registry()?.registry, + contributors: bibliographicContributorsForRegistration, + }; + } + + return component; + }) + ); + } + }); + } + + ngOnInit(): void { + this.registryId.set(this.route.parent?.parent?.snapshot.params['id']); + + if (this.registryId()) { + this.actions.getRegistryComponents(this.registryId()); + this.actions.getRegistryById(this.registryId(), true); + } + } + + fetchContributorsForComponent(componentId: string): void { + this.actions.getBibliographicContributorsForRegistration(componentId); + } + + reviewComponentDetails(id: string): void { + this.router.navigate(['/registries', id, 'overview']); + } +} diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index e83315faa..4e89eb52f 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -2,6 +2,7 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; +import { RegistryComponentsState } from '@osf/features/registry/store/registry-components'; import { RegistryFilesState } from '@osf/features/registry/store/registry-files'; import { RegistryLinksState } from '@osf/features/registry/store/registry-links'; import { RegistryMetadataState } from '@osf/features/registry/store/registry-metadata'; @@ -78,6 +79,14 @@ export const registryRoutes: Routes = [ context: ResourceType.Registration, }, }, + { + path: 'components', + loadComponent: () => + import('./pages/registry-components/registry-components.component').then( + (c) => c.RegistryComponentsComponent + ), + providers: [provideStates([RegistryComponentsState, RegistryLinksState])], + }, { path: 'resources', loadComponent: () => diff --git a/src/app/features/registry/services/index.ts b/src/app/features/registry/services/index.ts index f1f11c008..accd0b1ff 100644 --- a/src/app/features/registry/services/index.ts +++ b/src/app/features/registry/services/index.ts @@ -1,3 +1,4 @@ +export * from './registry-components.service'; export * from './registry-links.service'; export * from './registry-metadata.service'; export * from './registry-overview.service'; diff --git a/src/app/features/registry/services/registry-components.service.ts b/src/app/features/registry/services/registry-components.service.ts new file mode 100644 index 000000000..939184deb --- /dev/null +++ b/src/app/features/registry/services/registry-components.service.ts @@ -0,0 +1,35 @@ +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@osf/core/services'; + +import { RegistryComponentsMapper } from '../mappers'; +import { RegistryComponentsJsonApiResponse, RegistryComponentsResponseJsonApi } from '../models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class RegistryComponentsService { + private readonly jsonApiService = inject(JsonApiService); + private readonly apiUrl = environment.apiUrl; + + getRegistryComponents(registryId: string, page = 1, pageSize = 10): Observable { + const params: Record = { + page: page, + 'page[size]': pageSize, + }; + + return this.jsonApiService + .get(`${this.apiUrl}/registrations/${registryId}/children/`, params) + .pipe( + map((response) => ({ + data: response.data.map(RegistryComponentsMapper.fromApiResponse), + meta: response.meta, + })) + ); + } +} diff --git a/src/app/features/registry/store/registry-components/index.ts b/src/app/features/registry/store/registry-components/index.ts new file mode 100644 index 000000000..948d2eb4e --- /dev/null +++ b/src/app/features/registry/store/registry-components/index.ts @@ -0,0 +1,4 @@ +export * from './registry-components.actions'; +export * from './registry-components.model'; +export * from './registry-components.selectors'; +export * from './registry-components.state'; diff --git a/src/app/features/registry/store/registry-components/registry-components.actions.ts b/src/app/features/registry/store/registry-components/registry-components.actions.ts new file mode 100644 index 000000000..d87c1760c --- /dev/null +++ b/src/app/features/registry/store/registry-components/registry-components.actions.ts @@ -0,0 +1,8 @@ +export class GetRegistryComponents { + static readonly type = '[RegistryComponents] Get Registry Components'; + constructor( + public registryId: string, + public page?: number, + public pageSize?: number + ) {} +} diff --git a/src/app/features/registry/store/registry-components/registry-components.model.ts b/src/app/features/registry/store/registry-components/registry-components.model.ts new file mode 100644 index 000000000..a285b7853 --- /dev/null +++ b/src/app/features/registry/store/registry-components/registry-components.model.ts @@ -0,0 +1,7 @@ +import { AsyncStateWithTotalCount } from '@shared/models'; + +import { RegistryComponentModel } from '../../models'; + +export interface RegistryComponentsStateModel { + registryComponents: AsyncStateWithTotalCount; +} diff --git a/src/app/features/registry/store/registry-components/registry-components.selectors.ts b/src/app/features/registry/store/registry-components/registry-components.selectors.ts new file mode 100644 index 000000000..1e96ff884 --- /dev/null +++ b/src/app/features/registry/store/registry-components/registry-components.selectors.ts @@ -0,0 +1,16 @@ +import { Selector } from '@ngxs/store'; + +import { RegistryComponentsStateModel } from './registry-components.model'; +import { RegistryComponentsState } from './registry-components.state'; + +export class RegistryComponentsSelectors { + @Selector([RegistryComponentsState]) + static getRegistryComponents(state: RegistryComponentsStateModel) { + return state.registryComponents.data; + } + + @Selector([RegistryComponentsState]) + static getRegistryComponentsLoading(state: RegistryComponentsStateModel) { + return state.registryComponents.isLoading; + } +} diff --git a/src/app/features/registry/store/registry-components/registry-components.state.ts b/src/app/features/registry/store/registry-components/registry-components.state.ts new file mode 100644 index 000000000..9635e1364 --- /dev/null +++ b/src/app/features/registry/store/registry-components/registry-components.state.ts @@ -0,0 +1,47 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { catchError, tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { handleSectionError } from '@core/handlers'; + +import { RegistryComponentsService } from '../../services/registry-components.service'; + +import { GetRegistryComponents } from './registry-components.actions'; +import { RegistryComponentsStateModel } from './registry-components.model'; + +const initialState: RegistryComponentsStateModel = { + registryComponents: { data: [], isLoading: false, error: null, totalCount: 0 }, +}; + +@State({ + name: 'registryComponents', + defaults: initialState, +}) +@Injectable() +export class RegistryComponentsState { + private readonly registryComponentsService = inject(RegistryComponentsService); + + @Action(GetRegistryComponents) + getRegistryComponents(ctx: StateContext, action: GetRegistryComponents) { + const state = ctx.getState(); + ctx.patchState({ + registryComponents: { ...state.registryComponents, isLoading: true, error: null }, + }); + + return this.registryComponentsService.getRegistryComponents(action.registryId, action.page, action.pageSize).pipe( + tap((response) => { + ctx.patchState({ + registryComponents: { + data: response.data, + isLoading: false, + error: null, + totalCount: response.meta.total, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'registryComponents', error)) + ); + } +} diff --git a/src/app/features/registry/store/registry-overview/registry-overview.actions.ts b/src/app/features/registry/store/registry-overview/registry-overview.actions.ts index 1fb066991..cc39787eb 100644 --- a/src/app/features/registry/store/registry-overview/registry-overview.actions.ts +++ b/src/app/features/registry/store/registry-overview/registry-overview.actions.ts @@ -3,7 +3,10 @@ import { RegistrationQuestions } from '@osf/features/registry/models'; export class GetRegistryById { static readonly type = '[Registry Overview] Get Registry By Id'; - constructor(public id: string) {} + constructor( + public id: string, + public isComponentPage?: boolean + ) {} } export class GetRegistrySubjects { diff --git a/src/app/features/registry/store/registry-overview/registry-overview.state.ts b/src/app/features/registry/store/registry-overview/registry-overview.state.ts index 9f0c47952..ce2bda31a 100644 --- a/src/app/features/registry/store/registry-overview/registry-overview.state.ts +++ b/src/app/features/registry/store/registry-overview/registry-overview.state.ts @@ -66,7 +66,7 @@ export class RegistryOverviewState { error: null, }, }); - if (registryOverview?.registrationSchemaLink && registryOverview?.questions) { + if (registryOverview?.registrationSchemaLink && registryOverview?.questions && !action.isComponentPage) { ctx.dispatch(new GetSchemaBlocks(registryOverview.registrationSchemaLink, registryOverview.questions)); } }, diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e14d85615..022e0cfb0 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2084,6 +2084,10 @@ "noLinkedProjectsOrComponentsFound": "No linked projects or components found", "linkedRegistrations": "Linked registrations", "noLinkedRegistrationsFound": "No linked registrations found" + }, + "components": { + "title": "Components", + "noComponentsFound": "No components found" } }, "truncatedText": {