From 369af3bf16050f0965accc9d7f83cd19213aa45f Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Mon, 21 Jul 2025 22:40:01 +0300 Subject: [PATCH 1/9] feat(metadata): moved metadata project to reusable component --- ...ata-affiliated-institutions.component.html | 4 +- ...adata-affiliated-institutions.component.ts | 4 +- ...oject-metadata-contributors.component.html | 4 +- ...project-metadata-contributors.component.ts | 4 +- ...roject-metadata-description.component.html | 4 +- .../project-metadata-description.component.ts | 4 +- .../project-metadata-funding.component.html | 4 +- .../project-metadata-funding.component.ts | 4 +- .../project-metadata-license.component.html | 4 +- .../project-metadata-license.component.ts | 4 +- ...ct-metadata-publication-doi.component.html | 4 +- ...ject-metadata-publication-doi.component.ts | 5 +- .../project-metadata-subjects.component.html | 6 +- .../project-metadata-subjects.component.scss | 0 .../project-metadata-subjects.component.ts | 47 +---- .../metadata/helpers/cedar-metadata.helper.ts | 1 + .../metadata/project-metadata.component.html | 94 ++------- .../metadata/project-metadata.component.ts | 181 +++++++++++------- .../mappers/project-overview.mapper.ts | 4 +- .../models/project-overview.models.ts | 53 ++--- .../shared-metadata.component.html | 80 ++++++++ .../shared-metadata.component.spec.ts | 22 +++ .../shared-metadata.component.ts | 61 ++++++ 23 files changed, 358 insertions(+), 240 deletions(-) delete mode 100644 src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.scss create mode 100644 src/app/shared/components/shared-metadata/shared-metadata.component.html create mode 100644 src/app/shared/components/shared-metadata/shared-metadata.component.spec.ts create mode 100644 src/app/shared/components/shared-metadata/shared-metadata.component.ts diff --git a/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html b/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html index 9abbe7fb7..c9bc91e43 100644 --- a/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html +++ b/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html @@ -9,9 +9,9 @@

{{ 'project.overview.metadata.affiliatedInstitutions' | translate }}

