diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index c0b578a4f..26e99f4a2 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -7,6 +7,7 @@ import { MyProjectsState } from '@osf/features/my-projects/store'; import { PreprintsState } from '@osf/features/preprints/store'; import { AnalyticsState } from '@osf/features/project/analytics/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; +import { RegistrationsState } from '@osf/features/project/registrations/store'; import { SettingsState } from '@osf/features/project/settings/store'; import { WikiState } from '@osf/features/project/wiki/store/wiki.state'; import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; @@ -33,5 +34,6 @@ export const STATES = [ CollectionsState, WikiState, MeetingsState, + RegistrationsState, PreprintsState, ]; diff --git a/src/app/features/project/registrations/components/registration-card/registration-card.component.html b/src/app/features/project/registrations/components/registration-card/registration-card.component.html index 1659d3c30..6ac0530eb 100644 --- a/src/app/features/project/registrations/components/registration-card/registration-card.component.html +++ b/src/app/features/project/registrations/components/registration-card/registration-card.component.html @@ -1,115 +1,108 @@ -@if (registrationData()) { -
- -
-
- @if (registrationData()?.status === 'withdrawn') { - - } +
+ +
+
+ @if (registrationData().status === RegistrationStatus.WITHDRAWN) { + + } - @if (registrationData()?.status === 'in_progress') { - - } + @if (registrationData().status === RegistrationStatus.IN_PROGRESS) { + + } -

{{ registrationData()?.title }}

+

{{ registrationData().title }}