> - @if (currentProject()?.affiliatedInstitutions?.length) { + @if (affiliatedInstitutions()) {
- @for (institution of currentProject()!.affiliatedInstitutions!; track institution.id) { + @for (institution of affiliatedInstitutions(); track institution.id) {
@if (institution.logo) { diff --git a/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts b/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts index d06dce556..e7b1428b6 100644 --- a/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts +++ b/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts @@ -5,7 +5,7 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { ProjectOverview } from '@osf/features/project/overview/models'; +import { ProjectAffiliatedInstitutions } from '@osf/features/project/overview/models'; @Component({ selector: 'osf-project-metadata-affiliated-institutions', @@ -16,5 +16,5 @@ import { ProjectOverview } from '@osf/features/project/overview/models'; export class ProjectMetadataAffiliatedInstitutionsComponent { openEditAffiliatedInstitutionsDialog = output(); - currentProject = input.required(); + affiliatedInstitutions = input([]); } diff --git a/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.html b/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.html index 77dbe94ba..b16a0d5dc 100644 --- a/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.html +++ b/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.html @@ -9,9 +9,9 @@

{{ 'project.overview.metadata.contributors' | translate }}

>
- @if (currentProject()?.contributors) { + @if (contributors()) {
- @for (contributor of currentProject()?.contributors; track contributor.id) { + @for (contributor of contributors(); track contributor.id) {
{{ contributor.fullName }} {{ $last ? '' : ',' }} diff --git a/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts b/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts index 4c7768c3e..d170c2bcd 100644 --- a/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts +++ b/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts @@ -5,7 +5,7 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { ProjectOverview } from '@osf/features/project/overview/models'; +import { ProjectOverviewContributor } from '@osf/features/project/overview/models'; @Component({ selector: 'osf-project-metadata-contributors', @@ -16,5 +16,5 @@ import { ProjectOverview } from '@osf/features/project/overview/models'; export class ProjectMetadataContributorsComponent { openEditContributorDialog = output(); - currentProject = input.required(); + contributors = input.required(); } diff --git a/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.html b/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.html index 81a97cfc0..680349ad8 100644 --- a/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.html +++ b/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.html @@ -9,8 +9,8 @@

{{ 'project.overview.metadata.description' | translate }}

>
- @if (currentProject()?.description) { -

{{ currentProject()?.description }}

+ @if (description()) { +

{{ description() }}

} @else {

{{ 'project.overview.metadata.noDescription' | translate }}

} diff --git a/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.ts b/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.ts index 8f5f35a17..14461e41d 100644 --- a/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.ts +++ b/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.ts @@ -5,8 +5,6 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { ProjectOverview } from '@osf/features/project/overview/models'; - @Component({ selector: 'osf-project-metadata-description', imports: [Card, Button, TranslatePipe], @@ -16,5 +14,5 @@ import { ProjectOverview } from '@osf/features/project/overview/models'; export class ProjectMetadataDescriptionComponent { openEditDescriptionDialog = output(); - currentProject = input.required(); + description = input.required(); } diff --git a/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.html b/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.html index cc66c8927..e9e0d0186 100644 --- a/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.html +++ b/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.html @@ -9,9 +9,9 @@

{{ 'project.overview.metadata.fundingSupport' | translate }}

>
- @if (currentProject()?.supplements?.length) { + @if (supplements()) {
- @for (supplement of currentProject()!.supplements!; track supplement.id) { + @for (supplement of supplements(); track supplement.id) {

{{ supplement.title }}

{{ supplement.dateCreated | date: 'MMM d, y' }}

diff --git a/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.ts b/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.ts index 95d90c996..e170c3b8f 100644 --- a/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.ts +++ b/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.ts @@ -6,7 +6,7 @@ import { Card } from 'primeng/card'; import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { ProjectOverview } from '@osf/features/project/overview/models'; +import { ProjectSupplements } from '@osf/features/project/overview/models'; @Component({ selector: 'osf-project-metadata-funding', @@ -17,5 +17,5 @@ import { ProjectOverview } from '@osf/features/project/overview/models'; export class ProjectMetadataFundingComponent { openEditFundingDialog = output(); - currentProject = input.required(); + supplements = input([]); } diff --git a/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.html b/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.html index 16df1fe0a..113231940 100644 --- a/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.html +++ b/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.html @@ -9,9 +9,9 @@

{{ 'project.overview.metadata.license' | translate }}

>
- @if (currentProject()?.license) { + @if (license()) {
-

{{ currentProject()?.license }}

+

{{ license().name }}

} @else {
diff --git a/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.ts b/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.ts index dcfb48bbc..8211b1e11 100644 --- a/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.ts +++ b/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.ts @@ -5,7 +5,7 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { ProjectOverview } from '@osf/features/project/overview/models'; +import { License } from '@shared/models'; @Component({ selector: 'osf-project-metadata-license', @@ -16,5 +16,5 @@ import { ProjectOverview } from '@osf/features/project/overview/models'; export class ProjectMetadataLicenseComponent { openEditLicenseDialog = output(); - currentProject = input.required(); + license = input({} as License); } diff --git a/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html b/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html index 1ddbe9779..89bd85459 100644 --- a/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html +++ b/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html @@ -9,9 +9,9 @@

{{ 'project.overview.metadata.publication' | translate }}

>
- @if (currentProject()?.identifiers?.length) { + @if (identifiers() && identifiers().length) {
- @for (identifier of currentProject()!.identifiers!; track identifier.id) { + @for (identifier of identifiers()!; track identifier.id) { @if (identifier.category === 'doi') {

{{ identifier.value }}

} diff --git a/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts b/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts index f3e4f87dd..a93956ea2 100644 --- a/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts +++ b/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts @@ -5,16 +5,17 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { ProjectOverview } from '@osf/features/project/overview/models'; +import { ProjectIdentifiers } from '@osf/features/project/overview/models'; @Component({ selector: 'osf-project-metadata-publication-doi', imports: [Button, Card, TranslatePipe], templateUrl: './project-metadata-publication-doi.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) export class ProjectMetadataPublicationDoiComponent { openEditPublicationDoiDialog = output(); - currentProject = input.required(); + identifiers = input([]); } diff --git a/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.html b/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.html index 991d700d1..5c6529cfc 100644 --- a/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.html +++ b/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.html @@ -2,8 +2,8 @@ diff --git a/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.scss b/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.ts b/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.ts index 10a3a81e6..d4fb3b804 100644 --- a/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.ts +++ b/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.ts @@ -1,18 +1,8 @@ -import { createDispatchMap, select } from '@ngxs/store'; - import { Card } from 'primeng/card'; -import { ChangeDetectionStrategy, Component, input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { ResourceType } from '@osf/shared/enums'; import { SubjectModel } from '@osf/shared/models'; -import { - FetchChildrenSubjects, - FetchSelectedSubjects, - FetchSubjects, - SubjectsSelectors, - UpdateResourceSubjects, -} from '@osf/shared/stores'; import { SubjectsComponent } from '@shared/components'; @Component({ @@ -20,34 +10,13 @@ import { SubjectsComponent } from '@shared/components'; imports: [SubjectsComponent, Card], templateUrl: './project-metadata-subjects.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) -export class ProjectMetadataSubjectsComponent implements OnInit { - projectId = input(); - - protected selectedSubjects = select(SubjectsSelectors.getSelectedSubjects); - protected isSubjectsUpdating = select(SubjectsSelectors.areSelectedSubjectsLoading); - - protected actions = createDispatchMap({ - fetchSubjects: FetchSubjects, - fetchSelectedSubjects: FetchSelectedSubjects, - fetchChildrenSubjects: FetchChildrenSubjects, - updateResourceSubjects: UpdateResourceSubjects, - }); - - ngOnInit(): void { - this.actions.fetchSubjects(ResourceType.Project); - this.actions.fetchSelectedSubjects(this.projectId()!, ResourceType.Project); - } - - getSubjectChildren(parentId: string) { - this.actions.fetchChildrenSubjects(parentId); - } - - searchSubjects(search: string) { - this.actions.fetchSubjects(ResourceType.Project, this.projectId()!, search); - } +export class ProjectMetadataSubjectsComponent { + selectedSubjects = input.required(); + isSubjectsUpdating = input.required(); - updateSelectedSubjects(subjects: SubjectModel[]) { - this.actions.updateResourceSubjects(this.projectId()!, ResourceType.Project, subjects); - } + getSubjectChildren = output(); + searchSubjects = output(); + updateSelectedSubjects = output(); } diff --git a/src/app/features/project/metadata/helpers/cedar-metadata.helper.ts b/src/app/features/project/metadata/helpers/cedar-metadata.helper.ts index f49a4e5c4..9ee0ecc35 100644 --- a/src/app/features/project/metadata/helpers/cedar-metadata.helper.ts +++ b/src/app/features/project/metadata/helpers/cedar-metadata.helper.ts @@ -4,6 +4,7 @@ export class CedarMetadataHelper { return items.map((item) => { const safeItem = typeof item === 'object' && item !== null ? (item as Record) : {}; + return { '@id': safeItem['@id'] ?? '', '@type': safeItem['@type'] ?? '', diff --git a/src/app/features/project/metadata/project-metadata.component.html b/src/app/features/project/metadata/project-metadata.component.html index b477e407a..080ba1e4c 100644 --- a/src/app/features/project/metadata/project-metadata.component.html +++ b/src/app/features/project/metadata/project-metadata.component.html @@ -24,83 +24,23 @@ @for (tab of tabs(); track $index) { @if (tab.type === 'project') { -
-
- -
-
-

- {{ 'project.overview.metadata.dateCreated' | translate }} -

- -

- {{ currentProject()?.dateCreated | date: 'MMM d, y, h:mm a' }} -

-
- -
-

- {{ 'project.overview.metadata.dateUpdated' | translate }} -

- -

- {{ currentProject()?.dateModified | date: 'MMM d, y, h:mm a' }} -

-
-
-
- - - - - - - - - - -
- -
- - - - - -
-

{{ 'project.overview.metadata.tags' | translate }}

- - -
-
- - -
-
+ } @else {
@if (selectedCedarTemplate() && selectedCedarRecord()) { diff --git a/src/app/features/project/metadata/project-metadata.component.ts b/src/app/features/project/metadata/project-metadata.component.ts index e603d6e5e..32b64e672 100644 --- a/src/app/features/project/metadata/project-metadata.component.ts +++ b/src/app/features/project/metadata/project-metadata.component.ts @@ -53,8 +53,18 @@ import { UpdateProjectDetails, } from '@osf/features/project/metadata/store'; import { ResourceType } from '@osf/shared/enums'; -import { ContributorsSelectors, GetAllContributors } from '@osf/shared/stores'; +import { + ContributorsSelectors, + FetchChildrenSubjects, + FetchSelectedSubjects, + FetchSubjects, + GetAllContributors, + SubjectsSelectors, + UpdateResourceSubjects, +} from '@osf/shared/stores'; import { LoadingSpinnerComponent, SubHeaderComponent, TagsInputComponent } from '@shared/components'; +import { SharedMetadataComponent } from '@shared/components/shared-metadata/shared-metadata.component'; +import { SubjectModel } from '@shared/models'; import { CustomConfirmationService, LoaderService, ToastService } from '@shared/services'; @Component({ @@ -80,11 +90,13 @@ import { CustomConfirmationService, LoaderService, ToastService } from '@shared/ Tabs, LoadingSpinnerComponent, TagsInputComponent, + SharedMetadataComponent, ], templateUrl: './project-metadata.component.html', styleUrl: './project-metadata.component.scss', providers: [DialogService], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) export class ProjectMetadataComponent implements OnInit { private readonly route = inject(ActivatedRoute); @@ -96,6 +108,8 @@ export class ProjectMetadataComponent implements OnInit { private readonly loaderService = inject(LoaderService); private readonly customConfirmationService = inject(CustomConfirmationService); + private projectId = ''; + tabs = signal<{ id: string; label: string; type: 'project' | 'cedar' }[]>([]); protected readonly selectedTab = signal('project'); @@ -115,6 +129,11 @@ export class ProjectMetadataComponent implements OnInit { getCedarTemplates: GetCedarMetadataTemplates, createCedarRecord: CreateCedarMetadataRecord, updateCedarRecord: UpdateCedarMetadataRecord, + + fetchSubjects: FetchSubjects, + fetchSelectedSubjects: FetchSelectedSubjects, + fetchChildrenSubjects: FetchChildrenSubjects, + updateResourceSubjects: UpdateResourceSubjects, }); protected currentProject = select(ProjectMetadataSelectors.getProject); @@ -126,6 +145,8 @@ export class ProjectMetadataComponent implements OnInit { protected currentUser = select(UserSelectors.getCurrentUser); protected cedarRecords = select(ProjectMetadataSelectors.getCedarRecords); protected cedarTemplates = select(ProjectMetadataSelectors.getCedarTemplates); + protected selectedSubjects = select(SubjectsSelectors.getSelectedSubjects); + protected isSubjectsUpdating = select(SubjectsSelectors.areSelectedSubjectsLoading); constructor() { effect(() => { @@ -135,11 +156,12 @@ export class ProjectMetadataComponent implements OnInit { const baseTabs = [{ id: 'project', label: project.title, type: 'project' as const }]; - const cedarTabs = records.map((record) => ({ - id: record.id || '', - label: record.embeds?.template?.data?.attributes?.schema_name || `Record ${record.id}`, - type: 'cedar' as const, - })); + const cedarTabs = + records?.map((record) => ({ + id: record.id || '', + label: record.embeds?.template?.data?.attributes?.schema_name || `Record ${record.id}`, + type: 'cedar' as const, + })) || []; this.tabs.set([...baseTabs, ...cedarTabs]); @@ -163,14 +185,16 @@ export class ProjectMetadataComponent implements OnInit { } ngOnInit(): void { - const projectId = this.route.parent?.parent?.snapshot.params['id']; + this.projectId = this.route.parent?.parent?.snapshot.params['id']; - if (projectId) { - this.actions.getProject(projectId); - this.actions.getCustomItemMetadata(projectId); - this.actions.getContributors(projectId, ResourceType.Project); - this.actions.getCedarRecords(projectId); + if (this.projectId) { + this.actions.getProject(this.projectId); + this.actions.getCustomItemMetadata(this.projectId); + this.actions.getContributors(this.projectId, ResourceType.Project); + this.actions.getCedarRecords(this.projectId); this.actions.getCedarTemplates(); + this.actions.fetchSubjects(ResourceType.Project); + this.actions.fetchSelectedSubjects(this.projectId!, ResourceType.Project); const user = this.currentUser(); if (user?.id) { @@ -179,27 +203,6 @@ export class ProjectMetadataComponent implements OnInit { } } - private handleRouteBasedTabSelection(): void { - const recordId = this.route.snapshot.paramMap.get('recordId'); - - if (!recordId) { - this.selectedTab.set('project'); - this.selectedCedarRecord.set(null); - this.selectedCedarTemplate.set(null); - return; - } - - const tab = this.tabs().find((tab) => tab.id === recordId); - - if (tab) { - this.selectedTab.set('project'); - - if (tab.type === 'cedar') { - this.loadCedarRecord(tab.id); - } - } - } - onTagsChanged(tags: string[]): void { const projectId = this.currentProject()?.id; if (projectId) { @@ -234,13 +237,6 @@ export class ProjectMetadataComponent implements OnInit { }); } - private refreshContributorsData(): void { - const projectId = this.route.parent?.parent?.snapshot.params['id']; - if (projectId) { - this.actions.getContributors(projectId, ResourceType.Project); - } - } - openEditDescriptionDialog(): void { const dialogRef = this.dialogService.open(DescriptionDialogComponent, { header: this.translateService.instant('project.metadata.description.dialog.header'), @@ -459,18 +455,20 @@ export class ProjectMetadataComponent implements OnInit { } onTabChange(tabId: string | number): void { - const tab = this.tabs().find((x) => x.id === tabId); + const tab = this.tabs().find((x) => x.id === tabId.toString()); if (!tab) { return; } + this.selectedTab.set(tab.id); + if (tab.type === 'cedar') { this.loadCedarRecord(tab.id); const currentRecordId = this.route.snapshot.paramMap.get('recordId'); if (currentRecordId !== tab.id) { - this.router.navigate(['..', tab.id], { relativeTo: this.route }); + this.router.navigate(['metadata', tab.id], { relativeTo: this.route.parent?.parent }); } } else { this.selectedCedarRecord.set(null); @@ -478,38 +476,11 @@ export class ProjectMetadataComponent implements OnInit { const currentRecordId = this.route.snapshot.paramMap.get('recordId'); if (currentRecordId) { - this.router.navigate(['.'], { relativeTo: this.route }); + this.router.navigate(['metadata'], { relativeTo: this.route.parent?.parent }); } } } - private loadCedarRecord(recordId: string): void { - const records = this.cedarRecords(); - const templates = this.cedarTemplates(); - - const record = records.find((r) => r.id === recordId); - if (!record) { - return; - } - - this.selectedCedarRecord.set(record); - this.cedarFormReadonly.set(true); - - const templateId = record.relationships?.template?.data?.id; - if (templateId && templates?.data) { - const template = templates.data.find((t) => t.id === templateId); - if (template) { - this.selectedCedarTemplate.set(template); - } else { - this.selectedCedarTemplate.set(null); - this.actions.getCedarTemplates(); - } - } else { - this.selectedCedarTemplate.set(null); - this.actions.getCedarTemplates(); - } - } - onCedarFormEdit(): void { this.cedarFormReadonly.set(false); } @@ -561,4 +532,74 @@ export class ProjectMetadataComponent implements OnInit { onCedarFormChangeTemplate(): void { this.router.navigate(['add'], { relativeTo: this.route }); } + + getSubjectChildren(parentId: string) { + this.actions.fetchChildrenSubjects(parentId); + } + + searchSubjects(search: string) { + this.actions.fetchSubjects(ResourceType.Project, this.projectId, search); + } + + updateSelectedSubjects(subjects: SubjectModel[]) { + this.actions.updateResourceSubjects(this.projectId, ResourceType.Project, subjects); + } + + private refreshContributorsData(): void { + if (this.projectId) { + this.actions.getContributors(this.projectId, ResourceType.Project); + } + } + + private loadCedarRecord(recordId: string): void { + const records = this.cedarRecords(); + const templates = this.cedarTemplates(); + + if (!records) { + return; + } + + const record = records.find((r) => r.id === recordId); + if (!record) { + return; + } + + this.selectedCedarRecord.set(record); + this.cedarFormReadonly.set(true); + + const templateId = record.relationships?.template?.data?.id; + if (templateId && templates?.data) { + const template = templates.data.find((t) => t.id === templateId); + if (template) { + this.selectedCedarTemplate.set(template); + } else { + this.selectedCedarTemplate.set(null); + this.actions.getCedarTemplates(); + } + } else { + this.selectedCedarTemplate.set(null); + this.actions.getCedarTemplates(); + } + } + + private handleRouteBasedTabSelection(): void { + const recordId = this.route.snapshot.paramMap.get('recordId'); + + if (!recordId) { + this.selectedTab.set('project'); + this.selectedCedarRecord.set(null); + this.selectedCedarTemplate.set(null); + return; + } + + const tab = this.tabs().find((tab) => tab.id === recordId); + + if (tab) { + this.selectedTab.set(tab.id); + + if (tab.type === 'cedar') { + this.loadCedarRecord(tab.id); + } + } + } } diff --git a/src/app/features/project/overview/mappers/project-overview.mapper.ts b/src/app/features/project/overview/mappers/project-overview.mapper.ts index b840f42b1..0ee87f0cc 100644 --- a/src/app/features/project/overview/mappers/project-overview.mapper.ts +++ b/src/app/features/project/overview/mappers/project-overview.mapper.ts @@ -1,3 +1,5 @@ +import { License } from '@shared/models'; + import { ComponentGetResponseJsoApi, ComponentOverview, @@ -28,7 +30,7 @@ export class ProjectOverviewMapper { year: response.attributes.node_license.year, } : undefined, - license: response.embeds.license?.data?.attributes, + license: response.embeds.license?.data?.attributes as unknown as License, doi: response.attributes.doi, publicationDoi: response.attributes.publication_doi, analyticsKey: response.attributes.analytics_key, diff --git a/src/app/features/project/overview/models/project-overview.models.ts b/src/app/features/project/overview/models/project-overview.models.ts index ce507c7ac..19f8c6f25 100644 --- a/src/app/features/project/overview/models/project-overview.models.ts +++ b/src/app/features/project/overview/models/project-overview.models.ts @@ -1,4 +1,5 @@ import { JsonApiResponse } from '@osf/core/models'; +import { License } from '@shared/models'; export interface ProjectOverviewContributor { familyName: string; @@ -67,11 +68,7 @@ export interface ProjectOverview { copyrightHolders: string[]; year: string; }; - license?: { - name: string; - text: string; - url: string; - }; + license?: License; doi?: string; publicationDoi?: string; storage?: { @@ -80,19 +77,8 @@ export interface ProjectOverview { storageLimitStatus: string; storageUsage: string; }; - identifiers?: { - id: string; - type: string; - category: string; - value: string; - }[]; - supplements?: { - id: string; - type: string; - title: string; - dateCreated: string; - url: string; - }[]; + identifiers?: ProjectIdentifiers[]; + supplements?: ProjectSupplements[]; analyticsKey: string; currentUserCanComment: boolean; currentUserPermissions: string[]; @@ -105,13 +91,7 @@ export interface ProjectOverview { id: string; type: string; }; - affiliatedInstitutions?: { - id: string; - type: string; - name: string; - description: string; - logo: string; - }[]; + affiliatedInstitutions?: ProjectAffiliatedInstitutions[]; forksCount: number; viewOnlyLinksCount: number; links: { @@ -276,3 +256,26 @@ export interface ProjectOverviewGetResponseJsoApi { export interface ProjectOverviewResponseJsonApi extends JsonApiResponse { data: ProjectOverviewGetResponseJsoApi; } + +export interface ProjectIdentifiers { + id: string; + type: string; + category: string; + value: string; +} + +export interface ProjectAffiliatedInstitutions { + id: string; + type: string; + name: string; + description: string; + logo: string; +} + +export interface ProjectSupplements { + id: string; + type: string; + title: string; + dateCreated: string; + url: string; +} diff --git a/src/app/shared/components/shared-metadata/shared-metadata.component.html b/src/app/shared/components/shared-metadata/shared-metadata.component.html new file mode 100644 index 000000000..809714297 --- /dev/null +++ b/src/app/shared/components/shared-metadata/shared-metadata.component.html @@ -0,0 +1,80 @@ +
+
+ +
+
+

+ {{ 'project.overview.metadata.dateCreated' | translate }} +

+ +

+ {{ currentInstance().dateCreated | date: 'MMM d, y, h:mm a' }} +

+
+ +
+

+ {{ 'project.overview.metadata.dateUpdated' | translate }} +

+ +

+ {{ currentInstance().dateModified | date: 'MMM d, y, h:mm a' }} +

+
+
+
+ + + + + + + + + + +
+ +
+ + + + + +
+

{{ 'project.overview.metadata.tags' | translate }}

+ + +
+
+ + +
+
diff --git a/src/app/shared/components/shared-metadata/shared-metadata.component.spec.ts b/src/app/shared/components/shared-metadata/shared-metadata.component.spec.ts new file mode 100644 index 000000000..c86786f42 --- /dev/null +++ b/src/app/shared/components/shared-metadata/shared-metadata.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SharedMetadataComponent } from './shared-metadata.component'; + +describe('SharedMetadataComponent', () => { + let component: SharedMetadataComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SharedMetadataComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SharedMetadataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/shared-metadata/shared-metadata.component.ts b/src/app/shared/components/shared-metadata/shared-metadata.component.ts new file mode 100644 index 000000000..9ea29c0a6 --- /dev/null +++ b/src/app/shared/components/shared-metadata/shared-metadata.component.ts @@ -0,0 +1,61 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Card } from 'primeng/card'; + +import { DatePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; + +import { + ProjectMetadataAffiliatedInstitutionsComponent, + ProjectMetadataContributorsComponent, + ProjectMetadataDescriptionComponent, + ProjectMetadataFundingComponent, + ProjectMetadataLicenseComponent, + ProjectMetadataPublicationDoiComponent, + ProjectMetadataResourceInformationComponent, + ProjectMetadataSubjectsComponent, +} from '@osf/features/project/metadata/components'; +import { CustomItemMetadataRecord } from '@osf/features/project/metadata/models'; +import { ProjectOverview } from '@osf/features/project/overview/models'; +import { TagsInputComponent } from '@shared/components'; +import { SubjectModel } from '@shared/models'; + +@Component({ + selector: 'osf-shared-metadata', + imports: [ + ProjectMetadataSubjectsComponent, + TranslatePipe, + TagsInputComponent, + ProjectMetadataPublicationDoiComponent, + ProjectMetadataLicenseComponent, + ProjectMetadataAffiliatedInstitutionsComponent, + ProjectMetadataFundingComponent, + ProjectMetadataResourceInformationComponent, + ProjectMetadataDescriptionComponent, + ProjectMetadataContributorsComponent, + DatePipe, + Card, + ], + templateUrl: './shared-metadata.component.html', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SharedMetadataComponent { + currentInstance = input.required(); + customItemMetadata = input.required(); + selectedSubjects = input.required(); + isSubjectsUpdating = input.required(); + + openEditContributorDialog = output(); + openEditDescriptionDialog = output(); + openEditResourceInformationDialog = output(); + openEditFundingDialog = output(); + openEditAffiliatedInstitutionsDialog = output(); + openEditLicenseDialog = output(); + handleEditDoi = output(); + tagsChanged = output(); + + getSubjectChildren = output(); + searchSubjects = output(); + updateSelectedSubjects = output(); +} From ab3637992e32f7e85199c13b10cfa9bf16562855 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Mon, 21 Jul 2025 22:47:06 +0300 Subject: [PATCH 2/9] feat(registry-metadata): added page --- src/app/features/registry/pages/index.ts | 1 + .../registry-metadata.component.html | 1 + .../registry-metadata.component.scss | 0 .../registry-metadata.component.spec.ts | 22 +++++++++++++++++++ .../registry-metadata.component.ts | 10 +++++++++ src/app/features/registry/registry.routes.ts | 5 +++++ 6 files changed, 39 insertions(+) create mode 100644 src/app/features/registry/pages/registry-metadata/registry-metadata.component.html create mode 100644 src/app/features/registry/pages/registry-metadata/registry-metadata.component.scss create mode 100644 src/app/features/registry/pages/registry-metadata/registry-metadata.component.spec.ts create mode 100644 src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts diff --git a/src/app/features/registry/pages/index.ts b/src/app/features/registry/pages/index.ts index de079877f..b6f51cf0b 100644 --- a/src/app/features/registry/pages/index.ts +++ b/src/app/features/registry/pages/index.ts @@ -1,2 +1,3 @@ +export * from './registry-metadata/registry-metadata.component'; export * from '@osf/features/registry/pages/registry-files/registry-files.component'; export * from '@osf/features/registry/pages/registry-overview/registry-overview.component'; diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html new file mode 100644 index 000000000..a94f7cb29 --- /dev/null +++ b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html @@ -0,0 +1 @@ +

registry-metadata works!

diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.scss b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.spec.ts b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.spec.ts new file mode 100644 index 000000000..6d4445e0c --- /dev/null +++ b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegistryMetadataComponent } from './registry-metadata.component'; + +describe('RegistryMetadataComponent', () => { + let component: RegistryMetadataComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegistryMetadataComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegistryMetadataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts new file mode 100644 index 000000000..f3200b694 --- /dev/null +++ b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-registry-metadata', + imports: [], + templateUrl: './registry-metadata.component.html', + styleUrl: './registry-metadata.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegistryMetadataComponent {} diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index fef5eec8f..e88ab2723 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -27,6 +27,11 @@ export const registryRoutes: Routes = [ loadComponent: () => import('./pages/registry-overview/registry-overview.component').then((c) => c.RegistryOverviewComponent), }, + { + path: 'metadata', + loadComponent: () => + import('./pages/registry-metadata/registry-metadata.component').then((c) => c.RegistryMetadataComponent), + }, { path: 'contributors', loadComponent: () => From ba89f4080a5e8aa257008631b885931b8aba1af5 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Tue, 22 Jul 2025 00:50:32 +0300 Subject: [PATCH 3/9] feat(registry-metadata): added structure --- .../metadata/project-metadata.component.ts | 27 +- src/app/features/registry/mappers/index.ts | 1 + .../mappers/registry-metadata.mapper.ts | 92 +++++ src/app/features/registry/models/index.ts | 1 + .../models/registry-metadata.models.ts | 195 ++++++++++ .../registry-metadata.component.html | 50 ++- .../registry-metadata.component.ts | 209 ++++++++++- src/app/features/registry/registry.routes.ts | 2 + src/app/features/registry/services/index.ts | 1 + .../services/registry-metadata.service.ts | 118 ++++++ .../registry/store/registry-metadata/index.ts | 4 + .../registry-metadata.actions.ts | 59 +++ .../registry-metadata.model.ts | 19 + .../registry-metadata.selectors.ts | 71 ++++ .../registry-metadata.state.ts | 346 ++++++++++++++++++ src/app/shared/services/subjects.service.ts | 13 +- .../stores/subjects/subjects.actions.ts | 3 +- .../shared/stores/subjects/subjects.state.ts | 7 +- 18 files changed, 1184 insertions(+), 34 deletions(-) create mode 100644 src/app/features/registry/mappers/registry-metadata.mapper.ts create mode 100644 src/app/features/registry/models/registry-metadata.models.ts create mode 100644 src/app/features/registry/services/registry-metadata.service.ts create mode 100644 src/app/features/registry/store/registry-metadata/index.ts create mode 100644 src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts create mode 100644 src/app/features/registry/store/registry-metadata/registry-metadata.model.ts create mode 100644 src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts create mode 100644 src/app/features/registry/store/registry-metadata/registry-metadata.state.ts diff --git a/src/app/features/project/metadata/project-metadata.component.ts b/src/app/features/project/metadata/project-metadata.component.ts index 32b64e672..130d5dd1d 100644 --- a/src/app/features/project/metadata/project-metadata.component.ts +++ b/src/app/features/project/metadata/project-metadata.component.ts @@ -2,29 +2,17 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { Card } from 'primeng/card'; import { DialogService } from 'primeng/dynamicdialog'; import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'primeng/tabs'; import { EMPTY, filter, switchMap } from 'rxjs'; -import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; import { UserSelectors } from '@osf/core/store/user'; -import { - CedarTemplateFormComponent, - ProjectMetadataAffiliatedInstitutionsComponent, - ProjectMetadataContributorsComponent, - ProjectMetadataDescriptionComponent, - ProjectMetadataFundingComponent, - ProjectMetadataLicenseComponent, - ProjectMetadataPublicationDoiComponent, - ProjectMetadataResourceInformationComponent, - ProjectMetadataSubjectsComponent, -} from '@osf/features/project/metadata/components'; +import { CedarTemplateFormComponent } from '@osf/features/project/metadata/components'; import { AffiliatedInstitutionsDialogComponent, ContributorsDialogComponent, @@ -62,7 +50,7 @@ import { SubjectsSelectors, UpdateResourceSubjects, } from '@osf/shared/stores'; -import { LoadingSpinnerComponent, SubHeaderComponent, TagsInputComponent } from '@shared/components'; +import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'; import { SharedMetadataComponent } from '@shared/components/shared-metadata/shared-metadata.component'; import { SubjectModel } from '@shared/models'; import { CustomConfirmationService, LoaderService, ToastService } from '@shared/services'; @@ -71,16 +59,6 @@ import { CustomConfirmationService, LoaderService, ToastService } from '@shared/ selector: 'osf-project-metadata', imports: [ SubHeaderComponent, - Card, - DatePipe, - ProjectMetadataContributorsComponent, - ProjectMetadataDescriptionComponent, - ProjectMetadataResourceInformationComponent, - ProjectMetadataLicenseComponent, - ProjectMetadataPublicationDoiComponent, - ProjectMetadataSubjectsComponent, - ProjectMetadataFundingComponent, - ProjectMetadataAffiliatedInstitutionsComponent, CedarTemplateFormComponent, TranslatePipe, Tab, @@ -89,7 +67,6 @@ import { CustomConfirmationService, LoaderService, ToastService } from '@shared/ TabPanels, Tabs, LoadingSpinnerComponent, - TagsInputComponent, SharedMetadataComponent, ], templateUrl: './project-metadata.component.html', diff --git a/src/app/features/registry/mappers/index.ts b/src/app/features/registry/mappers/index.ts index 2d7d7308c..629991406 100644 --- a/src/app/features/registry/mappers/index.ts +++ b/src/app/features/registry/mappers/index.ts @@ -1,2 +1,3 @@ +export * from './registry-metadata.mapper'; export * from './registry-overview.mapper'; export * from './registry-schema-block.mapper'; diff --git a/src/app/features/registry/mappers/registry-metadata.mapper.ts b/src/app/features/registry/mappers/registry-metadata.mapper.ts new file mode 100644 index 000000000..5638443c9 --- /dev/null +++ b/src/app/features/registry/mappers/registry-metadata.mapper.ts @@ -0,0 +1,92 @@ +import { ProjectOverviewContributor } from '@osf/features/project/overview/models'; +import { RegistryStatus, RevisionReviewStates } from '@shared/enums'; + +import { + BibliographicContributor, + BibliographicContributorData, + BibliographicContributorsJsonApi, +} from '../models/registry-metadata.models'; +import { RegistryOverview } from '../models/registry-overview.models'; + +export class RegistryMetadataMapper { + static fromMetadataApiResponse(response: Record): RegistryOverview { + const attributes = response['attributes'] as Record; + const embeds = response['embeds'] as Record; + + const contributors: ProjectOverviewContributor[] = []; + if (embeds && embeds['contributors']) { + const contributorsData = (embeds['contributors'] as Record)['data'] as Record[]; + contributorsData?.forEach((contributor) => { + const contributorEmbeds = contributor['embeds'] as Record; + if (contributorEmbeds && contributorEmbeds['users']) { + const userData = (contributorEmbeds['users'] as Record)['data'] as Record; + const userAttributes = userData['attributes'] as Record; + + contributors.push({ + id: userData['id'] as string, + type: userData['type'] as string, + fullName: userAttributes['full_name'] as string, + givenName: userAttributes['given_name'] as string, + familyName: userAttributes['family_name'] as string, + middleName: (userAttributes['middle_name'] as string) || '', + }); + } + }); + } + + return { + id: response['id'] as string, + type: (response['type'] as string) || 'registrations', + title: attributes['title'] as string, + description: attributes['description'] as string, + category: attributes['category'] as string, + tags: (attributes['tags'] as string[]) || [], + dateCreated: attributes['date_created'] as string, + dateModified: attributes['date_modified'] as string, + dateRegistered: attributes['date_registered'] as string, + registrationType: (attributes['registration_type'] as string) || '', + doi: (attributes['doi'] as string) || '', + isPublic: attributes['public'] as boolean, + isFork: attributes['fork'] as boolean, + accessRequestsEnabled: attributes['access_requests_enabled'] as boolean, + wikiEnabled: attributes['wiki_enabled'] as boolean, + currentUserCanComment: attributes['current_user_can_comment'] as boolean, + currentUserPermissions: (attributes['current_user_permissions'] as string[]) || [], + currentUserIsContributor: attributes['current_user_is_contributor'] as boolean, + currentUserIsContributorOrGroupMember: attributes['current_user_is_contributor_or_group_member'] as boolean, + analyticsKey: (attributes['analytics_key'] as string) || '', + contributors: contributors, + subjects: Array.isArray(attributes['subjects']) ? attributes['subjects'].flat() : attributes['subjects'], + forksCount: 0, + citation: '', + hasData: false, + hasAnalyticCode: false, + hasMaterials: false, + hasPapers: false, + hasSupplements: false, + questions: {}, + registrationSchemaLink: '', + associatedProjectId: '', + schemaResponses: [], + status: attributes['status'] as RegistryStatus, + revisionStatus: attributes['revision_status'] as RevisionReviewStates, + links: { + files: '', + }, + } as RegistryOverview; + } + + static mapBibliographicContributors(response: BibliographicContributorsJsonApi): BibliographicContributor[] { + return response.data.map((contributor: BibliographicContributorData) => ({ + id: contributor.id, + index: contributor.attributes.index, + user: { + id: contributor.embeds.users.data.id, + fullName: contributor.embeds.users.data.attributes.full_name, + profileImage: contributor.embeds.users.data.links.profile_image, + htmlUrl: contributor.embeds.users.data.links.html, + iri: contributor.embeds.users.data.links.iri, + }, + })); + } +} diff --git a/src/app/features/registry/models/index.ts b/src/app/features/registry/models/index.ts index 05202a94a..1bb999279 100644 --- a/src/app/features/registry/models/index.ts +++ b/src/app/features/registry/models/index.ts @@ -3,6 +3,7 @@ export * from './get-registry-overview-json-api.model'; export * from './get-registry-schema-block-json-api.model'; export * from './get-resource-subjects-json-api.model'; export * from './registry-institution.model'; +export * from './registry-metadata.models'; export * from './registry-overview.models'; export * from './registry-schema-block.model'; export * from './registry-subject.model'; diff --git a/src/app/features/registry/models/registry-metadata.models.ts b/src/app/features/registry/models/registry-metadata.models.ts new file mode 100644 index 000000000..0a5e7d1bc --- /dev/null +++ b/src/app/features/registry/models/registry-metadata.models.ts @@ -0,0 +1,195 @@ +export interface BibliographicContributorsJsonApi { + data: BibliographicContributorData[]; + meta: { + total: number; + per_page: number; + total_bibliographic: number; + version: string; + }; + links: { + self: string; + first: string | null; + last: string | null; + prev: string | null; + next: string | null; + }; +} + +export interface BibliographicContributorData { + id: string; + type: string; + attributes: { + index: number; + }; + relationships: { + users: { + links: { + related: { + href: string; + meta: Record; + }; + }; + data: { + id: string; + type: string; + }; + }; + }; + embeds: { + users: { + data: { + id: string; + type: string; + attributes: { + full_name: string; + }; + links: { + html: string; + profile_image: string; + self: string; + iri: string; + }; + }; + }; + }; + links: { + self: string; + }; +} + +export interface BibliographicContributor { + id: string; + index: number; + user: { + id: string; + fullName: string; + profileImage: string; + htmlUrl: string; + iri: string; + }; +} + +export interface CustomItemMetadataRecord { + language?: string; + resource_type_general?: string; + funders?: { + funder_name: string; + funder_identifier?: string; + funder_identifier_type?: string; + award_number?: string; + award_uri?: string; + award_title?: string; + }[]; +} + +export interface CustomItemMetadataResponse { + data: { + id: string; + type: string; + attributes: CustomItemMetadataRecord; + }; +} + +export interface CrossRefFunder { + id: number; + location: string; + name: string; + alt_names: string[]; + uri: string; + replaces: number[]; + replaced_by: number | null; + tokens: string[]; +} + +export interface CrossRefFundersResponse { + status: string; + message_type: string; + message_version: string; + message: { + facets: Record; + total_results: number; + items: CrossRefFunder[]; + items_per_page: number; + }; +} + +export interface UserInstitution { + id: string; + type: string; + attributes: { + name: string; + description: string; + logo_path: string | null; + }; +} + +export interface UserInstitutionsResponse { + data: UserInstitution[]; + meta: { + total: number; + per_page: number; + version: string; + }; + links: { + self: string; + first: string | null; + last: string | null; + prev: string | null; + next: string | null; + }; +} + +export interface RegistryMetadata { + tags?: string[]; + description?: string; + category?: string; + doi?: boolean; + node_license?: { + id: string; + type: string; + }; + institutions?: { + id: string; + type: string; + }[]; +} + +export interface RegistrySubjectsJsonApi { + data: RegistrySubjectData[]; + links: { + first: string | null; + last: string | null; + prev: string | null; + next: string | null; + meta: { + total: number; + per_page: number; + }; + }; + meta: { + version: string; + }; +} + +export interface RegistrySubjectData { + id: string; + type: string; + attributes: { + text: string; + taxonomy_name: string; + }; + relationships: { + children: { + links: { + related: { + href: string; + meta: Record; + }; + }; + }; + }; + links: { + self: string; + iri: string; + }; +} diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html index a94f7cb29..2c9fb9c88 100644 --- a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html +++ b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html @@ -1 +1,49 @@ -

registry-metadata works!

+
+ + @if (!tabs().length) { +
+ +
+ } + + @if (tabs().length) { + + + @for (item of tabs(); track $index) { + {{ item.label | translate }} + } + + + + @for (tab of tabs(); track $index) { + + @if (tab.type === 'registry') { + + } + + } + + + } +
diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts index f3200b694..0ca218bf8 100644 --- a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts +++ b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts @@ -1,10 +1,213 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + +import { DialogService } from 'primeng/dynamicdialog'; +import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'primeng/tabs'; + +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { UserSelectors } from '@osf/core/store/user'; +import { CustomItemMetadataRecord } from '@osf/features/project/metadata/models'; +import { ProjectOverview } from '@osf/features/project/overview/models'; +import { + ContributorsSelectors, + FetchChildrenSubjects, + FetchSelectedSubjects, + FetchSubjects, + GetAllContributors, + SubjectsSelectors, + UpdateResourceSubjects, +} from '@osf/shared/stores'; +import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'; +import { SharedMetadataComponent } from '@shared/components/shared-metadata/shared-metadata.component'; +import { ResourceType } from '@shared/enums'; +import { SubjectModel } from '@shared/models'; +import { CustomConfirmationService, LoaderService, ToastService } from '@shared/services'; + +import { + GetBibliographicContributors, + GetCustomItemMetadata, + GetFundersList, + GetRegistryForMetadata, + GetRegistrySubjects, + GetUserInstitutions, + RegistryMetadataSelectors, + UpdateCustomItemMetadata, + UpdateRegistryDetails, +} from '../../store/registry-metadata'; @Component({ selector: 'osf-registry-metadata', - imports: [], + imports: [ + SubHeaderComponent, + TranslatePipe, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + LoadingSpinnerComponent, + SharedMetadataComponent, + ], templateUrl: './registry-metadata.component.html', styleUrl: './registry-metadata.component.scss', + providers: [DialogService], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) -export class RegistryMetadataComponent {} +export class RegistryMetadataComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + private readonly dialogService = inject(DialogService); + private readonly translateService = inject(TranslateService); + private readonly toastService = inject(ToastService); + private readonly loaderService = inject(LoaderService); + private readonly customConfirmationService = inject(CustomConfirmationService); + + private registryId = ''; + + tabs = signal<{ id: string; label: string; type: 'registry' }[]>([]); + protected readonly selectedTab = signal('registry'); + + protected actions = createDispatchMap({ + getRegistry: GetRegistryForMetadata, + getBibliographicContributors: GetBibliographicContributors, + updateRegistryDetails: UpdateRegistryDetails, + getCustomItemMetadata: GetCustomItemMetadata, + updateCustomItemMetadata: UpdateCustomItemMetadata, + getFundersList: GetFundersList, + getContributors: GetAllContributors, + getUserInstitutions: GetUserInstitutions, + getRegistrySubjects: GetRegistrySubjects, + + fetchSubjects: FetchSubjects, + fetchSelectedSubjects: FetchSelectedSubjects, + fetchChildrenSubjects: FetchChildrenSubjects, + updateResourceSubjects: UpdateResourceSubjects, + }); + + protected currentRegistry = select(RegistryMetadataSelectors.getRegistry); + protected currentRegistryLoading = select(RegistryMetadataSelectors.getRegistryLoading); + protected bibliographicContributors = select(RegistryMetadataSelectors.getBibliographicContributors); + protected bibliographicContributorsLoading = select(RegistryMetadataSelectors.getBibliographicContributorsLoading); + protected customItemMetadata = select(RegistryMetadataSelectors.getCustomItemMetadata); + protected fundersList = select(RegistryMetadataSelectors.getFundersList); + protected contributors = select(ContributorsSelectors.getContributors); + protected isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); + protected currentUser = select(UserSelectors.getCurrentUser); + protected selectedSubjects = select(SubjectsSelectors.getSelectedSubjects); + protected isSubjectsUpdating = select(SubjectsSelectors.areSelectedSubjectsLoading); + + constructor() { + effect(() => { + const registry = this.currentRegistry(); + if (!registry) return; + + const baseTabs = [{ id: 'registry', label: registry.title, type: 'registry' as const }]; + + this.tabs.set(baseTabs); + }); + } + + ngOnInit(): void { + this.registryId = this.route.parent?.parent?.snapshot.params['id']; + + if (this.registryId) { + this.actions.getRegistry(this.registryId); + this.actions.getBibliographicContributors(this.registryId); + this.actions.getCustomItemMetadata(this.registryId); + this.actions.getContributors(this.registryId, ResourceType.Registration); + this.actions.getRegistrySubjects(this.registryId); + this.actions.fetchSubjects(ResourceType.Registration, this.registryId, '', true); + this.actions.fetchSelectedSubjects(this.registryId, ResourceType.Registration); + + const user = this.currentUser(); + if (user?.id) { + this.actions.getUserInstitutions(user.id); + } + } + } + + openAddRecord(): void { + this.router.navigate(['add'], { relativeTo: this.route }); + } + + onTagsChanged(tags: string[]): void { + const registryId = this.currentRegistry()?.id; + if (registryId) { + this.actions.updateRegistryDetails(registryId, { tags }); + } + } + + openEditContributorDialog(): void { + // Similar implementation to project metadata + // For now, just show the bibliographic contributors from the API + console.log('Bibliographic contributors:', this.bibliographicContributors()); + } + + openEditDescriptionDialog(): void { + // Similar implementation to project metadata + console.log('Edit description for registry:', this.currentRegistry()); + } + + openEditResourceInformationDialog(): void { + // Similar implementation to project metadata + console.log('Edit resource information for registry:', this.currentRegistry()); + } + + openEditLicenseDialog(): void { + // Similar implementation to project metadata + console.log('Edit license for registry:', this.currentRegistry()); + } + + openEditFundingDialog(): void { + this.actions.getFundersList(); + // Similar implementation to project metadata + console.log('Edit funding for registry:', this.currentRegistry()); + } + + openEditAffiliatedInstitutionsDialog(): void { + // Similar implementation to project metadata + console.log('Edit affiliated institutions for registry:', this.currentRegistry()); + } + + handleEditDoi(): void { + this.customConfirmationService.confirmDelete({ + headerKey: this.translateService.instant('project.metadata.doi.dialog.createConfirm.header'), + messageKey: this.translateService.instant('project.metadata.doi.dialog.createConfirm.message'), + acceptLabelKey: this.translateService.instant('common.buttons.create'), + acceptLabelType: 'primary', + onConfirm: () => { + const registryId = this.currentRegistry()?.id; + if (registryId) { + this.actions.updateRegistryDetails(registryId, { doi: true }).subscribe({ + next: () => this.toastService.showSuccess('registry.metadata.doi.created'), + }); + } + }, + }); + } + + getSubjectChildren(parentId: string) { + this.actions.fetchChildrenSubjects(parentId); + } + + searchSubjects(search: string) { + this.actions.fetchSubjects(ResourceType.Registration, this.registryId, search, true); + } + + updateSelectedSubjects(subjects: SubjectModel[]) { + this.actions.updateResourceSubjects(this.registryId, ResourceType.Registration, subjects); + } + + getCurrentInstanceForTemplate(): ProjectOverview { + return this.currentRegistry() as unknown as ProjectOverview; + } + + getCustomMetadataForTemplate(): CustomItemMetadataRecord { + return this.customItemMetadata() as unknown as CustomItemMetadataRecord; + } +} diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index e88ab2723..b629556f9 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -3,6 +3,7 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; import { RegistryFilesState } from '@osf/features/registry/store/registry-files'; +import { RegistryMetadataState } from '@osf/features/registry/store/registry-metadata'; import { RegistryOverviewState } from '@osf/features/registry/store/registry-overview'; import { ResourceType } from '@osf/shared/enums'; import { ContributorsState, ViewOnlyLinkState } from '@osf/shared/stores'; @@ -31,6 +32,7 @@ export const registryRoutes: Routes = [ path: 'metadata', loadComponent: () => import('./pages/registry-metadata/registry-metadata.component').then((c) => c.RegistryMetadataComponent), + providers: [provideStates([RegistryMetadataState])], }, { path: 'contributors', diff --git a/src/app/features/registry/services/index.ts b/src/app/features/registry/services/index.ts index 08edb6ef5..e65075448 100644 --- a/src/app/features/registry/services/index.ts +++ b/src/app/features/registry/services/index.ts @@ -1 +1,2 @@ +export * from './registry-metadata.service'; export * from './registry-overview.service'; diff --git a/src/app/features/registry/services/registry-metadata.service.ts b/src/app/features/registry/services/registry-metadata.service.ts new file mode 100644 index 000000000..92483afe4 --- /dev/null +++ b/src/app/features/registry/services/registry-metadata.service.ts @@ -0,0 +1,118 @@ +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@osf/core/services'; + +import { RegistryMetadataMapper } from '../mappers'; +import { + BibliographicContributorsJsonApi, + CrossRefFundersResponse, + CustomItemMetadataRecord, + CustomItemMetadataResponse, + RegistryOverview, + RegistrySubjectsJsonApi, + UserInstitutionsResponse, +} from '../models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class RegistryMetadataService { + private readonly jsonApiService = inject(JsonApiService); + private readonly apiUrl = environment.apiUrl; + + getBibliographicContributors( + registryId: string, + page = 1, + pageSize = 100 + ): Observable { + const params: Record = { + 'fields[contributors]': 'index,users', + 'fields[users]': 'full_name', + page: page, + 'page[size]': pageSize, + }; + + return this.jsonApiService.get( + `${this.apiUrl}/registrations/${registryId}/bibliographic_contributors/`, + params + ); + } + + getCustomItemMetadata(guid: string): Observable { + return this.jsonApiService.get(`${this.apiUrl}/custom_item_metadata_records/${guid}/`); + } + + updateCustomItemMetadata(guid: string, metadata: CustomItemMetadataRecord): Observable { + return this.jsonApiService.put(`${this.apiUrl}/custom_item_metadata_records/${guid}/`, { + data: { + type: 'custom-item-metadata-records', + attributes: metadata, + }, + }); + } + + getFundersList(searchQuery?: string): Observable { + let url = environment.funderApiUrl; + + if (searchQuery && searchQuery.trim()) { + url += `&query=${encodeURIComponent(searchQuery.trim())}`; + } + + return this.jsonApiService.get(url); + } + + getRegistryForMetadata(registryId: string): Observable { + const params: Record = { + 'embed[]': ['contributors', 'affiliated_institutions', 'identifiers', 'license', 'subjects_acceptable'], + 'fields[institutions]': 'assets,description,name', + 'fields[users]': 'family_name,full_name,given_name,middle_name', + 'fields[subjects]': 'text,taxonomy', + }; + + return this.jsonApiService + .get<{ data: Record }>(`${environment.apiUrl}/registrations/${registryId}/`, params) + .pipe(map((response) => RegistryMetadataMapper.fromMetadataApiResponse(response.data))); + } + + updateRegistryDetails(registryId: string, updates: Partial>): Observable { + const payload = { + data: { + id: registryId, + type: 'registrations', + attributes: updates, + }, + }; + + return this.jsonApiService + .patch>(`${this.apiUrl}/registrations/${registryId}`, payload) + .pipe(map((response) => RegistryMetadataMapper.fromMetadataApiResponse(response))); + } + + getUserInstitutions(userId: string, page = 1, pageSize = 10): Observable { + const params = { + page: page.toString(), + 'page[size]': pageSize.toString(), + }; + + return this.jsonApiService.get(`${this.apiUrl}/users/${userId}/institutions/`, { + params, + }); + } + + getRegistrySubjects(registryId: string, page = 1, pageSize = 100): Observable { + const params: Record = { + 'page[size]': pageSize, + page: page, + }; + + return this.jsonApiService.get( + `${this.apiUrl}/registrations/${registryId}/subjects/`, + params + ); + } +} diff --git a/src/app/features/registry/store/registry-metadata/index.ts b/src/app/features/registry/store/registry-metadata/index.ts new file mode 100644 index 000000000..d73dba1f8 --- /dev/null +++ b/src/app/features/registry/store/registry-metadata/index.ts @@ -0,0 +1,4 @@ +export * from './registry-metadata.actions'; +export * from './registry-metadata.model'; +export * from './registry-metadata.selectors'; +export * from './registry-metadata.state'; diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts new file mode 100644 index 000000000..fb98bb5c0 --- /dev/null +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts @@ -0,0 +1,59 @@ +import { CustomItemMetadataRecord, RegistryMetadata } from '../../models/registry-metadata.models'; + +export class GetRegistryForMetadata { + static readonly type = '[RegistryMetadata] Get Registry For Metadata'; + constructor(public registryId: string) {} +} + +export class GetBibliographicContributors { + static readonly type = '[RegistryMetadata] Get Bibliographic Contributors'; + constructor( + public registryId: string, + public page?: number, + public pageSize?: number + ) {} +} + +export class GetCustomItemMetadata { + static readonly type = '[RegistryMetadata] Get Custom Item Metadata'; + constructor(public guid: string) {} +} + +export class UpdateCustomItemMetadata { + static readonly type = '[RegistryMetadata] Update Custom Item Metadata'; + constructor( + public guid: string, + public metadata: CustomItemMetadataRecord + ) {} +} + +export class UpdateRegistryDetails { + static readonly type = '[RegistryMetadata] Update Registry Details'; + constructor( + public registryId: string, + public updates: Partial + ) {} +} + +export class GetFundersList { + static readonly type = '[RegistryMetadata] Get Funders List'; + constructor(public search?: string) {} +} + +export class GetUserInstitutions { + static readonly type = '[RegistryMetadata] Get User Institutions'; + constructor( + public userId: string, + public page?: number, + public pageSize?: number + ) {} +} + +export class GetRegistrySubjects { + static readonly type = '[RegistryMetadata] Get Registry Subjects'; + constructor( + public registryId: string, + public page?: number, + public pageSize?: number + ) {} +} diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts new file mode 100644 index 000000000..7956d36e1 --- /dev/null +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts @@ -0,0 +1,19 @@ +import { AsyncStateModel } from '@shared/models'; + +import { RegistryOverview } from '../../models'; +import { + BibliographicContributor, + CrossRefFunder, + CustomItemMetadataRecord, + RegistrySubjectData, + UserInstitution, +} from '../../models/registry-metadata.models'; + +export interface RegistryMetadataStateModel { + registry: AsyncStateModel; + bibliographicContributors: AsyncStateModel; + customItemMetadata: AsyncStateModel; + fundersList: AsyncStateModel; + userInstitutions: AsyncStateModel; + subjects: AsyncStateModel; +} diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts new file mode 100644 index 000000000..75465dab7 --- /dev/null +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts @@ -0,0 +1,71 @@ +import { Selector } from '@ngxs/store'; + +import { RegistryMetadataStateModel } from './registry-metadata.model'; +import { RegistryMetadataState } from './registry-metadata.state'; + +export class RegistryMetadataSelectors { + @Selector([RegistryMetadataState]) + static getRegistry(state: RegistryMetadataStateModel) { + return state.registry.data; + } + + @Selector([RegistryMetadataState]) + static getRegistryLoading(state: RegistryMetadataStateModel) { + return state.registry.isLoading; + } + + @Selector([RegistryMetadataState]) + static getBibliographicContributors(state: RegistryMetadataStateModel) { + return state.bibliographicContributors.data; + } + + @Selector([RegistryMetadataState]) + static getBibliographicContributorsLoading(state: RegistryMetadataStateModel) { + return state.bibliographicContributors.isLoading; + } + + @Selector([RegistryMetadataState]) + static getCustomItemMetadata(state: RegistryMetadataStateModel) { + return state.customItemMetadata.data; + } + + @Selector([RegistryMetadataState]) + static getCustomItemMetadataLoading(state: RegistryMetadataStateModel) { + return state.customItemMetadata.isLoading; + } + + @Selector([RegistryMetadataState]) + static getFundersList(state: RegistryMetadataStateModel) { + return state.fundersList.data; + } + + @Selector([RegistryMetadataState]) + static getFundersLoading(state: RegistryMetadataStateModel) { + return state.fundersList.isLoading; + } + + @Selector([RegistryMetadataState]) + static getUserInstitutions(state: RegistryMetadataStateModel) { + return state.userInstitutions.data; + } + + @Selector([RegistryMetadataState]) + static getUserInstitutionsLoading(state: RegistryMetadataStateModel): boolean { + return state.userInstitutions.isLoading; + } + + @Selector([RegistryMetadataState]) + static getSubjects(state: RegistryMetadataStateModel) { + return state.subjects.data; + } + + @Selector([RegistryMetadataState]) + static getSubjectsLoading(state: RegistryMetadataStateModel) { + return state.subjects.isLoading; + } + + @Selector([RegistryMetadataState]) + static getError(state: RegistryMetadataStateModel) { + return state.registry.error; + } +} diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts new file mode 100644 index 000000000..c0df0f460 --- /dev/null +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts @@ -0,0 +1,346 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { finalize, tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { RegistryMetadataMapper } from '../../mappers'; +import { RegistryMetadataService } from '../../services/registry-metadata.service'; + +import { + GetBibliographicContributors, + GetCustomItemMetadata, + GetFundersList, + GetRegistryForMetadata, + GetRegistrySubjects, + GetUserInstitutions, + UpdateCustomItemMetadata, + UpdateRegistryDetails, +} from './registry-metadata.actions'; +import { RegistryMetadataStateModel } from './registry-metadata.model'; + +const initialState: RegistryMetadataStateModel = { + registry: { data: null, isLoading: false, error: null }, + bibliographicContributors: { data: [], isLoading: false, error: null }, + customItemMetadata: { data: null, isLoading: false, error: null }, + fundersList: { data: [], isLoading: false, error: null }, + userInstitutions: { data: [], isLoading: false, error: null }, + subjects: { data: [], isLoading: false, error: null }, +}; + +@State({ + name: 'registryMetadata', + defaults: initialState, +}) +@Injectable() +export class RegistryMetadataState { + private readonly registryMetadataService = inject(RegistryMetadataService); + + @Action(GetRegistryForMetadata) + getRegistryForMetadata(ctx: StateContext, action: GetRegistryForMetadata) { + ctx.patchState({ + registry: { + data: null, + isLoading: true, + error: null, + }, + }); + + return this.registryMetadataService.getRegistryForMetadata(action.registryId).pipe( + tap({ + next: (registry) => { + ctx.patchState({ + registry: { + data: registry, + isLoading: false, + error: null, + }, + }); + }, + error: (error) => { + ctx.patchState({ + registry: { + data: ctx.getState().registry.data, + error: error.message, + isLoading: false, + }, + }); + }, + }), + finalize(() => + ctx.patchState({ + registry: { + data: ctx.getState().registry.data, + error: null, + isLoading: false, + }, + }) + ) + ); + } + + @Action(GetBibliographicContributors) + getBibliographicContributors(ctx: StateContext, action: GetBibliographicContributors) { + ctx.patchState({ + bibliographicContributors: { + data: [], + isLoading: true, + error: null, + }, + }); + + return this.registryMetadataService + .getBibliographicContributors(action.registryId, action.page, action.pageSize) + .pipe( + tap({ + next: (response) => { + const contributors = RegistryMetadataMapper.mapBibliographicContributors(response); + ctx.patchState({ + bibliographicContributors: { + data: contributors, + isLoading: false, + error: null, + }, + }); + }, + error: (error) => { + ctx.patchState({ + bibliographicContributors: { + data: [], + isLoading: false, + error: error.message, + }, + }); + }, + }), + finalize(() => + ctx.patchState({ + bibliographicContributors: { + ...ctx.getState().bibliographicContributors, + isLoading: false, + }, + }) + ) + ); + } + + @Action(GetRegistrySubjects) + getRegistrySubjects(ctx: StateContext, action: GetRegistrySubjects) { + ctx.patchState({ + subjects: { + data: [], + isLoading: true, + error: null, + }, + }); + + return this.registryMetadataService.getRegistrySubjects(action.registryId, action.page, action.pageSize).pipe( + tap({ + next: (response) => { + ctx.patchState({ + subjects: { + data: response.data, + isLoading: false, + error: null, + }, + }); + }, + error: (error) => { + ctx.patchState({ + subjects: { + data: [], + isLoading: false, + error: error.message, + }, + }); + }, + }), + finalize(() => + ctx.patchState({ + subjects: { + ...ctx.getState().subjects, + isLoading: false, + }, + }) + ) + ); + } + + @Action(GetCustomItemMetadata) + getCustomItemMetadata(ctx: StateContext, action: GetCustomItemMetadata) { + ctx.patchState({ + customItemMetadata: { data: null, isLoading: true, error: null }, + }); + + return this.registryMetadataService.getCustomItemMetadata(action.guid).pipe( + tap({ + next: (response) => { + ctx.patchState({ + customItemMetadata: { data: response.data.attributes, isLoading: false, error: null }, + }); + }, + error: (error) => { + ctx.patchState({ + customItemMetadata: { data: null, isLoading: false, error: error.message }, + }); + }, + }), + finalize(() => + ctx.patchState({ + customItemMetadata: { + ...ctx.getState().customItemMetadata, + isLoading: false, + }, + }) + ) + ); + } + + @Action(UpdateCustomItemMetadata) + updateCustomItemMetadata(ctx: StateContext, action: UpdateCustomItemMetadata) { + ctx.patchState({ + customItemMetadata: { data: null, isLoading: true, error: null }, + }); + + return this.registryMetadataService.updateCustomItemMetadata(action.guid, action.metadata).pipe( + tap({ + next: (response) => { + ctx.patchState({ + customItemMetadata: { data: response.data.attributes, isLoading: false, error: null }, + }); + }, + error: (error) => { + ctx.patchState({ + customItemMetadata: { ...ctx.getState().customItemMetadata, isLoading: false, error: error.message }, + }); + }, + }), + finalize(() => ctx.patchState({ customItemMetadata: { ...ctx.getState().customItemMetadata, isLoading: false } })) + ); + } + + @Action(UpdateRegistryDetails) + updateRegistryDetails(ctx: StateContext, action: UpdateRegistryDetails) { + ctx.patchState({ + registry: { + ...ctx.getState().registry, + isLoading: true, + error: null, + }, + }); + + return this.registryMetadataService.updateRegistryDetails(action.registryId, action.updates).pipe( + tap({ + next: (updatedRegistry) => { + const currentRegistry = ctx.getState().registry.data; + + ctx.patchState({ + registry: { + data: { + ...currentRegistry, + ...updatedRegistry, + }, + error: null, + isLoading: false, + }, + }); + }, + error: (error) => { + ctx.patchState({ + registry: { + ...ctx.getState().registry, + error: error.message, + isLoading: false, + }, + }); + }, + }), + finalize(() => + ctx.patchState({ + registry: { + ...ctx.getState().registry, + error: null, + isLoading: false, + }, + }) + ) + ); + } + + @Action(GetFundersList) + getFundersList(ctx: StateContext, action: GetFundersList) { + ctx.patchState({ + fundersList: { data: [], isLoading: true, error: null }, + }); + + return this.registryMetadataService.getFundersList(action.search).pipe( + tap({ + next: (response) => { + ctx.patchState({ + fundersList: { data: response.message.items, isLoading: false, error: null }, + }); + }, + error: (error) => { + ctx.patchState({ + fundersList: { + ...ctx.getState().fundersList, + isLoading: false, + error: error.message, + }, + }); + }, + }), + finalize(() => + ctx.patchState({ + fundersList: { + ...ctx.getState().fundersList, + isLoading: false, + }, + }) + ) + ); + } + + @Action(GetUserInstitutions) + getUserInstitutions(ctx: StateContext, action: GetUserInstitutions) { + ctx.patchState({ + userInstitutions: { + data: [], + isLoading: true, + error: null, + }, + }); + + return this.registryMetadataService.getUserInstitutions(action.userId, action.page, action.pageSize).pipe( + tap({ + next: (response) => { + ctx.patchState({ + userInstitutions: { + data: response.data, + isLoading: false, + error: null, + }, + }); + }, + error: (error) => { + ctx.patchState({ + userInstitutions: { + ...ctx.getState().userInstitutions, + error: error.message, + isLoading: false, + }, + }); + }, + }), + finalize(() => + ctx.patchState({ + userInstitutions: { + ...ctx.getState().userInstitutions, + error: null, + isLoading: false, + }, + }) + ) + ); + } +} diff --git a/src/app/shared/services/subjects.service.ts b/src/app/shared/services/subjects.service.ts index 643493104..98429e28b 100644 --- a/src/app/shared/services/subjects.service.ts +++ b/src/app/shared/services/subjects.service.ts @@ -25,12 +25,21 @@ export class SubjectsService { [ResourceType.DraftRegistration, 'draft_registrations'], ]); - getSubjects(resourceType: ResourceType, resourceId?: string, search?: string): Observable { - const baseUrl = + getSubjects( + resourceType: ResourceType, + resourceId?: string, + search?: string, + isMetadataRegistry = false + ): Observable { + let baseUrl = resourceType === ResourceType.Project ? `${this.apiUrl}/subjects/` : `${this.apiUrl}/providers/${this.urlMap.get(resourceType)}/${resourceId}/subjects/`; + if (isMetadataRegistry) { + baseUrl = baseUrl.replace('/providers', ''); + } + const params: Record = { 'page[size]': '100', sort: 'text', diff --git a/src/app/shared/stores/subjects/subjects.actions.ts b/src/app/shared/stores/subjects/subjects.actions.ts index 34891fe20..af46c07a5 100644 --- a/src/app/shared/stores/subjects/subjects.actions.ts +++ b/src/app/shared/stores/subjects/subjects.actions.ts @@ -7,7 +7,8 @@ export class FetchSubjects { constructor( public resourceType: ResourceType | undefined, public resourceId?: string, - public search?: string + public search?: string, + public isMetadataRegistry?: boolean ) {} } diff --git a/src/app/shared/stores/subjects/subjects.state.ts b/src/app/shared/stores/subjects/subjects.state.ts index 04a6ff62c..2e26c0fe6 100644 --- a/src/app/shared/stores/subjects/subjects.state.ts +++ b/src/app/shared/stores/subjects/subjects.state.ts @@ -42,7 +42,10 @@ export class SubjectsState { private readonly subjectsService = inject(SubjectsService); @Action(FetchSubjects) - fetchSubjects(ctx: StateContext, { resourceId, resourceType, search }: FetchSubjects) { + fetchSubjects( + ctx: StateContext, + { resourceId, resourceType, search, isMetadataRegistry }: FetchSubjects + ) { if (!resourceType) { return; } @@ -60,7 +63,7 @@ export class SubjectsState { }, }); - return this.subjectsService.getSubjects(resourceType, resourceId, search).pipe( + return this.subjectsService.getSubjects(resourceType, resourceId, search, isMetadataRegistry).pipe( tap((subjects) => { if (search) { ctx.patchState({ From c183d7142c20b9497029bcf3e11ce3193acad0bc Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Tue, 22 Jul 2025 20:22:51 +0300 Subject: [PATCH 4/9] feat(metadata): added issues for cedar metadata --- .../add-metadata/add-metadata.component.ts | 4 - src/app/features/registry/pages/index.ts | 1 + .../registry-metadata-add.component.html | 82 ++++++++ .../registry-metadata-add.component.scss | 7 + .../registry-metadata-add.component.ts | 195 ++++++++++++++++++ .../registry-metadata.component.html | 22 +- .../registry-metadata.component.ts | 187 ++++++++++++++++- src/app/features/registry/registry.routes.ts | 14 ++ .../services/registry-metadata.service.ts | 34 +++ .../registry-metadata.actions.ts | 30 +++ .../registry-metadata.model.ts | 8 + .../registry-metadata.selectors.ts | 30 +++ .../registry-metadata.state.ts | 120 +++++++++++ 13 files changed, 726 insertions(+), 8 deletions(-) create mode 100644 src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html create mode 100644 src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.scss create mode 100644 src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts diff --git a/src/app/features/project/metadata/pages/add-metadata/add-metadata.component.ts b/src/app/features/project/metadata/pages/add-metadata/add-metadata.component.ts index 167cb67d4..7295f549b 100644 --- a/src/app/features/project/metadata/pages/add-metadata/add-metadata.component.ts +++ b/src/app/features/project/metadata/pages/add-metadata/add-metadata.component.ts @@ -57,10 +57,6 @@ export class AddMetadataComponent implements OnInit { updateCedarMetadataRecord: UpdateCedarMetadataRecord, }); - get isEditingExistingRecord(): boolean { - return !!this.activatedRoute.snapshot.params['record-id']; - } - constructor() { effect(() => { const records = this.cedarRecords(); diff --git a/src/app/features/registry/pages/index.ts b/src/app/features/registry/pages/index.ts index b6f51cf0b..369382d7f 100644 --- a/src/app/features/registry/pages/index.ts +++ b/src/app/features/registry/pages/index.ts @@ -1,3 +1,4 @@ export * from './registry-metadata/registry-metadata.component'; +export * from './registry-metadata-add/registry-metadata-add.component'; export * from '@osf/features/registry/pages/registry-files/registry-files.component'; export * from '@osf/features/registry/pages/registry-overview/registry-overview.component'; diff --git a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html new file mode 100644 index 000000000..8b85e40a1 --- /dev/null +++ b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html @@ -0,0 +1,82 @@ + + +@if (!selectedTemplate()) { + @if (cedarTemplatesLoading()) { +
+ +
+ } @else { +
+
+
+

{{ 'project.metadata.addMetadata.selectTemplate' | translate }}

+
+
+ +
+ @for (meta of cedarTemplates()?.data; track meta.id) { + + } +
+ +
+ + @if (hasMultiplePages()) { + + } +
+
+
+ } +} @else { + +} diff --git a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.scss b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.scss new file mode 100644 index 000000000..75061e4f9 --- /dev/null +++ b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.scss @@ -0,0 +1,7 @@ +.metadata { + flex-basis: calc(50% - 1.5rem); + + @media (max-width: 576px) { + flex-basis: 100%; + } +} diff --git a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts new file mode 100644 index 000000000..74986cedc --- /dev/null +++ b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts @@ -0,0 +1,195 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Tooltip } from 'primeng/tooltip'; + +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { CedarTemplateFormComponent } from '@osf/features/project/metadata/components'; +import { + CedarMetadataDataTemplateJsonApi, + CedarMetadataRecord, + CedarMetadataRecordData, + CedarRecordDataBinding, +} from '@osf/features/project/metadata/models'; +import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'; +import { ToastService } from '@shared/services'; + +import { + CreateCedarMetadataRecord, + GetCedarMetadataTemplates, + GetRegistryCedarMetadataRecords, + RegistryMetadataSelectors, +} from '../../store/registry-metadata'; + +@Component({ + selector: 'osf-registry-metadata-add', + imports: [SubHeaderComponent, CedarTemplateFormComponent, LoadingSpinnerComponent, TranslatePipe, Button, Tooltip], + templateUrl: './registry-metadata-add.component.html', + styleUrl: './registry-metadata-add.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class RegistryMetadataAddComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + private readonly toastService = inject(ToastService); + + isEditMode = true; + + private registryId = ''; + + existingRecord = signal(null); + selectedTemplate = signal(null); + isSubmitting = signal(false); + + protected actions = createDispatchMap({ + getCedarTemplates: GetCedarMetadataTemplates, + getCedarRecords: GetRegistryCedarMetadataRecords, + createCedarRecord: CreateCedarMetadataRecord, + }); + + protected cedarRecords = select(RegistryMetadataSelectors.getCedarRecords); + protected cedarTemplates = select(RegistryMetadataSelectors.getCedarTemplates); + protected cedarTemplatesLoading = select(RegistryMetadataSelectors.getCedarTemplatesLoading); + protected cedarRecord = select(RegistryMetadataSelectors.getCedarRecord); + + constructor() { + effect(() => { + const records = this.cedarRecords(); + const cedarTemplatesData = this.cedarTemplates()?.data; + const recordId = this.route.snapshot.params['record-id']; + + if (!records || !cedarTemplatesData) { + return; + } + + if (recordId) { + const existingRecord = records.find((record) => { + return record.id === recordId; + }); + + if (existingRecord) { + const templateId = existingRecord.relationships.template.data.id; + const matchingTemplate = cedarTemplatesData.find((template) => template.id === templateId); + + if (matchingTemplate) { + this.selectedTemplate.set(matchingTemplate); + this.existingRecord.set(existingRecord); + this.isEditMode = false; + } + } + } else { + this.selectedTemplate.set(null); + this.existingRecord.set(null); + this.isEditMode = true; + } + }); + } + + ngOnInit(): void { + this.registryId = this.route.parent?.parent?.snapshot.params['id']; + + if (this.registryId) { + this.actions.getCedarTemplates(); + this.actions.getCedarRecords(this.registryId); + } + } + + hasMultiplePages(): boolean { + const templates = this.cedarTemplates(); + return !!(templates?.links?.first && templates?.links?.last && templates.links.first !== templates.links.last); + } + + hasNextPage(): boolean { + const templates = this.cedarTemplates(); + return !!templates?.links?.next; + } + + hasExistingRecord(templateId: string): boolean { + const records = this.cedarRecords(); + if (!records) return false; + + return records.some((record) => record.relationships.template.data.id === templateId); + } + + onTemplateSelected(template: CedarMetadataDataTemplateJsonApi): void { + this.selectedTemplate.set(template); + } + + onSubmit(data: CedarRecordDataBinding): void { + const registryId = this.registryId; + if (!registryId) return; + + this.isSubmitting.set(true); + + const model = { + data: { + type: 'cedar_metadata_records' as const, + attributes: { + metadata: data.data, + is_published: false, + }, + relationships: { + template: { + data: { + type: 'cedar-metadata-templates' as const, + id: data.id, + }, + }, + target: { + data: { + type: 'registrations' as const, + id: registryId, + }, + }, + }, + }, + } as unknown as CedarMetadataRecord; + + this.actions + .createCedarRecord(model) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.isSubmitting.set(false); + this.toastService.showSuccess('CEDAR record created successfully'); + this.router.navigate(['../metadata', this.cedarRecord()?.data.id], { relativeTo: this.route.parent }); + }, + error: () => { + this.isSubmitting.set(false); + this.toastService.showError('Failed to create CEDAR record'); + }, + }); + } + + onChangeTemplate(): void { + this.selectedTemplate.set(null); + } + + toggleEditMode(): void { + this.isEditMode = !this.isEditMode; + } + + onNext(): void { + const templates = this.cedarTemplates(); + if (!templates?.links?.next) { + return; + } + this.actions.getCedarTemplates(templates.links.next); + } + + onCancel(): void { + const templates = this.cedarTemplates(); + if (templates?.links?.first && templates?.links?.last && templates.links.first !== templates.links.last) { + this.actions.getCedarTemplates(); + } else { + this.router.navigate(['..'], { relativeTo: this.route }); + } + } +} diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html index 2c9fb9c88..fd90b2ad2 100644 --- a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html +++ b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html @@ -5,6 +5,7 @@ (buttonClick)="openAddRecord()" [title]="'project.overview.metadata.title' | translate" /> + @if (!tabs().length) {
@@ -12,7 +13,7 @@ } @if (tabs().length) { - + @for (item of tabs(); track $index) { {{ item.label | translate }} @@ -40,6 +41,25 @@ (searchSubjects)="searchSubjects($event)" (updateSelectedSubjects)="updateSelectedSubjects($event)" /> + } @else { +
+ @if (selectedCedarTemplate() && selectedCedarRecord()) { + + } @else { +
+

{{ 'project.metadata.addMetadata.loadingCedar' | translate }}

+

{{ tab.label }}

+
+ } +
} } diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts index 0ca218bf8..61579738b 100644 --- a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts +++ b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.ts @@ -6,10 +6,18 @@ import { DialogService } from 'primeng/dynamicdialog'; import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'primeng/tabs'; import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; import { UserSelectors } from '@osf/core/store/user'; -import { CustomItemMetadataRecord } from '@osf/features/project/metadata/models'; +import { CedarTemplateFormComponent } from '@osf/features/project/metadata/components'; +import { + CedarMetadataDataTemplateJsonApi, + CedarMetadataRecord, + CedarMetadataRecordData, + CedarRecordDataBinding, + CustomItemMetadataRecord, +} from '@osf/features/project/metadata/models'; import { ProjectOverview } from '@osf/features/project/overview/models'; import { ContributorsSelectors, @@ -27,13 +35,17 @@ import { SubjectModel } from '@shared/models'; import { CustomConfirmationService, LoaderService, ToastService } from '@shared/services'; import { + CreateCedarMetadataRecord, GetBibliographicContributors, + GetCedarMetadataTemplates, GetCustomItemMetadata, GetFundersList, + GetRegistryCedarMetadataRecords, GetRegistryForMetadata, GetRegistrySubjects, GetUserInstitutions, RegistryMetadataSelectors, + UpdateCedarMetadataRecord, UpdateCustomItemMetadata, UpdateRegistryDetails, } from '../../store/registry-metadata'; @@ -50,6 +62,7 @@ import { Tabs, LoadingSpinnerComponent, SharedMetadataComponent, + CedarTemplateFormComponent, ], templateUrl: './registry-metadata.component.html', styleUrl: './registry-metadata.component.scss', @@ -69,9 +82,13 @@ export class RegistryMetadataComponent implements OnInit { private registryId = ''; - tabs = signal<{ id: string; label: string; type: 'registry' }[]>([]); + tabs = signal<{ id: string; label: string; type: 'registry' | 'cedar' }[]>([]); protected readonly selectedTab = signal('registry'); + selectedCedarRecord = signal(null); + selectedCedarTemplate = signal(null); + cedarFormReadonly = signal(true); + protected actions = createDispatchMap({ getRegistry: GetRegistryForMetadata, getBibliographicContributors: GetBibliographicContributors, @@ -82,6 +99,10 @@ export class RegistryMetadataComponent implements OnInit { getContributors: GetAllContributors, getUserInstitutions: GetUserInstitutions, getRegistrySubjects: GetRegistrySubjects, + getCedarRecords: GetRegistryCedarMetadataRecords, + getCedarTemplates: GetCedarMetadataTemplates, + createCedarRecord: CreateCedarMetadataRecord, + updateCedarRecord: UpdateCedarMetadataRecord, fetchSubjects: FetchSubjects, fetchSelectedSubjects: FetchSelectedSubjects, @@ -100,15 +121,42 @@ export class RegistryMetadataComponent implements OnInit { protected currentUser = select(UserSelectors.getCurrentUser); protected selectedSubjects = select(SubjectsSelectors.getSelectedSubjects); protected isSubjectsUpdating = select(SubjectsSelectors.areSelectedSubjectsLoading); + protected cedarRecords = select(RegistryMetadataSelectors.getCedarRecords); + protected cedarTemplates = select(RegistryMetadataSelectors.getCedarTemplates); constructor() { effect(() => { + const records = this.cedarRecords(); const registry = this.currentRegistry(); if (!registry) return; const baseTabs = [{ id: 'registry', label: registry.title, type: 'registry' as const }]; - this.tabs.set(baseTabs); + const cedarTabs = + records?.map((record) => ({ + id: record.id || '', + label: record.embeds?.template?.data?.attributes?.schema_name || `Record ${record.id}`, + type: 'cedar' as const, + })) || []; + + this.tabs.set([...baseTabs, ...cedarTabs]); + + this.handleRouteBasedTabSelection(); + }); + + effect(() => { + const templates = this.cedarTemplates(); + const selectedRecord = this.selectedCedarRecord(); + + if (selectedRecord && templates?.data && !this.selectedCedarTemplate()) { + const templateId = selectedRecord.relationships?.template?.data?.id; + if (templateId) { + const template = templates.data.find((t) => t.id === templateId); + if (template) { + this.selectedCedarTemplate.set(template); + } + } + } }); } @@ -121,6 +169,8 @@ export class RegistryMetadataComponent implements OnInit { this.actions.getCustomItemMetadata(this.registryId); this.actions.getContributors(this.registryId, ResourceType.Registration); this.actions.getRegistrySubjects(this.registryId); + this.actions.getCedarRecords(this.registryId); + this.actions.getCedarTemplates(); this.actions.fetchSubjects(ResourceType.Registration, this.registryId, '', true); this.actions.fetchSelectedSubjects(this.registryId, ResourceType.Registration); @@ -210,4 +260,135 @@ export class RegistryMetadataComponent implements OnInit { getCustomMetadataForTemplate(): CustomItemMetadataRecord { return this.customItemMetadata() as unknown as CustomItemMetadataRecord; } + + onTabChange(tabId: string | number): void { + const tab = this.tabs().find((x) => x.id === tabId.toString()); + + if (!tab) { + return; + } + + this.selectedTab.set(tab.id); + + if (tab.type === 'cedar') { + this.loadCedarRecord(tab.id); + + const currentRecordId = this.route.snapshot.paramMap.get('recordId'); + if (currentRecordId !== tab.id) { + this.router.navigate(['metadata', tab.id], { relativeTo: this.route.parent?.parent }); + } + } else { + this.selectedCedarRecord.set(null); + this.selectedCedarTemplate.set(null); + + const currentRecordId = this.route.snapshot.paramMap.get('recordId'); + if (currentRecordId) { + this.router.navigate(['metadata'], { relativeTo: this.route.parent?.parent }); + } + } + } + + onCedarFormEdit(): void { + this.cedarFormReadonly.set(false); + } + + onCedarFormSubmit(data: CedarRecordDataBinding): void { + const registryId = this.currentRegistry()?.id; + const selectedRecord = this.selectedCedarRecord(); + + if (!registryId || !selectedRecord) return; + + const model = { + data: { + type: 'cedar_metadata_records' as const, + attributes: { + metadata: data.data, + is_published: false, + }, + relationships: { + template: { + data: { + type: 'cedar-metadata-templates' as const, + id: data.id, + }, + }, + target: { + data: { + type: 'registrations' as const, + id: registryId, + }, + }, + }, + }, + } as unknown as CedarMetadataRecord; + + if (selectedRecord.id) { + this.actions + .updateCedarRecord(model, selectedRecord.id) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.cedarFormReadonly.set(true); + this.toastService.showSuccess('CEDAR record updated successfully'); + this.actions.getCedarRecords(registryId); + }, + }); + } + } + + onCedarFormChangeTemplate(): void { + this.router.navigate(['add'], { relativeTo: this.route }); + } + + private loadCedarRecord(recordId: string): void { + const records = this.cedarRecords(); + const templates = this.cedarTemplates(); + + if (!records) { + return; + } + + const record = records.find((r) => r.id === recordId); + if (!record) { + return; + } + + this.selectedCedarRecord.set(record); + this.cedarFormReadonly.set(true); + + const templateId = record.relationships?.template?.data?.id; + if (templateId && templates?.data) { + const template = templates.data.find((t) => t.id === templateId); + if (template) { + this.selectedCedarTemplate.set(template); + } else { + this.selectedCedarTemplate.set(null); + this.actions.getCedarTemplates(); + } + } else { + this.selectedCedarTemplate.set(null); + this.actions.getCedarTemplates(); + } + } + + private handleRouteBasedTabSelection(): void { + const recordId = this.route.snapshot.paramMap.get('recordId'); + + if (!recordId) { + this.selectedTab.set('registry'); + this.selectedCedarRecord.set(null); + this.selectedCedarTemplate.set(null); + return; + } + + const tab = this.tabs().find((tab) => tab.id === recordId); + + if (tab) { + this.selectedTab.set(tab.id); + + if (tab.type === 'cedar') { + this.loadCedarRecord(tab.id); + } + } + } } diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index b629556f9..e2db7e720 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -34,6 +34,20 @@ export const registryRoutes: Routes = [ import('./pages/registry-metadata/registry-metadata.component').then((c) => c.RegistryMetadataComponent), providers: [provideStates([RegistryMetadataState])], }, + { + path: 'metadata/add', + loadComponent: () => + import('./pages/registry-metadata-add/registry-metadata-add.component').then( + (c) => c.RegistryMetadataAddComponent + ), + providers: [provideStates([RegistryMetadataState])], + }, + { + path: 'metadata/:recordId', + loadComponent: () => + import('./pages/registry-metadata/registry-metadata.component').then((c) => c.RegistryMetadataComponent), + providers: [provideStates([RegistryMetadataState])], + }, { path: 'contributors', loadComponent: () => diff --git a/src/app/features/registry/services/registry-metadata.service.ts b/src/app/features/registry/services/registry-metadata.service.ts index 92483afe4..01e4700fa 100644 --- a/src/app/features/registry/services/registry-metadata.service.ts +++ b/src/app/features/registry/services/registry-metadata.service.ts @@ -4,6 +4,11 @@ import { map } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@osf/core/services'; +import { + CedarMetadataRecord, + CedarMetadataRecordJsonApi, + CedarMetadataTemplateJsonApi, +} from '@osf/features/project/metadata/models'; import { RegistryMetadataMapper } from '../mappers'; import { @@ -115,4 +120,33 @@ export class RegistryMetadataService { params ); } + + getRegistryCedarMetadataRecords(registryId: string): Observable { + const params: Record = { + embed: 'template', + 'page[size]': 20, + }; + + return this.jsonApiService.get( + `${this.apiUrl}/registrations/${registryId}/cedar_metadata_records/`, + params + ); + } + + getCedarMetadataTemplates(url?: string): Observable { + return this.jsonApiService.get( + url || `${environment.apiDomainUrl}/_/cedar_metadata_templates/?adapterOptions[sort]=schema_name` + ); + } + + createCedarMetadataRecord(data: CedarMetadataRecord): Observable { + return this.jsonApiService.post(`${environment.apiDomainUrl}/_/cedar_metadata_records/`, data); + } + + updateCedarMetadataRecord(data: CedarMetadataRecord, recordId: string): Observable { + return this.jsonApiService.patch( + `${environment.apiDomainUrl}/_/cedar_metadata_records/${recordId}/`, + data + ); + } } diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts index fb98bb5c0..ca8b1c9e0 100644 --- a/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts @@ -1,3 +1,5 @@ +import { CedarMetadataRecord, CedarMetadataRecordData } from '@osf/features/project/metadata/models'; + import { CustomItemMetadataRecord, RegistryMetadata } from '../../models/registry-metadata.models'; export class GetRegistryForMetadata { @@ -57,3 +59,31 @@ export class GetRegistrySubjects { public pageSize?: number ) {} } + +export class GetCedarMetadataTemplates { + static readonly type = '[RegistryMetadata] Get Cedar Metadata Templates'; + constructor(public url?: string) {} +} + +export class GetRegistryCedarMetadataRecords { + static readonly type = '[RegistryMetadata] Get Registry Cedar Metadata Records'; + constructor(public registryId: string) {} +} + +export class CreateCedarMetadataRecord { + static readonly type = '[RegistryMetadata] Create Cedar Metadata Record'; + constructor(public record: CedarMetadataRecord) {} +} + +export class UpdateCedarMetadataRecord { + static readonly type = '[RegistryMetadata] Update Cedar Metadata Record'; + constructor( + public record: CedarMetadataRecord, + public recordId: string + ) {} +} + +export class AddCedarMetadataRecordToState { + static readonly type = '[RegistryMetadata] Add Cedar Metadata Record To State'; + constructor(public record: CedarMetadataRecordData) {} +} diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts index 7956d36e1..a03f71a61 100644 --- a/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts @@ -1,3 +1,8 @@ +import { + CedarMetadataRecord, + CedarMetadataRecordData, + CedarMetadataTemplateJsonApi, +} from '@osf/features/project/metadata/models'; import { AsyncStateModel } from '@shared/models'; import { RegistryOverview } from '../../models'; @@ -16,4 +21,7 @@ export interface RegistryMetadataStateModel { fundersList: AsyncStateModel; userInstitutions: AsyncStateModel; subjects: AsyncStateModel; + cedarTemplates: AsyncStateModel; + cedarRecord: AsyncStateModel; + cedarRecords: AsyncStateModel; } diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts index 75465dab7..d974ba725 100644 --- a/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts @@ -64,6 +64,36 @@ export class RegistryMetadataSelectors { return state.subjects.isLoading; } + @Selector([RegistryMetadataState]) + static getCedarTemplates(state: RegistryMetadataStateModel) { + return state.cedarTemplates.data; + } + + @Selector([RegistryMetadataState]) + static getCedarTemplatesLoading(state: RegistryMetadataStateModel) { + return state.cedarTemplates.isLoading; + } + + @Selector([RegistryMetadataState]) + static getCedarRecord(state: RegistryMetadataStateModel) { + return state.cedarRecord.data; + } + + @Selector([RegistryMetadataState]) + static getCedarRecordLoading(state: RegistryMetadataStateModel) { + return state.cedarRecord.isLoading; + } + + @Selector([RegistryMetadataState]) + static getCedarRecords(state: RegistryMetadataStateModel) { + return state.cedarRecords.data; + } + + @Selector([RegistryMetadataState]) + static getCedarRecordsLoading(state: RegistryMetadataStateModel) { + return state.cedarRecords.isLoading; + } + @Selector([RegistryMetadataState]) static getError(state: RegistryMetadataStateModel) { return state.registry.error; diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts index c0df0f460..75c90b42b 100644 --- a/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts @@ -4,16 +4,23 @@ import { finalize, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { CedarMetadataRecord, CedarMetadataRecordJsonApi } from '@osf/features/project/metadata/models'; + import { RegistryMetadataMapper } from '../../mappers'; import { RegistryMetadataService } from '../../services/registry-metadata.service'; import { + AddCedarMetadataRecordToState, + CreateCedarMetadataRecord, GetBibliographicContributors, + GetCedarMetadataTemplates, GetCustomItemMetadata, GetFundersList, + GetRegistryCedarMetadataRecords, GetRegistryForMetadata, GetRegistrySubjects, GetUserInstitutions, + UpdateCedarMetadataRecord, UpdateCustomItemMetadata, UpdateRegistryDetails, } from './registry-metadata.actions'; @@ -26,6 +33,9 @@ const initialState: RegistryMetadataStateModel = { fundersList: { data: [], isLoading: false, error: null }, userInstitutions: { data: [], isLoading: false, error: null }, subjects: { data: [], isLoading: false, error: null }, + cedarTemplates: { data: null, isLoading: false, error: null }, + cedarRecord: { data: null, isLoading: false, error: null }, + cedarRecords: { data: [], isLoading: false, error: null }, }; @State({ @@ -343,4 +353,114 @@ export class RegistryMetadataState { ) ); } + + @Action(GetCedarMetadataTemplates) + getCedarMetadataTemplates(ctx: StateContext, action: GetCedarMetadataTemplates) { + ctx.patchState({ + cedarTemplates: { + data: null, + isLoading: true, + error: null, + }, + }); + + return this.registryMetadataService.getCedarMetadataTemplates(action.url).pipe( + tap({ + next: (response) => { + ctx.patchState({ + cedarTemplates: { + data: response, + error: null, + isLoading: false, + }, + }); + }, + error: (error) => { + ctx.patchState({ + cedarTemplates: { + ...ctx.getState().cedarTemplates, + error: error.message, + isLoading: false, + }, + }); + }, + }), + finalize(() => + ctx.patchState({ + cedarTemplates: { + ...ctx.getState().cedarTemplates, + isLoading: false, + }, + }) + ) + ); + } + + @Action(GetRegistryCedarMetadataRecords) + getRegistryCedarMetadataRecords( + ctx: StateContext, + action: GetRegistryCedarMetadataRecords + ) { + ctx.patchState({ + cedarRecords: { + data: [], + isLoading: true, + error: null, + }, + }); + return this.registryMetadataService.getRegistryCedarMetadataRecords(action.registryId).pipe( + tap((response: CedarMetadataRecordJsonApi) => { + ctx.patchState({ + cedarRecords: { + data: response.data, + error: null, + isLoading: false, + }, + }); + }) + ); + } + + @Action(CreateCedarMetadataRecord) + createCedarMetadataRecord(ctx: StateContext, action: CreateCedarMetadataRecord) { + return this.registryMetadataService.createCedarMetadataRecord(action.record).pipe( + tap((response: CedarMetadataRecord) => { + ctx.dispatch(new AddCedarMetadataRecordToState(response.data)); + }) + ); + } + + @Action(UpdateCedarMetadataRecord) + updateCedarMetadataRecord(ctx: StateContext, action: UpdateCedarMetadataRecord) { + return this.registryMetadataService.updateCedarMetadataRecord(action.record, action.recordId).pipe( + tap((response: CedarMetadataRecord) => { + const state = ctx.getState(); + const updatedRecords = state.cedarRecords.data.map((record) => + record.id === action.recordId ? response.data : record + ); + ctx.patchState({ + cedarRecords: { + data: updatedRecords, + isLoading: false, + error: null, + }, + }); + }) + ); + } + + @Action(AddCedarMetadataRecordToState) + addCedarMetadataRecordToState(ctx: StateContext, action: AddCedarMetadataRecordToState) { + const state = ctx.getState(); + const updatedCedarRecords = [...state.cedarRecords.data, action.record]; + + ctx.setState({ + ...state, + cedarRecords: { + data: updatedCedarRecords, + error: null, + isLoading: false, + }, + }); + } } From 2dd4b472a681c9668016e5a83a9efc10dbb064b7 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Tue, 22 Jul 2025 23:10:41 +0300 Subject: [PATCH 5/9] feat(metadata): moved files to shared changed tags input --- .../add-metadata/add-metadata.component.ts | 2 +- .../metadata/project-metadata.component.ts | 18 ++-- .../registry-metadata-add.component.ts | 2 +- .../registry-metadata.component.html | 1 + .../registry-metadata.component.ts | 2 +- .../cedar-template-form.component.html | 0 .../cedar-template-form.component.scss | 0 .../cedar-template-form.component.ts | 1 + .../shared-metadata}/components/index.ts | 0 ...ata-affiliated-institutions.component.html | 0 ...-affiliated-institutions.component.spec.ts | 0 ...adata-affiliated-institutions.component.ts | 0 ...oject-metadata-contributors.component.html | 0 ...ct-metadata-contributors.component.spec.ts | 0 ...project-metadata-contributors.component.ts | 0 ...roject-metadata-description.component.html | 0 ...ect-metadata-description.component.spec.ts | 0 .../project-metadata-description.component.ts | 0 .../project-metadata-doi.component.html | 0 .../project-metadata-doi.component.spec.ts | 0 .../project-metadata-doi.component.ts | 0 .../project-metadata-funding.component.html | 0 ...project-metadata-funding.component.spec.ts | 0 .../project-metadata-funding.component.ts | 0 .../project-metadata-license.component.html | 0 ...project-metadata-license.component.spec.ts | 0 .../project-metadata-license.component.ts | 0 ...ct-metadata-publication-doi.component.html | 12 ++- ...metadata-publication-doi.component.spec.ts | 0 ...ject-metadata-publication-doi.component.ts | 1 + ...tadata-resource-information.component.html | 0 ...ata-resource-information.component.spec.ts | 0 ...metadata-resource-information.component.ts | 0 .../project-metadata-subjects.component.html | 0 ...roject-metadata-subjects.component.spec.ts | 0 .../project-metadata-subjects.component.ts | 0 ...iliated-institutions-dialog.component.html | 0 ...ated-institutions-dialog.component.spec.ts | 0 ...ffiliated-institutions-dialog.component.ts | 0 .../contributors-dialog.component.html | 0 .../contributors-dialog.component.spec.ts | 0 .../contributors-dialog.component.ts | 0 .../description-dialog.component.html | 0 .../description-dialog.component.scss | 0 .../description-dialog.component.spec.ts | 0 .../description-dialog.component.ts | 0 .../funding-dialog.component.html | 0 .../funding-dialog.component.spec.ts | 0 .../funding-dialog.component.ts | 8 +- .../shared-metadata}/dialogs/index.ts | 0 .../license-dialog.component.html | 0 .../license-dialog.component.scss | 0 .../license-dialog.component.spec.ts | 0 .../license-dialog.component.ts | 0 ...resource-information-dialog.component.html | 0 ...ource-information-dialog.component.spec.ts | 0 .../resource-information-dialog.component.ts | 0 .../shared-metadata.component.html | 1 + .../shared-metadata.component.ts | 12 ++- .../tags-input/tags-input.component.html | 38 ++++++-- .../tags-input/tags-input.component.scss | 45 +++++++++ .../tags-input/tags-input.component.ts | 94 ++++++++++++++++++- src/assets/styles/overrides/chip.scss | 6 +- 63 files changed, 198 insertions(+), 45 deletions(-) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/cedar-template-form/cedar-template-form.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/cedar-template-form/cedar-template-form.component.scss (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/cedar-template-form/cedar-template-form.component.ts (99%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/index.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-contributors/project-metadata-contributors.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-contributors/project-metadata-contributors.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-contributors/project-metadata-contributors.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-description/project-metadata-description.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-description/project-metadata-description.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-description/project-metadata-description.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-doi/project-metadata-doi.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-doi/project-metadata-doi.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-doi/project-metadata-doi.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-funding/project-metadata-funding.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-funding/project-metadata-funding.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-funding/project-metadata-funding.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-license/project-metadata-license.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-license/project-metadata-license.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-license/project-metadata-license.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html (73%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-publication-doi/project-metadata-publication-doi.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts (94%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-resource-information/project-metadata-resource-information.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-resource-information/project-metadata-resource-information.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-resource-information/project-metadata-resource-information.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-subjects/project-metadata-subjects.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-subjects/project-metadata-subjects.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/components/project-metadata-subjects/project-metadata-subjects.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/contributors-dialog/contributors-dialog.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/contributors-dialog/contributors-dialog.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/contributors-dialog/contributors-dialog.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/description-dialog/description-dialog.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/description-dialog/description-dialog.component.scss (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/description-dialog/description-dialog.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/description-dialog/description-dialog.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/funding-dialog/funding-dialog.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/funding-dialog/funding-dialog.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/funding-dialog/funding-dialog.component.ts (96%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/index.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/license-dialog/license-dialog.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/license-dialog/license-dialog.component.scss (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/license-dialog/license-dialog.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/license-dialog/license-dialog.component.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/resource-information-dialog/resource-information-dialog.component.html (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts (100%) rename src/app/{features/project/metadata => shared/components/shared-metadata}/dialogs/resource-information-dialog/resource-information-dialog.component.ts (100%) diff --git a/src/app/features/project/metadata/pages/add-metadata/add-metadata.component.ts b/src/app/features/project/metadata/pages/add-metadata/add-metadata.component.ts index 7295f549b..4686a4572 100644 --- a/src/app/features/project/metadata/pages/add-metadata/add-metadata.component.ts +++ b/src/app/features/project/metadata/pages/add-metadata/add-metadata.component.ts @@ -9,7 +9,6 @@ import { ChangeDetectionStrategy, Component, DestroyRef, effect, HostBinding, in import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; -import { CedarTemplateFormComponent } from '@osf/features/project/metadata/components'; import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecord, @@ -17,6 +16,7 @@ import { CedarRecordDataBinding, } from '@osf/features/project/metadata/models'; import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'; +import { CedarTemplateFormComponent } from '@shared/components/shared-metadata/components'; import { ToastService } from '@shared/services'; import { diff --git a/src/app/features/project/metadata/project-metadata.component.ts b/src/app/features/project/metadata/project-metadata.component.ts index 130d5dd1d..6fc759327 100644 --- a/src/app/features/project/metadata/project-metadata.component.ts +++ b/src/app/features/project/metadata/project-metadata.component.ts @@ -12,15 +12,6 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; import { UserSelectors } from '@osf/core/store/user'; -import { CedarTemplateFormComponent } from '@osf/features/project/metadata/components'; -import { - AffiliatedInstitutionsDialogComponent, - ContributorsDialogComponent, - DescriptionDialogComponent, - FundingDialogComponent, - LicenseDialogComponent, - ResourceInformationDialogComponent, -} from '@osf/features/project/metadata/dialogs'; import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecord, @@ -51,6 +42,15 @@ import { UpdateResourceSubjects, } from '@osf/shared/stores'; import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'; +import { CedarTemplateFormComponent } from '@shared/components/shared-metadata/components'; +import { + AffiliatedInstitutionsDialogComponent, + ContributorsDialogComponent, + DescriptionDialogComponent, + FundingDialogComponent, + LicenseDialogComponent, + ResourceInformationDialogComponent, +} from '@shared/components/shared-metadata/dialogs'; import { SharedMetadataComponent } from '@shared/components/shared-metadata/shared-metadata.component'; import { SubjectModel } from '@shared/models'; import { CustomConfirmationService, LoaderService, ToastService } from '@shared/services'; diff --git a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts index 74986cedc..f563ff161 100644 --- a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts +++ b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts @@ -9,7 +9,6 @@ import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; -import { CedarTemplateFormComponent } from '@osf/features/project/metadata/components'; import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecord, @@ -17,6 +16,7 @@ import { CedarRecordDataBinding, } from '@osf/features/project/metadata/models'; import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'; +import { CedarTemplateFormComponent } from '@shared/components/shared-metadata/components'; import { ToastService } from '@shared/services'; import { diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html index fd90b2ad2..c21f66aeb 100644 --- a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html +++ b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html @@ -25,6 +25,7 @@ @if (tab.type === 'registry') { (); diff --git a/src/app/features/project/metadata/components/index.ts b/src/app/shared/components/shared-metadata/components/index.ts similarity index 100% rename from src/app/features/project/metadata/components/index.ts rename to src/app/shared/components/shared-metadata/components/index.ts diff --git a/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html rename to src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html diff --git a/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.spec.ts b/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.spec.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.spec.ts diff --git a/src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts diff --git a/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.html similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.html rename to src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.html diff --git a/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.spec.ts b/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.spec.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.spec.ts diff --git a/src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts diff --git a/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.html similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.html rename to src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.html diff --git a/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.spec.ts b/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.spec.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.spec.ts diff --git a/src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-description/project-metadata-description.component.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.ts diff --git a/src/app/features/project/metadata/components/project-metadata-doi/project-metadata-doi.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-doi/project-metadata-doi.component.html similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-doi/project-metadata-doi.component.html rename to src/app/shared/components/shared-metadata/components/project-metadata-doi/project-metadata-doi.component.html diff --git a/src/app/features/project/metadata/components/project-metadata-doi/project-metadata-doi.component.spec.ts b/src/app/shared/components/shared-metadata/components/project-metadata-doi/project-metadata-doi.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-doi/project-metadata-doi.component.spec.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-doi/project-metadata-doi.component.spec.ts diff --git a/src/app/features/project/metadata/components/project-metadata-doi/project-metadata-doi.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-doi/project-metadata-doi.component.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-doi/project-metadata-doi.component.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-doi/project-metadata-doi.component.ts diff --git a/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.html similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.html rename to src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.html diff --git a/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.spec.ts b/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.spec.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.spec.ts diff --git a/src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-funding/project-metadata-funding.component.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.ts diff --git a/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.html similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.html rename to src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.html diff --git a/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.spec.ts b/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.spec.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.spec.ts diff --git a/src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-license/project-metadata-license.component.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.ts diff --git a/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html similarity index 73% rename from src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html rename to src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html index 89bd85459..928046214 100644 --- a/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html +++ b/src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.html @@ -2,11 +2,13 @@

{{ 'project.overview.metadata.publication' | translate }}

- + @if (!hideEditDoi()) { + + }
@if (identifiers() && identifiers().length) { diff --git a/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.spec.ts b/src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.spec.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.spec.ts diff --git a/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts similarity index 94% rename from src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts index a93956ea2..e43a19bb4 100644 --- a/src/app/features/project/metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts +++ b/src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts @@ -18,4 +18,5 @@ export class ProjectMetadataPublicationDoiComponent { openEditPublicationDoiDialog = output(); identifiers = input([]); + hideEditDoi = input(false); } diff --git a/src/app/features/project/metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.html similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.html rename to src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.html diff --git a/src/app/features/project/metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.spec.ts b/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.spec.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.spec.ts diff --git a/src/app/features/project/metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.ts diff --git a/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.html similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.html rename to src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.html diff --git a/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.spec.ts b/src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.spec.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.spec.ts diff --git a/src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.ts similarity index 100% rename from src/app/features/project/metadata/components/project-metadata-subjects/project-metadata-subjects.component.ts rename to src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.ts diff --git a/src/app/features/project/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.html b/src/app/shared/components/shared-metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.html similarity index 100% rename from src/app/features/project/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.html rename to src/app/shared/components/shared-metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.html diff --git a/src/app/features/project/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.spec.ts b/src/app/shared/components/shared-metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.spec.ts rename to src/app/shared/components/shared-metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.spec.ts diff --git a/src/app/features/project/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.ts rename to src/app/shared/components/shared-metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.ts diff --git a/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.html b/src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.html similarity index 100% rename from src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.html rename to src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.html diff --git a/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.spec.ts b/src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.spec.ts rename to src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.spec.ts diff --git a/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts rename to src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.ts diff --git a/src/app/features/project/metadata/dialogs/description-dialog/description-dialog.component.html b/src/app/shared/components/shared-metadata/dialogs/description-dialog/description-dialog.component.html similarity index 100% rename from src/app/features/project/metadata/dialogs/description-dialog/description-dialog.component.html rename to src/app/shared/components/shared-metadata/dialogs/description-dialog/description-dialog.component.html diff --git a/src/app/features/project/metadata/dialogs/description-dialog/description-dialog.component.scss b/src/app/shared/components/shared-metadata/dialogs/description-dialog/description-dialog.component.scss similarity index 100% rename from src/app/features/project/metadata/dialogs/description-dialog/description-dialog.component.scss rename to src/app/shared/components/shared-metadata/dialogs/description-dialog/description-dialog.component.scss diff --git a/src/app/features/project/metadata/dialogs/description-dialog/description-dialog.component.spec.ts b/src/app/shared/components/shared-metadata/dialogs/description-dialog/description-dialog.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/description-dialog/description-dialog.component.spec.ts rename to src/app/shared/components/shared-metadata/dialogs/description-dialog/description-dialog.component.spec.ts diff --git a/src/app/features/project/metadata/dialogs/description-dialog/description-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/description-dialog/description-dialog.component.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/description-dialog/description-dialog.component.ts rename to src/app/shared/components/shared-metadata/dialogs/description-dialog/description-dialog.component.ts diff --git a/src/app/features/project/metadata/dialogs/funding-dialog/funding-dialog.component.html b/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.html similarity index 100% rename from src/app/features/project/metadata/dialogs/funding-dialog/funding-dialog.component.html rename to src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.html diff --git a/src/app/features/project/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts b/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts rename to src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts diff --git a/src/app/features/project/metadata/dialogs/funding-dialog/funding-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts similarity index 96% rename from src/app/features/project/metadata/dialogs/funding-dialog/funding-dialog.component.ts rename to src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts index a6b8a5b96..a1185bcc6 100644 --- a/src/app/features/project/metadata/dialogs/funding-dialog/funding-dialog.component.ts +++ b/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts @@ -13,9 +13,6 @@ import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { ProjectMetadataSelectors } from '@osf/features/project/metadata/store'; -import { ProjectOverview } from '@osf/features/project/overview/models'; - import { FunderOption, FundingDialogResult, @@ -23,8 +20,9 @@ import { FundingEntryForm, FundingForm, SupplementData, -} from '../../models'; -import { GetFundersList } from '../../store/project-metadata.actions'; +} from '@osf/features/project/metadata/models'; +import { GetFundersList, ProjectMetadataSelectors } from '@osf/features/project/metadata/store'; +import { ProjectOverview } from '@osf/features/project/overview/models'; @Component({ selector: 'osf-funding-dialog', diff --git a/src/app/features/project/metadata/dialogs/index.ts b/src/app/shared/components/shared-metadata/dialogs/index.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/index.ts rename to src/app/shared/components/shared-metadata/dialogs/index.ts diff --git a/src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.html b/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.html similarity index 100% rename from src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.html rename to src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.html diff --git a/src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.scss b/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.scss similarity index 100% rename from src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.scss rename to src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.scss diff --git a/src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.spec.ts b/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.spec.ts rename to src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.spec.ts diff --git a/src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.ts rename to src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.ts diff --git a/src/app/features/project/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.html b/src/app/shared/components/shared-metadata/dialogs/resource-information-dialog/resource-information-dialog.component.html similarity index 100% rename from src/app/features/project/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.html rename to src/app/shared/components/shared-metadata/dialogs/resource-information-dialog/resource-information-dialog.component.html diff --git a/src/app/features/project/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts b/src/app/shared/components/shared-metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts rename to src/app/shared/components/shared-metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts diff --git a/src/app/features/project/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts similarity index 100% rename from src/app/features/project/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts rename to src/app/shared/components/shared-metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts diff --git a/src/app/shared/components/shared-metadata/shared-metadata.component.html b/src/app/shared/components/shared-metadata/shared-metadata.component.html index 809714297..ca960cdfe 100644 --- a/src/app/shared/components/shared-metadata/shared-metadata.component.html +++ b/src/app/shared/components/shared-metadata/shared-metadata.component.html @@ -58,6 +58,7 @@

diff --git a/src/app/shared/components/shared-metadata/shared-metadata.component.ts b/src/app/shared/components/shared-metadata/shared-metadata.component.ts index 9ea29c0a6..565caa9c6 100644 --- a/src/app/shared/components/shared-metadata/shared-metadata.component.ts +++ b/src/app/shared/components/shared-metadata/shared-metadata.component.ts @@ -5,6 +5,11 @@ import { Card } from 'primeng/card'; import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { CustomItemMetadataRecord } from '@osf/features/project/metadata/models'; +import { ProjectOverview } from '@osf/features/project/overview/models'; +import { TagsInputComponent } from '@shared/components'; +import { SubjectModel } from '@shared/models'; + import { ProjectMetadataAffiliatedInstitutionsComponent, ProjectMetadataContributorsComponent, @@ -14,11 +19,7 @@ import { ProjectMetadataPublicationDoiComponent, ProjectMetadataResourceInformationComponent, ProjectMetadataSubjectsComponent, -} from '@osf/features/project/metadata/components'; -import { CustomItemMetadataRecord } from '@osf/features/project/metadata/models'; -import { ProjectOverview } from '@osf/features/project/overview/models'; -import { TagsInputComponent } from '@shared/components'; -import { SubjectModel } from '@shared/models'; +} from './components'; @Component({ selector: 'osf-shared-metadata', @@ -45,6 +46,7 @@ export class SharedMetadataComponent { customItemMetadata = input.required(); selectedSubjects = input.required(); isSubjectsUpdating = input.required(); + hideEditDoi = input(false); openEditContributorDialog = output(); openEditDescriptionDialog = output(); diff --git a/src/app/shared/components/tags-input/tags-input.component.html b/src/app/shared/components/tags-input/tags-input.component.html index 4b62ccf56..4815011cf 100644 --- a/src/app/shared/components/tags-input/tags-input.component.html +++ b/src/app/shared/components/tags-input/tags-input.component.html @@ -1,11 +1,29 @@ -
- - {{ 'common.hint.tagSeparators' | translate }} +
+
+ @for (tag of localTags(); track $index) { + + } + + +
+ + + {{ 'common.hint.tagSeparators' | translate }} +
diff --git a/src/app/shared/components/tags-input/tags-input.component.scss b/src/app/shared/components/tags-input/tags-input.component.scss index e69de29bb..9ed984add 100644 --- a/src/app/shared/components/tags-input/tags-input.component.scss +++ b/src/app/shared/components/tags-input/tags-input.component.scss @@ -0,0 +1,45 @@ +.tags-input-container { + min-height: 2.5rem; + border: 1px solid var(--p-inputtext-border-color); + background: var(--p-inputtext-background); + transition: + border-color 0.2s, + box-shadow 0.2s; + cursor: text; + + &:focus-within { + border-color: var(--p-inputtext-focus-border-color); + box-shadow: var(--p-inputtext-focus-ring-shadow); + } + + &:hover { + border-color: var(--p-inputtext-hover-border-color); + } + + .tag-input-field { + border: none; + outline: none; + background: transparent; + flex: 1; + min-width: 120px; + font-size: 1rem; + padding: 0; + margin: 0; + + &::placeholder { + color: var(--p-inputtext-placeholder-color); + } + } + + .p-chip-inline { + font-size: 0.875rem; + + .p-chip { + font-size: inherit; + } + } +} + +.tags-input-container:focus-within .tag-input-field { + outline: none; +} diff --git a/src/app/shared/components/tags-input/tags-input.component.ts b/src/app/shared/components/tags-input/tags-input.component.ts index 67c4957df..e8de22185 100644 --- a/src/app/shared/components/tags-input/tags-input.component.ts +++ b/src/app/shared/components/tags-input/tags-input.component.ts @@ -1,13 +1,23 @@ import { TranslatePipe } from '@ngx-translate/core'; -import { Chips } from 'primeng/chips'; +import { Chip } from 'primeng/chip'; +import { InputText } from 'primeng/inputtext'; -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + effect, + ElementRef, + input, + output, + signal, + viewChild, +} from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'osf-tags-input', - imports: [TranslatePipe, FormsModule, Chips], + imports: [TranslatePipe, FormsModule, Chip, InputText], templateUrl: './tags-input.component.html', styleUrl: './tags-input.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -17,7 +27,81 @@ export class TagsInputComponent { required = input(false); tagsChanged = output(); - onTagsChange(tags: string[]): void { - this.tagsChanged.emit(tags); + inputValue = signal(''); + inputElement = viewChild>('tagInput'); + + localTags = signal([]); + private isLocalUpdate = false; + + constructor() { + effect(() => { + const incoming = this.tags(); + + if (!this.isLocalUpdate) { + this.localTags.set([...incoming]); + } + + this.isLocalUpdate = false; + }); + } + + onContainerClick(): void { + this.inputElement()?.nativeElement.focus(); + } + + onContainerKeydown(event: KeyboardEvent): void { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + this.inputElement()?.nativeElement.focus(); + } + } + + onInputKeydown(event: KeyboardEvent): void { + const target = event.target as HTMLInputElement; + const value = target.value.trim(); + + if ((event.key === 'Enter' || event.key === ',' || event.key === ' ') && value) { + event.preventDefault(); + this.addTag(value); + target.value = ''; + this.inputValue.set(''); + } else if (event.key === 'Backspace' && !value && this.localTags().length > 0) { + this.removeTag(this.localTags().length - 1); + } + } + + onInputBlur(event: FocusEvent): void { + const target = event.target as HTMLInputElement; + const value = target.value.trim(); + + if (value) { + this.addTag(value); + target.value = ''; + this.inputValue.set(''); + } + } + + private addTag(tagValue: string): void { + const currentTags = this.localTags(); + const normalizedValue = tagValue.replace(/[,\s]+/g, ' ').trim(); + + if (normalizedValue && !currentTags.includes(normalizedValue)) { + const updatedTags = [...currentTags, normalizedValue]; + + this.localTags.set(updatedTags); + this.isLocalUpdate = true; + + this.tagsChanged.emit(updatedTags); + } + } + + removeTag(index: number): void { + const currentTags = this.localTags(); + const updatedTags = currentTags.filter((_, i) => i !== index); + + this.localTags.set(updatedTags); + this.isLocalUpdate = true; + + this.tagsChanged.emit(updatedTags); } } diff --git a/src/assets/styles/overrides/chip.scss b/src/assets/styles/overrides/chip.scss index 661d1a9f7..fd31e98a5 100644 --- a/src/assets/styles/overrides/chip.scss +++ b/src/assets/styles/overrides/chip.scss @@ -32,8 +32,8 @@ } } -p-chips.ng-invalid.ng-touched { - .p-inputchips-input { - border-color: var(--p-inputchips-invalid-border-color); +.osf-tags-input { + input.ng-invalid.ng-touched { + border-color: var(--p-inputtext-invalid-border-color); } } From bbe9307eabcab25b94f03065a2b38d9514d1d64c Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Thu, 24 Jul 2025 00:35:30 +0300 Subject: [PATCH 6/9] feat(registries-metadata): add page --- src/app/core/interceptors/auth.interceptor.ts | 20 +- .../metadata/project-metadata.component.ts | 2 - .../metadata/services/metadata.service.ts | 2 +- .../mappers/registry-metadata.mapper.ts | 40 ++ src/app/features/registry/models/index.ts | 2 + .../registry-contributor-json-api.model.ts | 114 +++++ .../registry-institutions-json-api.model.ts | 29 ++ .../models/registry-overview.models.ts | 8 +- .../registry-metadata-add.component.ts | 1 - .../registry-metadata.component.html | 7 +- .../registry-metadata.component.ts | 282 ++++++++++-- .../services/registry-metadata.service.ts | 109 ++++- .../registry-metadata.actions.ts | 61 ++- .../registry-metadata.model.ts | 13 +- .../registry-metadata.selectors.ts | 22 +- .../registry-metadata.state.ts | 413 ++++++++---------- .../components/license/license.component.html | 4 +- .../components/license/license.component.ts | 2 + .../cedar-template-form.component.ts | 1 - ...ata-affiliated-institutions.component.html | 12 +- ...adata-affiliated-institutions.component.ts | 1 + ...oject-metadata-contributors.component.html | 12 +- ...project-metadata-contributors.component.ts | 4 +- ...roject-metadata-description.component.html | 12 +- .../project-metadata-description.component.ts | 4 +- .../project-metadata-funding.component.html | 41 +- .../project-metadata-funding.component.ts | 8 +- .../project-metadata-license.component.html | 12 +- .../project-metadata-license.component.ts | 2 +- ...ject-metadata-publication-doi.component.ts | 1 - ...tadata-resource-information.component.html | 18 +- ...metadata-resource-information.component.ts | 3 +- .../project-metadata-subjects.component.html | 1 + .../project-metadata-subjects.component.ts | 2 +- .../contributors-dialog.component.ts | 7 +- .../funding-dialog.component.ts | 27 +- .../license-dialog.component.html | 38 +- .../license-dialog.component.ts | 98 +++-- .../shared-metadata.component.html | 19 +- .../shared-metadata.component.ts | 4 +- .../subjects/subjects.component.html | 96 ++-- .../components/subjects/subjects.component.ts | 7 + .../tags-input/tags-input.component.html | 16 +- .../tags-input/tags-input.component.ts | 5 + 44 files changed, 1064 insertions(+), 518 deletions(-) create mode 100644 src/app/features/registry/models/registry-contributor-json-api.model.ts create mode 100644 src/app/features/registry/models/registry-institutions-json-api.model.ts diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts index 174408c69..7b6a9deaf 100644 --- a/src/app/core/interceptors/auth.interceptor.ts +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -11,15 +11,19 @@ export const authInterceptor: HttpInterceptorFn = ( // 2rjFZwmdDG4rtKj7hGkEMO6XyHBM2lN7XBbsA1e8OqcFhOWu6Z7fQZiheu9RXtzSeVrgOt roman nastyuk if (authToken) { - const authReq = req.clone({ - setHeaders: { - Authorization: `Bearer ${authToken}`, - Accept: req.responseType === 'text' ? '*/*' : 'application/vnd.api+json', - 'Content-Type': 'application/vnd.api+json', - }, - }); + if (!req.url.includes('/api.crossref.org/funders')) { + const authReq = req.clone({ + setHeaders: { + Authorization: `Bearer ${authToken}`, + Accept: req.responseType === 'text' ? '*/*' : 'application/vnd.api+json', + 'Content-Type': 'application/vnd.api+json', + }, + }); - return next(authReq); + return next(authReq); + } else { + return next(req); + } } return next(req); diff --git a/src/app/features/project/metadata/project-metadata.component.ts b/src/app/features/project/metadata/project-metadata.component.ts index 6fc759327..cd7c4a4bf 100644 --- a/src/app/features/project/metadata/project-metadata.component.ts +++ b/src/app/features/project/metadata/project-metadata.component.ts @@ -73,7 +73,6 @@ import { CustomConfirmationService, LoaderService, ToastService } from '@shared/ styleUrl: './project-metadata.component.scss', providers: [DialogService], changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, }) export class ProjectMetadataComponent implements OnInit { private readonly route = inject(ActivatedRoute); @@ -285,7 +284,6 @@ export class ProjectMetadataComponent implements OnInit { ) .subscribe({ next: () => this.toastService.showSuccess('project.metadata.resourceInformation.updated'), - error: () => this.toastService.showError('project.metadata.resourceInformation.updateFailed'), }); } diff --git a/src/app/features/project/metadata/services/metadata.service.ts b/src/app/features/project/metadata/services/metadata.service.ts index 0515535d8..a6c56b206 100644 --- a/src/app/features/project/metadata/services/metadata.service.ts +++ b/src/app/features/project/metadata/services/metadata.service.ts @@ -40,7 +40,7 @@ export class MetadataService { } getFundersList(searchQuery?: string): Observable { - let url = environment.funderApiUrl; + let url = `${environment.funderApiUrl}funders?mailto=support%40osf.io`; if (searchQuery && searchQuery.trim()) { url += `&query=${encodeURIComponent(searchQuery.trim())}`; diff --git a/src/app/features/registry/mappers/registry-metadata.mapper.ts b/src/app/features/registry/mappers/registry-metadata.mapper.ts index 5638443c9..d94a07ef1 100644 --- a/src/app/features/registry/mappers/registry-metadata.mapper.ts +++ b/src/app/features/registry/mappers/registry-metadata.mapper.ts @@ -1,5 +1,6 @@ import { ProjectOverviewContributor } from '@osf/features/project/overview/models'; import { RegistryStatus, RevisionReviewStates } from '@shared/enums'; +import { License } from '@shared/models'; import { BibliographicContributor, @@ -12,6 +13,7 @@ export class RegistryMetadataMapper { static fromMetadataApiResponse(response: Record): RegistryOverview { const attributes = response['attributes'] as Record; const embeds = response['embeds'] as Record; + const relationships = response['relationships'] as Record; const contributors: ProjectOverviewContributor[] = []; if (embeds && embeds['contributors']) { @@ -34,6 +36,41 @@ export class RegistryMetadataMapper { }); } + let license: License | undefined; + let licenseUrl: string | undefined; + + if (embeds && embeds['license']) { + const licenseData = (embeds['license'] as Record)['data'] as Record; + if (licenseData) { + const licenseAttributes = licenseData['attributes'] as Record; + license = { + id: licenseData['id'] as string, + name: licenseAttributes['name'] as string, + text: licenseAttributes['text'] as string, + url: licenseAttributes['url'] as string, + requiredFields: (licenseAttributes['required_fields'] as string[]) || [], + }; + } + } else if (relationships && relationships['license']) { + const licenseRelationship = relationships['license'] as Record; + if (licenseRelationship['links']) { + const licenseLinks = licenseRelationship['links'] as Record; + if (licenseLinks['related'] && typeof licenseLinks['related'] === 'object') { + const relatedLinks = licenseLinks['related'] as Record; + licenseUrl = relatedLinks['href'] as string; + } + } + } + + let nodeLicense: { copyrightHolders: string[]; year: string } | undefined; + if (attributes['node_license']) { + const nodeLicenseData = attributes['node_license'] as Record; + nodeLicense = { + copyrightHolders: (nodeLicenseData['copyright_holders'] as string[]) || [], + year: (nodeLicenseData['year'] as string) || new Date().getFullYear().toString(), + }; + } + return { id: response['id'] as string, type: (response['type'] as string) || 'registrations', @@ -57,6 +94,9 @@ export class RegistryMetadataMapper { analyticsKey: (attributes['analytics_key'] as string) || '', contributors: contributors, subjects: Array.isArray(attributes['subjects']) ? attributes['subjects'].flat() : attributes['subjects'], + license: license, + nodeLicense: nodeLicense, + licenseUrl: licenseUrl, forksCount: 0, citation: '', hasData: false, diff --git a/src/app/features/registry/models/index.ts b/src/app/features/registry/models/index.ts index 1bb999279..e64ed4424 100644 --- a/src/app/features/registry/models/index.ts +++ b/src/app/features/registry/models/index.ts @@ -2,7 +2,9 @@ export * from './get-registry-institutions-json-api.model'; export * from './get-registry-overview-json-api.model'; export * from './get-registry-schema-block-json-api.model'; export * from './get-resource-subjects-json-api.model'; +export * from './registry-contributor-json-api.model'; export * from './registry-institution.model'; +export * from './registry-institutions-json-api.model'; export * from './registry-metadata.models'; export * from './registry-overview.models'; export * from './registry-schema-block.model'; diff --git a/src/app/features/registry/models/registry-contributor-json-api.model.ts b/src/app/features/registry/models/registry-contributor-json-api.model.ts new file mode 100644 index 000000000..3193d114e --- /dev/null +++ b/src/app/features/registry/models/registry-contributor-json-api.model.ts @@ -0,0 +1,114 @@ +export interface RegistryContributorJsonApi { + id: string; + type: 'contributors'; + attributes: { + index: number; + bibliographic: boolean; + permission: string; + unregistered_contributor: string | null; + is_curator: boolean; + }; + relationships: { + users: { + links: { + related: { + href: string; + meta: Record; + }; + }; + data: { + id: string; + type: 'users'; + }; + }; + node: { + links: { + related: { + href: string; + meta: Record; + }; + }; + data: { + id: string; + type: 'nodes'; + }; + }; + }; + embeds?: { + users: { + data: { + id: string; + type: 'users'; + attributes: { + full_name: string; + given_name: string; + middle_names: string; + family_name: string; + suffix: string; + date_registered: string; + active: boolean; + timezone: string; + locale: string; + social: Record; + employment: unknown[]; + education: unknown[]; + }; + relationships: Record; + links: { + html: string; + profile_image: string; + self: string; + iri: string; + }; + }; + }; + }; + links: { + self: string; + }; +} + +export interface RegistryContributorJsonApiResponse { + data: RegistryContributorJsonApi; + links: { + self: string; + }; + meta: { + version: string; + }; +} + +export interface RegistryContributorUpdateRequest { + data: { + id: string; + type: 'contributors'; + attributes: Record; + relationships: Record; + }; +} + +export interface RegistryContributorsListJsonApiResponse { + data: RegistryContributorJsonApi[]; + links: { + first: string | null; + last: string | null; + prev: string | null; + next: string | null; + meta: { + total: number; + per_page: number; + total_bibliographic: number; + }; + }; + meta: { + version: string; + }; +} + +export interface RegistryContributorAddRequest { + data: { + type: 'contributors'; + attributes: Record; + relationships: Record; + }; +} diff --git a/src/app/features/registry/models/registry-institutions-json-api.model.ts b/src/app/features/registry/models/registry-institutions-json-api.model.ts new file mode 100644 index 000000000..e3aab2ad1 --- /dev/null +++ b/src/app/features/registry/models/registry-institutions-json-api.model.ts @@ -0,0 +1,29 @@ +export interface RegistryInstitutionJsonApi { + id: string; + type: string; + attributes: { + name: string; + }; + links: { + self: string; + html: string; + iri: string; + }; +} + +export interface RegistryInstitutionsJsonApiResponse { + data: RegistryInstitutionJsonApi[]; + links: { + first: string | null; + last: string | null; + prev: string | null; + next: string | null; + meta: { + total: number; + per_page: number; + }; + }; + meta: { + version: string; + }; +} diff --git a/src/app/features/registry/models/registry-overview.models.ts b/src/app/features/registry/models/registry-overview.models.ts index 91cdc6d53..b9c34528a 100644 --- a/src/app/features/registry/models/registry-overview.models.ts +++ b/src/app/features/registry/models/registry-overview.models.ts @@ -1,6 +1,7 @@ import { ProjectOverviewContributor } from '@osf/features/project/overview/models'; import { RegistrationQuestions, RegistrySubject } from '@osf/features/registry/models'; import { RegistryStatus, RevisionReviewStates } from '@shared/enums'; +import { License } from '@shared/models'; export interface RegistryOverview { id: string; @@ -24,11 +25,8 @@ export interface RegistryOverview { copyrightHolders: string[]; year: string; }; - license?: { - name: string; - text: string; - url: string; - }; + license?: License; + licenseUrl?: string; identifiers?: { id: string; type: string; diff --git a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts index f563ff161..700e325e7 100644 --- a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts +++ b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.ts @@ -32,7 +32,6 @@ import { templateUrl: './registry-metadata-add.component.html', styleUrl: './registry-metadata-add.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, }) export class RegistryMetadataAddComponent implements OnInit { private readonly route = inject(ActivatedRoute); diff --git a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html index c21f66aeb..0f4c3d417 100644 --- a/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html +++ b/src/app/features/registry/pages/registry-metadata/registry-metadata.component.html @@ -1,6 +1,6 @@
@if (tab.type === 'registry') { { + const registry = this.currentRegistry(); + if (!registry) return false; + + const permissions = registry.currentUserPermissions || []; + return permissions.length === 1 && permissions[0] === 'read'; + }); + constructor() { effect(() => { const records = this.cedarRecords(); @@ -168,6 +195,7 @@ export class RegistryMetadataComponent implements OnInit { this.actions.getBibliographicContributors(this.registryId); this.actions.getCustomItemMetadata(this.registryId); this.actions.getContributors(this.registryId, ResourceType.Registration); + this.actions.getRegistryInstitutions(this.registryId); this.actions.getRegistrySubjects(this.registryId); this.actions.getCedarRecords(this.registryId); this.actions.getCedarTemplates(); @@ -193,52 +221,194 @@ export class RegistryMetadataComponent implements OnInit { } openEditContributorDialog(): void { - // Similar implementation to project metadata - // For now, just show the bibliographic contributors from the API - console.log('Bibliographic contributors:', this.bibliographicContributors()); + const dialogRef = this.dialogService.open(ContributorsDialogComponent, { + width: '800px', + header: this.translateService.instant('project.metadata.contributors.editContributors'), + focusOnShow: false, + closeOnEscape: true, + modal: true, + closable: true, + data: { + projectId: this.currentRegistry()?.id, + contributors: this.contributors(), + isLoading: this.isContributorsLoading(), + isRegistry: true, + }, + }); + + dialogRef.onClose.pipe(filter((result) => !!result && (result.refresh || result.saved))).subscribe({ + next: () => { + this.refreshContributorsData(); + this.toastService.showSuccess('project.metadata.contributors.updateSucceed'); + }, + }); } openEditDescriptionDialog(): void { - // Similar implementation to project metadata - console.log('Edit description for registry:', this.currentRegistry()); + const dialogRef = this.dialogService.open(DescriptionDialogComponent, { + header: this.translateService.instant('project.metadata.description.dialog.header'), + width: '500px', + focusOnShow: false, + closeOnEscape: true, + modal: true, + closable: true, + data: { + currentProject: this.currentRegistry(), + }, + }); + + dialogRef.onClose + .pipe( + filter((result) => !!result), + switchMap((result) => { + const registryId = this.currentRegistry()?.id; + if (registryId) { + return this.actions.updateRegistryDetails(registryId, { description: result }); + } + return EMPTY; + }) + ) + .subscribe({ + next: () => { + this.toastService.showSuccess('project.metadata.description.updated'); + const registryId = this.currentRegistry()?.id; + if (registryId) { + this.actions.getRegistry(registryId); + } + }, + }); } openEditResourceInformationDialog(): void { - // Similar implementation to project metadata - console.log('Edit resource information for registry:', this.currentRegistry()); - } + const dialogRef = this.dialogService.open(ResourceInformationDialogComponent, { + header: this.translateService.instant('project.metadata.resourceInformation.dialog.header'), + width: '500px', + focusOnShow: false, + closeOnEscape: true, + modal: true, + closable: true, + data: { + currentProject: this.currentRegistry(), + customItemMetadata: this.customItemMetadata(), + }, + }); - openEditLicenseDialog(): void { - // Similar implementation to project metadata - console.log('Edit license for registry:', this.currentRegistry()); + dialogRef.onClose + .pipe( + filter((result) => !!result && (result.resourceType || result.resourceLanguage)), + switchMap((result) => { + const registryId = this.currentRegistry()?.id; + if (registryId) { + const currentMetadata = this.customItemMetadata(); + + const updatedMetadata = { + ...currentMetadata, + language: result.resourceLanguage || currentMetadata?.language, + resource_type_general: result.resourceType || currentMetadata?.resource_type_general, + funders: currentMetadata?.funders, + }; + + return this.actions.updateCustomItemMetadata(registryId, updatedMetadata); + } + return EMPTY; + }) + ) + .subscribe({ + next: () => this.toastService.showSuccess('project.metadata.resourceInformation.updated'), + }); } openEditFundingDialog(): void { - this.actions.getFundersList(); - // Similar implementation to project metadata - console.log('Edit funding for registry:', this.currentRegistry()); - } + const dialogRef = this.dialogService.open(FundingDialogComponent, { + header: this.translateService.instant('project.metadata.funding.dialog.header'), + width: '600px', + focusOnShow: false, + closeOnEscape: true, + modal: true, + closable: true, + data: { + funders: this.customItemMetadata().funders, + }, + }); - openEditAffiliatedInstitutionsDialog(): void { - // Similar implementation to project metadata - console.log('Edit affiliated institutions for registry:', this.currentRegistry()); + dialogRef.onClose + .pipe( + filter((result) => !!result && result.fundingEntries), + switchMap((result) => { + const registryId = this.currentRegistry()?.id; + if (registryId) { + const currentMetadata = this.customItemMetadata() || { + language: 'en', + resource_type_general: 'Dataset', + funders: [], + }; + + const updatedMetadata = { + ...currentMetadata, + funders: result.fundingEntries.map( + (entry: { + funderName?: string; + funderIdentifier?: string; + funderIdentifierType?: string; + awardNumber?: string; + awardUri?: string; + awardTitle?: string; + }) => ({ + funder_name: entry.funderName || '', + funder_identifier: entry.funderIdentifier || '', + funder_identifier_type: entry.funderIdentifierType || '', + award_number: entry.awardNumber || '', + award_uri: entry.awardUri || '', + award_title: entry.awardTitle || '', + }) + ), + }; + + return this.actions.updateCustomItemMetadata(registryId, updatedMetadata); + } + + return EMPTY; + }) + ) + .subscribe({ + next: () => this.toastService.showSuccess('project.metadata.funding.updated'), + }); } - handleEditDoi(): void { - this.customConfirmationService.confirmDelete({ - headerKey: this.translateService.instant('project.metadata.doi.dialog.createConfirm.header'), - messageKey: this.translateService.instant('project.metadata.doi.dialog.createConfirm.message'), - acceptLabelKey: this.translateService.instant('common.buttons.create'), - acceptLabelType: 'primary', - onConfirm: () => { - const registryId = this.currentRegistry()?.id; - if (registryId) { - this.actions.updateRegistryDetails(registryId, { doi: true }).subscribe({ - next: () => this.toastService.showSuccess('registry.metadata.doi.created'), - }); - } + openEditAffiliatedInstitutionsDialog(): void { + const dialogRef = this.dialogService.open(AffiliatedInstitutionsDialogComponent, { + header: this.translateService.instant('project.metadata.affiliatedInstitutions.dialog.header'), + width: '500px', + focusOnShow: false, + closeOnEscape: true, + modal: true, + closable: true, + data: { + currentProject: this.getCurrentInstanceForTemplate(), }, }); + + dialogRef.onClose + .pipe( + filter((result) => !!result), + switchMap((result) => { + const registryId = this.currentRegistry()?.id; + if (registryId) { + const institutionsData = result.map((institutionId: string) => ({ + type: 'institutions', + id: institutionId, + })); + + return this.actions.updateRegistryInstitutions(registryId, institutionsData); + } + return EMPTY; + }) + ) + .subscribe({ + next: () => { + this.toastService.showSuccess('project.metadata.affiliatedInstitutions.updated'); + }, + }); } getSubjectChildren(parentId: string) { @@ -250,11 +420,27 @@ export class RegistryMetadataComponent implements OnInit { } updateSelectedSubjects(subjects: SubjectModel[]) { - this.actions.updateResourceSubjects(this.registryId, ResourceType.Registration, subjects); + const subjectData = subjects.map((subject) => ({ + type: 'subjects', + id: subject.id, + })); + this.actions.updateRegistrySubjects(this.registryId, subjectData); } getCurrentInstanceForTemplate(): ProjectOverview { - return this.currentRegistry() as unknown as ProjectOverview; + const registry = this.currentRegistry(); + const institutions = this.institutions(); + + const institutionsFormatted = + institutions?.map((inst) => ({ + id: inst.id, + name: inst.attributes.name, + })) || []; + + return { + ...registry, + institutions: institutionsFormatted, + } as unknown as ProjectOverview; } getCustomMetadataForTemplate(): CustomItemMetadataRecord { @@ -391,4 +577,8 @@ export class RegistryMetadataComponent implements OnInit { } } } + + private refreshContributorsData(): void { + this.actions.getContributors(this.registryId, ResourceType.Registration); + } } diff --git a/src/app/features/registry/services/registry-metadata.service.ts b/src/app/features/registry/services/registry-metadata.service.ts index 01e4700fa..d138ba8a8 100644 --- a/src/app/features/registry/services/registry-metadata.service.ts +++ b/src/app/features/registry/services/registry-metadata.service.ts @@ -9,13 +9,17 @@ import { CedarMetadataRecordJsonApi, CedarMetadataTemplateJsonApi, } from '@osf/features/project/metadata/models'; +import { License } from '@shared/models'; import { RegistryMetadataMapper } from '../mappers'; import { BibliographicContributorsJsonApi, - CrossRefFundersResponse, CustomItemMetadataRecord, CustomItemMetadataResponse, + RegistryContributorAddRequest, + RegistryContributorJsonApiResponse, + RegistryContributorUpdateRequest, + RegistryInstitutionsJsonApiResponse, RegistryOverview, RegistrySubjectsJsonApi, UserInstitutionsResponse, @@ -53,22 +57,17 @@ export class RegistryMetadataService { } updateCustomItemMetadata(guid: string, metadata: CustomItemMetadataRecord): Observable { - return this.jsonApiService.put(`${this.apiUrl}/custom_item_metadata_records/${guid}/`, { - data: { - type: 'custom-item-metadata-records', - attributes: metadata, - }, - }); - } - - getFundersList(searchQuery?: string): Observable { - let url = environment.funderApiUrl; - - if (searchQuery && searchQuery.trim()) { - url += `&query=${encodeURIComponent(searchQuery.trim())}`; - } - - return this.jsonApiService.get(url); + return this.jsonApiService.patch( + `${this.apiUrl}/custom_item_metadata_records/${guid}/`, + { + data: { + id: guid, + type: 'custom-item-metadata-records', + attributes: metadata, + relationships: {}, + }, + } + ); } getRegistryForMetadata(registryId: string): Observable { @@ -149,4 +148,80 @@ export class RegistryMetadataService { data ); } + + updateRegistrySubjects( + registryId: string, + subjects: { type: string; id: string }[] + ): Observable<{ data: { type: string; id: string }[] }> { + return this.jsonApiService.patch<{ data: { type: string; id: string }[] }>( + `${this.apiUrl}/registrations/${registryId}/relationships/subjects/`, + { + data: subjects, + } + ); + } + + updateRegistryInstitutions( + registryId: string, + institutions: { type: string; id: string }[] + ): Observable<{ data: { type: string; id: string }[] }> { + return this.jsonApiService.patch<{ data: { type: string; id: string }[] }>( + `${this.apiUrl}/registrations/${registryId}/relationships/institutions/`, + { + data: institutions, + } + ); + } + + getLicenseFromUrl(licenseUrl: string): Observable { + return this.jsonApiService.get<{ data: Record }>(licenseUrl).pipe( + map((response) => { + const licenseData = response.data; + const attributes = licenseData['attributes'] as Record; + + return { + id: licenseData['id'] as string, + name: attributes['name'] as string, + text: attributes['text'] as string, + url: attributes['url'] as string, + requiredFields: (attributes['required_fields'] as string[]) || [], + } as License; + }) + ); + } + + getRegistryInstitutions( + registryId: string, + page = 1, + pageSize = 100 + ): Observable { + const params: Record = { + 'fields[institutions]': 'name', + page: page, + 'page[size]': pageSize, + }; + + return this.jsonApiService.get(`${this.apiUrl}/registrations/${registryId}/institutions/`, params); + } + + updateRegistryContributor( + registryId: string, + contributorId: string, + updateData: RegistryContributorUpdateRequest + ): Observable { + return this.jsonApiService.patch( + `${this.apiUrl}/registrations/${registryId}/contributors/${contributorId}/`, + updateData + ); + } + + addRegistryContributor( + registryId: string, + contributorData: RegistryContributorAddRequest + ): Observable { + return this.jsonApiService.post( + `${this.apiUrl}/registrations/${registryId}/contributors/`, + contributorData + ); + } } diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts index ca8b1c9e0..f7cec76ca 100644 --- a/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.actions.ts @@ -37,11 +37,6 @@ export class UpdateRegistryDetails { ) {} } -export class GetFundersList { - static readonly type = '[RegistryMetadata] Get Funders List'; - constructor(public search?: string) {} -} - export class GetUserInstitutions { static readonly type = '[RegistryMetadata] Get User Institutions'; constructor( @@ -60,6 +55,57 @@ export class GetRegistrySubjects { ) {} } +export class UpdateRegistrySubjects { + static readonly type = '[RegistryMetadata] Update Registry Subjects'; + constructor( + public registryId: string, + public subjects: { type: string; id: string }[] + ) {} +} + +export class UpdateRegistryInstitutions { + static readonly type = '[RegistryMetadata] Update Registry Institutions'; + constructor( + public registryId: string, + public institutions: { type: string; id: string }[] + ) {} +} + +export class GetRegistryInstitutions { + static readonly type = '[RegistryMetadata] Get Registry Institutions'; + constructor( + public registryId: string, + public page?: number, + public pageSize?: number + ) {} +} + +export class UpdateRegistryContributor { + static readonly type = '[RegistryMetadata] Update Registry Contributor'; + constructor( + public registryId: string, + public contributorId: string, + public updateData: { + id: string; + type: 'contributors'; + attributes: Record; + relationships: Record; + } + ) {} +} + +export class AddRegistryContributor { + static readonly type = '[RegistryMetadata] Add Registry Contributor'; + constructor( + public registryId: string, + public contributorData: { + type: 'contributors'; + attributes: Record; + relationships: Record; + } + ) {} +} + export class GetCedarMetadataTemplates { static readonly type = '[RegistryMetadata] Get Cedar Metadata Templates'; constructor(public url?: string) {} @@ -87,3 +133,8 @@ export class AddCedarMetadataRecordToState { static readonly type = '[RegistryMetadata] Add Cedar Metadata Record To State'; constructor(public record: CedarMetadataRecordData) {} } + +export class GetLicenseFromUrl { + static readonly type = '[RegistryMetadata] Get License From URL'; + constructor(public licenseUrl: string) {} +} diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts index a03f71a61..a55821781 100644 --- a/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.model.ts @@ -3,25 +3,26 @@ import { CedarMetadataRecordData, CedarMetadataTemplateJsonApi, } from '@osf/features/project/metadata/models'; -import { AsyncStateModel } from '@shared/models'; +import { AsyncStateModel, License } from '@shared/models'; -import { RegistryOverview } from '../../models'; import { BibliographicContributor, - CrossRefFunder, CustomItemMetadataRecord, + RegistryInstitutionJsonApi, + RegistryOverview, RegistrySubjectData, UserInstitution, -} from '../../models/registry-metadata.models'; +} from '../../models'; export interface RegistryMetadataStateModel { registry: AsyncStateModel; bibliographicContributors: AsyncStateModel; - customItemMetadata: AsyncStateModel; - fundersList: AsyncStateModel; + customItemMetadata: AsyncStateModel; userInstitutions: AsyncStateModel; + institutions: AsyncStateModel; subjects: AsyncStateModel; cedarTemplates: AsyncStateModel; cedarRecord: AsyncStateModel; cedarRecords: AsyncStateModel; + license: AsyncStateModel; } diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts index d974ba725..ffb10a74c 100644 --- a/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.selectors.ts @@ -9,6 +9,16 @@ export class RegistryMetadataSelectors { return state.registry.data; } + @Selector([RegistryMetadataState]) + static getLicense(state: RegistryMetadataStateModel) { + return state.license.data; + } + + @Selector([RegistryMetadataState]) + static getLicenseLoading(state: RegistryMetadataStateModel) { + return state.license.isLoading; + } + @Selector([RegistryMetadataState]) static getRegistryLoading(state: RegistryMetadataStateModel) { return state.registry.isLoading; @@ -30,18 +40,18 @@ export class RegistryMetadataSelectors { } @Selector([RegistryMetadataState]) - static getCustomItemMetadataLoading(state: RegistryMetadataStateModel) { - return state.customItemMetadata.isLoading; + static getInstitutions(state: RegistryMetadataStateModel) { + return state.institutions.data; } @Selector([RegistryMetadataState]) - static getFundersList(state: RegistryMetadataStateModel) { - return state.fundersList.data; + static getInstitutionsLoading(state: RegistryMetadataStateModel) { + return state.institutions.isLoading; } @Selector([RegistryMetadataState]) - static getFundersLoading(state: RegistryMetadataStateModel) { - return state.fundersList.isLoading; + static getCustomItemMetadataLoading(state: RegistryMetadataStateModel) { + return state.customItemMetadata.isLoading; } @Selector([RegistryMetadataState]) diff --git a/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts b/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts index 75c90b42b..c07f81eaa 100644 --- a/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts +++ b/src/app/features/registry/store/registry-metadata/registry-metadata.state.ts @@ -1,41 +1,51 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { finalize, tap } from 'rxjs'; +import { catchError, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { handleSectionError } from '@core/handlers'; import { CedarMetadataRecord, CedarMetadataRecordJsonApi } from '@osf/features/project/metadata/models'; +import { ResourceType } from '@shared/enums'; +import { GetAllContributors } from '@shared/stores'; import { RegistryMetadataMapper } from '../../mappers'; +import { CustomItemMetadataRecord } from '../../models'; import { RegistryMetadataService } from '../../services/registry-metadata.service'; import { AddCedarMetadataRecordToState, + AddRegistryContributor, CreateCedarMetadataRecord, GetBibliographicContributors, GetCedarMetadataTemplates, GetCustomItemMetadata, - GetFundersList, + GetLicenseFromUrl, GetRegistryCedarMetadataRecords, GetRegistryForMetadata, + GetRegistryInstitutions, GetRegistrySubjects, GetUserInstitutions, UpdateCedarMetadataRecord, UpdateCustomItemMetadata, + UpdateRegistryContributor, UpdateRegistryDetails, + UpdateRegistryInstitutions, + UpdateRegistrySubjects, } from './registry-metadata.actions'; import { RegistryMetadataStateModel } from './registry-metadata.model'; const initialState: RegistryMetadataStateModel = { registry: { data: null, isLoading: false, error: null }, bibliographicContributors: { data: [], isLoading: false, error: null }, - customItemMetadata: { data: null, isLoading: false, error: null }, - fundersList: { data: [], isLoading: false, error: null }, + customItemMetadata: { data: {}, isLoading: false, error: null }, userInstitutions: { data: [], isLoading: false, error: null }, + institutions: { data: [], isLoading: false, error: null }, subjects: { data: [], isLoading: false, error: null }, cedarTemplates: { data: null, isLoading: false, error: null }, cedarRecord: { data: null, isLoading: false, error: null }, cedarRecords: { data: [], isLoading: false, error: null }, + license: { data: null, isLoading: false, error: null }, }; @State({ @@ -57,35 +67,20 @@ export class RegistryMetadataState { }); return this.registryMetadataService.getRegistryForMetadata(action.registryId).pipe( - tap({ - next: (registry) => { - ctx.patchState({ - registry: { - data: registry, - isLoading: false, - error: null, - }, - }); - }, - error: (error) => { - ctx.patchState({ - registry: { - data: ctx.getState().registry.data, - error: error.message, - isLoading: false, - }, - }); - }, - }), - finalize(() => + tap((registry) => { ctx.patchState({ registry: { - data: ctx.getState().registry.data, - error: null, + data: registry, isLoading: false, + error: null, }, - }) - ) + }); + + if (registry.licenseUrl) { + ctx.dispatch(new GetLicenseFromUrl(registry.licenseUrl)); + } + }), + catchError((error) => handleSectionError(ctx, 'registry', error)) ); } @@ -102,35 +97,17 @@ export class RegistryMetadataState { return this.registryMetadataService .getBibliographicContributors(action.registryId, action.page, action.pageSize) .pipe( - tap({ - next: (response) => { - const contributors = RegistryMetadataMapper.mapBibliographicContributors(response); - ctx.patchState({ - bibliographicContributors: { - data: contributors, - isLoading: false, - error: null, - }, - }); - }, - error: (error) => { - ctx.patchState({ - bibliographicContributors: { - data: [], - isLoading: false, - error: error.message, - }, - }); - }, - }), - finalize(() => + tap((response) => { + const contributors = RegistryMetadataMapper.mapBibliographicContributors(response); ctx.patchState({ bibliographicContributors: { - ...ctx.getState().bibliographicContributors, + data: contributors, isLoading: false, + error: null, }, - }) - ) + }); + }), + catchError((error) => handleSectionError(ctx, 'bibliographicContributors', error)) ); } @@ -145,87 +122,79 @@ export class RegistryMetadataState { }); return this.registryMetadataService.getRegistrySubjects(action.registryId, action.page, action.pageSize).pipe( - tap({ - next: (response) => { - ctx.patchState({ - subjects: { - data: response.data, - isLoading: false, - error: null, - }, - }); - }, - error: (error) => { - ctx.patchState({ - subjects: { - data: [], - isLoading: false, - error: error.message, - }, - }); - }, - }), - finalize(() => + tap((response) => { ctx.patchState({ subjects: { - ...ctx.getState().subjects, + data: response.data, isLoading: false, + error: null, }, - }) - ) + }); + }), + catchError((error) => handleSectionError(ctx, 'subjects', error)) + ); + } + + @Action(GetRegistryInstitutions) + getRegistryInstitutions(ctx: StateContext, action: GetRegistryInstitutions) { + ctx.patchState({ + institutions: { + data: [], + isLoading: true, + error: null, + }, + }); + + return this.registryMetadataService.getRegistryInstitutions(action.registryId, action.page, action.pageSize).pipe( + tap((response) => { + ctx.patchState({ + institutions: { + data: response.data, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'institutions', error)) ); } @Action(GetCustomItemMetadata) getCustomItemMetadata(ctx: StateContext, action: GetCustomItemMetadata) { ctx.patchState({ - customItemMetadata: { data: null, isLoading: true, error: null }, + customItemMetadata: { data: {}, isLoading: true, error: null }, }); return this.registryMetadataService.getCustomItemMetadata(action.guid).pipe( - tap({ - next: (response) => { - ctx.patchState({ - customItemMetadata: { data: response.data.attributes, isLoading: false, error: null }, - }); - }, - error: (error) => { - ctx.patchState({ - customItemMetadata: { data: null, isLoading: false, error: error.message }, - }); - }, - }), - finalize(() => + tap((response) => { + const metadataAttributes = response?.data?.attributes || (response as unknown as CustomItemMetadataRecord); + ctx.patchState({ - customItemMetadata: { - ...ctx.getState().customItemMetadata, - isLoading: false, - }, - }) - ) + customItemMetadata: { data: metadataAttributes, isLoading: false, error: null }, + }); + }), + catchError((error) => handleSectionError(ctx, 'customItemMetadata', error)) ); } @Action(UpdateCustomItemMetadata) updateCustomItemMetadata(ctx: StateContext, action: UpdateCustomItemMetadata) { ctx.patchState({ - customItemMetadata: { data: null, isLoading: true, error: null }, + customItemMetadata: { data: {} as CustomItemMetadataRecord, isLoading: true, error: null }, }); return this.registryMetadataService.updateCustomItemMetadata(action.guid, action.metadata).pipe( - tap({ - next: (response) => { - ctx.patchState({ - customItemMetadata: { data: response.data.attributes, isLoading: false, error: null }, - }); - }, - error: (error) => { - ctx.patchState({ - customItemMetadata: { ...ctx.getState().customItemMetadata, isLoading: false, error: error.message }, - }); - }, + tap((response) => { + const metadataAttributes = response?.data?.attributes || (response as unknown as CustomItemMetadataRecord); + ctx.patchState({ + customItemMetadata: { + data: { ...ctx.getState().customItemMetadata.data, ...metadataAttributes }, + isLoading: false, + error: null, + }, + }); }), - finalize(() => ctx.patchState({ customItemMetadata: { ...ctx.getState().customItemMetadata, isLoading: false } })) + catchError((error) => handleSectionError(ctx, 'customItemMetadata', error)) ); } @@ -240,74 +209,21 @@ export class RegistryMetadataState { }); return this.registryMetadataService.updateRegistryDetails(action.registryId, action.updates).pipe( - tap({ - next: (updatedRegistry) => { - const currentRegistry = ctx.getState().registry.data; + tap((updatedRegistry) => { + const currentRegistry = ctx.getState().registry.data; - ctx.patchState({ - registry: { - data: { - ...currentRegistry, - ...updatedRegistry, - }, - error: null, - isLoading: false, - }, - }); - }, - error: (error) => { - ctx.patchState({ - registry: { - ...ctx.getState().registry, - error: error.message, - isLoading: false, - }, - }); - }, - }), - finalize(() => ctx.patchState({ registry: { - ...ctx.getState().registry, + data: { + ...currentRegistry, + ...updatedRegistry, + }, error: null, isLoading: false, }, - }) - ) - ); - } - - @Action(GetFundersList) - getFundersList(ctx: StateContext, action: GetFundersList) { - ctx.patchState({ - fundersList: { data: [], isLoading: true, error: null }, - }); - - return this.registryMetadataService.getFundersList(action.search).pipe( - tap({ - next: (response) => { - ctx.patchState({ - fundersList: { data: response.message.items, isLoading: false, error: null }, - }); - }, - error: (error) => { - ctx.patchState({ - fundersList: { - ...ctx.getState().fundersList, - isLoading: false, - error: error.message, - }, - }); - }, + }); }), - finalize(() => - ctx.patchState({ - fundersList: { - ...ctx.getState().fundersList, - isLoading: false, - }, - }) - ) + catchError((error) => handleSectionError(ctx, 'registry', error)) ); } @@ -322,35 +238,16 @@ export class RegistryMetadataState { }); return this.registryMetadataService.getUserInstitutions(action.userId, action.page, action.pageSize).pipe( - tap({ - next: (response) => { - ctx.patchState({ - userInstitutions: { - data: response.data, - isLoading: false, - error: null, - }, - }); - }, - error: (error) => { - ctx.patchState({ - userInstitutions: { - ...ctx.getState().userInstitutions, - error: error.message, - isLoading: false, - }, - }); - }, - }), - finalize(() => + tap((response) => { ctx.patchState({ userInstitutions: { - ...ctx.getState().userInstitutions, - error: null, + data: response.data, isLoading: false, + error: null, }, - }) - ) + }); + }), + catchError((error) => handleSectionError(ctx, 'userInstitutions', error)) ); } @@ -365,34 +262,16 @@ export class RegistryMetadataState { }); return this.registryMetadataService.getCedarMetadataTemplates(action.url).pipe( - tap({ - next: (response) => { - ctx.patchState({ - cedarTemplates: { - data: response, - error: null, - isLoading: false, - }, - }); - }, - error: (error) => { - ctx.patchState({ - cedarTemplates: { - ...ctx.getState().cedarTemplates, - error: error.message, - isLoading: false, - }, - }); - }, - }), - finalize(() => + tap((response) => { ctx.patchState({ cedarTemplates: { - ...ctx.getState().cedarTemplates, + data: response, + error: null, isLoading: false, }, - }) - ) + }); + }), + catchError((error) => handleSectionError(ctx, 'cedarTemplates', error)) ); } @@ -417,7 +296,8 @@ export class RegistryMetadataState { isLoading: false, }, }); - }) + }), + catchError((error) => handleSectionError(ctx, 'cedarRecords', error)) ); } @@ -463,4 +343,93 @@ export class RegistryMetadataState { }, }); } + + @Action(UpdateRegistrySubjects) + updateRegistrySubjects(ctx: StateContext, action: UpdateRegistrySubjects) { + return this.registryMetadataService.updateRegistrySubjects(action.registryId, action.subjects); + } + + @Action(UpdateRegistryInstitutions) + updateRegistryInstitutions(ctx: StateContext, action: UpdateRegistryInstitutions) { + return this.registryMetadataService.updateRegistryInstitutions(action.registryId, action.institutions).pipe( + tap({ + next: () => { + ctx.dispatch(new GetRegistryInstitutions(action.registryId)); + }, + }) + ); + } + + @Action(UpdateRegistryContributor) + updateRegistryContributor(ctx: StateContext, action: UpdateRegistryContributor) { + const updateRequest = { + data: action.updateData, + }; + + return this.registryMetadataService + .updateRegistryContributor(action.registryId, action.contributorId, updateRequest) + .pipe( + tap({ + next: () => { + ctx.dispatch(new GetBibliographicContributors(action.registryId)); + ctx.dispatch(new GetAllContributors(action.registryId, ResourceType.Registration)); + ctx.dispatch(new GetRegistryForMetadata(action.registryId)); + }, + }) + ); + } + + @Action(AddRegistryContributor) + addRegistryContributor(ctx: StateContext, action: AddRegistryContributor) { + const addRequest = { + data: action.contributorData, + }; + + return this.registryMetadataService.addRegistryContributor(action.registryId, addRequest).pipe( + tap({ + next: () => { + ctx.dispatch(new GetBibliographicContributors(action.registryId)); + ctx.dispatch(new GetAllContributors(action.registryId, ResourceType.Registration)); + ctx.dispatch(new GetRegistryForMetadata(action.registryId)); + }, + }) + ); + } + + @Action(GetLicenseFromUrl) + getLicenseFromUrl(ctx: StateContext, action: GetLicenseFromUrl) { + ctx.patchState({ + license: { + data: null, + isLoading: true, + error: null, + }, + }); + + return this.registryMetadataService.getLicenseFromUrl(action.licenseUrl).pipe( + tap((license) => { + ctx.patchState({ + license: { + data: license, + isLoading: false, + error: null, + }, + }); + + const currentRegistry = ctx.getState().registry.data; + if (currentRegistry) { + ctx.patchState({ + registry: { + ...ctx.getState().registry, + data: { + ...currentRegistry, + license: license, + }, + }, + }); + } + }), + catchError((error) => handleSectionError(ctx, 'license', error)) + ); + } } diff --git a/src/app/shared/components/license/license.component.html b/src/app/shared/components/license/license.component.html index 8fd8dcadf..fb87b212e 100644 --- a/src/app/shared/components/license/license.component.html +++ b/src/app/shared/components/license/license.component.html @@ -3,6 +3,8 @@ [(ngModel)]="selectedLicense" optionLabel="name" styleClass="" + appendTo="body" + [class.md:w-full]="fullWidthSelect()" [placeholder]="'shared.license.selectLicense' | translate" (onChange)="onSelectLicense($event.value)" class="mt-4 w-full md:w-6" @@ -35,7 +37,7 @@

- @if (selectedLicense()!.requiredFields.length) { + @if (selectedLicense()!.requiredFields.length && showInternalButtons()) {
(null); licenses = input.required(); isSubmitting = input(false); + showInternalButtons = input(true); + fullWidthSelect = input(false); selectedLicense = model(null); createLicense = output<{ id: string; licenseOptions: LicenseOptions }>(); selectLicense = output(); diff --git a/src/app/shared/components/shared-metadata/components/cedar-template-form/cedar-template-form.component.ts b/src/app/shared/components/shared-metadata/components/cedar-template-form/cedar-template-form.component.ts index a117ea733..90954aa47 100644 --- a/src/app/shared/components/shared-metadata/components/cedar-template-form/cedar-template-form.component.ts +++ b/src/app/shared/components/shared-metadata/components/cedar-template-form/cedar-template-form.component.ts @@ -35,7 +35,6 @@ interface CedarEditorElement extends HTMLElement { schemas: [CUSTOM_ELEMENTS_SCHEMA], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, }) export class CedarTemplateFormComponent implements OnInit { emitData = output(); diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html index c9bc91e43..af6b5d311 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html +++ b/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.html @@ -2,11 +2,13 @@

{{ 'project.overview.metadata.affiliatedInstitutions' | translate }}

- + @if (!readonly()) { + + }
@if (affiliatedInstitutions()) { diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts index e7b1428b6..fe0c2fa0d 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts +++ b/src/app/shared/components/shared-metadata/components/project-metadata-affiliated-institutions/project-metadata-affiliated-institutions.component.ts @@ -17,4 +17,5 @@ export class ProjectMetadataAffiliatedInstitutionsComponent { openEditAffiliatedInstitutionsDialog = output(); affiliatedInstitutions = input([]); + readonly = input(false); } diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.html index b16a0d5dc..ca74df140 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.html +++ b/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.html @@ -2,11 +2,13 @@

{{ 'project.overview.metadata.contributors' | translate }}

- + @if (!readonly()) { + + }
@if (contributors()) { diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts index d170c2bcd..994ace939 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts +++ b/src/app/shared/components/shared-metadata/components/project-metadata-contributors/project-metadata-contributors.component.ts @@ -15,6 +15,6 @@ import { ProjectOverviewContributor } from '@osf/features/project/overview/model }) export class ProjectMetadataContributorsComponent { openEditContributorDialog = output(); - - contributors = input.required(); + contributors = input([]); + readonly = input(false); } diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.html index 680349ad8..d682bd318 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.html +++ b/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.html @@ -2,11 +2,13 @@

{{ 'project.overview.metadata.description' | translate }}

- + @if (!readonly()) { + + }
@if (description()) { diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.ts index 14461e41d..d28dbd765 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.ts +++ b/src/app/shared/components/shared-metadata/components/project-metadata-description/project-metadata-description.component.ts @@ -13,6 +13,6 @@ import { ChangeDetectionStrategy, Component, input, output } from '@angular/core }) export class ProjectMetadataDescriptionComponent { openEditDescriptionDialog = output(); - - description = input.required(); + description = input.required(); + readonly = input(false); } diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.html index e9e0d0186..b551468ff 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.html +++ b/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.html @@ -2,21 +2,38 @@

{{ 'project.overview.metadata.fundingSupport' | translate }}

- + @if (!readonly()) { + + }
- @if (supplements()) { + @if (funders()) {
- @for (supplement of supplements(); track supplement.id) { -
-

{{ supplement.title }}

-

{{ supplement.dateCreated | date: 'MMM d, y' }}

- @if (supplement.url) { - {{ supplement.url }} + @for (funder of funders(); track funder.funder_identifier) { +
+

{{ 'project.files.detail.projectMetadata.fields.funder' | translate }}: {{ funder.funder_name }}

+ +

{{ 'project.files.detail.projectMetadata.fields.awardTitle' | translate }}: {{ funder.award_title }}

+ + @if (funder.award_uri) { +

+ {{ 'project.files.detail.projectMetadata.fields.awardUri' | translate }}: + {{ funder.award_title }} +

+ } + + @if (funder.award_number) { +

+ {{ 'project.files.detail.projectMetadata.fields.awardNumber' | translate }} : {{ funder.award_number }} +

+ } + + @if (funder.award_uri) { + {{ funder.award_uri }} }
} diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.ts index e170c3b8f..434a3a783 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.ts +++ b/src/app/shared/components/shared-metadata/components/project-metadata-funding/project-metadata-funding.component.ts @@ -3,19 +3,19 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; -import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { ProjectSupplements } from '@osf/features/project/overview/models'; +import { Funder } from '@osf/features/project/metadata/models'; @Component({ selector: 'osf-project-metadata-funding', - imports: [Button, Card, TranslatePipe, DatePipe], + imports: [Button, Card, TranslatePipe], templateUrl: './project-metadata-funding.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProjectMetadataFundingComponent { openEditFundingDialog = output(); - supplements = input([]); + funders = input([]); + readonly = input(false); } diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.html index 113231940..53bca92a9 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.html +++ b/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.html @@ -2,11 +2,13 @@

{{ 'project.overview.metadata.license' | translate }}

- + @if (!hideEditLicense()) { + + }
@if (license()) { diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.ts index 8211b1e11..23e031ff4 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.ts +++ b/src/app/shared/components/shared-metadata/components/project-metadata-license/project-metadata-license.component.ts @@ -15,6 +15,6 @@ import { License } from '@shared/models'; }) export class ProjectMetadataLicenseComponent { openEditLicenseDialog = output(); - + hideEditLicense = input(false); license = input({} as License); } diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts index e43a19bb4..fc06cc2fe 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts +++ b/src/app/shared/components/shared-metadata/components/project-metadata-publication-doi/project-metadata-publication-doi.component.ts @@ -12,7 +12,6 @@ import { ProjectIdentifiers } from '@osf/features/project/overview/models'; imports: [Button, Card, TranslatePipe], templateUrl: './project-metadata-publication-doi.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, }) export class ProjectMetadataPublicationDoiComponent { openEditPublicationDoiDialog = output(); diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.html index 18c70e4fc..77b345e2b 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.html +++ b/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.html @@ -2,23 +2,25 @@

{{ 'project.overview.metadata.resourceInformation' | translate }}

- + @if (!readonly()) { + + }
- @if (customItemMetadata()?.resource_type_general) { + @if (customItemMetadata().resource_type_general) {

{{ 'project.overview.metadata.resourceType' | translate }}: - {{ customItemMetadata()?.resource_type_general | titlecase }} + {{ customItemMetadata().resource_type_general | titlecase }}

{{ 'project.overview.metadata.resourceLanguage' | translate }}: - {{ getLanguageName(customItemMetadata()?.language || '') }} + {{ getLanguageName(customItemMetadata().language || '') }}

} @else { diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.ts b/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.ts index 64f17e4e2..0047146bc 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.ts +++ b/src/app/shared/components/shared-metadata/components/project-metadata-resource-information/project-metadata-resource-information.component.ts @@ -19,7 +19,8 @@ import { LanguageCodeModel } from '@shared/models'; export class ProjectMetadataResourceInformationComponent { openEditResourceInformationDialog = output(); - customItemMetadata = input.required(); + customItemMetadata = input.required(); + readonly = input(false); protected readonly languageCodes = languageCodes; getLanguageName(languageCode: string): string { diff --git a/src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.html b/src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.html index 5c6529cfc..75aaa17af 100644 --- a/src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.html +++ b/src/app/shared/components/shared-metadata/components/project-metadata-subjects/project-metadata-subjects.component.html @@ -1,5 +1,6 @@ (); isSubjectsUpdating = input.required(); + readonly = input(false); getSubjectChildren = output(); searchSubjects = output(); diff --git a/src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.ts index 4621dc55d..c563599cf 100644 --- a/src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.ts +++ b/src/app/shared/components/shared-metadata/dialogs/contributors-dialog/contributors-dialog.component.ts @@ -55,11 +55,14 @@ export class ContributorsDialogComponent implements OnInit { addContributor: AddContributor, }); + private readonly resourceType: ResourceType; private readonly projectId: string; constructor() { this.projectId = this.config.data?.projectId; + this.resourceType = this.config.data?.['isRegistry'] ? ResourceType.Registration : ResourceType.Project; + this.contributors.set(this.config.data?.contributors || []); this.isContributorsLoading.set(this.config.data?.isLoading || false); } @@ -91,7 +94,7 @@ export class ContributorsDialogComponent implements OnInit { .subscribe((res: ContributorDialogAddModel) => { if (res?.type === AddContributorType.Registered) { const addRequests = res.data.map((payload) => - this.actions.addContributor(this.projectId, ResourceType.Project, payload) + this.actions.addContributor(this.projectId, this.resourceType, payload) ); forkJoin(addRequests).subscribe(() => { @@ -104,7 +107,7 @@ export class ContributorsDialogComponent implements OnInit { removeContributor(contributor: ContributorModel): void { this.actions - .deleteContributor(this.projectId, ResourceType.Project, contributor.userId) + .deleteContributor(this.projectId, this.resourceType, contributor.userId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { diff --git a/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts index a1185bcc6..7c7bf6d70 100644 --- a/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts +++ b/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts @@ -14,6 +14,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { + Funder, FunderOption, FundingDialogResult, FundingEntryData, @@ -22,7 +23,6 @@ import { SupplementData, } from '@osf/features/project/metadata/models'; import { GetFundersList, ProjectMetadataSelectors } from '@osf/features/project/metadata/store'; -import { ProjectOverview } from '@osf/features/project/overview/models'; @Component({ selector: 'osf-funding-dialog', @@ -69,10 +69,6 @@ export class FundingDialogComponent implements OnInit { }); } - get currentProject(): ProjectOverview | null { - return this.config.data ? this.config.data.currentProject || null : null; - } - get fundingEntries() { return this.fundingForm.get('fundingEntries') as FormArray>; } @@ -80,7 +76,21 @@ export class FundingDialogComponent implements OnInit { ngOnInit(): void { this.actions.getFundersList(); - this.addFundingEntry(); + const configFunders = this.config.data?.funders; + if (configFunders && configFunders.length > 0) { + configFunders.forEach((funder: Funder) => { + this.addFundingEntry({ + funderName: funder.funder_name || '', + funderIdentifier: funder.funder_identifier || '', + funderIdentifierType: funder.funder_identifier_type || 'DOI', + awardTitle: funder.award_title || '', + awardUri: funder.award_uri || '', + awardNumber: funder.award_number || '', + }); + }); + } else { + this.addFundingEntry(); + } this.searchSubject .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) @@ -136,8 +146,8 @@ export class FundingDialogComponent implements OnInit { const entry = this.fundingEntries.at(index); entry.patchValue({ funderName: selectedFunder.name, - funderIdentifier: selectedFunder.id, - funderIdentifierType: 'DOI', + funderIdentifier: selectedFunder.uri, + funderIdentifierType: 'Crossref Funder ID', }); } } @@ -150,7 +160,6 @@ export class FundingDialogComponent implements OnInit { const result: FundingDialogResult = { fundingEntries: fundingData, - projectId: this.currentProject ? this.currentProject.id : undefined, }; this.dialogRef.close(result); diff --git a/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.html b/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.html index 78a9d5e98..719354bb9 100644 --- a/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.html +++ b/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.html @@ -2,33 +2,25 @@ @if (licensesLoading()) { } @else { -
-
- - -
+
+

{{ 'project.metadata.license.dialog.chooseLicense.label' | translate }}

- @if (selectedLicenseText()) { -
-

{{ selectedLicenseText() }}

-
- } - + +
}
- +
diff --git a/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.ts index c939898fe..dcd03f48e 100644 --- a/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.ts +++ b/src/app/shared/components/shared-metadata/dialogs/license-dialog/license-dialog.component.ts @@ -4,23 +4,17 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { Select } from 'primeng/select'; -import { ChangeDetectionStrategy, Component, effect, inject, OnInit, signal } from '@angular/core'; -import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal, viewChild } from '@angular/core'; import { ProjectOverview } from '@osf/features/project/overview/models'; -import { LoadingSpinnerComponent } from '@osf/shared/components'; -import { License, SelectOption } from '@shared/models'; +import { LicenseComponent, LoadingSpinnerComponent } from '@osf/shared/components'; +import { License, LicenseOptions } from '@shared/models'; import { LicensesSelectors, LoadAllLicenses } from '@shared/stores/licenses'; -interface LicenseForm { - licenseName: FormControl; -} - @Component({ selector: 'osf-license-dialog', - imports: [Button, Select, TranslatePipe, ReactiveFormsModule, LoadingSpinnerComponent], + imports: [Button, TranslatePipe, LoadingSpinnerComponent, LicenseComponent], templateUrl: './license-dialog.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -28,13 +22,6 @@ export class LicenseDialogComponent implements OnInit { protected dialogRef = inject(DynamicDialogRef); protected config = inject(DynamicDialogConfig); - licenseForm = new FormGroup({ - licenseName: new FormControl('', { - nonNullable: true, - validators: [Validators.required], - }), - }); - protected actions = createDispatchMap({ loadLicenses: LoadAllLicenses, }); @@ -42,60 +29,75 @@ export class LicenseDialogComponent implements OnInit { licenses = select(LicensesSelectors.getLicenses); licensesLoading = select(LicensesSelectors.getLoading); - selectedLicenseText = signal(''); - licenseOptions: SelectOption[] = []; + selectedLicenseId = signal(null); + selectedLicenseOptions = signal(null); currentProject: ProjectOverview | null = null; + isSubmitting = signal(false); - constructor() { - effect(() => { - const currentLicenses = this.licenses(); - - if (currentLicenses) { - this.licenseOptions = currentLicenses.map((license: License) => ({ - label: license.name, - value: license.id, - })); - } - }); - } + licenseComponent = viewChild('licenseComponent'); ngOnInit(): void { this.actions.loadLicenses(); - + this.currentProject = this.config.data?.currentProject || null; if (this.currentProject?.license) { - this.licenseForm.patchValue({ - licenseName: this.currentProject.license.name || '', - }); + this.selectedLicenseId.set(this.currentProject.license.id || null); + if (this.currentProject.nodeLicense) { + this.selectedLicenseOptions.set({ + copyrightHolders: this.currentProject.nodeLicense.copyrightHolders?.join(', ') || '', + year: this.currentProject.nodeLicense.year || new Date().getFullYear().toString(), + }); + } } + } - this.licenseForm.get('licenseName')?.valueChanges.subscribe((licenseName) => { - this.updateSelectedLicenseText(licenseName); - }); + onSelectLicense(license: License): void { + this.selectedLicenseId.set(license.id); } - private updateSelectedLicenseText(licenseId: string): void { - const selectedLicense = this.licenses().find((license: License) => license.id === licenseId); + onCreateLicense(event: { id: string; licenseOptions: LicenseOptions }): void { + const selectedLicense = this.licenses().find((license) => license.id === event.id); if (selectedLicense) { - this.selectedLicenseText.set(selectedLicense.text); - } else { - this.selectedLicenseText.set(''); + this.dialogRef.close({ + licenseName: selectedLicense.name, + licenseId: selectedLicense.id, + licenseOptions: event.licenseOptions, + projectId: this.currentProject?.id, + }); } + + this.isSubmitting.set(false); } save(): void { - if (this.licenseForm.valid) { - const formValue = this.licenseForm.getRawValue(); - const selectedLicense = this.licenses().find((license) => license.id === formValue.licenseName); + if ( + this.licenseComponent()?.selectedLicense()!.requiredFields.length && + this.licenseComponent()?.licenseForm.invalid + ) { + return; + } + + const selectedLicenseId = this.selectedLicenseId(); + if (!selectedLicenseId) return; + + const selectedLicense = this.licenses().find((license) => license.id === selectedLicenseId); + if (!selectedLicense) return; + + this.isSubmitting.set(true); + + if (selectedLicense.requiredFields?.length) { + this.licenseComponent()?.saveLicense(); + } else { this.dialogRef.close({ - licenseName: formValue.licenseName, - licenseId: selectedLicense?.id, + licenseName: selectedLicense.name, + licenseId: selectedLicense.id, projectId: this.currentProject?.id, }); } } cancel(): void { + this.licenseComponent()?.cancel(); this.dialogRef.close(); } } diff --git a/src/app/shared/components/shared-metadata/shared-metadata.component.html b/src/app/shared/components/shared-metadata/shared-metadata.component.html index ca960cdfe..f6f5b42d3 100644 --- a/src/app/shared/components/shared-metadata/shared-metadata.component.html +++ b/src/app/shared/components/shared-metadata/shared-metadata.component.html @@ -25,26 +25,31 @@

@@ -53,12 +58,13 @@

@@ -66,11 +72,16 @@

{{ 'project.overview.metadata.tags' | translate }}

- +
(); selectedSubjects = input.required(); isSubjectsUpdating = input.required(); - hideEditDoi = input(false); + hideEditDoiAndLicence = input(false); + readonly = input(false); openEditContributorDialog = output(); openEditDescriptionDialog = output(); diff --git a/src/app/shared/components/subjects/subjects.component.html b/src/app/shared/components/subjects/subjects.component.html index 7a7996439..39fc1c960 100644 --- a/src/app/shared/components/subjects/subjects.component.html +++ b/src/app/shared/components/subjects/subjects.component.html @@ -8,7 +8,7 @@

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

@@ -16,53 +16,55 @@

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

} - +@if (!readonly()) { + -@if (searchControl.value) { -
- @if (isSearching()) { - - - - - } @else { - @if (!searchedList().length) { -

{{ 'shared.subjects.noSubject' | translate }}

- } - @for (subjects of searchedList(); track $index) { -
- -
-} @else { - - +
+ } @else { + + + } } diff --git a/src/app/shared/components/subjects/subjects.component.ts b/src/app/shared/components/subjects/subjects.component.ts index e7bac58d6..cb7c793f5 100644 --- a/src/app/shared/components/subjects/subjects.component.ts +++ b/src/app/shared/components/subjects/subjects.component.ts @@ -33,6 +33,7 @@ export class SubjectsComponent { areSubjectsUpdating = input(false); isSearching = select(SubjectsSelectors.getSearchedSubjectsLoading); selected = input([]); + readonly = input(false); searchChanged = output(); loadChildren = output(); updateSelection = output(); @@ -64,6 +65,8 @@ export class SubjectsComponent { } selectSubject(subject: SubjectModel) { + if (this.readonly()) return; + const childrenIds = this.getChildrenIds([subject]); const updatedSelection = [...this.selected().filter((s) => !childrenIds.includes(s.id)), subject]; const parentSubjects = this.mapParentsSubject(subject.parent).filter( @@ -76,6 +79,8 @@ export class SubjectsComponent { } removeSubject(subject: SubjectModel) { + if (this.readonly()) return; + const updatedSelection = this.selected().filter( (s) => s.id !== subject.id && !this.getChildrenIds([subject]).includes(s.id) ); @@ -83,6 +88,8 @@ export class SubjectsComponent { } selectSearched(event: CheckboxChangeEvent, subjects: SubjectModel[]) { + if (this.readonly()) return; + if (event.checked) { this.updateSelection.emit([...this.selected(), ...subjects]); } else { diff --git a/src/app/shared/components/tags-input/tags-input.component.html b/src/app/shared/components/tags-input/tags-input.component.html index 4815011cf..02f8c1488 100644 --- a/src/app/shared/components/tags-input/tags-input.component.html +++ b/src/app/shared/components/tags-input/tags-input.component.html @@ -1,14 +1,15 @@
@for (tag of localTags(); track $index) { - + }
- - {{ 'common.hint.tagSeparators' | translate }} - + @if (!readonly()) { + + {{ 'common.hint.tagSeparators' | translate }} + + }
diff --git a/src/app/shared/components/tags-input/tags-input.component.ts b/src/app/shared/components/tags-input/tags-input.component.ts index e8de22185..3c8ba3678 100644 --- a/src/app/shared/components/tags-input/tags-input.component.ts +++ b/src/app/shared/components/tags-input/tags-input.component.ts @@ -25,6 +25,7 @@ import { FormsModule } from '@angular/forms'; export class TagsInputComponent { tags = input([]); required = input(false); + readonly = input(false); tagsChanged = output(); inputValue = signal(''); @@ -82,6 +83,8 @@ export class TagsInputComponent { } private addTag(tagValue: string): void { + if (this.readonly()) return; + const currentTags = this.localTags(); const normalizedValue = tagValue.replace(/[,\s]+/g, ' ').trim(); @@ -96,6 +99,8 @@ export class TagsInputComponent { } removeTag(index: number): void { + if (this.readonly()) return; + const currentTags = this.localTags(); const updatedTags = currentTags.filter((_, i) => i !== index); From b78f3c02a5071f9fd92b3b854c5c2c8e2858e62c Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Thu, 24 Jul 2025 10:07:26 +0300 Subject: [PATCH 7/9] feat(main): merge main --- .../project/overview/mappers/project-overview.mapper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/features/project/overview/mappers/project-overview.mapper.ts b/src/app/features/project/overview/mappers/project-overview.mapper.ts index 11d4f24d7..797d90b5a 100644 --- a/src/app/features/project/overview/mappers/project-overview.mapper.ts +++ b/src/app/features/project/overview/mappers/project-overview.mapper.ts @@ -1,3 +1,5 @@ +import { License } from '@shared/models'; + import { ProjectOverview, ProjectOverviewGetResponseJsoApi } from '../models'; export class ProjectOverviewMapper { @@ -77,6 +79,6 @@ export class ProjectOverviewMapper { rootFolder: response.relationships?.files?.links?.related?.href, iri: response.links?.iri, }, - }; + } as ProjectOverview; } } From f78d66b3249a2a8c94f4499accd729673238cd77 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Thu, 24 Jul 2025 15:57:25 +0300 Subject: [PATCH 8/9] feat(registry-metadata): added fixes by suggestions --- .../models/cedar-metadata-template.models.ts | 2 +- .../metadata/project-metadata.component.ts | 9 +- .../mappers/project-overview.mapper.ts | 2 +- .../registry/mappers/cedar-form.mapper.ts | 27 + src/app/features/registry/mappers/index.ts | 1 + .../registry-metadata-add.component.html | 21 +- .../registry-metadata-add.component.scss | 4 +- .../registry-metadata-add.component.spec.ts | 568 ++++++++++++++++++ .../registry-metadata-add.component.ts | 30 +- .../registry-metadata.component.ts | 35 +- .../registry-metadata.selectors.ts | 5 - .../registry-metadata.state.ts | 26 +- .../funding-dialog.component.html | 1 + .../funding-dialog.component.ts | 1 + src/app/shared/enums/index.ts | 1 + .../shared/enums/metadata-projects.enum.ts | 5 + src/app/shared/models/metadata-tabs.model.ts | 7 + src/assets/i18n/en.json | 2 + 18 files changed, 657 insertions(+), 90 deletions(-) create mode 100644 src/app/features/registry/mappers/cedar-form.mapper.ts create mode 100644 src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.spec.ts create mode 100644 src/app/shared/enums/metadata-projects.enum.ts create mode 100644 src/app/shared/models/metadata-tabs.model.ts diff --git a/src/app/features/project/metadata/models/cedar-metadata-template.models.ts b/src/app/features/project/metadata/models/cedar-metadata-template.models.ts index 681d716c2..75044e300 100644 --- a/src/app/features/project/metadata/models/cedar-metadata-template.models.ts +++ b/src/app/features/project/metadata/models/cedar-metadata-template.models.ts @@ -225,7 +225,7 @@ export interface CedarMetadataRecordData { }; target: { data: { - type: 'nodes'; + type: 'nodes' | 'registrations'; id: string; }; }; diff --git a/src/app/features/project/metadata/project-metadata.component.ts b/src/app/features/project/metadata/project-metadata.component.ts index cd7c4a4bf..bd3c3c942 100644 --- a/src/app/features/project/metadata/project-metadata.component.ts +++ b/src/app/features/project/metadata/project-metadata.component.ts @@ -31,7 +31,7 @@ import { UpdateCustomItemMetadata, UpdateProjectDetails, } from '@osf/features/project/metadata/store'; -import { ResourceType } from '@osf/shared/enums'; +import { MetadataProjectsEnum, ResourceType } from '@osf/shared/enums'; import { ContributorsSelectors, FetchChildrenSubjects, @@ -53,6 +53,7 @@ import { } from '@shared/components/shared-metadata/dialogs'; import { SharedMetadataComponent } from '@shared/components/shared-metadata/shared-metadata.component'; import { SubjectModel } from '@shared/models'; +import { MetadataTabsModel } from '@shared/models/metadata-tabs.model'; import { CustomConfirmationService, LoaderService, ToastService } from '@shared/services'; @Component({ @@ -86,7 +87,7 @@ export class ProjectMetadataComponent implements OnInit { private projectId = ''; - tabs = signal<{ id: string; label: string; type: 'project' | 'cedar' }[]>([]); + tabs = signal([]); protected readonly selectedTab = signal('project'); selectedCedarRecord = signal(null); @@ -130,13 +131,13 @@ export class ProjectMetadataComponent implements OnInit { const project = this.currentProject(); if (!project) return; - const baseTabs = [{ id: 'project', label: project.title, type: 'project' as const }]; + const baseTabs = [{ id: 'project', label: project.title, type: MetadataProjectsEnum.PROJECT }]; const cedarTabs = records?.map((record) => ({ id: record.id || '', label: record.embeds?.template?.data?.attributes?.schema_name || `Record ${record.id}`, - type: 'cedar' as const, + type: MetadataProjectsEnum.CEDAR, })) || []; this.tabs.set([...baseTabs, ...cedarTabs]); diff --git a/src/app/features/project/overview/mappers/project-overview.mapper.ts b/src/app/features/project/overview/mappers/project-overview.mapper.ts index 797d90b5a..79cbe3bcd 100644 --- a/src/app/features/project/overview/mappers/project-overview.mapper.ts +++ b/src/app/features/project/overview/mappers/project-overview.mapper.ts @@ -25,7 +25,7 @@ export class ProjectOverviewMapper { year: response.attributes.node_license.year, } : undefined, - license: response.embeds.license?.data?.attributes as unknown as License, + license: response.embeds.license?.data?.attributes as License, doi: response.attributes.doi, publicationDoi: response.attributes.publication_doi, analyticsKey: response.attributes.analytics_key, diff --git a/src/app/features/registry/mappers/cedar-form.mapper.ts b/src/app/features/registry/mappers/cedar-form.mapper.ts new file mode 100644 index 000000000..54c680f34 --- /dev/null +++ b/src/app/features/registry/mappers/cedar-form.mapper.ts @@ -0,0 +1,27 @@ +import { CedarMetadataRecord, CedarRecordDataBinding } from '@osf/features/project/metadata/models'; + +export function CedarFormMapper(data: CedarRecordDataBinding, registryId: string): CedarMetadataRecord { + return { + data: { + type: 'cedar_metadata_records' as const, + attributes: { + metadata: data.data, + is_published: false, + }, + relationships: { + template: { + data: { + type: 'cedar-metadata-templates' as const, + id: data.id, + }, + }, + target: { + data: { + type: 'registrations' as const, + id: registryId, + }, + }, + }, + }, + }; +} diff --git a/src/app/features/registry/mappers/index.ts b/src/app/features/registry/mappers/index.ts index 629991406..07eddfde6 100644 --- a/src/app/features/registry/mappers/index.ts +++ b/src/app/features/registry/mappers/index.ts @@ -1,3 +1,4 @@ +export * from './cedar-form.mapper'; export * from './registry-metadata.mapper'; export * from './registry-overview.mapper'; export * from './registry-schema-block.mapper'; diff --git a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html index 8b85e40a1..ea2723196 100644 --- a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html +++ b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html @@ -15,13 +15,16 @@

{{ 'project.metadata.addMetadata.selectTemplate' | translate }}

@for (meta of cedarTemplates()?.data; track meta.id) { -
diff --git a/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts b/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts index 7c7bf6d70..e0c72cb6b 100644 --- a/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts +++ b/src/app/shared/components/shared-metadata/dialogs/funding-dialog/funding-dialog.component.ts @@ -117,6 +117,7 @@ export class FundingDialogComponent implements OnInit { }), awardTitle: new FormControl(supplement ? supplement.title || supplement.awardTitle || '' : '', { nonNullable: true, + validators: [Validators.required], }), awardUri: new FormControl(supplement ? supplement.url || supplement.awardUri || '' : '', { nonNullable: true, diff --git a/src/app/shared/enums/index.ts b/src/app/shared/enums/index.ts index 29464b607..f4d0ac2f6 100644 --- a/src/app/shared/enums/index.ts +++ b/src/app/shared/enums/index.ts @@ -9,6 +9,7 @@ export * from './create-project-form-controls.enum'; export * from './file-menu-type.enum'; export * from './filter-type.enum'; export * from './get-resources-request-type.enum'; +export * from './metadata-projects.enum'; export * from './profile-addons-stepper.enum'; export * from './profile-settings-key.enum'; export * from './registration-review-states.enum'; diff --git a/src/app/shared/enums/metadata-projects.enum.ts b/src/app/shared/enums/metadata-projects.enum.ts new file mode 100644 index 000000000..ee93c95fd --- /dev/null +++ b/src/app/shared/enums/metadata-projects.enum.ts @@ -0,0 +1,5 @@ +export enum MetadataProjectsEnum { + PROJECT = 'project', + CEDAR = 'cedar', + REGISTRY = 'registry', +} diff --git a/src/app/shared/models/metadata-tabs.model.ts b/src/app/shared/models/metadata-tabs.model.ts new file mode 100644 index 000000000..300ae518a --- /dev/null +++ b/src/app/shared/models/metadata-tabs.model.ts @@ -0,0 +1,7 @@ +import { MetadataProjectsEnum } from '@shared/enums'; + +export interface MetadataTabsModel { + id: string; + label: string; + type: MetadataProjectsEnum; +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 3b14b7712..627b839a5 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -589,6 +589,8 @@ "noResourceInformation": "No resource information available", "notSpecified": "Not specified", "loading": "Loading metadata...", + "cedarRecordCreatedSuccessfully": "CEDAR record created successfully", + "failedToCreateCedarRecord": "Failed to create CEDAR record", "placeholders": { "edit": "Edit description here", "add": "Add description here" From ad37f557b8f97b80d32098d0d3854cc1d0e3bca9 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Thu, 24 Jul 2025 17:01:10 +0300 Subject: [PATCH 9/9] feat(registry-metadata): added fixes by suggestions --- .../registry-metadata-add.component.html | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html index ea2723196..1ab039dde 100644 --- a/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html +++ b/src/app/features/registry/pages/registry-metadata-add/registry-metadata-add.component.html @@ -15,14 +15,6 @@

{{ 'project.metadata.addMetadata.selectTemplate' | translate }}

@for (meta of cedarTemplates()?.data; track meta.id) { - - - - - - - - - - }