- @if (registrationData()?.status === 'in_progress') { - - } + @if (registrationData().status === RegistrationStatus.IN_PROGRESS) { + + } - @if (registrationData()?.status === 'withdrawn') { - - } -
+ @if (registrationData().status === RegistrationStatus.WITHDRAWN) { + + } +
-
-
-
- Registration template: - {{ registrationData()?.template }} -
+
+
+
+ {{ 'project.registrations.card.registrationTemplate' | translate }} + {{ registrationData().registrationSupplement }} +
-
- Registry: - {{ registrationData()?.registry }} -
+
+ {{ 'project.registrations.card.registry' | translate }} + {{ registrationData().registry }} +
-
- Registered: - {{ registrationData()?.registeredDate }} -
+
+ {{ 'project.registrations.card.registered' | translate }} + {{ registrationData().dateRegistered }} +
-
- Last Updated: - {{ registrationData()?.lastUpdated }} -
+
+ {{ 'project.registrations.card.lastUpdated' | translate }} + {{ registrationData().dateModified }} +
-
- Contributors: - - @for (contributor of registrationData()?.contributors; track contributor.name) { - {{ contributor.name }} - @if (!$last) { - , - } +
+ {{ 'project.overview.metadata.contributors' | translate }}: + + @for (contributor of registrationData().contributors; track contributor.name) { + {{ contributor.name }} + @if (!$last) { + , } - -
+ } +
+
-
- Description: -

{{ registrationData()?.description }}

-
+
+ {{ 'project.registrations.card.description' | translate }} +

{{ registrationData().description }}

+
-
- @if (registrationData()?.status === 'draft') { - - - - } @else { - - @if (registrationData()?.status === 'in_progress') { - - } +
+ @if (registrationData().status === RegistrationStatus.DRAFT) { + + + + } @else { + + @if (registrationData().status === RegistrationStatus.IN_PROGRESS) { + } -
+ }
+
- @if (registrationData()?.status !== 'draft') { - + }
- -
-} +
+ +
diff --git a/src/app/features/project/registrations/components/registration-card/registration-card.component.scss b/src/app/features/project/registrations/components/registration-card/registration-card.component.scss index f23fff06c..468f26648 100644 --- a/src/app/features/project/registrations/components/registration-card/registration-card.component.scss +++ b/src/app/features/project/registrations/components/registration-card/registration-card.component.scss @@ -11,16 +11,3 @@ } } } -.mobile { - flex-wrap: wrap; - flex-direction: column; -} - -//.mobile-btns { -// p-button { -// width: 100%; -// button { -// width: 100%; -// } -// } -//} diff --git a/src/app/features/project/registrations/components/registration-card/registration-card.component.ts b/src/app/features/project/registrations/components/registration-card/registration-card.component.ts index df82e2366..86cc3d598 100644 --- a/src/app/features/project/registrations/components/registration-card/registration-card.component.ts +++ b/src/app/features/project/registrations/components/registration-card/registration-card.component.ts @@ -1,23 +1,21 @@ +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; import { Tag } from 'primeng/tag'; -import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; - -import { IS_XSMALL } from '@shared/utils'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; -import { RegistrationCard } from '../../models'; +import { RegistrationModel, RegistrationStatus } from '../../models'; @Component({ selector: 'osf-registration-card', - imports: [Card, Button, Tag], + imports: [Card, Button, Tag, TranslatePipe], templateUrl: './registration-card.component.html', styleUrl: './registration-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegistrationCardComponent { - protected readonly isMobile = toSignal(inject(IS_XSMALL)); - - registrationData = input(); + readonly RegistrationStatus = RegistrationStatus; + readonly registrationData = input.required(); } diff --git a/src/app/features/project/registrations/mappers/index.ts b/src/app/features/project/registrations/mappers/index.ts new file mode 100644 index 000000000..5afc367f2 --- /dev/null +++ b/src/app/features/project/registrations/mappers/index.ts @@ -0,0 +1 @@ +export { RegistrationsMapper } from './registrations.mapper'; diff --git a/src/app/features/project/registrations/mappers/registrations.mapper.ts b/src/app/features/project/registrations/mappers/registrations.mapper.ts new file mode 100644 index 000000000..932ca5774 --- /dev/null +++ b/src/app/features/project/registrations/mappers/registrations.mapper.ts @@ -0,0 +1,23 @@ +import { RegistrationModel, RegistrationsGetResponse, RegistrationStatus } from '../models'; + +export class RegistrationsMapper { + static fromResponse(response: RegistrationsGetResponse): RegistrationModel { + return { + id: response.id, + type: response.type, + title: response.attributes?.title, + dateRegistered: response.attributes?.date_registered, + dateModified: response.attributes?.date_modified, + registrationSupplement: response.attributes?.registration_supplement, + registry: '', + description: response.attributes?.description, + withdrawn: response.attributes?.withdrawn, + lastFetched: Date.now(), + status: response.attributes?.withdrawn + ? RegistrationStatus.WITHDRAWN + : response.attributes?.date_modified + ? RegistrationStatus.IN_PROGRESS + : RegistrationStatus.DRAFT, + }; + } +} diff --git a/src/app/features/project/registrations/mock-data.ts b/src/app/features/project/registrations/mock-data.ts new file mode 100644 index 000000000..c5dc64c57 --- /dev/null +++ b/src/app/features/project/registrations/mock-data.ts @@ -0,0 +1,69 @@ +import { RegistrationModel, RegistrationStatus } from './models'; + +export const draftRegistrations: unknown = [ + { + title: 'Registration Name Example', + template: 'Open-Ended Registration', + registry: 'OSF Registries', + registeredDate: '6 Feb, 2025 15:30 GMT-0500', + lastUpdated: '13 Feb, 2025 12:13 GMT-0500', + contributors: [ + { name: 'Michael Pasek', link: '' }, + { name: 'Jeremy Ginges', link: '' }, + { name: 'Crystal Shackleford', link: '' }, + { name: 'ALLON VISHKIN', link: '' }, + ], + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt', + status: 'draft', + }, + { + title: 'Registration Name Example 2', + template: 'Open-Ended Registration 2', + registry: 'OSF Registries 2', + registeredDate: '6 Feb, 2025 15:30 GMT-0500', + lastUpdated: '13 Feb, 2025 12:13 GMT-0500', + contributors: [ + { name: 'Michael Pasek', link: '' }, + { name: 'Crystal Shackleford', link: '' }, + { name: 'ALLON VISHKIN', link: '' }, + ], + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt', + status: 'draft', + }, +]; + +export const submittedRegistrations: RegistrationModel[] = [ + { + id: '1', + type: 'registration', + withdrawn: false, + title: 'Registration 1', + registrationSupplement: 'Open-Ended Registration', + registry: 'OSF Registries', + dateRegistered: '16 Jan, 2022 15:30 GMT-0500', + dateModified: '11 May, 2023 12:13 GMT-0500', + contributors: [ + { name: 'Crystal Shackleford', link: '' }, + { name: 'ALLON VISHKIN', link: '' }, + ], + description: + 'Lorem ipsum dolor sit amet elit, sed do eiusmod tempor incididunt. Lorem elit, sed do eiusmod tempor incididunt, consectetur adipiscing elit, sed do.', + status: RegistrationStatus.IN_PROGRESS, + }, + { + id: '2', + type: 'registration', + withdrawn: true, + title: 'Registration Name Example 2', + registrationSupplement: 'Open-Ended Registration 2', + registry: 'OSF Registries 2', + dateRegistered: '2 Jan, 2023 11:30 GMT-0500', + dateModified: '4 Mar, 2024 12:55 GMT-0500', + contributors: [ + { name: 'Crystal Shackleford', link: '' }, + { name: 'Michael Pasek', link: '' }, + ], + description: 'Lorem consectetur adipiscing elit, sed do eiusmod tempor incididunt.', + status: RegistrationStatus.WITHDRAWN, + }, +]; diff --git a/src/app/features/project/registrations/models/index.ts b/src/app/features/project/registrations/models/index.ts index 980fc284e..af11e9261 100644 --- a/src/app/features/project/registrations/models/index.ts +++ b/src/app/features/project/registrations/models/index.ts @@ -1 +1 @@ -export * from './registration-card.interface'; +export * from './registrations.model'; diff --git a/src/app/features/project/registrations/models/registration-card.interface.ts b/src/app/features/project/registrations/models/registration-card.interface.ts deleted file mode 100644 index f22d0a9fb..000000000 --- a/src/app/features/project/registrations/models/registration-card.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface Contributor { - name: string; - link?: string; -} - -export type RegistrationStatus = 'draft' | 'withdrawn' | 'in_progress'; - -export interface RegistrationCard { - title: string; - template: string; - registry: string; - registeredDate: string; - lastUpdated: string; - contributors: Contributor[]; - description: string; - status: RegistrationStatus; -} diff --git a/src/app/features/project/registrations/models/registrations.model.ts b/src/app/features/project/registrations/models/registrations.model.ts new file mode 100644 index 000000000..a05067cb2 --- /dev/null +++ b/src/app/features/project/registrations/models/registrations.model.ts @@ -0,0 +1,40 @@ +export interface RegistrationsGetResponse { + id: string; + type: string; + attributes: RegistrationsAttributes; +} + +interface RegistrationsAttributes { + title: string; + date_registered: string; + date_modified: string; + registration_supplement: string; + description: string; + withdrawn: boolean; +} + +export interface RegistrationModel { + id: string; + type: string; + lastFetched?: number; + title: string; + dateRegistered: string; + dateModified: string; + registrationSupplement: string; + description: string; + withdrawn: boolean; + registry: string; + status: RegistrationStatus; + contributors?: Contributor[]; +} + +export interface Contributor { + name: string; + link?: string; +} +export enum RegistrationStatus { + DRAFT = 'draft', + IN_PROGRESS = 'in_progress', + WITHDRAWN = 'withdrawn', + SUBMITTED = 'submitted', +} diff --git a/src/app/features/project/registrations/registrations.component.html b/src/app/features/project/registrations/registrations.component.html index 4d83ff2fc..e70b3036a 100644 --- a/src/app/features/project/registrations/registrations.component.html +++ b/src/app/features/project/registrations/registrations.component.html @@ -1,40 +1,21 @@ -
- -
- - @if (!isMobile()) { - - Drafts - Submitted - - } - - - @if (isMobile()) { - + +@if (isRegistrationsLoading()) { + +} @else { +
+ @if (!registrations().length) { +

{{ 'project.registrations.emptyState' | translate }}

+ } @else { +
+ @for (registration of registrations(); track registration.id) { + } - - - @for (registration of draftRegistrations; track registration.registry) { - - } - - - - - @for (registration of submittedRegistrations; track registration.registry) { - - } - - - -
-
+
+ } +
+} diff --git a/src/app/features/project/registrations/registrations.component.ts b/src/app/features/project/registrations/registrations.component.ts index 97fba51b9..07ddaffd4 100644 --- a/src/app/features/project/registrations/registrations.component.ts +++ b/src/app/features/project/registrations/registrations.component.ts @@ -1,105 +1,45 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + import { DialogService } from 'primeng/dynamicdialog'; -import { Select } from 'primeng/select'; -import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'primeng/tabs'; -import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { map, of } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; -import { SubHeaderComponent } from '@osf/shared/components'; -import { TabOption } from '@osf/shared/models'; -import { IS_MEDIUM, IS_WEB, IS_XSMALL } from '@osf/shared/utils'; +import { LoadingSpinnerComponent, SubHeaderComponent } from '@osf/shared/components'; import { RegistrationCardComponent } from './components'; -import { RegistrationCard } from './models'; +import { GetRegistrations, RegistrationsSelectors } from './store'; @Component({ selector: 'osf-registrations', - imports: [ - RegistrationCardComponent, - Select, - SubHeaderComponent, - Tab, - TabList, - TabPanels, - TabPanel, - FormsModule, - Tabs, - ], + imports: [RegistrationCardComponent, SubHeaderComponent, FormsModule, TranslatePipe, LoadingSpinnerComponent], templateUrl: './registrations.component.html', styleUrl: './registrations.component.scss', providers: [DialogService], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RegistrationsComponent { - protected readonly defaultTabValue = 0; - protected readonly isDesktop = toSignal(inject(IS_WEB)); - protected readonly isTablet = toSignal(inject(IS_MEDIUM)); - protected readonly isMobile = toSignal(inject(IS_XSMALL)); - protected readonly tabOptions: TabOption[] = [ - { label: 'Drafts', value: 0 }, - { label: 'Submitted', value: 1 }, - ]; - protected readonly selectedTab = signal(this.defaultTabValue); - draftRegistrations: RegistrationCard[] = [ - { - title: 'Registration Name Example', - template: 'Open-Ended Registration', - registry: 'OSF Registries', - registeredDate: '6 Feb, 2025 15:30 GMT-0500', - lastUpdated: '13 Feb, 2025 12:13 GMT-0500', - contributors: [ - { name: 'Michael Pasek', link: '' }, - { name: 'Jeremy Ginges', link: '' }, - { name: 'Crystal Shackleford', link: '' }, - { name: 'ALLON VISHKIN', link: '' }, - ], - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt', - status: 'draft', - }, - { - title: 'Registration Name Example 2', - template: 'Open-Ended Registration 2', - registry: 'OSF Registries 2', - registeredDate: '6 Feb, 2025 15:30 GMT-0500', - lastUpdated: '13 Feb, 2025 12:13 GMT-0500', - contributors: [ - { name: 'Michael Pasek', link: '' }, - { name: 'Crystal Shackleford', link: '' }, - { name: 'ALLON VISHKIN', link: '' }, - ], - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt', - status: 'draft', - }, - ]; +export class RegistrationsComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + + readonly projectId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); + + protected registrations = select(RegistrationsSelectors.getRegistrations); + protected isRegistrationsLoading = select(RegistrationsSelectors.isRegistrationsLoading); + + protected actions = createDispatchMap({ getRegistrations: GetRegistrations }); + + ngOnInit(): void { + this.actions.getRegistrations(this.projectId()); + } - submittedRegistrations: RegistrationCard[] = [ - { - title: 'Registration 1', - template: 'Open-Ended Registration', - registry: 'OSF Registries', - registeredDate: '16 Jan, 2022 15:30 GMT-0500', - lastUpdated: '11 May, 2023 12:13 GMT-0500', - contributors: [ - { name: 'Crystal Shackleford', link: '' }, - { name: 'ALLON VISHKIN', link: '' }, - ], - description: - 'Lorem ipsum dolor sit amet elit, sed do eiusmod tempor incididunt. Lorem elit, sed do eiusmod tempor incididunt, consectetur adipiscing elit, sed do.', - status: 'in_progress', - }, - { - title: 'Registration Name Example 2', - template: 'Open-Ended Registration 2', - registry: 'OSF Registries 2', - registeredDate: '2 Jan, 2023 11:30 GMT-0500', - lastUpdated: '4 Mar, 2024 12:55 GMT-0500', - contributors: [ - { name: 'Crystal Shackleford', link: '' }, - { name: 'Michael Pasek', link: '' }, - ], - description: 'Lorem consectetur adipiscing elit, sed do eiusmod tempor incididunt.', - status: 'withdrawn', - }, - ]; + addRegistration(): void { + //TODO: Implement the logic to add a new registration. + console.log('Add Registration clicked'); + } } diff --git a/src/app/features/project/registrations/services/index.ts b/src/app/features/project/registrations/services/index.ts new file mode 100644 index 000000000..bb84a8b58 --- /dev/null +++ b/src/app/features/project/registrations/services/index.ts @@ -0,0 +1 @@ +export { RegistrationsService } from './registrations.service'; diff --git a/src/app/features/project/registrations/services/registrations.service.ts b/src/app/features/project/registrations/services/registrations.service.ts new file mode 100644 index 000000000..8ce21ecfa --- /dev/null +++ b/src/app/features/project/registrations/services/registrations.service.ts @@ -0,0 +1,35 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponse } from '@osf/core/models'; +import { JsonApiService } from '@osf/core/services'; + +import { submittedRegistrations } from '../mock-data'; +import { RegistrationModel, RegistrationsGetResponse } from '../models'; + +import { RegistrationsMapper } from './../mappers'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class RegistrationsService { + #jsonApiService = inject(JsonApiService); + + getRegistrations(projectId: string): Observable { + const params: Record = { + embed: 'contributors', + }; + const url = `${environment.apiUrl}/nodes/${projectId}/linked_by_registrations/`; + + return this.#jsonApiService.get>(url, params).pipe( + map((response) => { + return response.data.length + ? response.data.map((registration) => RegistrationsMapper.fromResponse(registration)) + : submittedRegistrations; + }) + ); + } +} diff --git a/src/app/features/project/registrations/store/index.ts b/src/app/features/project/registrations/store/index.ts new file mode 100644 index 000000000..13975a072 --- /dev/null +++ b/src/app/features/project/registrations/store/index.ts @@ -0,0 +1,4 @@ +export * from './registrations.actions'; +export * from './registrations.model'; +export * from './registrations.selectors'; +export * from './registrations.state'; diff --git a/src/app/features/project/registrations/store/registrations.actions.ts b/src/app/features/project/registrations/store/registrations.actions.ts new file mode 100644 index 000000000..cec5e1dfd --- /dev/null +++ b/src/app/features/project/registrations/store/registrations.actions.ts @@ -0,0 +1,5 @@ +export class GetRegistrations { + static readonly type = '[Registrations] Get Registrations'; + + constructor(public projectId: string) {} +} diff --git a/src/app/features/project/registrations/store/registrations.model.ts b/src/app/features/project/registrations/store/registrations.model.ts new file mode 100644 index 000000000..545e87bb1 --- /dev/null +++ b/src/app/features/project/registrations/store/registrations.model.ts @@ -0,0 +1,7 @@ +import { AsyncStateModel } from '@osf/shared/models/store'; + +import { RegistrationModel } from '../models'; + +export interface RegistrationsStateModel { + registrations: AsyncStateModel; +} diff --git a/src/app/features/project/registrations/store/registrations.selectors.ts b/src/app/features/project/registrations/store/registrations.selectors.ts new file mode 100644 index 000000000..4bc6e5a01 --- /dev/null +++ b/src/app/features/project/registrations/store/registrations.selectors.ts @@ -0,0 +1,21 @@ +import { Selector } from '@ngxs/store'; + +import { RegistrationsStateModel } from './registrations.model'; +import { RegistrationsState } from './registrations.state'; + +export class RegistrationsSelectors { + @Selector([RegistrationsState]) + static getRegistrations(state: RegistrationsStateModel) { + return state.registrations.data; + } + + @Selector([RegistrationsState]) + static isRegistrationsLoading(state: RegistrationsStateModel) { + return state.registrations.isLoading; + } + + @Selector([RegistrationsState]) + static getRegistrationsError(state: RegistrationsStateModel) { + return state.registrations.error; + } +} diff --git a/src/app/features/project/registrations/store/registrations.state.ts b/src/app/features/project/registrations/store/registrations.state.ts new file mode 100644 index 000000000..897e3c667 --- /dev/null +++ b/src/app/features/project/registrations/store/registrations.state.ts @@ -0,0 +1,61 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { catchError, tap, throwError } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { RegistrationsService } from '../services'; + +import { GetRegistrations } from './registrations.actions'; +import { RegistrationsStateModel } from './registrations.model'; + +@State({ + name: 'registrations', + defaults: { + registrations: { + data: [], + isLoading: false, + error: '', + }, + }, +}) +@Injectable() +export class RegistrationsState { + #registrationsService = inject(RegistrationsService); + + @Action(GetRegistrations) + getRegistrations(ctx: StateContext, action: GetRegistrations) { + const state = ctx.getState(); + + ctx.patchState({ + registrations: { ...state.registrations, isLoading: true, error: null }, + }); + + return this.#registrationsService.getRegistrations(action.projectId).pipe( + tap((registrations) => { + console.log('Fetched registrations:', registrations); + const state = ctx.getState(); + ctx.setState({ + ...state, + registrations: { + data: registrations, + isLoading: false, + error: '', + }, + }); + }), + catchError((error) => this.handleError(ctx, 'registrations', error)) + ); + } + + private handleError(ctx: StateContext, section: 'registrations', error: Error) { + ctx.patchState({ + [section]: { + ...ctx.getState()[section], + isLoading: false, + error: error.message, + }, + }); + return throwError(() => error); + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e88f0c087..9d3be5267 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -484,6 +484,18 @@ "duplicate": "Duplicate", "share": "Share" } + }, + "registrations": { + "addRegistration": "Add a Registration", + "emptyState": "There have been no completed registrations of this project.", + "card": { + "registrationTemplate": "Registration template:", + "registry": "Registry:", + "registered": "Registered:", + "lastUpdated": "Last Updated:", + "description": "Description:", + "openResources": "Open Resources" + } } }, "settings": { @@ -983,5 +995,14 @@ "github": "GitHub" }, "copyright": "Copyright © 2011-2025" + }, + "shared": { + "resources": { + "data": "Data", + "analyticCode": "Analytic Code", + "materials": "Materials", + "papers": "Papers", + "supplements": "Supplements" + } } }