From eaf094457a1bc495532183d5b9f62d2f2afe6d11 Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 2 Jul 2025 11:24:07 +0300 Subject: [PATCH 1/9] feat(contributors): updated imports --- .../contributors/contributors.component.html | 2 - .../contributors/contributors.component.ts | 29 +------------ .../preprints/constants/preprints.routes.ts | 2 +- .../services/contributors.service.ts | 6 +-- .../submit-preprint.actions.ts | 3 +- .../submit-preprint/submit-preprint.model.ts | 3 +- .../contributors/contributors.component.html | 3 -- .../contributors/contributors.component.ts | 42 +++--------------- .../contributors-dialog.component.ts | 8 ++-- .../metadata/project-metadata.component.ts | 2 +- .../metadata/project-metadata.routes.ts | 2 +- src/app/features/project/project.routes.ts | 12 +++++- .../profile-settings.component.scss | 1 - .../add-contributor-dialog.component.ts | 7 ++- .../add-contributor-item.component.ts | 4 +- ...registered-contributor-dialog.component.ts | 6 +-- .../contributors-list.component.ts | 43 ++++++++++++++----- .../components/contributors/services/index.ts | 1 - .../contributors}/contributors.constants.ts | 3 +- .../contributors/contributors.token.ts | 5 +++ .../contributors}/index.ts | 1 + src/app/shared/constants/index.ts | 1 + .../directives/stop-propagation.directive.ts | 5 +++ .../add-contributor-type.enum.ts | 0 .../contributors}/add-dialog-state.enum.ts | 0 .../contributor-permission.enum.ts | 0 .../enums => enums/contributors}/index.ts | 0 src/app/shared/enums/index.ts | 1 + .../contributors}/contributors.mapper.ts | 12 ++++-- .../mappers => mappers/contributors}/index.ts | 0 src/app/shared/mappers/index.ts | 1 + .../contributors}/contributor-add.model.ts | 0 .../contributor-dialog-add.model.ts | 2 +- .../contributor-response.model.ts | 0 .../contributors/contributor-service.model.ts | 14 ++++++ .../contributors}/contributor.model.ts | 0 .../models => models/contributors}/index.ts | 1 + .../unregistered-contributor-form.model.ts | 0 src/app/shared/models/index.ts | 1 + src/app/shared/services/index.ts | 1 + .../project-contributors.service.ts} | 19 ++++---- .../contributors}/contributors.actions.ts | 2 +- .../contributors}/contributors.model.ts | 3 +- .../contributors}/contributors.selectors.ts | 0 .../contributors}/contributors.state.ts | 4 +- .../store => stores/contributors}/index.ts | 0 src/app/shared/stores/index.ts | 2 + 47 files changed, 125 insertions(+), 129 deletions(-) delete mode 100644 src/app/shared/components/contributors/services/index.ts rename src/app/shared/{components/contributors/constants => constants/contributors}/contributors.constants.ts (91%) create mode 100644 src/app/shared/constants/contributors/contributors.token.ts rename src/app/shared/{components/contributors/constants => constants/contributors}/index.ts (52%) rename src/app/shared/{components/contributors/enums => enums/contributors}/add-contributor-type.enum.ts (100%) rename src/app/shared/{components/contributors/enums => enums/contributors}/add-dialog-state.enum.ts (100%) rename src/app/shared/{components/contributors/enums => enums/contributors}/contributor-permission.enum.ts (100%) rename src/app/shared/{components/contributors/enums => enums/contributors}/index.ts (100%) rename src/app/shared/{components/contributors/mappers => mappers/contributors}/contributors.mapper.ts (91%) rename src/app/shared/{components/contributors/mappers => mappers/contributors}/index.ts (100%) rename src/app/shared/{components/contributors/models => models/contributors}/contributor-add.model.ts (100%) rename src/app/shared/{components/contributors/models => models/contributors}/contributor-dialog-add.model.ts (75%) rename src/app/shared/{components/contributors/models => models/contributors}/contributor-response.model.ts (100%) create mode 100644 src/app/shared/models/contributors/contributor-service.model.ts rename src/app/shared/{components/contributors/models => models/contributors}/contributor.model.ts (100%) rename src/app/shared/{components/contributors/models => models/contributors}/index.ts (83%) rename src/app/shared/{components/contributors/models => models/contributors}/unregistered-contributor-form.model.ts (100%) rename src/app/shared/{components/contributors/services/contributors.service.ts => services/project-contributors.service.ts} (85%) rename src/app/shared/{components/contributors/store => stores/contributors}/contributors.actions.ts (95%) rename src/app/shared/{components/contributors/store => stores/contributors}/contributors.model.ts (82%) rename src/app/shared/{components/contributors/store => stores/contributors}/contributors.selectors.ts (100%) rename src/app/shared/{components/contributors/store => stores/contributors}/contributors.state.ts (97%) rename src/app/shared/{components/contributors/store => stores/contributors}/index.ts (100%) diff --git a/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.html b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.html index 944393108..0d0f6bd2a 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.html +++ b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.html @@ -11,8 +11,6 @@

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

[contributors]="contributors()" [isLoading]="isContributorsLoading()" (remove)="removeContributor($event)" - (showEducationHistory)="openEducationHistory($event)" - (showEmploymentHistory)="openEmploymentHistory($event)" />
@if (hasChanges) { diff --git a/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.ts b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.ts index 9980a2a29..682fa0949 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.ts +++ b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.ts @@ -21,14 +21,13 @@ import { SubmitPreprintSelectors, UpdateContributor, } from '@osf/features/preprints/store/submit-preprint'; -import { EducationHistoryDialogComponent, EmploymentHistoryDialogComponent } from '@osf/shared/components'; import { AddContributorDialogComponent, AddUnregisteredContributorDialogComponent, ContributorsListComponent, } from '@osf/shared/components/contributors'; -import { AddContributorType } from '@osf/shared/components/contributors/enums'; -import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/components/contributors/models'; +import { AddContributorType } from '@osf/shared/enums'; +import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/models'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { findChangedItems } from '@osf/shared/utils'; @@ -87,30 +86,6 @@ export class ContributorsComponent implements OnInit { }); } - openEmploymentHistory(contributor: ContributorModel) { - this.dialogService.open(EmploymentHistoryDialogComponent, { - width: '552px', - data: contributor.employment, - focusOnShow: false, - header: this.translateService.instant('project.contributors.table.headers.employment'), - closeOnEscape: true, - modal: true, - closable: true, - }); - } - - openEducationHistory(contributor: ContributorModel) { - this.dialogService.open(EducationHistoryDialogComponent, { - width: '552px', - data: contributor.education, - focusOnShow: false, - header: this.translateService.instant('project.contributors.table.headers.education'), - closeOnEscape: true, - modal: true, - closable: true, - }); - } - openAddContributorDialog() { const addedContributorIds = this.initialContributors().map((x) => x.userId); diff --git a/src/app/features/preprints/constants/preprints.routes.ts b/src/app/features/preprints/constants/preprints.routes.ts index 3d039e468..86c475611 100644 --- a/src/app/features/preprints/constants/preprints.routes.ts +++ b/src/app/features/preprints/constants/preprints.routes.ts @@ -8,7 +8,7 @@ import { PreprintsDiscoverState } from '@osf/features/preprints/store/preprints- import { PreprintsResourcesFiltersState } from '@osf/features/preprints/store/preprints-resources-filters'; import { PreprintsResourcesFiltersOptionsState } from '@osf/features/preprints/store/preprints-resources-filters-options'; import { SubmitPreprintState } from '@osf/features/preprints/store/submit-preprint'; -import { ContributorsState } from '@shared/components/contributors/store'; +import { ContributorsState } from '@osf/shared/stores'; export const preprintsRoutes: Routes = [ { diff --git a/src/app/features/preprints/services/contributors.service.ts b/src/app/features/preprints/services/contributors.service.ts index ace44a5f5..45be9bf70 100644 --- a/src/app/features/preprints/services/contributors.service.ts +++ b/src/app/features/preprints/services/contributors.service.ts @@ -4,9 +4,9 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiResponse } from '@core/models'; import { JsonApiService } from '@core/services'; -import { AddContributorType } from '@shared/components/contributors/enums'; -import { ContributorsMapper } from '@shared/components/contributors/mappers'; -import { ContributorAddModel, ContributorModel, ContributorResponse } from '@shared/components/contributors/models'; +import { AddContributorType } from '@osf/shared/enums'; +import { ContributorsMapper } from '@osf/shared/mappers'; +import { ContributorAddModel, ContributorModel, ContributorResponse } from '@osf/shared/models'; import { environment } from 'src/environments/environment'; diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index 9c3b26913..44909a660 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -1,8 +1,7 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; -import { ContributorAddModel, ContributorModel } from '@shared/components/contributors/models'; -import { LicenseOptions, OsfFile } from '@shared/models'; +import { ContributorAddModel, ContributorModel, LicenseOptions, OsfFile } from '@shared/models'; export class SetSelectedPreprintProviderId { static readonly type = '[Submit Preprint] Set Selected Preprint Provider Id'; diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts index 8a3ea7575..b62424697 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -1,8 +1,7 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint, PreprintFilesLinks } from '@osf/features/preprints/models'; -import { ContributorModel } from '@shared/components/contributors/models'; -import { AsyncStateModel, IdName, OsfFile } from '@shared/models'; +import { AsyncStateModel, ContributorModel, IdName, OsfFile } from '@shared/models'; import { License } from '@shared/models/license.model'; export interface SubmitPreprintStateModel { diff --git a/src/app/features/project/contributors/contributors.component.html b/src/app/features/project/contributors/contributors.component.html index aba7a8374..9ff4f7612 100644 --- a/src/app/features/project/contributors/contributors.component.html +++ b/src/app/features/project/contributors/contributors.component.html @@ -65,8 +65,6 @@

{{ 'navigation.project.contributors' | tra [contributors]="contributors()" [isLoading]="isContributorsLoading()" (remove)="removeContributor($event)" - (showEducationHistory)="openEducationHistory($event)" - (showEmploymentHistory)="openEmploymentHistory($event)" > @if (hasChanges) { @@ -75,7 +73,6 @@

{{ 'navigation.project.contributors' | tra class="w-3" styleClass="w-full" (click)="cancel()" - text severity="info" [label]="'common.buttons.cancel' | translate" > diff --git a/src/app/features/project/contributors/contributors.component.ts b/src/app/features/project/contributors/contributors.component.ts index b3d365851..a7fd2f109 100644 --- a/src/app/features/project/contributors/contributors.component.ts +++ b/src/app/features/project/contributors/contributors.component.ts @@ -14,20 +14,16 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { - EducationHistoryDialogComponent, - EmploymentHistoryDialogComponent, - SearchInputComponent, - ViewOnlyTableComponent, -} from '@osf/shared/components'; +import { SearchInputComponent, ViewOnlyTableComponent } from '@osf/shared/components'; import { AddContributorDialogComponent, AddUnregisteredContributorDialogComponent, ContributorsListComponent, } from '@osf/shared/components/contributors'; -import { BIBLIOGRAPHY_OPTIONS, PERMISSION_OPTIONS } from '@osf/shared/components/contributors/constants'; -import { AddContributorType, ContributorPermission } from '@osf/shared/components/contributors/enums'; -import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/components/contributors/models'; +import { BIBLIOGRAPHY_OPTIONS, PERMISSION_OPTIONS } from '@osf/shared/constants'; +import { AddContributorType, ContributorPermission } from '@osf/shared/enums'; +import { ContributorDialogAddModel, ContributorModel, SelectOption } from '@osf/shared/models'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { AddContributor, ContributorsSelectors, @@ -37,9 +33,7 @@ import { UpdateContributor, UpdatePermissionFilter, UpdateSearchValue, -} from '@osf/shared/components/contributors/store'; -import { SelectOption } from '@osf/shared/models'; -import { CustomConfirmationService, ToastService } from '@osf/shared/services'; +} from '@osf/shared/stores'; import { findChangedItems } from '@osf/shared/utils'; import { ViewOnlyLink, ViewOnlyLinkModel } from '../settings/models'; @@ -167,30 +161,6 @@ export class ContributorsComponent implements OnInit { }); } - openEmploymentHistory(contributor: ContributorModel) { - this.dialogService.open(EmploymentHistoryDialogComponent, { - width: '552px', - data: contributor.employment, - focusOnShow: false, - header: this.translateService.instant('project.contributors.table.headers.employment'), - closeOnEscape: true, - modal: true, - closable: true, - }); - } - - openEducationHistory(contributor: ContributorModel) { - this.dialogService.open(EducationHistoryDialogComponent, { - width: '552px', - data: contributor.education, - focusOnShow: false, - header: this.translateService.instant('project.contributors.table.headers.education'), - closeOnEscape: true, - modal: true, - closable: true, - }); - } - openAddContributorDialog() { const addedContributorIds = this.initialContributors().map((x) => x.userId); diff --git a/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts b/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts index 894414f81..17ce73f90 100644 --- a/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts +++ b/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts @@ -16,16 +16,16 @@ import { FormControl, FormsModule } from '@angular/forms'; import { SearchInputComponent } from '@osf/shared/components'; import { AddContributorDialogComponent } from '@osf/shared/components/contributors'; -import { AddContributorType } from '@osf/shared/components/contributors/enums'; -import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/components/contributors/models'; +import { AddContributorType } from '@osf/shared/enums'; +import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/models'; +import { ToastService } from '@osf/shared/services'; import { AddContributor, DeleteContributor, UpdateBibliographyFilter, UpdatePermissionFilter, UpdateSearchValue, -} from '@osf/shared/components/contributors/store'; -import { ToastService } from '@osf/shared/services'; +} from '@osf/shared/stores'; @Component({ selector: 'osf-contributors-dialog', diff --git a/src/app/features/project/metadata/project-metadata.component.ts b/src/app/features/project/metadata/project-metadata.component.ts index 58f509c06..ab056b0b3 100644 --- a/src/app/features/project/metadata/project-metadata.component.ts +++ b/src/app/features/project/metadata/project-metadata.component.ts @@ -53,7 +53,7 @@ import { UpdateProjectDetails, } from '@osf/features/project/metadata/store'; import { ProjectOverviewSubject } from '@osf/features/project/overview/models'; -import { ContributorsSelectors, GetAllContributors } from '@osf/shared/components/contributors/store'; +import { ContributorsSelectors, GetAllContributors } from '@osf/shared/stores'; import { LoadingSpinnerComponent, SubHeaderComponent, TagsInputComponent } from '@shared/components'; import { CustomConfirmationService, ToastService } from '@shared/services'; import { GetSubjects, SubjectsSelectors, UpdateProjectSubjects } from '@shared/stores/subjects'; diff --git a/src/app/features/project/metadata/project-metadata.routes.ts b/src/app/features/project/metadata/project-metadata.routes.ts index f8cb97526..8b588e30a 100644 --- a/src/app/features/project/metadata/project-metadata.routes.ts +++ b/src/app/features/project/metadata/project-metadata.routes.ts @@ -2,7 +2,7 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; -import { ContributorsState } from '@osf/shared/components/contributors/store'; +import { ContributorsState } from '@osf/shared/stores'; import { ProjectMetadataComponent } from './project-metadata.component'; diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index f685a5955..01a51b343 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -2,7 +2,9 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; -import { ContributorsState } from '@osf/shared/components/contributors/store'; +import { CONTRIBUTORS_SERVICE } from '@osf/shared/constants'; +import { ProjectContributorsService } from '@osf/shared/services'; +import { ContributorsState } from '@osf/shared/stores'; import { ProjectFilesState } from './files/store'; @@ -44,7 +46,13 @@ export const projectRoutes: Routes = [ path: 'contributors', loadComponent: () => import('../project/contributors/contributors.component').then((mod) => mod.ContributorsComponent), - providers: [provideStates([ContributorsState])], + providers: [ + provideStates([ContributorsState]), + { + provide: CONTRIBUTORS_SERVICE, + useClass: ProjectContributorsService, + }, + ], }, { path: 'analytics', diff --git a/src/app/features/settings/profile-settings/profile-settings.component.scss b/src/app/features/settings/profile-settings/profile-settings.component.scss index f1c816620..e9dab5325 100644 --- a/src/app/features/settings/profile-settings/profile-settings.component.scss +++ b/src/app/features/settings/profile-settings/profile-settings.component.scss @@ -1,4 +1,3 @@ -@use "assets/styles/variables" as var; @use "assets/styles/mixins" as mix; :host { diff --git a/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.ts b/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.ts index 6268a33c2..bbd98bea7 100644 --- a/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.ts +++ b/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.ts @@ -14,13 +14,12 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule } from '@angular/forms'; import { CustomPaginatorComponent, LoadingSpinnerComponent, SearchInputComponent } from '@osf/shared/components'; +import { AddContributorType, AddDialogState } from '@osf/shared/enums/contributors'; +import { ContributorAddModel, ContributorDialogAddModel } from '@osf/shared/models'; +import { ClearUsers, ContributorsSelectors, SearchUsers } from '@osf/shared/stores'; import { AddContributorItemComponent } from '../add-contributor-item/add-contributor-item.component'; -import { AddContributorType, AddDialogState } from './../enums'; -import { ContributorAddModel, ContributorDialogAddModel } from './../models'; -import { ClearUsers, ContributorsSelectors, SearchUsers } from './../store'; - @Component({ selector: 'osf-add-contributor-dialog', imports: [ diff --git a/src/app/shared/components/contributors/add-contributor-item/add-contributor-item.component.ts b/src/app/shared/components/contributors/add-contributor-item/add-contributor-item.component.ts index b7acd1fd5..4b46a7408 100644 --- a/src/app/shared/components/contributors/add-contributor-item/add-contributor-item.component.ts +++ b/src/app/shared/components/contributors/add-contributor-item/add-contributor-item.component.ts @@ -6,8 +6,8 @@ import { Select } from 'primeng/select'; import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { PERMISSION_OPTIONS } from './../constants'; -import { ContributorAddModel } from './../models'; +import { PERMISSION_OPTIONS } from '@osf/shared/constants'; +import { ContributorAddModel } from '@osf/shared/models'; @Component({ selector: 'osf-add-contributor-item', diff --git a/src/app/shared/components/contributors/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.ts b/src/app/shared/components/contributors/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.ts index 48976e037..cb5264064 100644 --- a/src/app/shared/components/contributors/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.ts +++ b/src/app/shared/components/contributors/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.ts @@ -8,12 +8,10 @@ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angula import { TextInputComponent } from '@osf/shared/components'; import { InputLimits } from '@osf/shared/constants'; +import { AddContributorType, ContributorPermission } from '@osf/shared/enums/contributors'; +import { ContributorAddModel, ContributorDialogAddModel, UnregisteredContributorForm } from '@osf/shared/models'; import { CustomValidators } from '@osf/shared/utils'; -import { AddContributorType, ContributorPermission } from './../enums'; -import { ContributorAddModel, UnregisteredContributorForm } from './../models'; -import { ContributorDialogAddModel } from './../models/contributor-dialog-add.model'; - @Component({ selector: 'osf-add-unregistered-contributor-dialog', imports: [Button, ReactiveFormsModule, TranslatePipe, TextInputComponent], diff --git a/src/app/shared/components/contributors/contributors-list/contributors-list.component.ts b/src/app/shared/components/contributors/contributors-list/contributors-list.component.ts index 65a5b754e..a0f5de543 100644 --- a/src/app/shared/components/contributors/contributors-list/contributors-list.component.ts +++ b/src/app/shared/components/contributors/contributors-list/contributors-list.component.ts @@ -1,20 +1,23 @@ -import { TranslatePipe } from '@ngx-translate/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Checkbox } from 'primeng/checkbox'; +import { DialogService } from 'primeng/dynamicdialog'; import { Skeleton } from 'primeng/skeleton'; import { TableModule } from 'primeng/table'; import { Tooltip } from 'primeng/tooltip'; -import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MY_PROJECTS_TABLE_PARAMS } from '@osf/core/constants'; -import { SelectComponent } from '@osf/shared/components'; -import { SelectOption, TableParameters } from '@osf/shared/models'; - -import { PERMISSION_OPTIONS } from '../constants'; -import { ContributorModel } from '../models'; +import { + EducationHistoryDialogComponent, + EmploymentHistoryDialogComponent, + SelectComponent, +} from '@osf/shared/components'; +import { PERMISSION_OPTIONS } from '@osf/shared/constants'; +import { ContributorModel, SelectOption, TableParameters } from '@osf/shared/models'; @Component({ selector: 'osf-contributors-list', @@ -22,14 +25,16 @@ import { ContributorModel } from '../models'; templateUrl: './contributors-list.component.html', styleUrl: './contributors-list.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [DialogService], }) export class ContributorsListComponent { contributors = input([]); isLoading = input(false); remove = output(); - showEducationHistory = output(); - showEmploymentHistory = output(); + + dialogService = inject(DialogService); + translateService = inject(TranslateService); protected readonly tableParams = signal({ ...MY_PROJECTS_TABLE_PARAMS }); protected readonly permissionsOptions: SelectOption[] = PERMISSION_OPTIONS; @@ -41,10 +46,26 @@ export class ContributorsListComponent { } protected openEducationHistory(contributor: ContributorModel) { - this.showEducationHistory.emit(contributor); + this.dialogService.open(EducationHistoryDialogComponent, { + width: '552px', + data: contributor.education, + focusOnShow: false, + header: this.translateService.instant('project.contributors.table.headers.education'), + closeOnEscape: true, + modal: true, + closable: true, + }); } protected openEmploymentHistory(contributor: ContributorModel) { - this.showEmploymentHistory.emit(contributor); + this.dialogService.open(EmploymentHistoryDialogComponent, { + width: '552px', + data: contributor.employment, + focusOnShow: false, + header: this.translateService.instant('project.contributors.table.headers.employment'), + closeOnEscape: true, + modal: true, + closable: true, + }); } } diff --git a/src/app/shared/components/contributors/services/index.ts b/src/app/shared/components/contributors/services/index.ts deleted file mode 100644 index f46646aaa..000000000 --- a/src/app/shared/components/contributors/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ContributorsService } from './contributors.service'; diff --git a/src/app/shared/components/contributors/constants/contributors.constants.ts b/src/app/shared/constants/contributors/contributors.constants.ts similarity index 91% rename from src/app/shared/components/contributors/constants/contributors.constants.ts rename to src/app/shared/constants/contributors/contributors.constants.ts index 77764ecb1..8e4b5f474 100644 --- a/src/app/shared/components/contributors/constants/contributors.constants.ts +++ b/src/app/shared/constants/contributors/contributors.constants.ts @@ -1,7 +1,6 @@ +import { ContributorPermission } from '@osf/shared/enums'; import { SelectOption } from '@osf/shared/models'; -import { ContributorPermission } from '../enums'; - export const PERMISSION_OPTIONS: SelectOption[] = [ { label: 'project.contributors.permissions.administrator', diff --git a/src/app/shared/constants/contributors/contributors.token.ts b/src/app/shared/constants/contributors/contributors.token.ts new file mode 100644 index 000000000..f8026988c --- /dev/null +++ b/src/app/shared/constants/contributors/contributors.token.ts @@ -0,0 +1,5 @@ +import { InjectionToken } from '@angular/core'; + +import { IContributorsService } from '@osf/shared/models'; + +export const CONTRIBUTORS_SERVICE = new InjectionToken('CONTRIBUTORS_SERVICE'); diff --git a/src/app/shared/components/contributors/constants/index.ts b/src/app/shared/constants/contributors/index.ts similarity index 52% rename from src/app/shared/components/contributors/constants/index.ts rename to src/app/shared/constants/contributors/index.ts index 86502635c..2ecfa8737 100644 --- a/src/app/shared/components/contributors/constants/index.ts +++ b/src/app/shared/constants/contributors/index.ts @@ -1 +1,2 @@ export * from './contributors.constants'; +export * from './contributors.token'; diff --git a/src/app/shared/constants/index.ts b/src/app/shared/constants/index.ts index b7598dcf2..3f86c85d5 100644 --- a/src/app/shared/constants/index.ts +++ b/src/app/shared/constants/index.ts @@ -1,6 +1,7 @@ export * from './addon-terms.const'; export * from './addons-category-options.const'; export * from './addons-tab-options.const'; +export * from './contributors'; export * from './input-limits.const'; export * from './input-validation-messages.const'; export * from './language.const'; diff --git a/src/app/shared/directives/stop-propagation.directive.ts b/src/app/shared/directives/stop-propagation.directive.ts index ef2d45b1f..bc003e73d 100644 --- a/src/app/shared/directives/stop-propagation.directive.ts +++ b/src/app/shared/directives/stop-propagation.directive.ts @@ -8,4 +8,9 @@ export class StopPropagationDirective { onClick(event: Event): void { event.stopPropagation(); } + + @HostListener('keydown', ['$event']) + keyDown(event: Event): void { + event.stopPropagation(); + } } diff --git a/src/app/shared/components/contributors/enums/add-contributor-type.enum.ts b/src/app/shared/enums/contributors/add-contributor-type.enum.ts similarity index 100% rename from src/app/shared/components/contributors/enums/add-contributor-type.enum.ts rename to src/app/shared/enums/contributors/add-contributor-type.enum.ts diff --git a/src/app/shared/components/contributors/enums/add-dialog-state.enum.ts b/src/app/shared/enums/contributors/add-dialog-state.enum.ts similarity index 100% rename from src/app/shared/components/contributors/enums/add-dialog-state.enum.ts rename to src/app/shared/enums/contributors/add-dialog-state.enum.ts diff --git a/src/app/shared/components/contributors/enums/contributor-permission.enum.ts b/src/app/shared/enums/contributors/contributor-permission.enum.ts similarity index 100% rename from src/app/shared/components/contributors/enums/contributor-permission.enum.ts rename to src/app/shared/enums/contributors/contributor-permission.enum.ts diff --git a/src/app/shared/components/contributors/enums/index.ts b/src/app/shared/enums/contributors/index.ts similarity index 100% rename from src/app/shared/components/contributors/enums/index.ts rename to src/app/shared/enums/contributors/index.ts diff --git a/src/app/shared/enums/index.ts b/src/app/shared/enums/index.ts index b953b1783..a9b856ade 100644 --- a/src/app/shared/enums/index.ts +++ b/src/app/shared/enums/index.ts @@ -3,6 +3,7 @@ export * from './addon-tab.enum'; export * from './addons-category.enum'; export * from './addons-credentials-format.enum'; export * from './breakpoint-queries.enum'; +export * from './contributors'; export * from './create-component-form-controls.enum'; export * from './create-project-form-controls.enum'; export * from './file-menu-type.enum'; diff --git a/src/app/shared/components/contributors/mappers/contributors.mapper.ts b/src/app/shared/mappers/contributors/contributors.mapper.ts similarity index 91% rename from src/app/shared/components/contributors/mappers/contributors.mapper.ts rename to src/app/shared/mappers/contributors/contributors.mapper.ts index 5024ca651..46101c53c 100644 --- a/src/app/shared/components/contributors/mappers/contributors.mapper.ts +++ b/src/app/shared/mappers/contributors/contributors.mapper.ts @@ -1,8 +1,12 @@ import { JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; -import { PaginatedData } from '@osf/shared/models'; - -import { AddContributorType, ContributorPermission } from '../enums'; -import { ContributorAddModel, ContributorAddRequestModel, ContributorModel, ContributorResponse } from '../models'; +import { AddContributorType, ContributorPermission } from '@osf/shared/enums'; +import { + ContributorAddModel, + ContributorAddRequestModel, + ContributorModel, + ContributorResponse, + PaginatedData, +} from '@osf/shared/models'; export class ContributorsMapper { static fromResponse(response: ContributorResponse[]): ContributorModel[] { diff --git a/src/app/shared/components/contributors/mappers/index.ts b/src/app/shared/mappers/contributors/index.ts similarity index 100% rename from src/app/shared/components/contributors/mappers/index.ts rename to src/app/shared/mappers/contributors/index.ts diff --git a/src/app/shared/mappers/index.ts b/src/app/shared/mappers/index.ts index 5c5cd00dd..8be2622e6 100644 --- a/src/app/shared/mappers/index.ts +++ b/src/app/shared/mappers/index.ts @@ -1,4 +1,5 @@ export * from './addon.mapper'; +export * from './contributors'; export * from './filters'; export * from './institutions'; export * from './licenses.mapper'; diff --git a/src/app/shared/components/contributors/models/contributor-add.model.ts b/src/app/shared/models/contributors/contributor-add.model.ts similarity index 100% rename from src/app/shared/components/contributors/models/contributor-add.model.ts rename to src/app/shared/models/contributors/contributor-add.model.ts diff --git a/src/app/shared/components/contributors/models/contributor-dialog-add.model.ts b/src/app/shared/models/contributors/contributor-dialog-add.model.ts similarity index 75% rename from src/app/shared/components/contributors/models/contributor-dialog-add.model.ts rename to src/app/shared/models/contributors/contributor-dialog-add.model.ts index 1e79099f7..86ce0b5e3 100644 --- a/src/app/shared/components/contributors/models/contributor-dialog-add.model.ts +++ b/src/app/shared/models/contributors/contributor-dialog-add.model.ts @@ -1,4 +1,4 @@ -import { AddContributorType } from '../enums'; +import { AddContributorType } from '@osf/shared/enums'; import { ContributorAddModel } from './contributor-add.model'; diff --git a/src/app/shared/components/contributors/models/contributor-response.model.ts b/src/app/shared/models/contributors/contributor-response.model.ts similarity index 100% rename from src/app/shared/components/contributors/models/contributor-response.model.ts rename to src/app/shared/models/contributors/contributor-response.model.ts diff --git a/src/app/shared/models/contributors/contributor-service.model.ts b/src/app/shared/models/contributors/contributor-service.model.ts new file mode 100644 index 000000000..cdd4b5317 --- /dev/null +++ b/src/app/shared/models/contributors/contributor-service.model.ts @@ -0,0 +1,14 @@ +import { Observable } from 'rxjs'; + +import { PaginatedData } from '@osf/shared/models'; + +import { ContributorModel } from './contributor.model'; +import { ContributorAddModel } from './contributor-add.model'; + +export interface IContributorsService { + getAllContributors(resourceId: string): Observable; + addContributor(resourceId: string, data: ContributorAddModel): Observable; + updateContributor(resourceId: string, data: ContributorModel): Observable; + deleteContributor(resourceId: string, userId: string): Observable; + searchUsers(value: string, page: number): Observable>; +} diff --git a/src/app/shared/components/contributors/models/contributor.model.ts b/src/app/shared/models/contributors/contributor.model.ts similarity index 100% rename from src/app/shared/components/contributors/models/contributor.model.ts rename to src/app/shared/models/contributors/contributor.model.ts diff --git a/src/app/shared/components/contributors/models/index.ts b/src/app/shared/models/contributors/index.ts similarity index 83% rename from src/app/shared/components/contributors/models/index.ts rename to src/app/shared/models/contributors/index.ts index 9047f2296..f5ea15a0c 100644 --- a/src/app/shared/components/contributors/models/index.ts +++ b/src/app/shared/models/contributors/index.ts @@ -2,4 +2,5 @@ export * from './contributor.model'; export * from './contributor-add.model'; export * from './contributor-dialog-add.model'; export * from './contributor-response.model'; +export * from './contributor-service.model'; export * from './unregistered-contributor-form.model'; diff --git a/src/app/shared/components/contributors/models/unregistered-contributor-form.model.ts b/src/app/shared/models/contributors/unregistered-contributor-form.model.ts similarity index 100% rename from src/app/shared/components/contributors/models/unregistered-contributor-form.model.ts rename to src/app/shared/models/contributors/unregistered-contributor-form.model.ts diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index a41243b27..a0bcca7b0 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -1,6 +1,7 @@ export * from './addons'; export * from './charts'; export * from './confirmation-options.model'; +export * from './contributors'; export * from './create-component-form.model'; export * from './create-project-form.model'; export * from './file-menu-action.model'; diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 0bb43c091..30ca04cc8 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -5,6 +5,7 @@ export { FiltersOptionsService } from './filters-options.service'; export { InstitutionsService } from './institutions.service'; export { LicensesService } from './licenses.service'; export { LoaderService } from './loader.service'; +export { ProjectContributorsService } from './project-contributors.service'; export { ResourceCardService } from './resource-card.service'; export { SearchService } from './search.service'; export { SubjectsService } from './subjects.service'; diff --git a/src/app/shared/components/contributors/services/contributors.service.ts b/src/app/shared/services/project-contributors.service.ts similarity index 85% rename from src/app/shared/components/contributors/services/contributors.service.ts rename to src/app/shared/services/project-contributors.service.ts index 7ecd09a8c..c4ca391e8 100644 --- a/src/app/shared/components/contributors/services/contributors.service.ts +++ b/src/app/shared/services/project-contributors.service.ts @@ -4,24 +4,23 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; -import { PaginatedData } from '@osf/shared/models'; +import { ContributorAddModel, ContributorModel, ContributorResponse, PaginatedData } from '@osf/shared/models'; import { AddContributorType } from '../enums'; -import { ContributorsMapper } from '../mappers'; -import { ContributorAddModel, ContributorModel, ContributorResponse } from '../models'; +import { ContributorsMapper } from '../mappers/contributors'; import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class ContributorsService { - #jsonApiService = inject(JsonApiService); +export class ProjectContributorsService { + private readonly jsonApiService = inject(JsonApiService); getAllContributors(projectId: string): Observable { const baseUrl = `${environment.apiUrl}/nodes/${projectId}/contributors`; - return this.#jsonApiService + return this.jsonApiService .get>(baseUrl) .pipe(map((response) => ContributorsMapper.fromResponse(response.data))); } @@ -29,7 +28,7 @@ export class ContributorsService { searchUsers(value: string, page = 1): Observable> { const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; - return this.#jsonApiService + return this.jsonApiService .get>(baseUrl) .pipe(map((response) => ContributorsMapper.fromUsersWithPaginationGetResponse(response))); } @@ -40,7 +39,7 @@ export class ContributorsService { const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; - return this.#jsonApiService + return this.jsonApiService .post(baseUrl, contributorData) .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); } @@ -50,7 +49,7 @@ export class ContributorsService { const contributorData = { data: ContributorsMapper.toContributorAddRequest(data) }; - return this.#jsonApiService + return this.jsonApiService .patch(baseUrl, contributorData) .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); } @@ -58,6 +57,6 @@ export class ContributorsService { deleteContributor(projectId: string, userId: string): Observable { const baseUrl = `${environment.apiUrl}/nodes/${projectId}/contributors/${userId}`; - return this.#jsonApiService.delete(baseUrl); + return this.jsonApiService.delete(baseUrl); } } diff --git a/src/app/shared/components/contributors/store/contributors.actions.ts b/src/app/shared/stores/contributors/contributors.actions.ts similarity index 95% rename from src/app/shared/components/contributors/store/contributors.actions.ts rename to src/app/shared/stores/contributors/contributors.actions.ts index eaca3b5b5..abea4480c 100644 --- a/src/app/shared/components/contributors/store/contributors.actions.ts +++ b/src/app/shared/stores/contributors/contributors.actions.ts @@ -1,4 +1,4 @@ -import { ContributorAddModel, ContributorModel } from '../models'; +import { ContributorAddModel, ContributorModel } from '@osf/shared/models'; export class GetAllContributors { static readonly type = '[Contributors] Get All Contributors'; diff --git a/src/app/shared/components/contributors/store/contributors.model.ts b/src/app/shared/stores/contributors/contributors.model.ts similarity index 82% rename from src/app/shared/components/contributors/store/contributors.model.ts rename to src/app/shared/stores/contributors/contributors.model.ts index e06694b6a..7e0c466a8 100644 --- a/src/app/shared/components/contributors/store/contributors.model.ts +++ b/src/app/shared/stores/contributors/contributors.model.ts @@ -1,7 +1,6 @@ +import { ContributorAddModel, ContributorModel } from '@osf/shared/models'; import { AsyncStateModel } from '@osf/shared/models/store'; -import { ContributorAddModel, ContributorModel } from '../models'; - export interface ContributorsStateModel { contributorsList: AsyncStateModel & { searchValue: string | null; diff --git a/src/app/shared/components/contributors/store/contributors.selectors.ts b/src/app/shared/stores/contributors/contributors.selectors.ts similarity index 100% rename from src/app/shared/components/contributors/store/contributors.selectors.ts rename to src/app/shared/stores/contributors/contributors.selectors.ts diff --git a/src/app/shared/components/contributors/store/contributors.state.ts b/src/app/shared/stores/contributors/contributors.state.ts similarity index 97% rename from src/app/shared/components/contributors/store/contributors.state.ts rename to src/app/shared/stores/contributors/contributors.state.ts index 7a69ff788..af2c83702 100644 --- a/src/app/shared/components/contributors/store/contributors.state.ts +++ b/src/app/shared/stores/contributors/contributors.state.ts @@ -4,7 +4,7 @@ import { catchError, of, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { ContributorsService } from '../services'; +import { CONTRIBUTORS_SERVICE } from '@osf/shared/constants'; import { AddContributor, @@ -40,7 +40,7 @@ import { ContributorsStateModel } from './contributors.model'; }) @Injectable() export class ContributorsState { - private readonly contributorsService = inject(ContributorsService); + private readonly contributorsService = inject(CONTRIBUTORS_SERVICE); @Action(GetAllContributors) getAllContributors(ctx: StateContext, action: GetAllContributors) { diff --git a/src/app/shared/components/contributors/store/index.ts b/src/app/shared/stores/contributors/index.ts similarity index 100% rename from src/app/shared/components/contributors/store/index.ts rename to src/app/shared/stores/contributors/index.ts diff --git a/src/app/shared/stores/index.ts b/src/app/shared/stores/index.ts index 7b72ba9d3..dab840107 100644 --- a/src/app/shared/stores/index.ts +++ b/src/app/shared/stores/index.ts @@ -1,3 +1,5 @@ export * from './addons'; +export * from './contributors'; export * from './institutions'; +export * from './licenses'; export * from './subjects'; From 17a9e398dff0494948eddb326148c9aea6da1f74 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 3 Jul 2025 10:20:27 +0300 Subject: [PATCH 2/9] feat(contributors): updated view only links --- .../core/constants/ngxs-states.constant.ts | 2 - .../create-view-link-dialog.component.html | 35 +++--- .../create-view-link-dialog.component.scss | 8 +- .../create-view-link-dialog.component.ts | 28 ++--- .../contributors/contributors.component.html | 1 + .../contributors/contributors.component.ts | 29 +++-- .../models/cedar-metadata-template.models.ts | 6 +- src/app/features/project/project.routes.ts | 6 +- ...settings-view-only-links-card.component.ts | 2 +- .../project/settings/mappers/index.ts | 1 - .../features/project/settings/models/index.ts | 3 - .../settings/models/link-table.model.ts | 14 --- .../settings/models/project-settings.model.ts | 3 - .../project/settings/services/index.ts | 1 - .../project/settings/settings.component.ts | 12 +- .../settings/store/settings.actions.ts | 33 +---- .../project/settings/store/settings.model.ts | 4 +- .../settings/store/settings.selectors.ts | 10 -- .../project/settings/store/settings.state.ts | 90 +------------- .../contributors-list.component.html | 43 ++++--- .../contributors-list.component.scss | 5 +- .../contributors-list.component.ts | 1 + .../view-only-table.component.ts | 2 +- src/app/shared/mappers/index.ts | 1 + .../mappers/view-only-links.mapper.ts | 11 +- src/app/shared/models/index.ts | 1 + src/app/shared/models/node-subject.model.ts | 10 -- .../shared/models/view-only-links/index.ts | 2 + .../view-only-link-response.model.ts | 18 +-- .../view-only-links}/view-only-link.model.ts | 0 src/app/shared/services/index.ts | 1 + .../services/view-only-links.service.ts | 12 +- src/app/shared/stores/index.ts | 1 + .../shared/stores/view-only-links/index.ts | 4 + .../view-only-links/view-only-link.actions.ts | 25 ++++ .../view-only-links/view-only-link.model.ts | 6 + .../view-only-link.selectors.ts | 16 +++ .../view-only-links/view-only-link.state.ts | 114 ++++++++++++++++++ src/assets/i18n/en.json | 6 + 39 files changed, 302 insertions(+), 265 deletions(-) delete mode 100644 src/app/features/project/settings/models/link-table.model.ts rename src/app/{features/project/settings => shared}/mappers/view-only-links.mapper.ts (79%) create mode 100644 src/app/shared/models/view-only-links/index.ts rename src/app/{features/project/settings/models => shared/models/view-only-links}/view-only-link-response.model.ts (67%) rename src/app/{features/project/settings/models => shared/models/view-only-links}/view-only-link.model.ts (100%) rename src/app/{features/project/settings => shared}/services/view-only-links.service.ts (75%) create mode 100644 src/app/shared/stores/view-only-links/index.ts create mode 100644 src/app/shared/stores/view-only-links/view-only-link.actions.ts create mode 100644 src/app/shared/stores/view-only-links/view-only-link.model.ts create mode 100644 src/app/shared/stores/view-only-links/view-only-link.selectors.ts create mode 100644 src/app/shared/stores/view-only-links/view-only-link.state.ts diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 936f30cab..f87927c9c 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -7,7 +7,6 @@ import { AnalyticsState } from '@osf/features/project/analytics/store'; import { ProjectMetadataState } from '@osf/features/project/metadata/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; import { RegistrationsState } from '@osf/features/project/registrations/store'; -import { SettingsState } from '@osf/features/project/settings/store'; import { WikiState } from '@osf/features/project/wiki/store/wiki.state'; import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; import { DeveloperAppsState } from '@osf/features/settings/developer-apps/store'; @@ -26,7 +25,6 @@ export const STATES = [ DeveloperAppsState, AccountSettingsState, AnalyticsState, - SettingsState, NotificationSubscriptionState, ProjectOverviewState, CollectionsState, diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html index eb0f4aef4..66a680e1f 100644 --- a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html +++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html @@ -1,16 +1,11 @@
- - +

@@ -18,20 +13,20 @@

- Anonymize contributor list for this link (e.g., for blind peer review). - Ensure the wiki pages, files, registration forms and add-ons do not contain identifying information. + {{ 'myProjects.settings.anonymizeContributorList' | translate }} + + {{ 'myProjects.settings.ensureNoInformation' | translate }} +


- Which components would you like to associate with this link? - Anyone with the private link can view—but not edit—the components associated with the link. + {{ 'myProjects.settings.whichComponentLink' | translate }} + + {{ 'myProjects.settings.anyonePrivateLink' | translate }} +

@@ -62,7 +57,7 @@
diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.scss b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.scss index 0cc299cb5..ab6cbcab1 100644 --- a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.scss +++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.scss @@ -1,7 +1,3 @@ -@use "assets/styles/variables" as var; - -:host { - .break-line { - border: 1px solid var.$grey-3; - } +.break-line { + border: 1px solid var(--grey-3); } diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts index 4651b3d24..23f049135 100644 --- a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts +++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts @@ -3,32 +3,36 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Checkbox } from 'primeng/checkbox'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { InputText } from 'primeng/inputtext'; import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { ViewOnlyLinkNodeModel } from '@osf/features/project/settings/models'; +import { TextInputComponent } from '@osf/shared/components'; +import { InputLimits } from '@osf/shared/constants'; +import { ViewOnlyLinkNodeModel } from '@osf/shared/models'; +import { CustomValidators } from '@osf/shared/utils'; @Component({ selector: 'osf-create-view-link-dialog', - imports: [Button, TranslatePipe, InputText, ReactiveFormsModule, FormsModule, Checkbox], + imports: [Button, TranslatePipe, ReactiveFormsModule, FormsModule, Checkbox, TextInputComponent], templateUrl: './create-view-link-dialog.component.html', styleUrl: './create-view-link-dialog.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class CreateViewLinkDialogComponent implements OnInit { + linkName = new FormControl('', { nonNullable: true, validators: [CustomValidators.requiredTrimmed()] }); + readonly dialogRef = inject(DynamicDialogRef); protected readonly config = inject(DynamicDialogConfig); + inputLimits = InputLimits; - protected selectedComponents = signal>({}); - protected linkName = signal(''); anonymous = signal(true); + protected selectedComponents = signal>({}); readonly projectId = signal(''); ngOnInit(): void { const data = (this.config.data?.['sharedComponents'] as ViewOnlyLinkNodeModel[]) || []; - this.projectId.set(this.config.data?.['projectId']); + this.projectId.set(this.config.data?.projectId); const initialState = data.reduce( (acc, curr) => { if (curr.id) { @@ -45,8 +49,8 @@ export class CreateViewLinkDialogComponent implements OnInit { return item.category === 'project' && item.id === this.projectId(); } - addContributor(): void { - if (!this.linkName()) return; + addLink(): void { + if (!this.linkName.value) return; const components = (this.config.data?.['sharedComponents'] as ViewOnlyLinkNodeModel[]) || []; const selectedIds = Object.entries(this.selectedComponents()).filter(([component, checked]) => checked); @@ -62,7 +66,7 @@ export class CreateViewLinkDialogComponent implements OnInit { const data = { attributes: { - name: this.linkName(), + name: this.linkName.value, anonymous: this.anonymous(), }, nodes: selected, @@ -71,10 +75,6 @@ export class CreateViewLinkDialogComponent implements OnInit { this.dialogRef.close(data); } - onLinkNameChange(value: string): void { - this.linkName.set(value); - } - onCheckboxToggle(id: string, checked: boolean): void { this.selectedComponents.update((prev) => ({ ...prev, [id]: checked })); } diff --git a/src/app/features/project/contributors/contributors.component.html b/src/app/features/project/contributors/contributors.component.html index 9ff4f7612..a256469dd 100644 --- a/src/app/features/project/contributors/contributors.component.html +++ b/src/app/features/project/contributors/contributors.component.html @@ -64,6 +64,7 @@

{{ 'navigation.project.contributors' | tra class="w-full" [contributors]="contributors()" [isLoading]="isContributorsLoading()" + [showCuratorColumn]="true" (remove)="removeContributor($event)" > diff --git a/src/app/features/project/contributors/contributors.component.ts b/src/app/features/project/contributors/contributors.component.ts index a7fd2f109..c93d96045 100644 --- a/src/app/features/project/contributors/contributors.component.ts +++ b/src/app/features/project/contributors/contributors.component.ts @@ -22,28 +22,31 @@ import { } from '@osf/shared/components/contributors'; import { BIBLIOGRAPHY_OPTIONS, PERMISSION_OPTIONS } from '@osf/shared/constants'; import { AddContributorType, ContributorPermission } from '@osf/shared/enums'; -import { ContributorDialogAddModel, ContributorModel, SelectOption } from '@osf/shared/models'; +import { + ContributorDialogAddModel, + ContributorModel, + SelectOption, + ViewOnlyLinkJsonApi, + ViewOnlyLinkModel, +} from '@osf/shared/models'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { AddContributor, ContributorsSelectors, + CreateViewOnlyLink, DeleteContributor, + DeleteViewOnlyLink, + FetchViewOnlyLinks, GetAllContributors, UpdateBibliographyFilter, UpdateContributor, UpdatePermissionFilter, UpdateSearchValue, + ViewOnlyLinkSelectors, } from '@osf/shared/stores'; import { findChangedItems } from '@osf/shared/utils'; -import { ViewOnlyLink, ViewOnlyLinkModel } from '../settings/models'; -import { - CreateViewOnlyLink, - DeleteViewOnlyLink, - GetProjectDetails, - GetViewOnlyLinksTable, - SettingsSelectors, -} from '../settings/store'; +import { GetProjectDetails, SettingsSelectors } from '../settings/store'; import { CreateViewLinkDialogComponent } from './components'; @@ -76,7 +79,7 @@ export class ContributorsComponent implements OnInit { private readonly route = inject(ActivatedRoute); private readonly projectId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); - protected viewOnlyLinks = select(SettingsSelectors.getViewOnlyLinks); + protected viewOnlyLinks = select(ViewOnlyLinkSelectors.getViewOnlyLinks); protected projectDetails = select(SettingsSelectors.getProjectDetails); protected readonly selectedPermission = signal(null); @@ -87,10 +90,10 @@ export class ContributorsComponent implements OnInit { protected initialContributors = select(ContributorsSelectors.getContributors); protected contributors = signal([]); protected readonly isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); - protected readonly isViewOnlyLinksLoading = select(SettingsSelectors.isViewOnlyLinksLoading); + protected readonly isViewOnlyLinksLoading = select(ViewOnlyLinkSelectors.isViewOnlyLinksLoading); protected actions = createDispatchMap({ - getViewOnlyLinks: GetViewOnlyLinksTable, + getViewOnlyLinks: FetchViewOnlyLinks, getProjectDetails: GetProjectDetails, getContributors: GetAllContributors, updateSearchValue: UpdateSearchValue, @@ -260,7 +263,7 @@ export class ContributorsComponent implements OnInit { }) .onClose.pipe( filter((res) => !!res), - switchMap((result) => this.actions.createViewOnlyLink(this.projectId(), result as ViewOnlyLink)), + switchMap((result) => this.actions.createViewOnlyLink(this.projectId(), result as ViewOnlyLinkJsonApi)), takeUntilDestroyed(this.destroyRef) ) .subscribe(); 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 a40fa942b..681d716c2 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 @@ -1,4 +1,4 @@ -import { PaginationLinks } from '@osf/features/project/settings/models'; +import { PaginationLinksJsonApi } from '@osf/core/models'; export interface CedarMetadataDataTemplateJsonApi { id: string; @@ -107,7 +107,7 @@ export interface CedarTemplateContext { export interface CedarMetadataTemplateJsonApi { data: CedarMetadataDataTemplateJsonApi[]; - links: PaginationLinks; + links: PaginationLinksJsonApi; } export interface FieldSchema { @@ -193,7 +193,7 @@ export interface CedarRecordDataBinding { export interface CedarMetadataRecordJsonApi { data: CedarMetadataRecordData[]; - links: PaginationLinks; + links: PaginationLinksJsonApi; meta: { per_page: number; total: number; diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 01a51b343..18bcda3c8 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -4,9 +4,10 @@ import { Routes } from '@angular/router'; import { CONTRIBUTORS_SERVICE } from '@osf/shared/constants'; import { ProjectContributorsService } from '@osf/shared/services'; -import { ContributorsState } from '@osf/shared/stores'; +import { ContributorsState, ViewOnlyLinkState } from '@osf/shared/stores'; import { ProjectFilesState } from './files/store'; +import { SettingsState } from './settings/store'; export const projectRoutes: Routes = [ { @@ -41,13 +42,14 @@ export const projectRoutes: Routes = [ { path: 'settings', loadComponent: () => import('../project/settings/settings.component').then((mod) => mod.SettingsComponent), + providers: [provideStates([SettingsState, ViewOnlyLinkState])], }, { path: 'contributors', loadComponent: () => import('../project/contributors/contributors.component').then((mod) => mod.ContributorsComponent), providers: [ - provideStates([ContributorsState]), + provideStates([ContributorsState, ViewOnlyLinkState]), { provide: CONTRIBUTORS_SERVICE, useClass: ProjectContributorsService, diff --git a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.ts b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.ts index 225168424..fa02ff12c 100644 --- a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.ts +++ b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.ts @@ -4,7 +4,7 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { PaginatedViewOnlyLinksModel, ViewOnlyLinkModel } from '@osf/features/project/settings/models'; +import { PaginatedViewOnlyLinksModel, ViewOnlyLinkModel } from '@osf/shared/models'; import { ViewOnlyTableComponent } from '@shared/components'; @Component({ diff --git a/src/app/features/project/settings/mappers/index.ts b/src/app/features/project/settings/mappers/index.ts index bf98d47fb..260acf2ac 100644 --- a/src/app/features/project/settings/mappers/index.ts +++ b/src/app/features/project/settings/mappers/index.ts @@ -1,2 +1 @@ export * from './settings.mapper'; -export * from './view-only-links.mapper'; diff --git a/src/app/features/project/settings/models/index.ts b/src/app/features/project/settings/models/index.ts index a7e66ecdc..57b6ecf76 100644 --- a/src/app/features/project/settings/models/index.ts +++ b/src/app/features/project/settings/models/index.ts @@ -1,8 +1,5 @@ -export * from './link-table.model'; export * from './project-details.model'; export * from './project-settings.model'; export * from './project-settings-response.model'; export * from './redirect-url-data.model'; export * from './right-control.model'; -export * from './view-only-link.model'; -export * from './view-only-link-response.model'; diff --git a/src/app/features/project/settings/models/link-table.model.ts b/src/app/features/project/settings/models/link-table.model.ts deleted file mode 100644 index 8baf42903..000000000 --- a/src/app/features/project/settings/models/link-table.model.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - ViewOnlyLinkCreatorModel, - ViewOnlyLinkNodeModel, -} from '@osf/features/project/settings/models/view-only-link.model'; - -export interface LinkTableModel { - id: string; - sharedComponents: string; - createdDate: string | Date; - createdBy: ViewOnlyLinkCreatorModel; - nodes: ViewOnlyLinkNodeModel[]; - anonymous: boolean; - link: string; -} diff --git a/src/app/features/project/settings/models/project-settings.model.ts b/src/app/features/project/settings/models/project-settings.model.ts index e1b9c81fd..d59109d38 100644 --- a/src/app/features/project/settings/models/project-settings.model.ts +++ b/src/app/features/project/settings/models/project-settings.model.ts @@ -1,5 +1,3 @@ -import { LinkTableModel } from '@osf/features/project/settings/models'; - export interface ProjectSettingsModel { id: string; attributes: { @@ -11,6 +9,5 @@ export interface ProjectSettingsModel { redirectLinkUrl: string; wikiEnabled: boolean; }; - linkTable: LinkTableModel[]; lastFetched?: number; } diff --git a/src/app/features/project/settings/services/index.ts b/src/app/features/project/settings/services/index.ts index daa503c39..31cb466fb 100644 --- a/src/app/features/project/settings/services/index.ts +++ b/src/app/features/project/settings/services/index.ts @@ -1,2 +1 @@ export { SettingsService } from './settings.service'; -export { ViewOnlyLinksService } from './view-only-links.service'; diff --git a/src/app/features/project/settings/settings.component.ts b/src/app/features/project/settings/settings.component.ts index bc7d54eb8..3c76b1eae 100644 --- a/src/app/features/project/settings/settings.component.ts +++ b/src/app/features/project/settings/settings.component.ts @@ -27,14 +27,11 @@ import { ProjectDetailsModel, ProjectSettingsAttributes, ProjectSettingsData, - ViewOnlyLinkModel, } from '@osf/features/project/settings/models'; import { DeleteProject, - DeleteViewOnlyLink, GetProjectDetails, GetProjectSettings, - GetViewOnlyLinksTable, SettingsSelectors, UpdateProjectDetails, UpdateProjectSettings, @@ -45,10 +42,11 @@ import { UpdateNotificationSubscriptionForNodeId, } from '@osf/features/settings/notifications/store'; import { CustomConfirmationService, LoaderService, ToastService } from '@osf/shared/services'; +import { DeleteViewOnlyLink, FetchViewOnlyLinks, ViewOnlyLinkSelectors } from '@osf/shared/stores'; import { CustomValidators } from '@osf/shared/utils'; import { SubHeaderComponent } from '@shared/components'; import { ProjectFormControls, SubscriptionEvent, SubscriptionFrequency } from '@shared/enums'; -import { UpdateNodeRequestModel } from '@shared/models'; +import { UpdateNodeRequestModel, ViewOnlyLinkModel } from '@shared/models'; @Component({ selector: 'osf-settings', @@ -85,14 +83,14 @@ export class SettingsComponent implements OnInit { protected settings = select(SettingsSelectors.getSettings); protected notifications = select(NotificationSubscriptionSelectors.getNotificationSubscriptionsByNodeId); protected projectDetails = select(SettingsSelectors.getProjectDetails); - protected viewOnlyLinks = select(SettingsSelectors.getViewOnlyLinks); - protected isViewOnlyLinksLoading = select(SettingsSelectors.isViewOnlyLinksLoading); + protected viewOnlyLinks = select(ViewOnlyLinkSelectors.getViewOnlyLinks); + protected isViewOnlyLinksLoading = select(ViewOnlyLinkSelectors.isViewOnlyLinksLoading); protected actions = createDispatchMap({ getSettings: GetProjectSettings, getNotifications: GetNotificationSubscriptionsByNodeId, getProjectDetails: GetProjectDetails, - getViewOnlyLinks: GetViewOnlyLinksTable, + getViewOnlyLinks: FetchViewOnlyLinks, updateProjectDetails: UpdateProjectDetails, updateProjectSettings: UpdateProjectSettings, updateNotificationSubscriptionForNodeId: UpdateNotificationSubscriptionForNodeId, diff --git a/src/app/features/project/settings/store/settings.actions.ts b/src/app/features/project/settings/store/settings.actions.ts index 86c8620d1..a8749e66c 100644 --- a/src/app/features/project/settings/store/settings.actions.ts +++ b/src/app/features/project/settings/store/settings.actions.ts @@ -1,26 +1,21 @@ -import { ProjectSettingsData, ViewOnlyLink } from '@osf/features/project/settings/models'; import { UpdateNodeRequestModel } from '@shared/models'; +import { ProjectSettingsData } from '../models'; + export class GetProjectSettings { - static readonly type = '[Settings] Get'; + static readonly type = '[Settings] Get Project Settings'; constructor(public projectId: string) {} } export class GetProjectDetails { - static readonly type = '[Project] Get'; - - constructor(public projectId: string) {} -} - -export class GetViewOnlyLinksTable { - static readonly type = '[Link] Table Get'; + static readonly type = '[Project] Get Project Details'; constructor(public projectId: string) {} } export class UpdateProjectSettings { - static readonly type = '[Settings] Update'; + static readonly type = '[Settings] Update Project Settings'; constructor(public payload: ProjectSettingsData) {} } @@ -31,24 +26,6 @@ export class UpdateProjectDetails { constructor(public payload: UpdateNodeRequestModel) {} } -export class CreateViewOnlyLink { - static readonly type = '[Link] Create'; - - constructor( - public projectId: string, - public payload: ViewOnlyLink - ) {} -} - -export class DeleteViewOnlyLink { - static readonly type = '[Link] Delete'; - - constructor( - public projectId: string, - public linkId: string - ) {} -} - export class DeleteProject { static readonly type = '[Settings] Delete Project'; diff --git a/src/app/features/project/settings/store/settings.model.ts b/src/app/features/project/settings/store/settings.model.ts index fca8a6f74..87dd0f1ab 100644 --- a/src/app/features/project/settings/store/settings.model.ts +++ b/src/app/features/project/settings/store/settings.model.ts @@ -1,9 +1,9 @@ -import { PaginatedViewOnlyLinksModel, ProjectSettingsModel } from '@osf/features/project/settings/models'; import { NodeData } from '@shared/models'; import { AsyncStateModel } from '@shared/models/store'; +import { ProjectSettingsModel } from '../models'; + export interface SettingsStateModel { settings: AsyncStateModel; projectDetails: AsyncStateModel; - viewOnlyLinks: AsyncStateModel; } diff --git a/src/app/features/project/settings/store/settings.selectors.ts b/src/app/features/project/settings/store/settings.selectors.ts index 27daf414b..de6a8ec5e 100644 --- a/src/app/features/project/settings/store/settings.selectors.ts +++ b/src/app/features/project/settings/store/settings.selectors.ts @@ -13,14 +13,4 @@ export class SettingsSelectors { static getProjectDetails(state: SettingsStateModel) { return state.projectDetails.data; } - - @Selector([SettingsState]) - static getViewOnlyLinks(state: SettingsStateModel) { - return state.viewOnlyLinks.data; - } - - @Selector([SettingsState]) - static isViewOnlyLinksLoading(state: SettingsStateModel) { - return state.viewOnlyLinks.isLoading; - } } diff --git a/src/app/features/project/settings/store/settings.state.ts b/src/app/features/project/settings/store/settings.state.ts index 7ee0bf05b..9ef016d59 100644 --- a/src/app/features/project/settings/store/settings.state.ts +++ b/src/app/features/project/settings/store/settings.state.ts @@ -1,5 +1,4 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { patch } from '@ngxs/store/operators'; import { map, of, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; @@ -7,21 +6,19 @@ import { catchError, tap } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { MyProjectsService } from '@osf/features/my-projects/services'; -import { PaginatedViewOnlyLinksModel, ProjectSettingsModel } from '@osf/features/project/settings/models'; -import { SettingsService, ViewOnlyLinksService } from '@osf/features/project/settings/services'; +import { SettingsService } from '@osf/features/project/settings/services'; import { - CreateViewOnlyLink, DeleteProject, - DeleteViewOnlyLink, GetProjectDetails, GetProjectSettings, - GetViewOnlyLinksTable, UpdateProjectDetails, UpdateProjectSettings, } from '@osf/features/project/settings/store/settings.actions'; import { SettingsStateModel } from '@osf/features/project/settings/store/settings.model'; import { NodeData } from '@shared/models'; +import { ProjectSettingsModel } from '../models'; + @State({ name: 'settings', defaults: { @@ -35,18 +32,12 @@ import { NodeData } from '@shared/models'; isLoading: false, error: null, }, - viewOnlyLinks: { - data: {} as PaginatedViewOnlyLinksModel, - isLoading: false, - error: null, - }, }, }) @Injectable() export class SettingsState { private readonly settingsService = inject(SettingsService); private readonly myProjectService = inject(MyProjectsService); - private readonly viewOnlyLinksService = inject(ViewOnlyLinksService); private readonly REFRESH_INTERVAL = 5 * 60 * 1000; @@ -140,29 +131,6 @@ export class SettingsState { ); } - @Action(GetViewOnlyLinksTable) - getViewOnlyLinksTable(ctx: StateContext, action: GetViewOnlyLinksTable) { - const state = ctx.getState(); - - ctx.patchState({ - viewOnlyLinks: { ...state.viewOnlyLinks, isLoading: true, error: null }, - }); - - return this.viewOnlyLinksService.getViewOnlyLinksData(action.projectId).pipe( - map((response) => response), - tap((links) => { - ctx.patchState({ - viewOnlyLinks: { - data: links, - isLoading: false, - error: null, - }, - }); - }), - catchError((error) => this.handleError(ctx, 'viewOnlyLinks', error)) - ); - } - @Action(UpdateProjectDetails) updateProjectDetails(ctx: StateContext, action: UpdateProjectDetails) { return this.myProjectService.updateProjectById(action.payload).pipe( @@ -210,58 +178,6 @@ export class SettingsState { ); } - @Action(CreateViewOnlyLink) - createViewOnlyLink(ctx: StateContext, action: CreateViewOnlyLink) { - const state = ctx.getState(); - - ctx.patchState({ - viewOnlyLinks: { ...state.viewOnlyLinks, isLoading: true, error: null }, - }); - - return this.viewOnlyLinksService.createViewOnlyLink(action.projectId, action.payload).pipe( - tap((data: PaginatedViewOnlyLinksModel) => { - ctx.patchState({ - viewOnlyLinks: { - data: { - ...state.viewOnlyLinks.data, - items: [data.items[0], ...state.viewOnlyLinks.data.items], - }, - isLoading: false, - error: null, - }, - }); - }), - catchError((error) => this.handleError(ctx, 'viewOnlyLinks', error)) - ); - } - - @Action(DeleteViewOnlyLink) - deleteViewOnlyLink(ctx: StateContext, action: DeleteViewOnlyLink) { - const state = ctx.getState(); - - ctx.patchState({ - viewOnlyLinks: { ...state.viewOnlyLinks, isLoading: true, error: null }, - }); - - return this.viewOnlyLinksService.deleteLink(action.projectId, action.linkId).pipe( - tap(() => { - ctx.setState( - patch({ - viewOnlyLinks: { - data: { - ...ctx.getState().viewOnlyLinks.data, - items: ctx.getState().viewOnlyLinks.data.items.filter((item) => item.id !== action.linkId), - }, - isLoading: false, - error: null, - }, - }) - ); - }), - catchError((error) => this.handleError(ctx, 'viewOnlyLinks', error)) - ); - } - @Action(DeleteProject) deleteProject(ctx: StateContext, action: DeleteProject) { return this.settingsService.deleteProject(action.projectId); diff --git a/src/app/shared/components/contributors/contributors-list/contributors-list.component.html b/src/app/shared/components/contributors/contributors-list/contributors-list.component.html index 96aacf3c3..e87261521 100644 --- a/src/app/shared/components/contributors/contributors-list/contributors-list.component.html +++ b/src/app/shared/components/contributors/contributors-list/contributors-list.component.html @@ -70,19 +70,21 @@

{{ 'project.contributors.bibliographicContributorInfo.heading' | translate }

- -
- {{ 'project.contributors.table.headers.curator' | translate }} - - -
-

{{ 'project.contributors.curatorInfo.heading' | translate }}

+ @if (showCuratorColumn()) { + +
+ {{ 'project.contributors.table.headers.curator' | translate }} + + +
+

{{ 'project.contributors.curatorInfo.heading' | translate }}

- {{ 'project.contributors.curatorInfo.description' | translate }} -
-
-
- + {{ 'project.contributors.curatorInfo.description' | translate }} +
+
+
+ + } {{ 'project.contributors.table.headers.employment' | translate }} {{ 'project.contributors.table.headers.education' | translate }} @@ -113,11 +115,18 @@

{{ 'project.contributors.curatorInfo.heading' | translate }}

- -
- -
- + @if (showCuratorColumn()) { + +
+ +
+ + } @if (contributor.employment?.length) { ([]); isLoading = input(false); + showCuratorColumn = input(false); remove = output(); diff --git a/src/app/shared/components/view-only-table/view-only-table.component.ts b/src/app/shared/components/view-only-table/view-only-table.component.ts index 61baa189a..974286825 100644 --- a/src/app/shared/components/view-only-table/view-only-table.component.ts +++ b/src/app/shared/components/view-only-table/view-only-table.component.ts @@ -9,7 +9,7 @@ import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; -import { PaginatedViewOnlyLinksModel, ViewOnlyLinkModel } from '@osf/features/project/settings/models'; +import { PaginatedViewOnlyLinksModel, ViewOnlyLinkModel } from '@osf/shared/models'; import { CopyButtonComponent } from '../copy-button/copy-button.component'; diff --git a/src/app/shared/mappers/index.ts b/src/app/shared/mappers/index.ts index 8be2622e6..fd4a7b2c3 100644 --- a/src/app/shared/mappers/index.ts +++ b/src/app/shared/mappers/index.ts @@ -5,3 +5,4 @@ export * from './institutions'; export * from './licenses.mapper'; export * from './resource-card'; export * from './subjects'; +export * from './view-only-links.mapper'; diff --git a/src/app/features/project/settings/mappers/view-only-links.mapper.ts b/src/app/shared/mappers/view-only-links.mapper.ts similarity index 79% rename from src/app/features/project/settings/mappers/view-only-links.mapper.ts rename to src/app/shared/mappers/view-only-links.mapper.ts index 2726cd954..f9583baa1 100644 --- a/src/app/features/project/settings/mappers/view-only-links.mapper.ts +++ b/src/app/shared/mappers/view-only-links.mapper.ts @@ -1,7 +1,12 @@ -import { PaginatedViewOnlyLinksModel, ViewOnlyLink, ViewOnlyLinkModel, ViewOnlyLinksResponseModel } from '../models/'; +import { + PaginatedViewOnlyLinksModel, + ViewOnlyLinkJsonApi, + ViewOnlyLinkModel, + ViewOnlyLinksResponseJsonApi, +} from '../models'; export class ViewOnlyLinksMapper { - static fromResponse(response: ViewOnlyLinksResponseModel, projectId: string): PaginatedViewOnlyLinksModel { + static fromResponse(response: ViewOnlyLinksResponseJsonApi, projectId: string): PaginatedViewOnlyLinksModel { const items: ViewOnlyLinkModel[] = response.data.map((item) => ({ id: item.id, link: `${document.baseURI}my-projects/${projectId}/overview?view_only=${item.attributes.key}`, @@ -25,7 +30,7 @@ export class ViewOnlyLinksMapper { }; } - static fromSingleResponse(response: ViewOnlyLink, projectId: string): PaginatedViewOnlyLinksModel { + static fromSingleResponse(response: ViewOnlyLinkJsonApi, projectId: string): PaginatedViewOnlyLinksModel { const item = response; const mappedItem: ViewOnlyLinkModel = { diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index f6d219180..c34522aaa 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -38,3 +38,4 @@ export * from './tutorial-step.model'; export * from './update-node-request.model'; export * from './user'; export * from './validation-params.model'; +export * from './view-only-links'; diff --git a/src/app/shared/models/node-subject.model.ts b/src/app/shared/models/node-subject.model.ts index 8e37b5964..62575185b 100644 --- a/src/app/shared/models/node-subject.model.ts +++ b/src/app/shared/models/node-subject.model.ts @@ -1,5 +1,3 @@ -import { LinkTableModel } from '@osf/features/project/settings/models'; - export interface NodeSubjectModel { id: string; text: string; @@ -68,11 +66,3 @@ export interface UpdateSubjectRequestJsonApi { id: string; type: 'subjects'; } - -export interface UpdateSubjectResponseJsonApi { - data: UpdateSubjectRequestJsonApi[]; - links: LinkTableModel; - meta: { - version: string; - }; -} diff --git a/src/app/shared/models/view-only-links/index.ts b/src/app/shared/models/view-only-links/index.ts new file mode 100644 index 000000000..8bc5e3616 --- /dev/null +++ b/src/app/shared/models/view-only-links/index.ts @@ -0,0 +1,2 @@ +export * from './view-only-link.model'; +export * from './view-only-link-response.model'; diff --git a/src/app/features/project/settings/models/view-only-link-response.model.ts b/src/app/shared/models/view-only-links/view-only-link-response.model.ts similarity index 67% rename from src/app/features/project/settings/models/view-only-link-response.model.ts rename to src/app/shared/models/view-only-links/view-only-link-response.model.ts index 5692975e1..29eb57cc0 100644 --- a/src/app/features/project/settings/models/view-only-link-response.model.ts +++ b/src/app/shared/models/view-only-links/view-only-link-response.model.ts @@ -1,14 +1,14 @@ import { UserGetResponse } from '@osf/core/models'; -export interface ViewOnlyLinksResponseModel { - data: ViewOnlyLink[]; - links: PaginationLinks; +export interface ViewOnlyLinksResponseJsonApi { + data: ViewOnlyLinkJsonApi[]; + links: PaginationLinksJsonApi; meta: { version: string; }; } -export interface ViewOnlyLink { +export interface ViewOnlyLinkJsonApi { id: string; type: 'view_only_links'; attributes: { @@ -25,7 +25,7 @@ export interface ViewOnlyLink { relationships: { creator: { links: { - related: LinkWithMeta; + related: LinkWithMetaJsonApi; }; data: { id: string; @@ -34,8 +34,8 @@ export interface ViewOnlyLink { }; nodes: { links: { - related: LinkWithMeta; - self: LinkWithMeta; + related: LinkWithMetaJsonApi; + self: LinkWithMetaJsonApi; }; }; }; @@ -44,12 +44,12 @@ export interface ViewOnlyLink { }; } -export interface LinkWithMeta { +export interface LinkWithMetaJsonApi { href: string; meta: Record; } -export interface PaginationLinks { +export interface PaginationLinksJsonApi { first: string | null; last: string | null; prev: string | null; diff --git a/src/app/features/project/settings/models/view-only-link.model.ts b/src/app/shared/models/view-only-links/view-only-link.model.ts similarity index 100% rename from src/app/features/project/settings/models/view-only-link.model.ts rename to src/app/shared/models/view-only-links/view-only-link.model.ts diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 0c4473184..4a297c4d2 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -11,3 +11,4 @@ export { ResourceCardService } from './resource-card.service'; export { SearchService } from './search.service'; export { SubjectsService } from './subjects.service'; export { ToastService } from './toast.service'; +export { ViewOnlyLinksService } from './view-only-links.service'; diff --git a/src/app/features/project/settings/services/view-only-links.service.ts b/src/app/shared/services/view-only-links.service.ts similarity index 75% rename from src/app/features/project/settings/services/view-only-links.service.ts rename to src/app/shared/services/view-only-links.service.ts index 574fca5f5..e0d95c373 100644 --- a/src/app/features/project/settings/services/view-only-links.service.ts +++ b/src/app/shared/services/view-only-links.service.ts @@ -6,7 +6,11 @@ import { JsonApiResponse } from '@core/models'; import { JsonApiService } from '@core/services'; import { ViewOnlyLinksMapper } from '../mappers'; -import { PaginatedViewOnlyLinksModel, ViewOnlyLink, ViewOnlyLinksResponseModel } from '../models'; +import { + PaginatedViewOnlyLinksModel, + ViewOnlyLinkJsonApi, + ViewOnlyLinksResponseJsonApi, +} from '../models/view-only-links'; import { environment } from 'src/environments/environment'; @@ -20,17 +24,17 @@ export class ViewOnlyLinksService { const params: Record = { embed: 'creator' }; return this.jsonApiService - .get(`${environment.apiUrl}/nodes/${projectId}/view_only_links`, params) + .get(`${environment.apiUrl}/nodes/${projectId}/view_only_links`, params) .pipe(map((response) => ViewOnlyLinksMapper.fromResponse(response, projectId))); } - createViewOnlyLink(projectId: string, payload: ViewOnlyLink): Observable { + createViewOnlyLink(projectId: string, payload: ViewOnlyLinkJsonApi): Observable { const data = { data: { ...payload } }; const params: Record = { embed: 'creator' }; return this.jsonApiService .post< - JsonApiResponse + JsonApiResponse >(`${environment.apiUrl}/nodes/${projectId}/view_only_links/`, data, params) .pipe(map((response) => ViewOnlyLinksMapper.fromSingleResponse(response.data, projectId))); } diff --git a/src/app/shared/stores/index.ts b/src/app/shared/stores/index.ts index 9a28c2406..a7650198c 100644 --- a/src/app/shared/stores/index.ts +++ b/src/app/shared/stores/index.ts @@ -4,3 +4,4 @@ export * from './institutions'; export * from './institutions-search'; export * from './licenses'; export * from './subjects'; +export * from './view-only-links'; diff --git a/src/app/shared/stores/view-only-links/index.ts b/src/app/shared/stores/view-only-links/index.ts new file mode 100644 index 000000000..b3ca751f0 --- /dev/null +++ b/src/app/shared/stores/view-only-links/index.ts @@ -0,0 +1,4 @@ +export * from './view-only-link.actions'; +export * from './view-only-link.model'; +export * from './view-only-link.selectors'; +export * from './view-only-link.state'; diff --git a/src/app/shared/stores/view-only-links/view-only-link.actions.ts b/src/app/shared/stores/view-only-links/view-only-link.actions.ts new file mode 100644 index 000000000..1f233061f --- /dev/null +++ b/src/app/shared/stores/view-only-links/view-only-link.actions.ts @@ -0,0 +1,25 @@ +import { ViewOnlyLinkJsonApi } from '@osf/shared/models'; + +export class FetchViewOnlyLinks { + static readonly type = '[Link] Fetch View Only Links'; + + constructor(public projectId: string) {} +} + +export class CreateViewOnlyLink { + static readonly type = '[Link] Create View Only Links'; + + constructor( + public projectId: string, + public payload: ViewOnlyLinkJsonApi + ) {} +} + +export class DeleteViewOnlyLink { + static readonly type = '[Link] Delete View Only Links'; + + constructor( + public projectId: string, + public linkId: string + ) {} +} diff --git a/src/app/shared/stores/view-only-links/view-only-link.model.ts b/src/app/shared/stores/view-only-links/view-only-link.model.ts new file mode 100644 index 000000000..0a8631744 --- /dev/null +++ b/src/app/shared/stores/view-only-links/view-only-link.model.ts @@ -0,0 +1,6 @@ +import { PaginatedViewOnlyLinksModel } from '@shared/models'; +import { AsyncStateModel } from '@shared/models/store'; + +export interface ViewOnlyLinkStateModel { + viewOnlyLinks: AsyncStateModel; +} diff --git a/src/app/shared/stores/view-only-links/view-only-link.selectors.ts b/src/app/shared/stores/view-only-links/view-only-link.selectors.ts new file mode 100644 index 000000000..34d33da52 --- /dev/null +++ b/src/app/shared/stores/view-only-links/view-only-link.selectors.ts @@ -0,0 +1,16 @@ +import { Selector } from '@ngxs/store'; + +import { ViewOnlyLinkStateModel } from './view-only-link.model'; +import { ViewOnlyLinkState } from './view-only-link.state'; + +export class ViewOnlyLinkSelectors { + @Selector([ViewOnlyLinkState]) + static getViewOnlyLinks(state: ViewOnlyLinkStateModel) { + return state.viewOnlyLinks.data; + } + + @Selector([ViewOnlyLinkState]) + static isViewOnlyLinksLoading(state: ViewOnlyLinkStateModel) { + return state.viewOnlyLinks.isLoading; + } +} diff --git a/src/app/shared/stores/view-only-links/view-only-link.state.ts b/src/app/shared/stores/view-only-links/view-only-link.state.ts new file mode 100644 index 000000000..9c6c1778b --- /dev/null +++ b/src/app/shared/stores/view-only-links/view-only-link.state.ts @@ -0,0 +1,114 @@ +import { Action, State, StateContext } from '@ngxs/store'; +import { patch } from '@ngxs/store/operators'; + +import { map, throwError } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; + +import { inject, Injectable } from '@angular/core'; + +import { ViewOnlyLinksService } from '@osf/shared/services'; +import { PaginatedViewOnlyLinksModel } from '@shared/models'; + +import { CreateViewOnlyLink, DeleteViewOnlyLink, FetchViewOnlyLinks } from './view-only-link.actions'; +import { ViewOnlyLinkStateModel } from './view-only-link.model'; + +@State({ + name: 'viewOnlyLinks', + defaults: { + viewOnlyLinks: { + data: {} as PaginatedViewOnlyLinksModel, + isLoading: false, + error: null, + }, + }, +}) +@Injectable() +export class ViewOnlyLinkState { + private readonly viewOnlyLinksService = inject(ViewOnlyLinksService); + + private handleError(ctx: StateContext, section: keyof ViewOnlyLinkStateModel, error: Error) { + ctx.patchState({ + [section]: { + ...ctx.getState()[section], + isLoading: false, + error: error.message, + }, + }); + return throwError(() => error); + } + + @Action(FetchViewOnlyLinks) + fetchViewOnlyLinks(ctx: StateContext, action: FetchViewOnlyLinks) { + const state = ctx.getState(); + + ctx.patchState({ + viewOnlyLinks: { ...state.viewOnlyLinks, isLoading: true, error: null }, + }); + + return this.viewOnlyLinksService.getViewOnlyLinksData(action.projectId).pipe( + map((response) => response), + tap((links) => { + ctx.patchState({ + viewOnlyLinks: { + data: links, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'viewOnlyLinks', error)) + ); + } + + @Action(CreateViewOnlyLink) + createViewOnlyLink(ctx: StateContext, action: CreateViewOnlyLink) { + const state = ctx.getState(); + + ctx.patchState({ + viewOnlyLinks: { ...state.viewOnlyLinks, isLoading: true, error: null }, + }); + + return this.viewOnlyLinksService.createViewOnlyLink(action.projectId, action.payload).pipe( + tap((data: PaginatedViewOnlyLinksModel) => { + ctx.patchState({ + viewOnlyLinks: { + data: { + ...state.viewOnlyLinks.data, + items: [data.items[0], ...state.viewOnlyLinks.data.items], + }, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'viewOnlyLinks', error)) + ); + } + + @Action(DeleteViewOnlyLink) + deleteViewOnlyLink(ctx: StateContext, action: DeleteViewOnlyLink) { + const state = ctx.getState(); + + ctx.patchState({ + viewOnlyLinks: { ...state.viewOnlyLinks, isLoading: true, error: null }, + }); + + return this.viewOnlyLinksService.deleteLink(action.projectId, action.linkId).pipe( + tap(() => { + ctx.setState( + patch({ + viewOnlyLinks: { + data: { + ...ctx.getState().viewOnlyLinks.data, + items: ctx.getState().viewOnlyLinks.data.items.filter((item) => item.id !== action.linkId), + }, + isLoading: false, + error: null, + }, + }) + ); + }), + catchError((error) => this.handleError(ctx, 'viewOnlyLinks', error)) + ); + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 04b582bd6..ec403b29c 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -326,6 +326,12 @@ "createdBy": "Created By", "anonymous": "Anonymous" }, + "anonymizeContributorList": "Anonymize contributor list for this link (e.g., for blind peer review).", + "ensureNoInformation": "Ensure the wiki pages, files, registration forms and add-ons do not contain identifying information.", + "linkName": "Link name", + "typeLinkName": "Type link name", + "whichComponentLink": "Which components would you like to associate with this link?", + "anyonePrivateLink": "Anyone with the private link can view—but not edit—the components associated with the link.", "accessRequests": "Access Requests", "accessRequestsText": "Allow users to request access to this project", "wiki": "Wiki", From fbdbcbc7fb129ed3d36b8b91ef0a3c58cbb8bd66 Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 4 Jul 2025 14:25:32 +0300 Subject: [PATCH 3/9] feat(contributors): updated analytics and view only links --- package.json | 2 - .../core/constants/ngxs-states.constant.ts | 2 - .../mappers/related-counts.mapper.ts | 6 +- .../project/analytics/services/index.ts | 3 +- ...ervice.ts => project-analytics.service.ts} | 8 +-- .../registration-analytics.service.ts | 34 ++++++++++ .../analytics/store/analytics.state.ts | 9 +-- .../contributors/contributors.component.scss | 3 +- .../contributors/contributors.component.ts | 33 +++++----- src/app/features/project/project.routes.ts | 17 ++++- .../features/registries/registries.routes.ts | 42 ++++++++++++- .../shared/constants/contributors/index.ts | 1 - .../analytics/analytics-service.model.ts | 8 +++ src/app/shared/models/analytics/index.ts | 1 + src/app/shared/models/index.ts | 1 + .../shared/models/view-only-links/index.ts | 1 + .../view-only-links-service.model.ts | 13 ++++ src/app/shared/services/index.ts | 2 +- ....ts => project-view-only-links.service.ts} | 7 ++- .../registration-contributors.service.ts | 62 +++++++++++++++++++ .../registration-view-only-links.service.ts | 50 +++++++++++++++ .../stores/contributors/contributors.state.ts | 2 +- .../view-only-links/view-only-link.actions.ts | 6 ++ .../view-only-links/view-only-link.model.ts | 3 +- .../view-only-link.selectors.ts | 5 ++ .../view-only-links/view-only-link.state.ts | 60 ++++++++++++++---- src/app/shared/tokens/analytics.token.ts | 5 ++ .../contributors.token.ts | 0 src/app/shared/tokens/index.ts | 4 ++ .../shared/tokens/view-only-links.token.ts | 5 ++ src/assets/i18n/en.json | 1 + 31 files changed, 342 insertions(+), 54 deletions(-) rename src/app/features/project/analytics/services/{analytics.service.ts => project-analytics.service.ts} (87%) create mode 100644 src/app/features/project/analytics/services/registration-analytics.service.ts create mode 100644 src/app/shared/models/analytics/analytics-service.model.ts create mode 100644 src/app/shared/models/analytics/index.ts create mode 100644 src/app/shared/models/view-only-links/view-only-links-service.model.ts rename src/app/shared/services/{view-only-links.service.ts => project-view-only-links.service.ts} (86%) create mode 100644 src/app/shared/services/registration-contributors.service.ts create mode 100644 src/app/shared/services/registration-view-only-links.service.ts create mode 100644 src/app/shared/tokens/analytics.token.ts rename src/app/shared/{constants/contributors => tokens}/contributors.token.ts (100%) create mode 100644 src/app/shared/tokens/view-only-links.token.ts diff --git a/package.json b/package.json index 5d0b56e6c..cf81bc27c 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "@angular/compiler-cli": "^19.2.0", "@commitlint/cli": "^19.7.1", "@commitlint/config-conventional": "^19.7.1", - "@types/jasmine": "~5.1.0", "@types/jest": "^29.5.14", "@types/markdown-it": "^14.1.2", "angular-eslint": "19.1.0", @@ -74,7 +73,6 @@ "eslint-plugin-unused-imports": "^4.1.4", "fantasticon": "^3.0.0", "husky": "^9.1.7", - "jasmine-core": "~5.6.0", "jest": "^29.7.0", "jest-preset-angular": "^14.5.5", "lint-staged": "^15.4.3", diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index f87927c9c..4091fed03 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -3,7 +3,6 @@ import { UserState } from '@core/store/user'; import { CollectionsState } from '@osf/features/collections/store'; import { MeetingsState } from '@osf/features/meetings/store'; import { MyProjectsState } from '@osf/features/my-projects/store'; -import { AnalyticsState } from '@osf/features/project/analytics/store'; import { ProjectMetadataState } from '@osf/features/project/metadata/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; import { RegistrationsState } from '@osf/features/project/registrations/store'; @@ -24,7 +23,6 @@ export const STATES = [ ProfileSettingsState, DeveloperAppsState, AccountSettingsState, - AnalyticsState, NotificationSubscriptionState, ProjectOverviewState, CollectionsState, diff --git a/src/app/features/project/analytics/mappers/related-counts.mapper.ts b/src/app/features/project/analytics/mappers/related-counts.mapper.ts index 7205d7859..9ada63557 100644 --- a/src/app/features/project/analytics/mappers/related-counts.mapper.ts +++ b/src/app/features/project/analytics/mappers/related-counts.mapper.ts @@ -4,9 +4,9 @@ export class RelatedCountsMapper { static fromResponse(response: RelatedCountsGetResponse): RelatedCountsModel { return { id: response.data.id, - forksCount: response.data.relationships.forks.links.related.meta.count, - linksToCount: response.data.relationships.linked_by_nodes.links.related.meta.count, - templateCount: response.meta.templated_by_count, + forksCount: response.data.relationships.forks?.links.related.meta.count || 0, + linksToCount: response.data.relationships.linked_by_nodes?.links.related.meta.count || 0, + templateCount: response.meta.templated_by_count || 0, }; } } diff --git a/src/app/features/project/analytics/services/index.ts b/src/app/features/project/analytics/services/index.ts index 58bd2917d..aff609037 100644 --- a/src/app/features/project/analytics/services/index.ts +++ b/src/app/features/project/analytics/services/index.ts @@ -1 +1,2 @@ -export { AnalyticsService } from './analytics.service'; +export { ProjectAnalyticsService } from './project-analytics.service'; +export { RegistrationAnalyticsService } from './registration-analytics.service'; diff --git a/src/app/features/project/analytics/services/analytics.service.ts b/src/app/features/project/analytics/services/project-analytics.service.ts similarity index 87% rename from src/app/features/project/analytics/services/analytics.service.ts rename to src/app/features/project/analytics/services/project-analytics.service.ts index ed9bcd76e..f096f2817 100644 --- a/src/app/features/project/analytics/services/analytics.service.ts +++ b/src/app/features/project/analytics/services/project-analytics.service.ts @@ -13,13 +13,13 @@ import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class AnalyticsService { - #jsonApiService = inject(JsonApiService); +export class ProjectAnalyticsService { + private readonly jsonApiService = inject(JsonApiService); getMetrics(projectId: string, dateRange: string): Observable { const baseUrl = `${environment.apiDomainUrl}/_/metrics/query/node_analytics`; - return this.#jsonApiService + return this.jsonApiService .get>(`${baseUrl}/${projectId}/${dateRange}`) .pipe(map((response) => AnalyticsMetricsMapper.fromResponse(response.data))); } @@ -27,7 +27,7 @@ export class AnalyticsService { getRelatedCounts(projectId: string) { const url = `${environment.apiUrl}/nodes/${projectId}/?related_counts=true`; - return this.#jsonApiService + return this.jsonApiService .get(url) .pipe(map((response) => RelatedCountsMapper.fromResponse(response))); } diff --git a/src/app/features/project/analytics/services/registration-analytics.service.ts b/src/app/features/project/analytics/services/registration-analytics.service.ts new file mode 100644 index 000000000..b55eb9ec2 --- /dev/null +++ b/src/app/features/project/analytics/services/registration-analytics.service.ts @@ -0,0 +1,34 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponse } from '@osf/core/models'; +import { JsonApiService } from '@osf/core/services'; + +import { AnalyticsMetricsMapper, RelatedCountsMapper } from '../mappers'; +import { AnalyticsMetricsGetResponse, AnalyticsMetricsModel, RelatedCountsGetResponse } from '../models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class RegistrationAnalyticsService { + private readonly jsonApiService = inject(JsonApiService); + + getMetrics(projectId: string, dateRange: string): Observable { + const baseUrl = `${environment.apiDomainUrl}/_/metrics/query/node_analytics`; + + return this.jsonApiService + .get>(`${baseUrl}/${projectId}/${dateRange}`) + .pipe(map((response) => AnalyticsMetricsMapper.fromResponse(response.data))); + } + + getRelatedCounts(projectId: string) { + const url = `${environment.apiUrl}/registrations/${projectId}/?related_counts=true`; + + return this.jsonApiService + .get(url) + .pipe(map((response) => RelatedCountsMapper.fromResponse(response))); + } +} diff --git a/src/app/features/project/analytics/store/analytics.state.ts b/src/app/features/project/analytics/store/analytics.state.ts index 614d196f1..594966bdb 100644 --- a/src/app/features/project/analytics/store/analytics.state.ts +++ b/src/app/features/project/analytics/store/analytics.state.ts @@ -5,8 +5,9 @@ import { catchError, of, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { ANALYTICS_SERVICE } from '@osf/shared/tokens'; + import { AnalyticsMetricsModel, RelatedCountsModel } from '../models'; -import { AnalyticsService } from '../services'; import { GetMetrics, GetRelatedCounts } from './analytics.actions'; import { AnalyticsStateModel } from './analytics.model'; @@ -28,7 +29,7 @@ import { AnalyticsStateModel } from './analytics.model'; }) @Injectable() export class AnalyticsState { - #analyticsService = inject(AnalyticsService); + private readonly analyticsService = inject(ANALYTICS_SERVICE); private readonly REFRESH_INTERVAL = 5 * 60 * 1000; @Action(GetMetrics) @@ -53,7 +54,7 @@ export class AnalyticsState { metrics: { ...state.metrics, isLoading: true, error: null }, }); - return this.#analyticsService.getMetrics(action.projectId, action.dateRange).pipe( + return this.analyticsService.getMetrics(action.projectId, action.dateRange).pipe( tap((metrics) => { const exists = state.metrics.data.some((m) => m.id === metrics.id); metrics.lastFetched = Date.now(); @@ -94,7 +95,7 @@ export class AnalyticsState { relatedCounts: { ...state.relatedCounts, isLoading: true, error: null }, }); - return this.#analyticsService.getRelatedCounts(action.projectId).pipe( + return this.analyticsService.getRelatedCounts(action.projectId).pipe( tap((relatedCounts) => { const exists = state.relatedCounts.data.some((rc) => rc.id === relatedCounts.id); relatedCounts.lastFetched = Date.now(); diff --git a/src/app/features/project/contributors/contributors.component.scss b/src/app/features/project/contributors/contributors.component.scss index 5d73bb17f..b262d0a74 100644 --- a/src/app/features/project/contributors/contributors.component.scss +++ b/src/app/features/project/contributors/contributors.component.scss @@ -1,8 +1,7 @@ -@use "assets/styles/variables" as var; @use "assets/styles/mixins" as mix; .contributors { - background-color: var.$white; + background-color: var(--white); padding: mix.rem(24px) mix.rem(16px) mix.rem(16px); row-gap: mix.rem(24px); diff --git a/src/app/features/project/contributors/contributors.component.ts b/src/app/features/project/contributors/contributors.component.ts index c93d96045..3a46f01d6 100644 --- a/src/app/features/project/contributors/contributors.component.ts +++ b/src/app/features/project/contributors/contributors.component.ts @@ -38,6 +38,7 @@ import { DeleteViewOnlyLink, FetchViewOnlyLinks, GetAllContributors, + GetResourceDetails, UpdateBibliographyFilter, UpdateContributor, UpdatePermissionFilter, @@ -46,8 +47,6 @@ import { } from '@osf/shared/stores'; import { findChangedItems } from '@osf/shared/utils'; -import { GetProjectDetails, SettingsSelectors } from '../settings/store'; - import { CreateViewLinkDialogComponent } from './components'; @Component({ @@ -77,10 +76,12 @@ export class ContributorsComponent implements OnInit { readonly customConfirmationService = inject(CustomConfirmationService); private readonly route = inject(ActivatedRoute); - private readonly projectId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); + private readonly resourceId = toSignal( + this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined) + ); protected viewOnlyLinks = select(ViewOnlyLinkSelectors.getViewOnlyLinks); - protected projectDetails = select(SettingsSelectors.getProjectDetails); + protected projectDetails = select(ViewOnlyLinkSelectors.getResourceDetails); protected readonly selectedPermission = signal(null); protected readonly selectedBibliography = signal(null); @@ -94,7 +95,7 @@ export class ContributorsComponent implements OnInit { protected actions = createDispatchMap({ getViewOnlyLinks: FetchViewOnlyLinks, - getProjectDetails: GetProjectDetails, + getResourceDetails: GetResourceDetails, getContributors: GetAllContributors, updateSearchValue: UpdateSearchValue, updatePermissionFilter: UpdatePermissionFilter, @@ -123,12 +124,12 @@ export class ContributorsComponent implements OnInit { } ngOnInit(): void { - const id = this.projectId(); + const id = this.resourceId(); if (id) { this.actions.getViewOnlyLinks(id); - this.actions.getProjectDetails(id); - this.actions.getContributors(this.projectId()); + this.actions.getResourceDetails(id); + this.actions.getContributors(this.resourceId()); } this.setSearchSubscription(); @@ -156,7 +157,7 @@ export class ContributorsComponent implements OnInit { const updatedContributors = findChangedItems(this.initialContributors(), this.contributors(), 'id'); const updateRequests = updatedContributors.map((payload) => - this.actions.updateContributor(this.projectId(), payload) + this.actions.updateContributor(this.resourceId(), payload) ); forkJoin(updateRequests).subscribe(() => { @@ -185,7 +186,7 @@ export class ContributorsComponent implements OnInit { if (res.type === AddContributorType.Unregistered) { this.openAddUnregisteredContributorDialog(); } else { - const addRequests = res.data.map((payload) => this.actions.addContributor(this.projectId(), payload)); + const addRequests = res.data.map((payload) => this.actions.addContributor(this.resourceId(), payload)); forkJoin(addRequests).subscribe(() => { this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); @@ -215,7 +216,7 @@ export class ContributorsComponent implements OnInit { const successMessage = this.translateService.instant('project.contributors.toastMessages.addSuccessMessage'); const params = { name: res.data[0].fullName }; - this.actions.addContributor(this.projectId(), res.data[0]).subscribe({ + this.actions.addContributor(this.resourceId(), res.data[0]).subscribe({ next: () => this.toastService.showSuccess(successMessage, params), }); } @@ -230,7 +231,7 @@ export class ContributorsComponent implements OnInit { acceptLabelKey: 'common.buttons.remove', onConfirm: () => { this.actions - .deleteContributor(this.projectId(), contributor.userId) + .deleteContributor(this.resourceId(), contributor.userId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => @@ -256,17 +257,17 @@ export class ContributorsComponent implements OnInit { width: '448px', focusOnShow: false, header: this.translateService.instant('project.contributors.createLinkDialog.dialogTitle'), - data: { sharedComponents, projectId: this.projectId() }, + data: { sharedComponents, projectId: this.resourceId() }, closeOnEscape: true, modal: true, closable: true, }) .onClose.pipe( filter((res) => !!res), - switchMap((result) => this.actions.createViewOnlyLink(this.projectId(), result as ViewOnlyLinkJsonApi)), + switchMap((result) => this.actions.createViewOnlyLink(this.resourceId(), result as ViewOnlyLinkJsonApi)), takeUntilDestroyed(this.destroyRef) ) - .subscribe(); + .subscribe(() => this.toastService.showSuccess('myProjects.settings.viewOnlyLinkCreated')); } deleteLinkItem(link: ViewOnlyLinkModel): void { @@ -276,7 +277,7 @@ export class ContributorsComponent implements OnInit { messageKey: 'myProjects.settings.delete.message', onConfirm: () => this.actions - .deleteViewOnlyLink(this.projectId(), link.id) + .deleteViewOnlyLink(this.resourceId(), link.id) .subscribe(() => this.toastService.showSuccess('myProjects.settings.delete.success')), }); } diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 18bcda3c8..6721bd22b 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -2,10 +2,12 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; -import { CONTRIBUTORS_SERVICE } from '@osf/shared/constants'; -import { ProjectContributorsService } from '@osf/shared/services'; +import { ProjectContributorsService, ProjectViewOnlyLinksService } from '@osf/shared/services'; import { ContributorsState, ViewOnlyLinkState } from '@osf/shared/stores'; +import { ANALYTICS_SERVICE, CONTRIBUTORS_SERVICE, VIEW_ONLY_LINKS_SERVICE } from '@osf/shared/tokens'; +import { ProjectAnalyticsService } from './analytics/services'; +import { AnalyticsState } from './analytics/store'; import { ProjectFilesState } from './files/store'; import { SettingsState } from './settings/store'; @@ -54,11 +56,22 @@ export const projectRoutes: Routes = [ provide: CONTRIBUTORS_SERVICE, useClass: ProjectContributorsService, }, + { + provide: VIEW_ONLY_LINKS_SERVICE, + useClass: ProjectViewOnlyLinksService, + }, ], }, { path: 'analytics', loadComponent: () => import('../project/analytics/analytics.component').then((mod) => mod.AnalyticsComponent), + providers: [ + provideStates([AnalyticsState]), + { + provide: ANALYTICS_SERVICE, + useClass: ProjectAnalyticsService, + }, + ], }, { path: 'wiki', diff --git a/src/app/features/registries/registries.routes.ts b/src/app/features/registries/registries.routes.ts index a2acf5af8..32856c9f7 100644 --- a/src/app/features/registries/registries.routes.ts +++ b/src/app/features/registries/registries.routes.ts @@ -4,10 +4,15 @@ import { Routes } from '@angular/router'; import { RegistriesComponent } from '@osf/features/registries/registries.component'; import { RegistriesState } from '@osf/features/registries/store'; -import { ContributorsState, SubjectsState } from '@osf/shared/stores'; +import { RegistrationContributorsService } from '@osf/shared/services/registration-contributors.service'; +import { RegistrationViewOnlyLinksService } from '@osf/shared/services/registration-view-only-links.service'; +import { ContributorsState, SubjectsState, ViewOnlyLinkState } from '@osf/shared/stores'; +import { ANALYTICS_SERVICE, CONTRIBUTORS_SERVICE, VIEW_ONLY_LINKS_SERVICE } from '@osf/shared/tokens'; import { SUBJECTS_SERVICE } from '@osf/shared/tokens/subjects.token'; import { ModerationState } from '../moderation/store'; +import { RegistrationAnalyticsService } from '../project/analytics/services'; +import { AnalyticsState } from '../project/analytics/store'; import { RegistrationSubjectsService } from './services'; @@ -16,7 +21,7 @@ export const registriesRoutes: Routes = [ path: '', component: RegistriesComponent, providers: [ - provideStates([RegistriesState, ContributorsState, SubjectsState]), + provideStates([RegistriesState, SubjectsState]), { provide: SUBJECTS_SERVICE, useClass: RegistrationSubjectsService, @@ -67,6 +72,39 @@ export const registriesRoutes: Routes = [ }, ], }, + { + path: ':id', + children: [ + { + path: 'contributors', + loadComponent: () => + import('../project/contributors/contributors.component').then((mod) => mod.ContributorsComponent), + providers: [ + provideStates([ContributorsState, ViewOnlyLinkState]), + { + provide: CONTRIBUTORS_SERVICE, + useClass: RegistrationContributorsService, + }, + { + provide: VIEW_ONLY_LINKS_SERVICE, + useClass: RegistrationViewOnlyLinksService, + }, + ], + }, + { + path: 'analytics', + loadComponent: () => + import('../project/analytics/analytics.component').then((mod) => mod.AnalyticsComponent), + providers: [ + provideStates([AnalyticsState]), + { + provide: ANALYTICS_SERVICE, + useClass: RegistrationAnalyticsService, + }, + ], + }, + ], + }, ], }, ]; diff --git a/src/app/shared/constants/contributors/index.ts b/src/app/shared/constants/contributors/index.ts index 2ecfa8737..86502635c 100644 --- a/src/app/shared/constants/contributors/index.ts +++ b/src/app/shared/constants/contributors/index.ts @@ -1,2 +1 @@ export * from './contributors.constants'; -export * from './contributors.token'; diff --git a/src/app/shared/models/analytics/analytics-service.model.ts b/src/app/shared/models/analytics/analytics-service.model.ts new file mode 100644 index 000000000..4981b7189 --- /dev/null +++ b/src/app/shared/models/analytics/analytics-service.model.ts @@ -0,0 +1,8 @@ +import { Observable } from 'rxjs'; + +import { AnalyticsMetricsModel, RelatedCountsModel } from '@osf/features/project/analytics/models'; + +export interface IAnalyticsService { + getMetrics(projectId: string, dateRange: string): Observable; + getRelatedCounts(projectId: string): Observable; +} diff --git a/src/app/shared/models/analytics/index.ts b/src/app/shared/models/analytics/index.ts new file mode 100644 index 000000000..558943691 --- /dev/null +++ b/src/app/shared/models/analytics/index.ts @@ -0,0 +1 @@ +export * from './analytics-service.model'; diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index c34522aaa..d0811a926 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -1,4 +1,5 @@ export * from './addons'; +export * from './analytics'; export * from './brand.model'; export * from './charts'; export * from './confirmation-options.model'; diff --git a/src/app/shared/models/view-only-links/index.ts b/src/app/shared/models/view-only-links/index.ts index 8bc5e3616..f568a449b 100644 --- a/src/app/shared/models/view-only-links/index.ts +++ b/src/app/shared/models/view-only-links/index.ts @@ -1,2 +1,3 @@ export * from './view-only-link.model'; export * from './view-only-link-response.model'; +export * from './view-only-links-service.model'; diff --git a/src/app/shared/models/view-only-links/view-only-links-service.model.ts b/src/app/shared/models/view-only-links/view-only-links-service.model.ts new file mode 100644 index 000000000..7dac51db2 --- /dev/null +++ b/src/app/shared/models/view-only-links/view-only-links-service.model.ts @@ -0,0 +1,13 @@ +import { Observable } from 'rxjs'; + +import { NodeResponseModel } from '../node-response.model'; + +import { PaginatedViewOnlyLinksModel } from './view-only-link.model'; +import { ViewOnlyLinkJsonApi } from './view-only-link-response.model'; + +export interface IViewOnlyLinksService { + getResourceById(projectId: string): Observable; + getViewOnlyLinksData(projectId: string): Observable; + createViewOnlyLink(projectId: string, payload: ViewOnlyLinkJsonApi): Observable; + deleteLink(projectId: string, linkId: string): Observable; +} diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 4a297c4d2..678ac1cb7 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -7,8 +7,8 @@ export { InstitutionsService } from './institutions.service'; export { LicensesService } from './licenses.service'; export { LoaderService } from './loader.service'; export { ProjectContributorsService } from './project-contributors.service'; +export { ProjectViewOnlyLinksService } from './project-view-only-links.service'; export { ResourceCardService } from './resource-card.service'; export { SearchService } from './search.service'; export { SubjectsService } from './subjects.service'; export { ToastService } from './toast.service'; -export { ViewOnlyLinksService } from './view-only-links.service'; diff --git a/src/app/shared/services/view-only-links.service.ts b/src/app/shared/services/project-view-only-links.service.ts similarity index 86% rename from src/app/shared/services/view-only-links.service.ts rename to src/app/shared/services/project-view-only-links.service.ts index e0d95c373..0461c0217 100644 --- a/src/app/shared/services/view-only-links.service.ts +++ b/src/app/shared/services/project-view-only-links.service.ts @@ -6,6 +6,7 @@ import { JsonApiResponse } from '@core/models'; import { JsonApiService } from '@core/services'; import { ViewOnlyLinksMapper } from '../mappers'; +import { NodeResponseModel } from '../models'; import { PaginatedViewOnlyLinksModel, ViewOnlyLinkJsonApi, @@ -17,9 +18,13 @@ import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class ViewOnlyLinksService { +export class ProjectViewOnlyLinksService { private readonly jsonApiService = inject(JsonApiService); + getResourceById(projectId: string): Observable { + return this.jsonApiService.get(`${environment.apiUrl}/nodes/${projectId}`); + } + getViewOnlyLinksData(projectId: string): Observable { const params: Record = { embed: 'creator' }; diff --git a/src/app/shared/services/registration-contributors.service.ts b/src/app/shared/services/registration-contributors.service.ts new file mode 100644 index 000000000..f40e9bcb1 --- /dev/null +++ b/src/app/shared/services/registration-contributors.service.ts @@ -0,0 +1,62 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; +import { JsonApiService } from '@osf/core/services'; +import { ContributorAddModel, ContributorModel, ContributorResponse, PaginatedData } from '@osf/shared/models'; + +import { AddContributorType } from '../enums'; +import { ContributorsMapper } from '../mappers/contributors'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class RegistrationContributorsService { + private readonly jsonApiService = inject(JsonApiService); + + getAllContributors(projectId: string): Observable { + const baseUrl = `${environment.apiUrl}/registrations/${projectId}/contributors`; + + return this.jsonApiService + .get>(baseUrl) + .pipe(map((response) => ContributorsMapper.fromResponse(response.data))); + } + + searchUsers(value: string, page = 1): Observable> { + const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; + + return this.jsonApiService + .get>(baseUrl) + .pipe(map((response) => ContributorsMapper.fromUsersWithPaginationGetResponse(response))); + } + + addContributor(projectId: string, data: ContributorAddModel): Observable { + const baseUrl = `${environment.apiUrl}/registrations/${projectId}/contributors/`; + const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; + + const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; + + return this.jsonApiService + .post(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); + } + + updateContributor(projectId: string, data: ContributorModel): Observable { + const baseUrl = `${environment.apiUrl}/registrations/${projectId}/contributors/${data.userId}`; + + const contributorData = { data: ContributorsMapper.toContributorAddRequest(data) }; + + return this.jsonApiService + .patch(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); + } + + deleteContributor(projectId: string, userId: string): Observable { + const baseUrl = `${environment.apiUrl}/registrations/${projectId}/contributors/${userId}`; + + return this.jsonApiService.delete(baseUrl); + } +} diff --git a/src/app/shared/services/registration-view-only-links.service.ts b/src/app/shared/services/registration-view-only-links.service.ts new file mode 100644 index 000000000..ece077ec8 --- /dev/null +++ b/src/app/shared/services/registration-view-only-links.service.ts @@ -0,0 +1,50 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponse } from '@core/models'; +import { JsonApiService } from '@core/services'; + +import { ViewOnlyLinksMapper } from '../mappers'; +import { NodeResponseModel } from '../models'; +import { + PaginatedViewOnlyLinksModel, + ViewOnlyLinkJsonApi, + ViewOnlyLinksResponseJsonApi, +} from '../models/view-only-links'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class RegistrationViewOnlyLinksService { + private readonly jsonApiService = inject(JsonApiService); + + getResourceById(resourceId: string): Observable { + return this.jsonApiService.get(`${environment.apiUrl}/registrations/${resourceId}`); + } + + getViewOnlyLinksData(resourceId: string): Observable { + const params: Record = { embed: 'creator' }; + + return this.jsonApiService + .get(`${environment.apiUrl}/registrations/${resourceId}/view_only_links`, params) + .pipe(map((response) => ViewOnlyLinksMapper.fromResponse(response, resourceId))); + } + + createViewOnlyLink(resourceId: string, payload: ViewOnlyLinkJsonApi): Observable { + const data = { data: { ...payload } }; + const params: Record = { embed: 'creator' }; + + return this.jsonApiService + .post< + JsonApiResponse + >(`${environment.apiUrl}/registrations/${resourceId}/view_only_links/`, data, params) + .pipe(map((response) => ViewOnlyLinksMapper.fromSingleResponse(response.data, resourceId))); + } + + deleteLink(resourceId: string, linkId: string): Observable { + return this.jsonApiService.delete(`${environment.apiUrl}/registrations/${resourceId}/view_only_links/${linkId}`); + } +} diff --git a/src/app/shared/stores/contributors/contributors.state.ts b/src/app/shared/stores/contributors/contributors.state.ts index af2c83702..88c1ece85 100644 --- a/src/app/shared/stores/contributors/contributors.state.ts +++ b/src/app/shared/stores/contributors/contributors.state.ts @@ -4,7 +4,7 @@ import { catchError, of, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { CONTRIBUTORS_SERVICE } from '@osf/shared/constants'; +import { CONTRIBUTORS_SERVICE } from '@osf/shared/tokens'; import { AddContributor, diff --git a/src/app/shared/stores/view-only-links/view-only-link.actions.ts b/src/app/shared/stores/view-only-links/view-only-link.actions.ts index 1f233061f..ff677fced 100644 --- a/src/app/shared/stores/view-only-links/view-only-link.actions.ts +++ b/src/app/shared/stores/view-only-links/view-only-link.actions.ts @@ -1,5 +1,11 @@ import { ViewOnlyLinkJsonApi } from '@osf/shared/models'; +export class GetResourceDetails { + static readonly type = '[Project] Get Resource Details'; + + constructor(public projectId: string) {} +} + export class FetchViewOnlyLinks { static readonly type = '[Link] Fetch View Only Links'; diff --git a/src/app/shared/stores/view-only-links/view-only-link.model.ts b/src/app/shared/stores/view-only-links/view-only-link.model.ts index 0a8631744..a016c5636 100644 --- a/src/app/shared/stores/view-only-links/view-only-link.model.ts +++ b/src/app/shared/stores/view-only-links/view-only-link.model.ts @@ -1,6 +1,7 @@ -import { PaginatedViewOnlyLinksModel } from '@shared/models'; +import { NodeData, PaginatedViewOnlyLinksModel } from '@shared/models'; import { AsyncStateModel } from '@shared/models/store'; export interface ViewOnlyLinkStateModel { viewOnlyLinks: AsyncStateModel; + resourceDetails: AsyncStateModel; } diff --git a/src/app/shared/stores/view-only-links/view-only-link.selectors.ts b/src/app/shared/stores/view-only-links/view-only-link.selectors.ts index 34d33da52..16450624a 100644 --- a/src/app/shared/stores/view-only-links/view-only-link.selectors.ts +++ b/src/app/shared/stores/view-only-links/view-only-link.selectors.ts @@ -13,4 +13,9 @@ export class ViewOnlyLinkSelectors { static isViewOnlyLinksLoading(state: ViewOnlyLinkStateModel) { return state.viewOnlyLinks.isLoading; } + + @Selector([ViewOnlyLinkState]) + static getResourceDetails(state: ViewOnlyLinkStateModel) { + return state.resourceDetails.data; + } } diff --git a/src/app/shared/stores/view-only-links/view-only-link.state.ts b/src/app/shared/stores/view-only-links/view-only-link.state.ts index 9c6c1778b..6316bca13 100644 --- a/src/app/shared/stores/view-only-links/view-only-link.state.ts +++ b/src/app/shared/stores/view-only-links/view-only-link.state.ts @@ -6,10 +6,15 @@ import { catchError, tap } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; -import { ViewOnlyLinksService } from '@osf/shared/services'; -import { PaginatedViewOnlyLinksModel } from '@shared/models'; +import { VIEW_ONLY_LINKS_SERVICE } from '@osf/shared/tokens'; +import { NodeData, PaginatedViewOnlyLinksModel } from '@shared/models'; -import { CreateViewOnlyLink, DeleteViewOnlyLink, FetchViewOnlyLinks } from './view-only-link.actions'; +import { + CreateViewOnlyLink, + DeleteViewOnlyLink, + FetchViewOnlyLinks, + GetResourceDetails, +} from './view-only-link.actions'; import { ViewOnlyLinkStateModel } from './view-only-link.model'; @State({ @@ -20,21 +25,43 @@ import { ViewOnlyLinkStateModel } from './view-only-link.model'; isLoading: false, error: null, }, + resourceDetails: { + data: {} as NodeData, + isLoading: false, + error: null, + }, }, }) @Injectable() export class ViewOnlyLinkState { - private readonly viewOnlyLinksService = inject(ViewOnlyLinksService); + private readonly viewOnlyLinksService = inject(VIEW_ONLY_LINKS_SERVICE); + + @Action(GetResourceDetails) + getResourceDetails(ctx: StateContext, action: GetResourceDetails) { + const state = ctx.getState(); - private handleError(ctx: StateContext, section: keyof ViewOnlyLinkStateModel, error: Error) { ctx.patchState({ - [section]: { - ...ctx.getState()[section], - isLoading: false, - error: error.message, - }, + resourceDetails: { ...state.resourceDetails, isLoading: true, error: null }, }); - return throwError(() => error); + + return this.viewOnlyLinksService.getResourceById(action.projectId).pipe( + map((response) => response?.data as NodeData), + tap((details) => { + const updatedDetails = { + ...details, + lastFetched: Date.now(), + }; + + ctx.patchState({ + resourceDetails: { + data: updatedDetails, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'resourceDetails', error)) + ); } @Action(FetchViewOnlyLinks) @@ -111,4 +138,15 @@ export class ViewOnlyLinkState { catchError((error) => this.handleError(ctx, 'viewOnlyLinks', error)) ); } + + private handleError(ctx: StateContext, section: keyof ViewOnlyLinkStateModel, error: Error) { + ctx.patchState({ + [section]: { + ...ctx.getState()[section], + isLoading: false, + error: error.message, + }, + }); + return throwError(() => error); + } } diff --git a/src/app/shared/tokens/analytics.token.ts b/src/app/shared/tokens/analytics.token.ts new file mode 100644 index 000000000..c7ba59851 --- /dev/null +++ b/src/app/shared/tokens/analytics.token.ts @@ -0,0 +1,5 @@ +import { InjectionToken } from '@angular/core'; + +import { IAnalyticsService } from '@osf/shared/models'; + +export const ANALYTICS_SERVICE = new InjectionToken('ANALYTICS_SERVICE'); diff --git a/src/app/shared/constants/contributors/contributors.token.ts b/src/app/shared/tokens/contributors.token.ts similarity index 100% rename from src/app/shared/constants/contributors/contributors.token.ts rename to src/app/shared/tokens/contributors.token.ts diff --git a/src/app/shared/tokens/index.ts b/src/app/shared/tokens/index.ts index e69de29bb..02405922b 100644 --- a/src/app/shared/tokens/index.ts +++ b/src/app/shared/tokens/index.ts @@ -0,0 +1,4 @@ +export * from './analytics.token'; +export * from './contributors.token'; +export * from './subjects.token'; +export * from './view-only-links.token'; diff --git a/src/app/shared/tokens/view-only-links.token.ts b/src/app/shared/tokens/view-only-links.token.ts new file mode 100644 index 000000000..517cc1543 --- /dev/null +++ b/src/app/shared/tokens/view-only-links.token.ts @@ -0,0 +1,5 @@ +import { InjectionToken } from '@angular/core'; + +import { IViewOnlyLinksService } from '@osf/shared/models'; + +export const VIEW_ONLY_LINKS_SERVICE = new InjectionToken('VIEW_ONLY_LINKS_SERVICE'); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index ec403b29c..459153966 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -326,6 +326,7 @@ "createdBy": "Created By", "anonymous": "Anonymous" }, + "viewOnlyLinkCreated": "View only link successfully created.", "anonymizeContributorList": "Anonymize contributor list for this link (e.g., for blind peer review).", "ensureNoInformation": "Ensure the wiki pages, files, registration forms and add-ons do not contain identifying information.", "linkName": "Link name", From 6131da5b339c0276a77a0dbd101632bdb777ae30 Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 4 Jul 2025 16:23:10 +0300 Subject: [PATCH 4/9] fix(contributors): updated preprint contributors --- .../contributors/contributors.component.html | 2 +- .../contributors/contributors.component.ts | 36 ++++--- .../metadata-step.component.html | 2 +- .../features/preprints/preprints.routes.ts | 7 ++ .../services/preprint-contributors.service.ts | 22 ++++- .../submit-preprint.actions.ts | 24 +---- .../submit-preprint/submit-preprint.model.ts | 3 +- .../submit-preprint.selectors.ts | 10 -- .../submit-preprint/submit-preprint.state.ts | 98 +------------------ .../metadata/project-metadata.routes.ts | 5 - .../services/project-contributors.service.ts | 10 +- .../registration-contributors.service.ts | 10 +- .../contributors/contributors.actions.ts | 8 +- .../stores/contributors/contributors.state.ts | 16 +++ 14 files changed, 86 insertions(+), 167 deletions(-) diff --git a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.html b/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.html index 0d0f6bd2a..18e837dc1 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.html +++ b/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.html @@ -15,7 +15,7 @@

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

@if (hasChanges) {
- +
} diff --git a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts b/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts index 682fa0949..d2471379e 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts +++ b/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts @@ -10,17 +10,10 @@ import { TableModule } from 'primeng/table'; import { filter, forkJoin } from 'rxjs'; -import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, OnInit, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; -import { - AddContributor, - DeleteContributor, - FetchContributors, - SubmitPreprintSelectors, - UpdateContributor, -} from '@osf/features/preprints/store/submit-preprint'; import { AddContributorDialogComponent, AddUnregisteredContributorDialogComponent, @@ -29,6 +22,13 @@ import { import { AddContributorType } from '@osf/shared/enums'; import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/models'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; +import { + AddContributor, + ContributorsSelectors, + DeleteContributor, + GetAllContributors, + UpdateContributor, +} from '@osf/shared/stores'; import { findChangedItems } from '@osf/shared/utils'; @Component({ @@ -40,19 +40,21 @@ import { findChangedItems } from '@osf/shared/utils'; providers: [DialogService], }) export class ContributorsComponent implements OnInit { + preprintId = input(''); + readonly destroyRef = inject(DestroyRef); readonly translateService = inject(TranslateService); readonly dialogService = inject(DialogService); readonly toastService = inject(ToastService); readonly customConfirmationService = inject(CustomConfirmationService); - protected initialContributors = select(SubmitPreprintSelectors.getContributors); + protected initialContributors = select(ContributorsSelectors.getContributors); protected contributors = signal([]); - protected readonly isContributorsLoading = select(SubmitPreprintSelectors.areContributorsLoading); + protected readonly isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); protected actions = createDispatchMap({ - getContributors: FetchContributors, + getContributors: GetAllContributors, deleteContributor: DeleteContributor, updateContributor: UpdateContributor, addContributor: AddContributor, @@ -69,7 +71,7 @@ export class ContributorsComponent implements OnInit { } ngOnInit(): void { - this.actions.getContributors(); + this.actions.getContributors(this.preprintId()); } cancel() { @@ -79,7 +81,9 @@ export class ContributorsComponent implements OnInit { save() { const updatedContributors = findChangedItems(this.initialContributors(), this.contributors(), 'id'); - const updateRequests = updatedContributors.map((payload) => this.actions.updateContributor(payload)); + const updateRequests = updatedContributors.map((payload) => + this.actions.updateContributor(this.preprintId(), payload) + ); forkJoin(updateRequests).subscribe(() => { this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); @@ -107,7 +111,7 @@ export class ContributorsComponent implements OnInit { if (res.type === AddContributorType.Unregistered) { this.openAddUnregisteredContributorDialog(); } else { - const addRequests = res.data.map((payload) => this.actions.addContributor(payload)); + const addRequests = res.data.map((payload) => this.actions.addContributor(this.preprintId(), payload)); forkJoin(addRequests).subscribe(() => { this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); @@ -137,7 +141,7 @@ export class ContributorsComponent implements OnInit { const successMessage = this.translateService.instant('project.contributors.toastMessages.addSuccessMessage'); const params = { name: res.data[0].fullName }; - this.actions.addContributor(res.data[0]).subscribe({ + this.actions.addContributor(this.preprintId(), res.data[0]).subscribe({ next: () => this.toastService.showSuccess(successMessage, params), }); } @@ -152,7 +156,7 @@ export class ContributorsComponent implements OnInit { acceptLabelKey: 'common.buttons.remove', onConfirm: () => { this.actions - .deleteContributor(contributor.userId) + .deleteContributor(this.preprintId(), contributor.userId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => diff --git a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html b/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html index da7bc2379..a5a3977c5 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html +++ b/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html @@ -1,7 +1,7 @@

Metadata

- +
diff --git a/src/app/features/preprints/preprints.routes.ts b/src/app/features/preprints/preprints.routes.ts index 3bb1126f2..a616fc8db 100644 --- a/src/app/features/preprints/preprints.routes.ts +++ b/src/app/features/preprints/preprints.routes.ts @@ -9,6 +9,9 @@ import { PreprintsResourcesFiltersState } from '@osf/features/preprints/store/pr import { PreprintsResourcesFiltersOptionsState } from '@osf/features/preprints/store/preprints-resources-filters-options'; import { SubmitPreprintState } from '@osf/features/preprints/store/submit-preprint'; import { ContributorsState } from '@osf/shared/stores'; +import { CONTRIBUTORS_SERVICE } from '@osf/shared/tokens'; + +import { PreprintContributorsService } from './services'; export const preprintsRoutes: Routes = [ { @@ -23,6 +26,10 @@ export const preprintsRoutes: Routes = [ SubmitPreprintState, ContributorsState, ]), + { + provide: CONTRIBUTORS_SERVICE, + useClass: PreprintContributorsService, + }, ], children: [ { diff --git a/src/app/features/preprints/services/preprint-contributors.service.ts b/src/app/features/preprints/services/preprint-contributors.service.ts index 4fc553c22..20d5b1629 100644 --- a/src/app/features/preprints/services/preprint-contributors.service.ts +++ b/src/app/features/preprints/services/preprint-contributors.service.ts @@ -2,27 +2,41 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { JsonApiResponse } from '@core/models'; +import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@core/models'; import { JsonApiService } from '@core/services'; import { AddContributorType } from '@osf/shared/enums'; import { ContributorsMapper } from '@osf/shared/mappers'; -import { ContributorAddModel, ContributorModel, ContributorResponse } from '@osf/shared/models'; +import { + ContributorAddModel, + ContributorModel, + ContributorResponse, + IContributorsService, + PaginatedData, +} from '@osf/shared/models'; import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class PreprintContributorsService { +export class PreprintContributorsService implements IContributorsService { private jsonApiService = inject(JsonApiService); private apiUrl = environment.apiUrl; - getContributors(preprintId: string): Observable { + getAllContributors(preprintId: string): Observable { return this.jsonApiService .get>(`${this.apiUrl}/preprints/${preprintId}/contributors/`) .pipe(map((contributors) => ContributorsMapper.fromResponse(contributors.data))); } + searchUsers(value: string, page = 1): Observable> { + const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; + + return this.jsonApiService + .get>(baseUrl) + .pipe(map((response) => ContributorsMapper.fromUsersWithPaginationGetResponse(response))); + } + addContributor(preprintId: string, data: ContributorAddModel): Observable { const baseUrl = `${this.apiUrl}/preprints/${preprintId}/contributors/`; const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index 44909a660..51b493804 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -1,7 +1,7 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; -import { ContributorAddModel, ContributorModel, LicenseOptions, OsfFile } from '@shared/models'; +import { LicenseOptions, OsfFile } from '@shared/models'; export class SetSelectedPreprintProviderId { static readonly type = '[Submit Preprint] Set Selected Preprint Provider Id'; @@ -78,28 +78,6 @@ export class GetProjectFilesByLink { constructor(public filesLink: string) {} } -export class FetchContributors { - static readonly type = '[Submit Preprint] Fetch Contributors'; -} - -export class AddContributor { - static readonly type = '[Submit Preprint] Add Contributor'; - - constructor(public contributor: ContributorAddModel) {} -} - -export class UpdateContributor { - static readonly type = '[Submit Preprint] Update Contributor'; - - constructor(public contributor: ContributorModel) {} -} - -export class DeleteContributor { - static readonly type = '[Submit Preprint] Delete Contributor'; - - constructor(public userId: string) {} -} - export class FetchLicenses { static readonly type = '[Submit Preprint] Fetch Licenses'; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts index b62424697..8ca3ff6cf 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -1,7 +1,7 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint, PreprintFilesLinks } from '@osf/features/preprints/models'; -import { AsyncStateModel, ContributorModel, IdName, OsfFile } from '@shared/models'; +import { AsyncStateModel, IdName, OsfFile } from '@shared/models'; import { License } from '@shared/models/license.model'; export interface SubmitPreprintStateModel { @@ -12,6 +12,5 @@ export interface SubmitPreprintStateModel { preprintFiles: AsyncStateModel; availableProjects: AsyncStateModel; projectFiles: AsyncStateModel; - contributors: AsyncStateModel; licenses: AsyncStateModel; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts index 5eb2a2d39..4f434547c 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -58,16 +58,6 @@ export class SubmitPreprintSelectors { return state.projectFiles.isLoading; } - @Selector([SubmitPreprintState]) - static getContributors(state: SubmitPreprintStateModel) { - return state.contributors.data; - } - - @Selector([SubmitPreprintState]) - static areContributorsLoading(state: SubmitPreprintStateModel) { - return state.contributors.isLoading; - } - @Selector([SubmitPreprintState]) static getLicenses(state: SubmitPreprintStateModel) { return state.licenses.data; diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 4b34b0d07..366b966c7 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -1,5 +1,5 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { insertItem, patch, removeItem, updateItem } from '@ngxs/store/operators'; +import { patch } from '@ngxs/store/operators'; import { EMPTY, filter, switchMap, tap, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -19,11 +19,8 @@ import { OsfFile } from '@shared/models'; import { FilesService } from '@shared/services'; import { - AddContributor, CopyFileFromProject, CreatePreprint, - DeleteContributor, - FetchContributors, FetchLicenses, GetAvailableProjects, GetPreprintFiles, @@ -36,7 +33,6 @@ import { SetSelectedPreprintFileSource, SetSelectedPreprintProviderId, SubmitPreprintStateModel, - UpdateContributor, UpdatePreprint, UploadFile, } from './'; @@ -72,11 +68,6 @@ import { isLoading: false, error: null, }, - contributors: { - data: [], - isLoading: false, - error: null, - }, licenses: { data: [], isLoading: false, @@ -310,11 +301,6 @@ export class SubmitPreprintState { isLoading: false, error: null, }, - contributors: { - data: [], - isLoading: false, - error: null, - }, licenses: { data: [], isLoading: false, @@ -370,88 +356,6 @@ export class SubmitPreprintState { ); } - @Action(FetchContributors) - fetchContributors(ctx: StateContext) { - const createdPreprint = ctx.getState().createdPreprint.data; - if (!createdPreprint) { - return; - } - - ctx.setState(patch({ contributors: patch({ isLoading: true }) })); - - return this.contributorsService.getContributors(createdPreprint.id).pipe( - tap((contributors) => { - ctx.setState(patch({ contributors: patch({ isLoading: false, data: contributors }) })); - }), - catchError((error) => this.handleError(ctx, 'contributors', error)) - ); - } - - @Action(AddContributor) - addContributor(ctx: StateContext, action: AddContributor) { - const createdPreprint = ctx.getState().createdPreprint.data; - if (!createdPreprint) { - return; - } - - ctx.setState(patch({ contributors: patch({ isLoading: true }) })); - - return this.contributorsService.addContributor(createdPreprint.id, action.contributor).pipe( - tap((contributor) => { - ctx.setState(patch({ contributors: patch({ isLoading: false, data: insertItem(contributor) }) })); - }), - catchError((error) => this.handleError(ctx, 'contributors', error)) - ); - } - - @Action(UpdateContributor) - updateContributor(ctx: StateContext, action: UpdateContributor) { - const createdPreprint = ctx.getState().createdPreprint.data; - if (!createdPreprint) { - return; - } - - ctx.setState(patch({ contributors: patch({ isLoading: true }) })); - - return this.contributorsService.updateContributor(createdPreprint.id, action.contributor).pipe( - tap((contributor) => { - ctx.setState( - patch({ - contributors: patch({ - isLoading: false, - data: updateItem((item) => item.id === action.contributor.id, contributor), - }), - }) - ); - }), - catchError((error) => this.handleError(ctx, 'contributors', error)) - ); - } - - @Action(DeleteContributor) - deleteContributor(ctx: StateContext, action: DeleteContributor) { - const createdPreprint = ctx.getState().createdPreprint.data; - if (!createdPreprint) { - return; - } - - ctx.setState(patch({ contributors: patch({ isLoading: true }) })); - - return this.contributorsService.deleteContributor(createdPreprint.id, action.userId).pipe( - tap(() => { - ctx.setState( - patch({ - contributors: patch({ - isLoading: false, - data: removeItem((item) => action.userId === item.userId), - }), - }) - ); - }), - catchError((error) => this.handleError(ctx, 'contributors', error)) - ); - } - @Action(FetchLicenses) fetchLicenses(ctx: StateContext) { const providerId = ctx.getState().selectedProviderId; diff --git a/src/app/features/project/metadata/project-metadata.routes.ts b/src/app/features/project/metadata/project-metadata.routes.ts index 8b588e30a..4e743b5f1 100644 --- a/src/app/features/project/metadata/project-metadata.routes.ts +++ b/src/app/features/project/metadata/project-metadata.routes.ts @@ -1,16 +1,11 @@ -import { provideStates } from '@ngxs/store'; - import { Routes } from '@angular/router'; -import { ContributorsState } from '@osf/shared/stores'; - import { ProjectMetadataComponent } from './project-metadata.component'; export const projectMetadataRoutes: Routes = [ { path: '', component: ProjectMetadataComponent, - providers: [provideStates([ContributorsState])], }, { path: 'add', diff --git a/src/app/shared/services/project-contributors.service.ts b/src/app/shared/services/project-contributors.service.ts index c4ca391e8..547bfd03f 100644 --- a/src/app/shared/services/project-contributors.service.ts +++ b/src/app/shared/services/project-contributors.service.ts @@ -4,7 +4,13 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; -import { ContributorAddModel, ContributorModel, ContributorResponse, PaginatedData } from '@osf/shared/models'; +import { + ContributorAddModel, + ContributorModel, + ContributorResponse, + IContributorsService, + PaginatedData, +} from '@osf/shared/models'; import { AddContributorType } from '../enums'; import { ContributorsMapper } from '../mappers/contributors'; @@ -14,7 +20,7 @@ import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class ProjectContributorsService { +export class ProjectContributorsService implements IContributorsService { private readonly jsonApiService = inject(JsonApiService); getAllContributors(projectId: string): Observable { diff --git a/src/app/shared/services/registration-contributors.service.ts b/src/app/shared/services/registration-contributors.service.ts index f40e9bcb1..0ce0d6b29 100644 --- a/src/app/shared/services/registration-contributors.service.ts +++ b/src/app/shared/services/registration-contributors.service.ts @@ -4,7 +4,13 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; -import { ContributorAddModel, ContributorModel, ContributorResponse, PaginatedData } from '@osf/shared/models'; +import { + ContributorAddModel, + ContributorModel, + ContributorResponse, + IContributorsService, + PaginatedData, +} from '@osf/shared/models'; import { AddContributorType } from '../enums'; import { ContributorsMapper } from '../mappers/contributors'; @@ -14,7 +20,7 @@ import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class RegistrationContributorsService { +export class RegistrationContributorsService implements IContributorsService { private readonly jsonApiService = inject(JsonApiService); getAllContributors(projectId: string): Observable { diff --git a/src/app/shared/stores/contributors/contributors.actions.ts b/src/app/shared/stores/contributors/contributors.actions.ts index abea4480c..2bf9bcf60 100644 --- a/src/app/shared/stores/contributors/contributors.actions.ts +++ b/src/app/shared/stores/contributors/contributors.actions.ts @@ -3,7 +3,7 @@ import { ContributorAddModel, ContributorModel } from '@osf/shared/models'; export class GetAllContributors { static readonly type = '[Contributors] Get All Contributors'; - constructor(public projectId: string) {} + constructor(public projectId: string | undefined | null) {} } export class UpdateSearchValue { @@ -28,7 +28,7 @@ export class AddContributor { static readonly type = '[Contributors] Add Contributor'; constructor( - public projectId: string, + public projectId: string | undefined | null, public contributor: ContributorAddModel ) {} } @@ -37,7 +37,7 @@ export class UpdateContributor { static readonly type = '[Contributors] Update Contributor'; constructor( - public projectId: string, + public projectId: string | undefined | null, public contributor: ContributorModel ) {} } @@ -46,7 +46,7 @@ export class DeleteContributor { static readonly type = '[Contributors] Delete Contributor'; constructor( - public projectId: string, + public projectId: string | undefined | null, public contributorId: string ) {} } diff --git a/src/app/shared/stores/contributors/contributors.state.ts b/src/app/shared/stores/contributors/contributors.state.ts index 88c1ece85..79f0b5982 100644 --- a/src/app/shared/stores/contributors/contributors.state.ts +++ b/src/app/shared/stores/contributors/contributors.state.ts @@ -50,6 +50,10 @@ export class ContributorsState { contributorsList: { ...state.contributorsList, isLoading: true, error: null }, }); + if (!action.projectId) { + return; + } + return this.contributorsService.getAllContributors(action.projectId).pipe( tap((contributors) => { ctx.patchState({ @@ -72,6 +76,10 @@ export class ContributorsState { contributorsList: { ...state.contributorsList, isLoading: true, error: null }, }); + if (!action.projectId) { + return; + } + return this.contributorsService.addContributor(action.projectId, action.contributor).pipe( tap((contributor) => { const currentState = ctx.getState(); @@ -96,6 +104,10 @@ export class ContributorsState { contributorsList: { ...state.contributorsList, isLoading: true, error: null }, }); + if (!action.projectId) { + return; + } + return this.contributorsService.updateContributor(action.projectId, action.contributor).pipe( tap((updatedContributor) => { const currentState = ctx.getState(); @@ -122,6 +134,10 @@ export class ContributorsState { contributorsList: { ...state.contributorsList, isLoading: true, error: null }, }); + if (!action.projectId) { + return; + } + return this.contributorsService.deleteContributor(action.projectId, action.contributorId).pipe( tap(() => { ctx.patchState({ From a36d164259906d9c81d9a1331ae1696122017045 Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 4 Jul 2025 17:37:45 +0300 Subject: [PATCH 5/9] fix(contributors): updated registration contributors --- .../contributors/contributors.component.html | 10 +- .../contributors/contributors.component.ts | 24 ++-- .../features/registries/registries.routes.ts | 21 ++-- ...raft-registration-contributors.service.ts} | 30 +++-- src/app/features/registries/services/index.ts | 2 +- .../registries/store/default.state.ts | 5 - .../store/handlers/contributors.handlers.ts | 106 ------------------ .../registries/store/handlers/index.ts | 1 - .../registries/store/registries.actions.ts | 35 +----- .../registries/store/registries.model.ts | 3 +- .../registries/store/registries.selectors.ts | 5 - .../registries/store/registries.state.ts | 26 ----- .../services/project-contributors.service.ts | 4 +- .../registration-contributors.service.ts | 4 +- 14 files changed, 53 insertions(+), 223 deletions(-) rename src/app/features/registries/services/{registration-contributors.service.ts => draft-registration-contributors.service.ts} (62%) delete mode 100644 src/app/features/registries/store/handlers/contributors.handlers.ts diff --git a/src/app/features/registries/components/metadata/contributors/contributors.component.html b/src/app/features/registries/components/metadata/contributors/contributors.component.html index b5a05c306..5319ef732 100644 --- a/src/app/features/registries/components/metadata/contributors/contributors.component.html +++ b/src/app/features/registries/components/metadata/contributors/contributors.component.html @@ -1,23 +1,25 @@

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

+ + /> +
@if (hasChanges) {
- - + +
} + />
diff --git a/src/app/features/registries/components/metadata/contributors/contributors.component.ts b/src/app/features/registries/components/metadata/contributors/contributors.component.ts index 52140c1eb..10ebb774c 100644 --- a/src/app/features/registries/components/metadata/contributors/contributors.component.ts +++ b/src/app/features/registries/components/metadata/contributors/contributors.component.ts @@ -14,13 +14,6 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { - AddContributor, - DeleteContributor, - FetchContributors, - RegistriesSelectors, - UpdateContributor, -} from '@osf/features/registries/store'; import { AddContributorDialogComponent, AddUnregisteredContributorDialogComponent, @@ -30,7 +23,13 @@ import { BIBLIOGRAPHY_OPTIONS, PERMISSION_OPTIONS } from '@osf/shared/constants' import { AddContributorType, ContributorPermission } from '@osf/shared/enums'; import { ContributorDialogAddModel, ContributorModel, SelectOption } from '@osf/shared/models'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; -import { ContributorsSelectors } from '@osf/shared/stores'; +import { + AddContributor, + ContributorsSelectors, + DeleteContributor, + GetAllContributors, + UpdateContributor, +} from '@osf/shared/stores'; import { findChangedItems } from '@osf/shared/utils'; @Component({ @@ -56,13 +55,13 @@ export class ContributorsComponent implements OnInit { protected readonly permissionsOptions: SelectOption[] = PERMISSION_OPTIONS; protected readonly bibliographyOptions: SelectOption[] = BIBLIOGRAPHY_OPTIONS; - protected initialContributors = select(RegistriesSelectors.getContributors); + protected initialContributors = select(ContributorsSelectors.getContributors); protected contributors = signal([]); protected readonly isContributorsLoading = select(ContributorsSelectors.isContributorsLoading); protected actions = createDispatchMap({ - getContributors: FetchContributors, + getContributors: GetAllContributors, deleteContributor: DeleteContributor, updateContributor: UpdateContributor, addContributor: AddContributor, @@ -79,10 +78,7 @@ export class ContributorsComponent implements OnInit { } ngOnInit(): void { - const draftId = this.draftId(); - if (draftId) { - this.actions.getContributors(draftId); - } + this.actions.getContributors(this.draftId()); } onFocusOut() { diff --git a/src/app/features/registries/registries.routes.ts b/src/app/features/registries/registries.routes.ts index a576600ec..cd1b896e5 100644 --- a/src/app/features/registries/registries.routes.ts +++ b/src/app/features/registries/registries.routes.ts @@ -6,21 +6,14 @@ import { RegistriesComponent } from '@osf/features/registries/registries.compone import { RegistriesState } from '@osf/features/registries/store'; import { RegistrationViewOnlyLinksService } from '@osf/shared/services/registration-view-only-links.service'; import { ContributorsState, SubjectsState, ViewOnlyLinkState } from '@osf/shared/stores'; -import { ANALYTICS_SERVICE, CONTRIBUTORS_SERVICE, VIEW_ONLY_LINKS_SERVICE } from '@osf/shared/tokens'; -import { SUBJECTS_SERVICE } from '@osf/shared/tokens/subjects.token'; +import { ANALYTICS_SERVICE, CONTRIBUTORS_SERVICE, SUBJECTS_SERVICE, VIEW_ONLY_LINKS_SERVICE } from '@osf/shared/tokens'; import { ModerationState } from '../moderation/store'; import { RegistrationAnalyticsService } from '../project/analytics/services'; import { AnalyticsState } from '../project/analytics/store'; -import { - LicensesHandlers, - ProjectsHandlers, - ProvidersHandlers, - RegistrationContributorsHandlers, - SubjectsHandlers, -} from './store/handlers'; -import { LicensesService, RegistrationContributorsService, RegistrationSubjectsService } from './services'; +import { LicensesHandlers, ProjectsHandlers, ProvidersHandlers, SubjectsHandlers } from './store/handlers'; +import { DraftRegistrationContributorsService, LicensesService, RegistrationSubjectsService } from './services'; export const registriesRoutes: Routes = [ { @@ -31,15 +24,17 @@ export const registriesRoutes: Routes = [ ProvidersHandlers, ProjectsHandlers, LicensesHandlers, - RegistrationContributorsHandlers, SubjectsHandlers, RegistrationSubjectsService, - RegistrationContributorsService, LicensesService, { provide: SUBJECTS_SERVICE, useClass: RegistrationSubjectsService, }, + { + provide: CONTRIBUTORS_SERVICE, + useClass: DraftRegistrationContributorsService, + }, ], children: [ { @@ -97,7 +92,7 @@ export const registriesRoutes: Routes = [ provideStates([ContributorsState, ViewOnlyLinkState]), { provide: CONTRIBUTORS_SERVICE, - useClass: RegistrationContributorsService, + useClass: DraftRegistrationContributorsService, }, { provide: VIEW_ONLY_LINKS_SERVICE, diff --git a/src/app/features/registries/services/registration-contributors.service.ts b/src/app/features/registries/services/draft-registration-contributors.service.ts similarity index 62% rename from src/app/features/registries/services/registration-contributors.service.ts rename to src/app/features/registries/services/draft-registration-contributors.service.ts index b490ba880..9e25e3cd9 100644 --- a/src/app/features/registries/services/registration-contributors.service.ts +++ b/src/app/features/registries/services/draft-registration-contributors.service.ts @@ -2,25 +2,39 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { JsonApiResponse } from '@osf/core/models'; +import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; -import { AddContributorType } from '@osf/shared/components/contributors/enums'; -import { ContributorsMapper } from '@osf/shared/components/contributors/mappers'; -import { ContributorAddModel, ContributorModel, ContributorResponse } from '@osf/shared/components/contributors/models'; +import { AddContributorType } from '@osf/shared/enums'; +import { ContributorsMapper } from '@osf/shared/mappers'; +import { + ContributorAddModel, + ContributorModel, + ContributorResponse, + IContributorsService, + PaginatedData, +} from '@osf/shared/models'; import { environment } from 'src/environments/environment'; @Injectable() -export class RegistrationContributorsService { +export class DraftRegistrationContributorsService implements IContributorsService { private apiUrl = environment.apiUrl; private readonly jsonApiService = inject(JsonApiService); - getContributors(draftId: string): Observable { + getAllContributors(draftId: string): Observable { return this.jsonApiService .get>(`${this.apiUrl}/draft_registrations/${draftId}/contributors/`) .pipe(map((contributors) => ContributorsMapper.fromResponse(contributors.data))); } + searchUsers(value: string, page = 1): Observable> { + const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; + + return this.jsonApiService + .get>(baseUrl) + .pipe(map((response) => ContributorsMapper.fromUsersWithPaginationGetResponse(response))); + } + addContributor(draftId: string, data: ContributorAddModel): Observable { const baseUrl = `${this.apiUrl}/draft_registrations/${draftId}/contributors/`; const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; @@ -28,8 +42,8 @@ export class RegistrationContributorsService { const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; return this.jsonApiService - .post(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); + .post>(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor.data))); } updateContributor(draftId: string, data: ContributorModel): Observable { diff --git a/src/app/features/registries/services/index.ts b/src/app/features/registries/services/index.ts index a81908564..f3c83a0fd 100644 --- a/src/app/features/registries/services/index.ts +++ b/src/app/features/registries/services/index.ts @@ -1,6 +1,6 @@ +export * from './draft-registration-contributors.service'; export * from './licenses.service'; export * from './projects.service'; export * from './providers.service'; -export * from './registration-contributors.service'; export * from './registration-subjects.service'; export * from './registries.service'; diff --git a/src/app/features/registries/store/default.state.ts b/src/app/features/registries/store/default.state.ts index a9e3c4d4c..61ff48e6f 100644 --- a/src/app/features/registries/store/default.state.ts +++ b/src/app/features/registries/store/default.state.ts @@ -17,11 +17,6 @@ export const DefaultState: RegistriesStateModel = { isSubmitting: false, error: null, }, - contributorsList: { - data: [], - isLoading: false, - error: null, - }, registries: { data: [], isLoading: false, diff --git a/src/app/features/registries/store/handlers/contributors.handlers.ts b/src/app/features/registries/store/handlers/contributors.handlers.ts deleted file mode 100644 index f228381c4..000000000 --- a/src/app/features/registries/store/handlers/contributors.handlers.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { StateContext } from '@ngxs/store'; - -import { catchError, tap } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { handleSectionError } from '@osf/core/handlers/state-error.handler'; - -import { RegistrationContributorsService } from '../../services/registration-contributors.service'; -import { AddContributor, DeleteContributor, FetchContributors, UpdateContributor } from '../registries.actions'; -import { RegistriesStateModel } from '../registries.model'; - -@Injectable() -export class RegistrationContributorsHandlers { - contributorsService = inject(RegistrationContributorsService); - - fetchContributors(ctx: StateContext, action: FetchContributors) { - const state = ctx.getState(); - - ctx.patchState({ - contributorsList: { ...state.contributorsList, isLoading: true, error: null }, - }); - - return this.contributorsService.getContributors(action.draftId).pipe( - tap((contributors) => { - ctx.patchState({ - contributorsList: { - ...state.contributorsList, - data: contributors, - isLoading: false, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'contributorsList', error)) - ); - } - - addContributor(ctx: StateContext, action: AddContributor) { - const state = ctx.getState(); - - ctx.patchState({ - contributorsList: { ...state.contributorsList, isLoading: true, error: null }, - }); - - return this.contributorsService.addContributor(action.draftId, action.contributor).pipe( - tap((contributor) => { - const currentState = ctx.getState(); - - ctx.patchState({ - contributorsList: { - ...currentState.contributorsList, - data: [...currentState.contributorsList.data, contributor], - isLoading: false, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'contributorsList', error)) - ); - } - - updateContributor(ctx: StateContext, action: UpdateContributor) { - const state = ctx.getState(); - - ctx.patchState({ - contributorsList: { ...state.contributorsList, isLoading: true, error: null }, - }); - - return this.contributorsService.updateContributor(action.draftId, action.contributor).pipe( - tap((updatedContributor) => { - const currentState = ctx.getState(); - - ctx.patchState({ - contributorsList: { - ...currentState.contributorsList, - data: currentState.contributorsList.data.map((contributor) => - contributor.id === updatedContributor.id ? updatedContributor : contributor - ), - isLoading: false, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'contributorsList', error)) - ); - } - - deleteContributor(ctx: StateContext, action: DeleteContributor) { - const state = ctx.getState(); - - ctx.patchState({ - contributorsList: { ...state.contributorsList, isLoading: true, error: null }, - }); - - return this.contributorsService.deleteContributor(action.draftId, action.contributorId).pipe( - tap(() => { - ctx.patchState({ - contributorsList: { - ...state.contributorsList, - data: state.contributorsList.data.filter((contributor) => contributor.userId !== action.contributorId), - isLoading: false, - }, - }); - }), - catchError((error) => handleSectionError(ctx, 'contributorsList', error)) - ); - } -} diff --git a/src/app/features/registries/store/handlers/index.ts b/src/app/features/registries/store/handlers/index.ts index 383d0b6bc..b3c19c705 100644 --- a/src/app/features/registries/store/handlers/index.ts +++ b/src/app/features/registries/store/handlers/index.ts @@ -1,4 +1,3 @@ -export * from './contributors.handlers'; export * from './licenses.handlers'; export * from './projects.handlers'; export * from './providers.handlers'; diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index c5294bda1..c0e4cb9be 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -1,4 +1,4 @@ -import { ContributorAddModel, ContributorModel, LicenseOptions, Subject } from '@osf/shared/models'; +import { LicenseOptions, Subject } from '@osf/shared/models'; export class GetRegistries { static readonly type = '[Registries] Get Registries'; @@ -32,39 +32,6 @@ export class FetchSchemaBlocks { constructor(public registrationSchemaId: string) {} } -export class FetchContributors { - static readonly type = '[Registries] Fetch Contributors'; - - constructor(public draftId: string) {} -} - -export class AddContributor { - static readonly type = '[Registries] Add Contributor'; - - constructor( - public draftId: string, - public contributor: ContributorAddModel - ) {} -} - -export class UpdateContributor { - static readonly type = '[Registries] Update Contributor'; - - constructor( - public draftId: string, - public contributor: ContributorModel - ) {} -} - -export class DeleteContributor { - static readonly type = '[Registries] Delete Contributor'; - - constructor( - public draftId: string, - public contributorId: string - ) {} -} - export class FetchLicenses { static readonly type = '[Registries] Fetch Licenses'; } diff --git a/src/app/features/registries/store/registries.model.ts b/src/app/features/registries/store/registries.model.ts index afd2d9058..fea0f82e5 100644 --- a/src/app/features/registries/store/registries.model.ts +++ b/src/app/features/registries/store/registries.model.ts @@ -1,4 +1,4 @@ -import { AsyncStateModel, ContributorModel, License, Resource, Subject } from '@shared/models'; +import { AsyncStateModel, License, Resource, Subject } from '@shared/models'; import { PageSchema, Project, Provider } from '../models'; import { Registration } from '../models/registration.model'; @@ -7,7 +7,6 @@ export interface RegistriesStateModel { providers: AsyncStateModel; projects: AsyncStateModel; draftRegistration: AsyncStateModel; - contributorsList: AsyncStateModel; registries: AsyncStateModel; licenses: AsyncStateModel; registrationSubjects: AsyncStateModel; diff --git a/src/app/features/registries/store/registries.selectors.ts b/src/app/features/registries/store/registries.selectors.ts index c3fb6dcf2..5e26c1f17 100644 --- a/src/app/features/registries/store/registries.selectors.ts +++ b/src/app/features/registries/store/registries.selectors.ts @@ -38,11 +38,6 @@ export class RegistriesSelectors { return state.draftRegistration.isLoading || state.draftRegistration.isSubmitting || state.pagesSchema.isLoading; } - @Selector([RegistriesState]) - static getContributors(state: RegistriesStateModel) { - return state.contributorsList.data; - } - @Selector([RegistriesState]) static getRegistries(state: RegistriesStateModel): Resource[] { return state.registries.data; diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index 4b04264e6..27c6ec8d3 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -11,18 +11,14 @@ import { getResourceTypes } from '@osf/shared/utils'; import { RegistriesService } from '../services'; -import { RegistrationContributorsHandlers } from './handlers/contributors.handlers'; import { LicensesHandlers } from './handlers/licenses.handlers'; import { ProjectsHandlers } from './handlers/projects.handlers'; import { ProvidersHandlers } from './handlers/providers.handlers'; import { SubjectsHandlers } from './handlers/subjects.handlers'; import { DefaultState } from './default.state'; import { - AddContributor, CreateDraft, - DeleteContributor, DeleteDraft, - FetchContributors, FetchDraft, FetchLicenses, FetchRegistrationSubjects, @@ -31,7 +27,6 @@ import { GetProviders, GetRegistries, SaveLicense, - UpdateContributor, UpdateRegistrationSubjects, } from './registries.actions'; import { RegistriesStateModel } from './registries.model'; @@ -49,7 +44,6 @@ export class RegistriesState { projectsHandler = inject(ProjectsHandlers); licensesHandler = inject(LicensesHandlers); subjectsHandler = inject(SubjectsHandlers); - contributorsHandler = inject(RegistrationContributorsHandlers); @Action(GetRegistries) getRegistries(ctx: StateContext) { @@ -189,26 +183,6 @@ export class RegistriesState { ); } - @Action(FetchContributors) - fetchContributors(ctx: StateContext, action: FetchContributors) { - return this.contributorsHandler.fetchContributors(ctx, action); - } - - @Action(AddContributor) - addContributor(ctx: StateContext, action: AddContributor) { - return this.contributorsHandler.addContributor(ctx, action); - } - - @Action(UpdateContributor) - updateContributor(ctx: StateContext, action: UpdateContributor) { - return this.contributorsHandler.updateContributor(ctx, action); - } - - @Action(DeleteContributor) - deleteContributor(ctx: StateContext, action: DeleteContributor) { - return this.contributorsHandler.deleteContributor(ctx, action); - } - @Action(FetchLicenses) fetchLicenses(ctx: StateContext) { return this.licensesHandler.fetchLicenses(ctx); diff --git a/src/app/shared/services/project-contributors.service.ts b/src/app/shared/services/project-contributors.service.ts index 547bfd03f..582a952d3 100644 --- a/src/app/shared/services/project-contributors.service.ts +++ b/src/app/shared/services/project-contributors.service.ts @@ -46,8 +46,8 @@ export class ProjectContributorsService implements IContributorsService { const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; return this.jsonApiService - .post(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); + .post>(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor.data))); } updateContributor(projectId: string, data: ContributorModel): Observable { diff --git a/src/app/shared/services/registration-contributors.service.ts b/src/app/shared/services/registration-contributors.service.ts index 0ce0d6b29..5d2c2ff4b 100644 --- a/src/app/shared/services/registration-contributors.service.ts +++ b/src/app/shared/services/registration-contributors.service.ts @@ -46,8 +46,8 @@ export class RegistrationContributorsService implements IContributorsService { const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; return this.jsonApiService - .post(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); + .post>(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor.data))); } updateContributor(projectId: string, data: ContributorModel): Observable { From 33a5bbcba24667412497b18f1f35d23df44fe7ed Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 7 Jul 2025 14:00:54 +0300 Subject: [PATCH 6/9] feat(contributors): updated shared states --- .../contributors/contributors.component.ts | 14 ++-- .../features/preprints/preprints.routes.ts | 7 -- src/app/features/preprints/services/index.ts | 1 - .../services/preprint-contributors.service.ts | 64 ----------------- .../submit-preprint/submit-preprint.state.ts | 8 +-- .../project/analytics/analytics.component.ts | 18 +++-- ...lytics.service.ts => analytics.service.ts} | 17 +++-- .../project/analytics/services/index.ts | 3 +- .../registration-analytics.service.ts | 34 ---------- .../analytics/store/analytics.actions.ts | 9 ++- .../analytics/store/analytics.state.ts | 17 +++-- .../contributors/contributors.component.ts | 29 +++++--- .../contributors-dialog.component.ts | 8 ++- .../metadata/project-metadata.component.ts | 5 +- src/app/features/project/project.routes.ts | 27 ++------ .../project/settings/settings.component.ts | 6 +- .../contributors/contributors.component.ts | 14 ++-- .../features/registries/registries.routes.ts | 35 +++------- ...draft-registration-contributors.service.ts | 62 ----------------- src/app/features/registries/services/index.ts | 1 - src/app/shared/enums/resource-type.enum.ts | 1 + .../analytics/analytics-service.model.ts | 8 --- src/app/shared/models/analytics/index.ts | 1 - .../contributors/contributor-service.model.ts | 14 ---- src/app/shared/models/contributors/index.ts | 1 - src/app/shared/models/index.ts | 1 - .../shared/models/view-only-links/index.ts | 1 - .../view-only-links-service.model.ts | 13 ---- ...ors.service.ts => contributors.service.ts} | 57 ++++++++++------ src/app/shared/services/index.ts | 4 +- .../project-view-only-links.service.ts | 50 -------------- .../registration-contributors.service.ts | 68 ------------------- .../registration-view-only-links.service.ts | 50 -------------- .../services/view-only-links.service.ts | 64 +++++++++++++++++ .../contributors/contributors.actions.ts | 15 ++-- .../stores/contributors/contributors.state.ts | 44 ++++++------ .../view-only-links/view-only-link.actions.ts | 17 +++-- .../view-only-links/view-only-link.state.ts | 28 ++++++-- src/app/shared/tokens/analytics.token.ts | 5 -- src/app/shared/tokens/contributors.token.ts | 5 -- src/app/shared/tokens/index.ts | 3 - .../shared/tokens/view-only-links.token.ts | 5 -- 42 files changed, 275 insertions(+), 559 deletions(-) delete mode 100644 src/app/features/preprints/services/preprint-contributors.service.ts rename src/app/features/project/analytics/services/{project-analytics.service.ts => analytics.service.ts} (62%) delete mode 100644 src/app/features/project/analytics/services/registration-analytics.service.ts delete mode 100644 src/app/features/registries/services/draft-registration-contributors.service.ts delete mode 100644 src/app/shared/models/analytics/analytics-service.model.ts delete mode 100644 src/app/shared/models/analytics/index.ts delete mode 100644 src/app/shared/models/contributors/contributor-service.model.ts delete mode 100644 src/app/shared/models/view-only-links/view-only-links-service.model.ts rename src/app/shared/services/{project-contributors.service.ts => contributors.service.ts} (50%) delete mode 100644 src/app/shared/services/project-view-only-links.service.ts delete mode 100644 src/app/shared/services/registration-contributors.service.ts delete mode 100644 src/app/shared/services/registration-view-only-links.service.ts create mode 100644 src/app/shared/services/view-only-links.service.ts delete mode 100644 src/app/shared/tokens/analytics.token.ts delete mode 100644 src/app/shared/tokens/contributors.token.ts delete mode 100644 src/app/shared/tokens/view-only-links.token.ts diff --git a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts b/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts index d2471379e..9544d8690 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts +++ b/src/app/features/preprints/components/stepper/metadata-step/contributors/contributors.component.ts @@ -19,7 +19,7 @@ import { AddUnregisteredContributorDialogComponent, ContributorsListComponent, } from '@osf/shared/components/contributors'; -import { AddContributorType } from '@osf/shared/enums'; +import { AddContributorType, ResourceType } from '@osf/shared/enums'; import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/models'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { @@ -71,7 +71,7 @@ export class ContributorsComponent implements OnInit { } ngOnInit(): void { - this.actions.getContributors(this.preprintId()); + this.actions.getContributors(this.preprintId(), ResourceType.Preprint); } cancel() { @@ -82,7 +82,7 @@ export class ContributorsComponent implements OnInit { const updatedContributors = findChangedItems(this.initialContributors(), this.contributors(), 'id'); const updateRequests = updatedContributors.map((payload) => - this.actions.updateContributor(this.preprintId(), payload) + this.actions.updateContributor(this.preprintId(), ResourceType.Preprint, payload) ); forkJoin(updateRequests).subscribe(() => { @@ -111,7 +111,9 @@ export class ContributorsComponent implements OnInit { if (res.type === AddContributorType.Unregistered) { this.openAddUnregisteredContributorDialog(); } else { - const addRequests = res.data.map((payload) => this.actions.addContributor(this.preprintId(), payload)); + const addRequests = res.data.map((payload) => + this.actions.addContributor(this.preprintId(), ResourceType.Preprint, payload) + ); forkJoin(addRequests).subscribe(() => { this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); @@ -141,7 +143,7 @@ export class ContributorsComponent implements OnInit { const successMessage = this.translateService.instant('project.contributors.toastMessages.addSuccessMessage'); const params = { name: res.data[0].fullName }; - this.actions.addContributor(this.preprintId(), res.data[0]).subscribe({ + this.actions.addContributor(this.preprintId(), ResourceType.Preprint, res.data[0]).subscribe({ next: () => this.toastService.showSuccess(successMessage, params), }); } @@ -156,7 +158,7 @@ export class ContributorsComponent implements OnInit { acceptLabelKey: 'common.buttons.remove', onConfirm: () => { this.actions - .deleteContributor(this.preprintId(), contributor.userId) + .deleteContributor(this.preprintId(), ResourceType.Preprint, contributor.userId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => diff --git a/src/app/features/preprints/preprints.routes.ts b/src/app/features/preprints/preprints.routes.ts index a616fc8db..3bb1126f2 100644 --- a/src/app/features/preprints/preprints.routes.ts +++ b/src/app/features/preprints/preprints.routes.ts @@ -9,9 +9,6 @@ import { PreprintsResourcesFiltersState } from '@osf/features/preprints/store/pr import { PreprintsResourcesFiltersOptionsState } from '@osf/features/preprints/store/preprints-resources-filters-options'; import { SubmitPreprintState } from '@osf/features/preprints/store/submit-preprint'; import { ContributorsState } from '@osf/shared/stores'; -import { CONTRIBUTORS_SERVICE } from '@osf/shared/tokens'; - -import { PreprintContributorsService } from './services'; export const preprintsRoutes: Routes = [ { @@ -26,10 +23,6 @@ export const preprintsRoutes: Routes = [ SubmitPreprintState, ContributorsState, ]), - { - provide: CONTRIBUTORS_SERVICE, - useClass: PreprintContributorsService, - }, ], children: [ { diff --git a/src/app/features/preprints/services/index.ts b/src/app/features/preprints/services/index.ts index 339550d00..04a25545e 100644 --- a/src/app/features/preprints/services/index.ts +++ b/src/app/features/preprints/services/index.ts @@ -1,4 +1,3 @@ -export { PreprintContributorsService } from './preprint-contributors.service'; export { PreprintFilesService } from './preprint-files.service'; export { PreprintLicensesService } from './preprint-licenses.service'; export { PreprintProvidersService } from './preprint-providers.service'; diff --git a/src/app/features/preprints/services/preprint-contributors.service.ts b/src/app/features/preprints/services/preprint-contributors.service.ts deleted file mode 100644 index 20d5b1629..000000000 --- a/src/app/features/preprints/services/preprint-contributors.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { map, Observable } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@core/models'; -import { JsonApiService } from '@core/services'; -import { AddContributorType } from '@osf/shared/enums'; -import { ContributorsMapper } from '@osf/shared/mappers'; -import { - ContributorAddModel, - ContributorModel, - ContributorResponse, - IContributorsService, - PaginatedData, -} from '@osf/shared/models'; - -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class PreprintContributorsService implements IContributorsService { - private jsonApiService = inject(JsonApiService); - private apiUrl = environment.apiUrl; - - getAllContributors(preprintId: string): Observable { - return this.jsonApiService - .get>(`${this.apiUrl}/preprints/${preprintId}/contributors/`) - .pipe(map((contributors) => ContributorsMapper.fromResponse(contributors.data))); - } - - searchUsers(value: string, page = 1): Observable> { - const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; - - return this.jsonApiService - .get>(baseUrl) - .pipe(map((response) => ContributorsMapper.fromUsersWithPaginationGetResponse(response))); - } - - addContributor(preprintId: string, data: ContributorAddModel): Observable { - const baseUrl = `${this.apiUrl}/preprints/${preprintId}/contributors/`; - const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; - - const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; - - return this.jsonApiService - .post>(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor.data))); - } - - updateContributor(preprintId: string, data: ContributorModel): Observable { - const baseUrl = `${environment.apiUrl}/preprints/${preprintId}/contributors/${data.userId}`; - - const contributorData = { data: ContributorsMapper.toContributorAddRequest(data) }; - - return this.jsonApiService - .patch(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); - } - - deleteContributor(preprintId: string, contributorId: string): Observable { - return this.jsonApiService.delete(`${this.apiUrl}/preprints/${preprintId}/contributors/${contributorId}`); - } -} diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 366b966c7..ac79fcf81 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -9,12 +9,7 @@ import { inject, Injectable } from '@angular/core'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; -import { - PreprintContributorsService, - PreprintFilesService, - PreprintLicensesService, - PreprintsService, -} from '@osf/features/preprints/services'; +import { PreprintFilesService, PreprintLicensesService, PreprintsService } from '@osf/features/preprints/services'; import { OsfFile } from '@shared/models'; import { FilesService } from '@shared/services'; @@ -80,7 +75,6 @@ export class SubmitPreprintState { private preprintsService = inject(PreprintsService); private preprintFilesService = inject(PreprintFilesService); private fileService = inject(FilesService); - private contributorsService = inject(PreprintContributorsService); private licensesService = inject(PreprintLicensesService); @Action(SetSelectedPreprintProviderId) diff --git a/src/app/features/project/analytics/analytics.component.ts b/src/app/features/project/analytics/analytics.component.ts index a74d0d553..aca8fb103 100644 --- a/src/app/features/project/analytics/analytics.component.ts +++ b/src/app/features/project/analytics/analytics.component.ts @@ -7,12 +7,13 @@ import { SelectModule } from 'primeng/select'; import { map, of } from 'rxjs'; import { CommonModule, DatePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit, Signal, signal } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { BarChartComponent, LineChartComponent, PieChartComponent, SubHeaderComponent } from '@osf/shared/components'; +import { ResourceType } from '@osf/shared/enums'; import { DatasetInput } from '@osf/shared/models'; import { IS_WEB } from '@osf/shared/utils'; @@ -49,10 +50,13 @@ export class AnalyticsComponent implements OnInit { private readonly datePipe = inject(DatePipe); private readonly route = inject(ActivatedRoute); - readonly projectId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); + readonly resourceId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); + readonly resourceType: Signal = toSignal( + this.route.data.pipe(map((params) => params['resourceType'])) ?? of(undefined) + ); - protected analytics = select(AnalyticsSelectors.getMetrics(this.projectId())); - protected relatedCounts = select(AnalyticsSelectors.getRelatedCounts(this.projectId())); + protected analytics = select(AnalyticsSelectors.getMetrics(this.resourceId())); + protected relatedCounts = select(AnalyticsSelectors.getRelatedCounts(this.resourceId())); protected isMetricsLoading = select(AnalyticsSelectors.isMetricsLoading); protected isRelatedCountsLoading = select(AnalyticsSelectors.isRelatedCountsLoading); @@ -74,14 +78,14 @@ export class AnalyticsComponent implements OnInit { protected popularPagesDataset: DatasetInput[] = []; ngOnInit() { - this.actions.getMetrics(this.projectId(), this.selectedRange().value); - this.actions.getRelatedCounts(this.projectId()); + this.actions.getMetrics(this.resourceId(), this.selectedRange().value); + this.actions.getRelatedCounts(this.resourceId(), this.resourceType()); this.setData(); } onRangeChange(range: DateRangeOption) { this.selectedRange.set(range); - this.actions.getMetrics(this.projectId(), range.value); + this.actions.getMetrics(this.resourceId(), range.value); } private setData() { diff --git a/src/app/features/project/analytics/services/project-analytics.service.ts b/src/app/features/project/analytics/services/analytics.service.ts similarity index 62% rename from src/app/features/project/analytics/services/project-analytics.service.ts rename to src/app/features/project/analytics/services/analytics.service.ts index f096f2817..696abcd39 100644 --- a/src/app/features/project/analytics/services/project-analytics.service.ts +++ b/src/app/features/project/analytics/services/analytics.service.ts @@ -4,6 +4,7 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiResponse } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; +import { ResourceType } from '@osf/shared/enums'; import { AnalyticsMetricsMapper, RelatedCountsMapper } from '../mappers'; import { AnalyticsMetricsGetResponse, AnalyticsMetricsModel, RelatedCountsGetResponse } from '../models'; @@ -13,19 +14,25 @@ import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class ProjectAnalyticsService { +export class AnalyticsService { private readonly jsonApiService = inject(JsonApiService); - getMetrics(projectId: string, dateRange: string): Observable { + private readonly urlMap = new Map([ + [ResourceType.Project, 'nodes'], + [ResourceType.Registration, 'registrations'], + ]); + + getMetrics(resourceId: string, dateRange: string): Observable { const baseUrl = `${environment.apiDomainUrl}/_/metrics/query/node_analytics`; return this.jsonApiService - .get>(`${baseUrl}/${projectId}/${dateRange}`) + .get>(`${baseUrl}/${resourceId}/${dateRange}`) .pipe(map((response) => AnalyticsMetricsMapper.fromResponse(response.data))); } - getRelatedCounts(projectId: string) { - const url = `${environment.apiUrl}/nodes/${projectId}/?related_counts=true`; + getRelatedCounts(resourceId: string, resourceType: ResourceType) { + const resourcePath = this.urlMap.get(resourceType); + const url = `${environment.apiUrl}/${resourcePath}/${resourceId}/?related_counts=true`; return this.jsonApiService .get(url) diff --git a/src/app/features/project/analytics/services/index.ts b/src/app/features/project/analytics/services/index.ts index aff609037..58bd2917d 100644 --- a/src/app/features/project/analytics/services/index.ts +++ b/src/app/features/project/analytics/services/index.ts @@ -1,2 +1 @@ -export { ProjectAnalyticsService } from './project-analytics.service'; -export { RegistrationAnalyticsService } from './registration-analytics.service'; +export { AnalyticsService } from './analytics.service'; diff --git a/src/app/features/project/analytics/services/registration-analytics.service.ts b/src/app/features/project/analytics/services/registration-analytics.service.ts deleted file mode 100644 index b55eb9ec2..000000000 --- a/src/app/features/project/analytics/services/registration-analytics.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { map, Observable } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiResponse } from '@osf/core/models'; -import { JsonApiService } from '@osf/core/services'; - -import { AnalyticsMetricsMapper, RelatedCountsMapper } from '../mappers'; -import { AnalyticsMetricsGetResponse, AnalyticsMetricsModel, RelatedCountsGetResponse } from '../models'; - -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class RegistrationAnalyticsService { - private readonly jsonApiService = inject(JsonApiService); - - getMetrics(projectId: string, dateRange: string): Observable { - const baseUrl = `${environment.apiDomainUrl}/_/metrics/query/node_analytics`; - - return this.jsonApiService - .get>(`${baseUrl}/${projectId}/${dateRange}`) - .pipe(map((response) => AnalyticsMetricsMapper.fromResponse(response.data))); - } - - getRelatedCounts(projectId: string) { - const url = `${environment.apiUrl}/registrations/${projectId}/?related_counts=true`; - - return this.jsonApiService - .get(url) - .pipe(map((response) => RelatedCountsMapper.fromResponse(response))); - } -} diff --git a/src/app/features/project/analytics/store/analytics.actions.ts b/src/app/features/project/analytics/store/analytics.actions.ts index b2b4103e4..e61e07d96 100644 --- a/src/app/features/project/analytics/store/analytics.actions.ts +++ b/src/app/features/project/analytics/store/analytics.actions.ts @@ -1,10 +1,12 @@ +import { ResourceType } from '@osf/shared/enums'; + import { DateRange } from '../enums'; export class GetMetrics { static readonly type = '[Analytics] Get Metrics'; constructor( - public projectId: string, + public resourceId: string, public dateRange: DateRange ) {} } @@ -12,5 +14,8 @@ export class GetMetrics { export class GetRelatedCounts { static readonly type = '[Analytics] Get Related Counts'; - constructor(public projectId: string) {} + constructor( + public resourceId: string, + public resourceType: ResourceType | undefined + ) {} } diff --git a/src/app/features/project/analytics/store/analytics.state.ts b/src/app/features/project/analytics/store/analytics.state.ts index 594966bdb..7e61181ad 100644 --- a/src/app/features/project/analytics/store/analytics.state.ts +++ b/src/app/features/project/analytics/store/analytics.state.ts @@ -5,9 +5,8 @@ import { catchError, of, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { ANALYTICS_SERVICE } from '@osf/shared/tokens'; - import { AnalyticsMetricsModel, RelatedCountsModel } from '../models'; +import { AnalyticsService } from '../services'; import { GetMetrics, GetRelatedCounts } from './analytics.actions'; import { AnalyticsStateModel } from './analytics.model'; @@ -29,13 +28,13 @@ import { AnalyticsStateModel } from './analytics.model'; }) @Injectable() export class AnalyticsState { - private readonly analyticsService = inject(ANALYTICS_SERVICE); + private readonly analyticsService = inject(AnalyticsService); private readonly REFRESH_INTERVAL = 5 * 60 * 1000; @Action(GetMetrics) getMetrics(ctx: StateContext, action: GetMetrics) { const state = ctx.getState(); - const metricsId = `${action.projectId}:${action.dateRange}`; + const metricsId = `${action.resourceId}:${action.dateRange}`; const cachedData = state.metrics.data.find((m) => m.id === metricsId); const shouldRefresh = this.shouldRefresh(cachedData?.lastFetched); @@ -54,7 +53,7 @@ export class AnalyticsState { metrics: { ...state.metrics, isLoading: true, error: null }, }); - return this.analyticsService.getMetrics(action.projectId, action.dateRange).pipe( + return this.analyticsService.getMetrics(action.resourceId, action.dateRange).pipe( tap((metrics) => { const exists = state.metrics.data.some((m) => m.id === metrics.id); metrics.lastFetched = Date.now(); @@ -76,7 +75,7 @@ export class AnalyticsState { @Action(GetRelatedCounts) getRelatedCounts(ctx: StateContext, action: GetRelatedCounts) { const state = ctx.getState(); - const relatedCountsId = action.projectId; + const relatedCountsId = action.resourceId; const cachedData = state.relatedCounts.data.find((rc) => rc.id === relatedCountsId); const shouldRefresh = this.shouldRefresh(cachedData?.lastFetched); @@ -95,7 +94,11 @@ export class AnalyticsState { relatedCounts: { ...state.relatedCounts, isLoading: true, error: null }, }); - return this.analyticsService.getRelatedCounts(action.projectId).pipe( + if (!action.resourceType) { + return; + } + + return this.analyticsService.getRelatedCounts(action.resourceId, action.resourceType).pipe( tap((relatedCounts) => { const exists = state.relatedCounts.data.some((rc) => rc.id === relatedCounts.id); relatedCounts.lastFetched = Date.now(); diff --git a/src/app/features/project/contributors/contributors.component.ts b/src/app/features/project/contributors/contributors.component.ts index 3a46f01d6..115703c18 100644 --- a/src/app/features/project/contributors/contributors.component.ts +++ b/src/app/features/project/contributors/contributors.component.ts @@ -9,7 +9,7 @@ import { TableModule } from 'primeng/table'; import { debounceTime, distinctUntilChanged, filter, forkJoin, map, of, switchMap } from 'rxjs'; -import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, Signal, signal } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; @@ -21,7 +21,7 @@ import { ContributorsListComponent, } from '@osf/shared/components/contributors'; import { BIBLIOGRAPHY_OPTIONS, PERMISSION_OPTIONS } from '@osf/shared/constants'; -import { AddContributorType, ContributorPermission } from '@osf/shared/enums'; +import { AddContributorType, ContributorPermission, ResourceType } from '@osf/shared/enums'; import { ContributorDialogAddModel, ContributorModel, @@ -79,6 +79,9 @@ export class ContributorsComponent implements OnInit { private readonly resourceId = toSignal( this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined) ); + readonly resourceType: Signal = toSignal( + this.route.data.pipe(map((params) => params['resourceType'])) ?? of(undefined) + ); protected viewOnlyLinks = select(ViewOnlyLinkSelectors.getViewOnlyLinks); protected projectDetails = select(ViewOnlyLinkSelectors.getResourceDetails); @@ -127,9 +130,9 @@ export class ContributorsComponent implements OnInit { const id = this.resourceId(); if (id) { - this.actions.getViewOnlyLinks(id); - this.actions.getResourceDetails(id); - this.actions.getContributors(this.resourceId()); + this.actions.getViewOnlyLinks(id, this.resourceType()); + this.actions.getResourceDetails(id, this.resourceType()); + this.actions.getContributors(id, this.resourceType()); } this.setSearchSubscription(); @@ -157,7 +160,7 @@ export class ContributorsComponent implements OnInit { const updatedContributors = findChangedItems(this.initialContributors(), this.contributors(), 'id'); const updateRequests = updatedContributors.map((payload) => - this.actions.updateContributor(this.resourceId(), payload) + this.actions.updateContributor(this.resourceId(), ResourceType.Project, payload) ); forkJoin(updateRequests).subscribe(() => { @@ -186,7 +189,9 @@ export class ContributorsComponent implements OnInit { if (res.type === AddContributorType.Unregistered) { this.openAddUnregisteredContributorDialog(); } else { - const addRequests = res.data.map((payload) => this.actions.addContributor(this.resourceId(), payload)); + const addRequests = res.data.map((payload) => + this.actions.addContributor(this.resourceId(), ResourceType.Project, payload) + ); forkJoin(addRequests).subscribe(() => { this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); @@ -216,7 +221,7 @@ export class ContributorsComponent implements OnInit { const successMessage = this.translateService.instant('project.contributors.toastMessages.addSuccessMessage'); const params = { name: res.data[0].fullName }; - this.actions.addContributor(this.resourceId(), res.data[0]).subscribe({ + this.actions.addContributor(this.resourceId(), ResourceType.Project, res.data[0]).subscribe({ next: () => this.toastService.showSuccess(successMessage, params), }); } @@ -231,7 +236,7 @@ export class ContributorsComponent implements OnInit { acceptLabelKey: 'common.buttons.remove', onConfirm: () => { this.actions - .deleteContributor(this.resourceId(), contributor.userId) + .deleteContributor(this.resourceId(), ResourceType.Project, contributor.userId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => @@ -264,7 +269,9 @@ export class ContributorsComponent implements OnInit { }) .onClose.pipe( filter((res) => !!res), - switchMap((result) => this.actions.createViewOnlyLink(this.resourceId(), result as ViewOnlyLinkJsonApi)), + switchMap((result) => + this.actions.createViewOnlyLink(this.resourceId(), this.resourceType(), result as ViewOnlyLinkJsonApi) + ), takeUntilDestroyed(this.destroyRef) ) .subscribe(() => this.toastService.showSuccess('myProjects.settings.viewOnlyLinkCreated')); @@ -277,7 +284,7 @@ export class ContributorsComponent implements OnInit { messageKey: 'myProjects.settings.delete.message', onConfirm: () => this.actions - .deleteViewOnlyLink(this.resourceId(), link.id) + .deleteViewOnlyLink(this.resourceId(), this.resourceType(), link.id) .subscribe(() => this.toastService.showSuccess('myProjects.settings.delete.success')), }); } diff --git a/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts b/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts index 17ce73f90..4621dc55d 100644 --- a/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts +++ b/src/app/features/project/metadata/dialogs/contributors-dialog/contributors-dialog.component.ts @@ -16,7 +16,7 @@ import { FormControl, FormsModule } from '@angular/forms'; import { SearchInputComponent } from '@osf/shared/components'; import { AddContributorDialogComponent } from '@osf/shared/components/contributors'; -import { AddContributorType } from '@osf/shared/enums'; +import { AddContributorType, ResourceType } from '@osf/shared/enums'; import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/models'; import { ToastService } from '@osf/shared/services'; import { @@ -90,7 +90,9 @@ export class ContributorsDialogComponent implements OnInit { .onClose.pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((res: ContributorDialogAddModel) => { if (res?.type === AddContributorType.Registered) { - const addRequests = res.data.map((payload) => this.actions.addContributor(this.projectId, payload)); + const addRequests = res.data.map((payload) => + this.actions.addContributor(this.projectId, ResourceType.Project, payload) + ); forkJoin(addRequests).subscribe(() => { this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); @@ -102,7 +104,7 @@ export class ContributorsDialogComponent implements OnInit { removeContributor(contributor: ContributorModel): void { this.actions - .deleteContributor(this.projectId, contributor.userId) + .deleteContributor(this.projectId, ResourceType.Project, contributor.userId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { diff --git a/src/app/features/project/metadata/project-metadata.component.ts b/src/app/features/project/metadata/project-metadata.component.ts index ab056b0b3..a69df88c3 100644 --- a/src/app/features/project/metadata/project-metadata.component.ts +++ b/src/app/features/project/metadata/project-metadata.component.ts @@ -53,6 +53,7 @@ import { UpdateProjectDetails, } from '@osf/features/project/metadata/store'; import { ProjectOverviewSubject } from '@osf/features/project/overview/models'; +import { ResourceType } from '@osf/shared/enums'; import { ContributorsSelectors, GetAllContributors } from '@osf/shared/stores'; import { LoadingSpinnerComponent, SubHeaderComponent, TagsInputComponent } from '@shared/components'; import { CustomConfirmationService, ToastService } from '@shared/services'; @@ -173,7 +174,7 @@ export class ProjectMetadataComponent implements OnInit { this.actions.getHighlightedSubjects(); this.actions.getProject(projectId); this.actions.getCustomItemMetadata(projectId); - this.actions.getContributors(projectId); + this.actions.getContributors(projectId, ResourceType.Project); this.actions.getCedarRecords(projectId); this.actions.getCedarTemplates(); @@ -250,7 +251,7 @@ export class ProjectMetadataComponent implements OnInit { private refreshContributorsData(): void { const projectId = this.route.parent?.parent?.snapshot.params['id']; if (projectId) { - this.actions.getContributors(projectId); + this.actions.getContributors(projectId, ResourceType.Project); } } diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 6721bd22b..35e628155 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -2,11 +2,9 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; -import { ProjectContributorsService, ProjectViewOnlyLinksService } from '@osf/shared/services'; +import { ResourceType } from '@osf/shared/enums'; import { ContributorsState, ViewOnlyLinkState } from '@osf/shared/stores'; -import { ANALYTICS_SERVICE, CONTRIBUTORS_SERVICE, VIEW_ONLY_LINKS_SERVICE } from '@osf/shared/tokens'; -import { ProjectAnalyticsService } from './analytics/services'; import { AnalyticsState } from './analytics/store'; import { ProjectFilesState } from './files/store'; import { SettingsState } from './settings/store'; @@ -30,6 +28,7 @@ export const projectRoutes: Routes = [ path: 'metadata', loadChildren: () => import('../project/metadata/project-metadata.routes').then((mod) => mod.projectMetadataRoutes), + providers: [provideStates([ContributorsState])], }, { path: 'files', @@ -50,28 +49,14 @@ export const projectRoutes: Routes = [ path: 'contributors', loadComponent: () => import('../project/contributors/contributors.component').then((mod) => mod.ContributorsComponent), - providers: [ - provideStates([ContributorsState, ViewOnlyLinkState]), - { - provide: CONTRIBUTORS_SERVICE, - useClass: ProjectContributorsService, - }, - { - provide: VIEW_ONLY_LINKS_SERVICE, - useClass: ProjectViewOnlyLinksService, - }, - ], + data: { resourceType: ResourceType.Project }, + providers: [provideStates([ContributorsState, ViewOnlyLinkState])], }, { path: 'analytics', loadComponent: () => import('../project/analytics/analytics.component').then((mod) => mod.AnalyticsComponent), - providers: [ - provideStates([AnalyticsState]), - { - provide: ANALYTICS_SERVICE, - useClass: ProjectAnalyticsService, - }, - ], + data: { resourceType: ResourceType.Project }, + providers: [provideStates([AnalyticsState])], }, { path: 'wiki', diff --git a/src/app/features/project/settings/settings.component.ts b/src/app/features/project/settings/settings.component.ts index 3c76b1eae..92fb76863 100644 --- a/src/app/features/project/settings/settings.component.ts +++ b/src/app/features/project/settings/settings.component.ts @@ -45,7 +45,7 @@ import { CustomConfirmationService, LoaderService, ToastService } from '@osf/sha import { DeleteViewOnlyLink, FetchViewOnlyLinks, ViewOnlyLinkSelectors } from '@osf/shared/stores'; import { CustomValidators } from '@osf/shared/utils'; import { SubHeaderComponent } from '@shared/components'; -import { ProjectFormControls, SubscriptionEvent, SubscriptionFrequency } from '@shared/enums'; +import { ProjectFormControls, ResourceType, SubscriptionEvent, SubscriptionFrequency } from '@shared/enums'; import { UpdateNodeRequestModel, ViewOnlyLinkModel } from '@shared/models'; @Component({ @@ -122,7 +122,7 @@ export class SettingsComponent implements OnInit { this.actions.getSettings(id); this.actions.getNotifications(id); this.actions.getProjectDetails(id); - this.actions.getViewOnlyLinks(id); + this.actions.getViewOnlyLinks(id, ResourceType.Project); } } @@ -189,7 +189,7 @@ export class SettingsComponent implements OnInit { headerParams: { name: link.name }, messageKey: 'myProjects.settings.delete.message', onConfirm: () => { - this.actions.deleteViewOnlyLink(this.projectId(), link.id).subscribe(() => { + this.actions.deleteViewOnlyLink(this.projectId(), ResourceType.Project, link.id).subscribe(() => { this.toastService.showSuccess('myProjects.settings.delete.success'); this.loaderService.hide(); }); diff --git a/src/app/features/registries/components/metadata/contributors/contributors.component.ts b/src/app/features/registries/components/metadata/contributors/contributors.component.ts index 10ebb774c..dd3854f47 100644 --- a/src/app/features/registries/components/metadata/contributors/contributors.component.ts +++ b/src/app/features/registries/components/metadata/contributors/contributors.component.ts @@ -20,7 +20,7 @@ import { ContributorsListComponent, } from '@osf/shared/components/contributors'; import { BIBLIOGRAPHY_OPTIONS, PERMISSION_OPTIONS } from '@osf/shared/constants'; -import { AddContributorType, ContributorPermission } from '@osf/shared/enums'; +import { AddContributorType, ContributorPermission, ResourceType } from '@osf/shared/enums'; import { ContributorDialogAddModel, ContributorModel, SelectOption } from '@osf/shared/models'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; import { @@ -78,7 +78,7 @@ export class ContributorsComponent implements OnInit { } ngOnInit(): void { - this.actions.getContributors(this.draftId()); + this.actions.getContributors(this.draftId(), ResourceType.DraftRegistration); } onFocusOut() { @@ -94,7 +94,7 @@ export class ContributorsComponent implements OnInit { const updatedContributors = findChangedItems(this.initialContributors(), this.contributors(), 'id'); const updateRequests = updatedContributors.map((payload) => - this.actions.updateContributor(this.draftId(), payload) + this.actions.updateContributor(this.draftId(), ResourceType.DraftRegistration, payload) ); forkJoin(updateRequests).subscribe(() => { @@ -123,7 +123,9 @@ export class ContributorsComponent implements OnInit { if (res.type === AddContributorType.Unregistered) { this.openAddUnregisteredContributorDialog(); } else { - const addRequests = res.data.map((payload) => this.actions.addContributor(this.draftId(), payload)); + const addRequests = res.data.map((payload) => + this.actions.addContributor(this.draftId(), ResourceType.DraftRegistration, payload) + ); forkJoin(addRequests).subscribe(() => { this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); @@ -153,7 +155,7 @@ export class ContributorsComponent implements OnInit { const successMessage = this.translateService.instant('project.contributors.toastMessages.addSuccessMessage'); const params = { name: res.data[0].fullName }; - this.actions.addContributor(this.draftId(), res.data[0]).subscribe({ + this.actions.addContributor(this.draftId(), ResourceType.DraftRegistration, res.data[0]).subscribe({ next: () => this.toastService.showSuccess(successMessage, params), }); } @@ -168,7 +170,7 @@ export class ContributorsComponent implements OnInit { acceptLabelKey: 'common.buttons.remove', onConfirm: () => { this.actions - .deleteContributor(this.draftId(), contributor.userId) + .deleteContributor(this.draftId(), ResourceType.DraftRegistration, contributor.userId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => diff --git a/src/app/features/registries/registries.routes.ts b/src/app/features/registries/registries.routes.ts index cd1b896e5..1d04accf5 100644 --- a/src/app/features/registries/registries.routes.ts +++ b/src/app/features/registries/registries.routes.ts @@ -4,16 +4,15 @@ import { Routes } from '@angular/router'; import { RegistriesComponent } from '@osf/features/registries/registries.component'; import { RegistriesState } from '@osf/features/registries/store'; -import { RegistrationViewOnlyLinksService } from '@osf/shared/services/registration-view-only-links.service'; -import { ContributorsState, SubjectsState, ViewOnlyLinkState } from '@osf/shared/stores'; -import { ANALYTICS_SERVICE, CONTRIBUTORS_SERVICE, SUBJECTS_SERVICE, VIEW_ONLY_LINKS_SERVICE } from '@osf/shared/tokens'; +import { ResourceType } from '@osf/shared/enums'; +import { ContributorsState, SubjectsState } from '@osf/shared/stores'; +import { SUBJECTS_SERVICE } from '@osf/shared/tokens'; import { ModerationState } from '../moderation/store'; -import { RegistrationAnalyticsService } from '../project/analytics/services'; import { AnalyticsState } from '../project/analytics/store'; import { LicensesHandlers, ProjectsHandlers, ProvidersHandlers, SubjectsHandlers } from './store/handlers'; -import { DraftRegistrationContributorsService, LicensesService, RegistrationSubjectsService } from './services'; +import { LicensesService, RegistrationSubjectsService } from './services'; export const registriesRoutes: Routes = [ { @@ -31,10 +30,6 @@ export const registriesRoutes: Routes = [ provide: SUBJECTS_SERVICE, useClass: RegistrationSubjectsService, }, - { - provide: CONTRIBUTORS_SERVICE, - useClass: DraftRegistrationContributorsService, - }, ], children: [ { @@ -88,29 +83,15 @@ export const registriesRoutes: Routes = [ path: 'contributors', loadComponent: () => import('../project/contributors/contributors.component').then((mod) => mod.ContributorsComponent), - providers: [ - provideStates([ContributorsState, ViewOnlyLinkState]), - { - provide: CONTRIBUTORS_SERVICE, - useClass: DraftRegistrationContributorsService, - }, - { - provide: VIEW_ONLY_LINKS_SERVICE, - useClass: RegistrationViewOnlyLinksService, - }, - ], + data: { resourceType: ResourceType.Registration }, + providers: [provideStates([ContributorsState])], }, { path: 'analytics', loadComponent: () => import('../project/analytics/analytics.component').then((mod) => mod.AnalyticsComponent), - providers: [ - provideStates([AnalyticsState]), - { - provide: ANALYTICS_SERVICE, - useClass: RegistrationAnalyticsService, - }, - ], + data: { resourceType: ResourceType.Registration }, + providers: [provideStates([AnalyticsState])], }, ], }, diff --git a/src/app/features/registries/services/draft-registration-contributors.service.ts b/src/app/features/registries/services/draft-registration-contributors.service.ts deleted file mode 100644 index 9e25e3cd9..000000000 --- a/src/app/features/registries/services/draft-registration-contributors.service.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { map, Observable } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; -import { JsonApiService } from '@osf/core/services'; -import { AddContributorType } from '@osf/shared/enums'; -import { ContributorsMapper } from '@osf/shared/mappers'; -import { - ContributorAddModel, - ContributorModel, - ContributorResponse, - IContributorsService, - PaginatedData, -} from '@osf/shared/models'; - -import { environment } from 'src/environments/environment'; - -@Injectable() -export class DraftRegistrationContributorsService implements IContributorsService { - private apiUrl = environment.apiUrl; - private readonly jsonApiService = inject(JsonApiService); - - getAllContributors(draftId: string): Observable { - return this.jsonApiService - .get>(`${this.apiUrl}/draft_registrations/${draftId}/contributors/`) - .pipe(map((contributors) => ContributorsMapper.fromResponse(contributors.data))); - } - - searchUsers(value: string, page = 1): Observable> { - const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; - - return this.jsonApiService - .get>(baseUrl) - .pipe(map((response) => ContributorsMapper.fromUsersWithPaginationGetResponse(response))); - } - - addContributor(draftId: string, data: ContributorAddModel): Observable { - const baseUrl = `${this.apiUrl}/draft_registrations/${draftId}/contributors/`; - const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; - - const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; - - return this.jsonApiService - .post>(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor.data))); - } - - updateContributor(draftId: string, data: ContributorModel): Observable { - const baseUrl = `${environment.apiUrl}/draft_registrations/${draftId}/contributors/${data.userId}`; - - const contributorData = { data: ContributorsMapper.toContributorAddRequest(data) }; - - return this.jsonApiService - .patch(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); - } - - deleteContributor(draftId: string, contributorId: string): Observable { - return this.jsonApiService.delete(`${this.apiUrl}/draft_registrations/${draftId}/contributors/${contributorId}`); - } -} diff --git a/src/app/features/registries/services/index.ts b/src/app/features/registries/services/index.ts index f3c83a0fd..99fc8de16 100644 --- a/src/app/features/registries/services/index.ts +++ b/src/app/features/registries/services/index.ts @@ -1,4 +1,3 @@ -export * from './draft-registration-contributors.service'; export * from './licenses.service'; export * from './projects.service'; export * from './providers.service'; diff --git a/src/app/shared/enums/resource-type.enum.ts b/src/app/shared/enums/resource-type.enum.ts index 61ac6ded6..e842d3a9a 100644 --- a/src/app/shared/enums/resource-type.enum.ts +++ b/src/app/shared/enums/resource-type.enum.ts @@ -6,4 +6,5 @@ export enum ResourceType { Preprint, ProjectComponent, Agent, + DraftRegistration, } diff --git a/src/app/shared/models/analytics/analytics-service.model.ts b/src/app/shared/models/analytics/analytics-service.model.ts deleted file mode 100644 index 4981b7189..000000000 --- a/src/app/shared/models/analytics/analytics-service.model.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Observable } from 'rxjs'; - -import { AnalyticsMetricsModel, RelatedCountsModel } from '@osf/features/project/analytics/models'; - -export interface IAnalyticsService { - getMetrics(projectId: string, dateRange: string): Observable; - getRelatedCounts(projectId: string): Observable; -} diff --git a/src/app/shared/models/analytics/index.ts b/src/app/shared/models/analytics/index.ts deleted file mode 100644 index 558943691..000000000 --- a/src/app/shared/models/analytics/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './analytics-service.model'; diff --git a/src/app/shared/models/contributors/contributor-service.model.ts b/src/app/shared/models/contributors/contributor-service.model.ts deleted file mode 100644 index cdd4b5317..000000000 --- a/src/app/shared/models/contributors/contributor-service.model.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Observable } from 'rxjs'; - -import { PaginatedData } from '@osf/shared/models'; - -import { ContributorModel } from './contributor.model'; -import { ContributorAddModel } from './contributor-add.model'; - -export interface IContributorsService { - getAllContributors(resourceId: string): Observable; - addContributor(resourceId: string, data: ContributorAddModel): Observable; - updateContributor(resourceId: string, data: ContributorModel): Observable; - deleteContributor(resourceId: string, userId: string): Observable; - searchUsers(value: string, page: number): Observable>; -} diff --git a/src/app/shared/models/contributors/index.ts b/src/app/shared/models/contributors/index.ts index f5ea15a0c..9047f2296 100644 --- a/src/app/shared/models/contributors/index.ts +++ b/src/app/shared/models/contributors/index.ts @@ -2,5 +2,4 @@ export * from './contributor.model'; export * from './contributor-add.model'; export * from './contributor-dialog-add.model'; export * from './contributor-response.model'; -export * from './contributor-service.model'; export * from './unregistered-contributor-form.model'; diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 01e1a30ba..6dc6a9157 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -1,5 +1,4 @@ export * from './addons'; -export * from './analytics'; export * from './brand.json-api.model'; export * from './brand.model'; export * from './charts'; diff --git a/src/app/shared/models/view-only-links/index.ts b/src/app/shared/models/view-only-links/index.ts index f568a449b..8bc5e3616 100644 --- a/src/app/shared/models/view-only-links/index.ts +++ b/src/app/shared/models/view-only-links/index.ts @@ -1,3 +1,2 @@ export * from './view-only-link.model'; export * from './view-only-link-response.model'; -export * from './view-only-links-service.model'; diff --git a/src/app/shared/models/view-only-links/view-only-links-service.model.ts b/src/app/shared/models/view-only-links/view-only-links-service.model.ts deleted file mode 100644 index 7dac51db2..000000000 --- a/src/app/shared/models/view-only-links/view-only-links-service.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Observable } from 'rxjs'; - -import { NodeResponseModel } from '../node-response.model'; - -import { PaginatedViewOnlyLinksModel } from './view-only-link.model'; -import { ViewOnlyLinkJsonApi } from './view-only-link-response.model'; - -export interface IViewOnlyLinksService { - getResourceById(projectId: string): Observable; - getViewOnlyLinksData(projectId: string): Observable; - createViewOnlyLink(projectId: string, payload: ViewOnlyLinkJsonApi): Observable; - deleteLink(projectId: string, linkId: string): Observable; -} diff --git a/src/app/shared/services/project-contributors.service.ts b/src/app/shared/services/contributors.service.ts similarity index 50% rename from src/app/shared/services/project-contributors.service.ts rename to src/app/shared/services/contributors.service.ts index 582a952d3..0224edbf4 100644 --- a/src/app/shared/services/project-contributors.service.ts +++ b/src/app/shared/services/contributors.service.ts @@ -4,27 +4,38 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; -import { - ContributorAddModel, - ContributorModel, - ContributorResponse, - IContributorsService, - PaginatedData, -} from '@osf/shared/models'; - -import { AddContributorType } from '../enums'; -import { ContributorsMapper } from '../mappers/contributors'; +import { AddContributorType, ResourceType } from '@osf/shared/enums'; +import { ContributorsMapper } from '@osf/shared/mappers/contributors'; +import { ContributorAddModel, ContributorModel, ContributorResponse, PaginatedData } from '@osf/shared/models'; import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) -export class ProjectContributorsService implements IContributorsService { +export class ContributorsService { private readonly jsonApiService = inject(JsonApiService); - getAllContributors(projectId: string): Observable { - const baseUrl = `${environment.apiUrl}/nodes/${projectId}/contributors`; + private readonly urlMap = new Map([ + [ResourceType.Project, 'nodes'], + [ResourceType.Registration, 'registrations'], + [ResourceType.Preprint, 'preprints'], + [ResourceType.DraftRegistration, 'draft_registrations'], + ]); + + private getBaseUrl(resourceType: ResourceType, resourceId: string): string { + const baseUrl = `${environment.apiUrl}`; + const resourcePath = this.urlMap.get(resourceType); + + if (!resourcePath) { + throw new Error(`Unsupported resource type: ${resourceType}`); + } + + return `${baseUrl}/${resourcePath}/${resourceId}/contributors`; + } + + getAllContributors(resourceType: ResourceType, resourceId: string): Observable { + const baseUrl = this.getBaseUrl(resourceType, resourceId); return this.jsonApiService .get>(baseUrl) @@ -39,8 +50,12 @@ export class ProjectContributorsService implements IContributorsService { .pipe(map((response) => ContributorsMapper.fromUsersWithPaginationGetResponse(response))); } - addContributor(projectId: string, data: ContributorAddModel): Observable { - const baseUrl = `${environment.apiUrl}/nodes/${projectId}/contributors/`; + addContributor( + resourceType: ResourceType, + resourceId: string, + data: ContributorAddModel + ): Observable { + const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/`; const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; @@ -50,8 +65,12 @@ export class ProjectContributorsService implements IContributorsService { .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor.data))); } - updateContributor(projectId: string, data: ContributorModel): Observable { - const baseUrl = `${environment.apiUrl}/nodes/${projectId}/contributors/${data.userId}`; + updateContributor( + resourceType: ResourceType, + resourceId: string, + data: ContributorModel + ): Observable { + const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/${data.userId}`; const contributorData = { data: ContributorsMapper.toContributorAddRequest(data) }; @@ -60,8 +79,8 @@ export class ProjectContributorsService implements IContributorsService { .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); } - deleteContributor(projectId: string, userId: string): Observable { - const baseUrl = `${environment.apiUrl}/nodes/${projectId}/contributors/${userId}`; + deleteContributor(resourceType: ResourceType, resourceId: string, userId: string): Observable { + const baseUrl = `${this.getBaseUrl(resourceType, resourceId)}/${userId}`; return this.jsonApiService.delete(baseUrl); } diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 678ac1cb7..836b6e4cd 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -1,14 +1,14 @@ export * from './addons'; export { BrandService } from './brand.service'; +export { ContributorsService } from './contributors.service'; export { CustomConfirmationService } from './custom-confirmation.service'; export { FilesService } from './files.service'; export { FiltersOptionsService } from './filters-options.service'; export { InstitutionsService } from './institutions.service'; export { LicensesService } from './licenses.service'; export { LoaderService } from './loader.service'; -export { ProjectContributorsService } from './project-contributors.service'; -export { ProjectViewOnlyLinksService } from './project-view-only-links.service'; export { ResourceCardService } from './resource-card.service'; export { SearchService } from './search.service'; export { SubjectsService } from './subjects.service'; export { ToastService } from './toast.service'; +export { ViewOnlyLinksService } from './view-only-links.service'; diff --git a/src/app/shared/services/project-view-only-links.service.ts b/src/app/shared/services/project-view-only-links.service.ts deleted file mode 100644 index 0461c0217..000000000 --- a/src/app/shared/services/project-view-only-links.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { map, Observable } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiResponse } from '@core/models'; -import { JsonApiService } from '@core/services'; - -import { ViewOnlyLinksMapper } from '../mappers'; -import { NodeResponseModel } from '../models'; -import { - PaginatedViewOnlyLinksModel, - ViewOnlyLinkJsonApi, - ViewOnlyLinksResponseJsonApi, -} from '../models/view-only-links'; - -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class ProjectViewOnlyLinksService { - private readonly jsonApiService = inject(JsonApiService); - - getResourceById(projectId: string): Observable { - return this.jsonApiService.get(`${environment.apiUrl}/nodes/${projectId}`); - } - - getViewOnlyLinksData(projectId: string): Observable { - const params: Record = { embed: 'creator' }; - - return this.jsonApiService - .get(`${environment.apiUrl}/nodes/${projectId}/view_only_links`, params) - .pipe(map((response) => ViewOnlyLinksMapper.fromResponse(response, projectId))); - } - - createViewOnlyLink(projectId: string, payload: ViewOnlyLinkJsonApi): Observable { - const data = { data: { ...payload } }; - const params: Record = { embed: 'creator' }; - - return this.jsonApiService - .post< - JsonApiResponse - >(`${environment.apiUrl}/nodes/${projectId}/view_only_links/`, data, params) - .pipe(map((response) => ViewOnlyLinksMapper.fromSingleResponse(response.data, projectId))); - } - - deleteLink(projectId: string, linkId: string): Observable { - return this.jsonApiService.delete(`${environment.apiUrl}/nodes/${projectId}/view_only_links/${linkId}`); - } -} diff --git a/src/app/shared/services/registration-contributors.service.ts b/src/app/shared/services/registration-contributors.service.ts deleted file mode 100644 index 5d2c2ff4b..000000000 --- a/src/app/shared/services/registration-contributors.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { map, Observable } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiResponse, JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; -import { JsonApiService } from '@osf/core/services'; -import { - ContributorAddModel, - ContributorModel, - ContributorResponse, - IContributorsService, - PaginatedData, -} from '@osf/shared/models'; - -import { AddContributorType } from '../enums'; -import { ContributorsMapper } from '../mappers/contributors'; - -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class RegistrationContributorsService implements IContributorsService { - private readonly jsonApiService = inject(JsonApiService); - - getAllContributors(projectId: string): Observable { - const baseUrl = `${environment.apiUrl}/registrations/${projectId}/contributors`; - - return this.jsonApiService - .get>(baseUrl) - .pipe(map((response) => ContributorsMapper.fromResponse(response.data))); - } - - searchUsers(value: string, page = 1): Observable> { - const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; - - return this.jsonApiService - .get>(baseUrl) - .pipe(map((response) => ContributorsMapper.fromUsersWithPaginationGetResponse(response))); - } - - addContributor(projectId: string, data: ContributorAddModel): Observable { - const baseUrl = `${environment.apiUrl}/registrations/${projectId}/contributors/`; - const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; - - const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; - - return this.jsonApiService - .post>(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor.data))); - } - - updateContributor(projectId: string, data: ContributorModel): Observable { - const baseUrl = `${environment.apiUrl}/registrations/${projectId}/contributors/${data.userId}`; - - const contributorData = { data: ContributorsMapper.toContributorAddRequest(data) }; - - return this.jsonApiService - .patch(baseUrl, contributorData) - .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); - } - - deleteContributor(projectId: string, userId: string): Observable { - const baseUrl = `${environment.apiUrl}/registrations/${projectId}/contributors/${userId}`; - - return this.jsonApiService.delete(baseUrl); - } -} diff --git a/src/app/shared/services/registration-view-only-links.service.ts b/src/app/shared/services/registration-view-only-links.service.ts deleted file mode 100644 index ece077ec8..000000000 --- a/src/app/shared/services/registration-view-only-links.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { map, Observable } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiResponse } from '@core/models'; -import { JsonApiService } from '@core/services'; - -import { ViewOnlyLinksMapper } from '../mappers'; -import { NodeResponseModel } from '../models'; -import { - PaginatedViewOnlyLinksModel, - ViewOnlyLinkJsonApi, - ViewOnlyLinksResponseJsonApi, -} from '../models/view-only-links'; - -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class RegistrationViewOnlyLinksService { - private readonly jsonApiService = inject(JsonApiService); - - getResourceById(resourceId: string): Observable { - return this.jsonApiService.get(`${environment.apiUrl}/registrations/${resourceId}`); - } - - getViewOnlyLinksData(resourceId: string): Observable { - const params: Record = { embed: 'creator' }; - - return this.jsonApiService - .get(`${environment.apiUrl}/registrations/${resourceId}/view_only_links`, params) - .pipe(map((response) => ViewOnlyLinksMapper.fromResponse(response, resourceId))); - } - - createViewOnlyLink(resourceId: string, payload: ViewOnlyLinkJsonApi): Observable { - const data = { data: { ...payload } }; - const params: Record = { embed: 'creator' }; - - return this.jsonApiService - .post< - JsonApiResponse - >(`${environment.apiUrl}/registrations/${resourceId}/view_only_links/`, data, params) - .pipe(map((response) => ViewOnlyLinksMapper.fromSingleResponse(response.data, resourceId))); - } - - deleteLink(resourceId: string, linkId: string): Observable { - return this.jsonApiService.delete(`${environment.apiUrl}/registrations/${resourceId}/view_only_links/${linkId}`); - } -} diff --git a/src/app/shared/services/view-only-links.service.ts b/src/app/shared/services/view-only-links.service.ts new file mode 100644 index 000000000..acc452d9d --- /dev/null +++ b/src/app/shared/services/view-only-links.service.ts @@ -0,0 +1,64 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponse } from '@core/models'; +import { JsonApiService } from '@core/services'; + +import { ResourceType } from '../enums'; +import { ViewOnlyLinksMapper } from '../mappers'; +import { NodeResponseModel } from '../models'; +import { + PaginatedViewOnlyLinksModel, + ViewOnlyLinkJsonApi, + ViewOnlyLinksResponseJsonApi, +} from '../models/view-only-links'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class ViewOnlyLinksService { + private readonly jsonApiService = inject(JsonApiService); + + private readonly urlMap = new Map([ + [ResourceType.Project, 'nodes'], + [ResourceType.Registration, 'registrations'], + ]); + + getResourceById(resourceId: string, resourceType: ResourceType): Observable { + const resourcePath = this.urlMap.get(resourceType); + return this.jsonApiService.get(`${environment.apiUrl}/${resourcePath}/${resourceId}`); + } + + getViewOnlyLinksData(projectId: string, resourceType: ResourceType): Observable { + const resourcePath = this.urlMap.get(resourceType); + const params: Record = { embed: 'creator' }; + + return this.jsonApiService + .get(`${environment.apiUrl}/${resourcePath}/${projectId}/view_only_links`, params) + .pipe(map((response) => ViewOnlyLinksMapper.fromResponse(response, projectId))); + } + + createViewOnlyLink( + projectId: string, + resourceType: ResourceType, + payload: ViewOnlyLinkJsonApi + ): Observable { + const resourcePath = this.urlMap.get(resourceType); + const data = { data: { ...payload } }; + const params: Record = { embed: 'creator' }; + + return this.jsonApiService + .post< + JsonApiResponse + >(`${environment.apiUrl}/${resourcePath}/${projectId}/view_only_links/`, data, params) + .pipe(map((response) => ViewOnlyLinksMapper.fromSingleResponse(response.data, projectId))); + } + + deleteLink(projectId: string, resourceType: ResourceType, linkId: string): Observable { + const resourcePath = this.urlMap.get(resourceType); + return this.jsonApiService.delete(`${environment.apiUrl}/${resourcePath}/${projectId}/view_only_links/${linkId}`); + } +} diff --git a/src/app/shared/stores/contributors/contributors.actions.ts b/src/app/shared/stores/contributors/contributors.actions.ts index 2bf9bcf60..b769c148e 100644 --- a/src/app/shared/stores/contributors/contributors.actions.ts +++ b/src/app/shared/stores/contributors/contributors.actions.ts @@ -1,9 +1,13 @@ +import { ResourceType } from '@osf/shared/enums'; import { ContributorAddModel, ContributorModel } from '@osf/shared/models'; export class GetAllContributors { static readonly type = '[Contributors] Get All Contributors'; - constructor(public projectId: string | undefined | null) {} + constructor( + public resourceId: string | undefined | null, + public resourceType: ResourceType | undefined + ) {} } export class UpdateSearchValue { @@ -28,7 +32,8 @@ export class AddContributor { static readonly type = '[Contributors] Add Contributor'; constructor( - public projectId: string | undefined | null, + public resourceId: string | undefined | null, + public resourceType: ResourceType | undefined, public contributor: ContributorAddModel ) {} } @@ -37,7 +42,8 @@ export class UpdateContributor { static readonly type = '[Contributors] Update Contributor'; constructor( - public projectId: string | undefined | null, + public resourceId: string | undefined | null, + public resourceType: ResourceType | undefined, public contributor: ContributorModel ) {} } @@ -46,7 +52,8 @@ export class DeleteContributor { static readonly type = '[Contributors] Delete Contributor'; constructor( - public projectId: string | undefined | null, + public resourceId: string | undefined | null, + public resourceType: ResourceType | undefined, public contributorId: string ) {} } diff --git a/src/app/shared/stores/contributors/contributors.state.ts b/src/app/shared/stores/contributors/contributors.state.ts index 79f0b5982..f57b60e03 100644 --- a/src/app/shared/stores/contributors/contributors.state.ts +++ b/src/app/shared/stores/contributors/contributors.state.ts @@ -4,7 +4,7 @@ import { catchError, of, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { CONTRIBUTORS_SERVICE } from '@osf/shared/tokens'; +import { ContributorsService } from '@osf/shared/services'; import { AddContributor, @@ -40,7 +40,7 @@ import { ContributorsStateModel } from './contributors.model'; }) @Injectable() export class ContributorsState { - private readonly contributorsService = inject(CONTRIBUTORS_SERVICE); + private readonly contributorsService = inject(ContributorsService); @Action(GetAllContributors) getAllContributors(ctx: StateContext, action: GetAllContributors) { @@ -50,11 +50,11 @@ export class ContributorsState { contributorsList: { ...state.contributorsList, isLoading: true, error: null }, }); - if (!action.projectId) { + if (!action.resourceId || !action.resourceType) { return; } - return this.contributorsService.getAllContributors(action.projectId).pipe( + return this.contributorsService.getAllContributors(action.resourceType, action.resourceId).pipe( tap((contributors) => { ctx.patchState({ contributorsList: { @@ -76,11 +76,11 @@ export class ContributorsState { contributorsList: { ...state.contributorsList, isLoading: true, error: null }, }); - if (!action.projectId) { + if (!action.resourceId || !action.resourceType) { return; } - return this.contributorsService.addContributor(action.projectId, action.contributor).pipe( + return this.contributorsService.addContributor(action.resourceType, action.resourceId, action.contributor).pipe( tap((contributor) => { const currentState = ctx.getState(); @@ -104,11 +104,11 @@ export class ContributorsState { contributorsList: { ...state.contributorsList, isLoading: true, error: null }, }); - if (!action.projectId) { + if (!action.resourceId || !action.resourceType) { return; } - return this.contributorsService.updateContributor(action.projectId, action.contributor).pipe( + return this.contributorsService.updateContributor(action.resourceType, action.resourceId, action.contributor).pipe( tap((updatedContributor) => { const currentState = ctx.getState(); @@ -134,22 +134,24 @@ export class ContributorsState { contributorsList: { ...state.contributorsList, isLoading: true, error: null }, }); - if (!action.projectId) { + if (!action.resourceId || !action.resourceType) { return; } - return this.contributorsService.deleteContributor(action.projectId, action.contributorId).pipe( - tap(() => { - ctx.patchState({ - contributorsList: { - ...state.contributorsList, - data: state.contributorsList.data.filter((contributor) => contributor.userId !== action.contributorId), - isLoading: false, - }, - }); - }), - catchError((error) => this.handleError(ctx, 'contributorsList', error)) - ); + return this.contributorsService + .deleteContributor(action.resourceType, action.resourceId, action.contributorId) + .pipe( + tap(() => { + ctx.patchState({ + contributorsList: { + ...state.contributorsList, + data: state.contributorsList.data.filter((contributor) => contributor.userId !== action.contributorId), + isLoading: false, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'contributorsList', error)) + ); } @Action(UpdateSearchValue) diff --git a/src/app/shared/stores/view-only-links/view-only-link.actions.ts b/src/app/shared/stores/view-only-links/view-only-link.actions.ts index ff677fced..a9e3004d4 100644 --- a/src/app/shared/stores/view-only-links/view-only-link.actions.ts +++ b/src/app/shared/stores/view-only-links/view-only-link.actions.ts @@ -1,22 +1,30 @@ +import { ResourceType } from '@osf/shared/enums'; import { ViewOnlyLinkJsonApi } from '@osf/shared/models'; export class GetResourceDetails { static readonly type = '[Project] Get Resource Details'; - constructor(public projectId: string) {} + constructor( + public resourceId: string, + public resourceType: ResourceType | undefined + ) {} } export class FetchViewOnlyLinks { static readonly type = '[Link] Fetch View Only Links'; - constructor(public projectId: string) {} + constructor( + public resourceId: string, + public resourceType: ResourceType | undefined + ) {} } export class CreateViewOnlyLink { static readonly type = '[Link] Create View Only Links'; constructor( - public projectId: string, + public resourceId: string, + public resourceType: ResourceType | undefined, public payload: ViewOnlyLinkJsonApi ) {} } @@ -25,7 +33,8 @@ export class DeleteViewOnlyLink { static readonly type = '[Link] Delete View Only Links'; constructor( - public projectId: string, + public resourceId: string, + public resourceType: ResourceType | undefined, public linkId: string ) {} } diff --git a/src/app/shared/stores/view-only-links/view-only-link.state.ts b/src/app/shared/stores/view-only-links/view-only-link.state.ts index 6316bca13..2e6735971 100644 --- a/src/app/shared/stores/view-only-links/view-only-link.state.ts +++ b/src/app/shared/stores/view-only-links/view-only-link.state.ts @@ -6,7 +6,7 @@ import { catchError, tap } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; -import { VIEW_ONLY_LINKS_SERVICE } from '@osf/shared/tokens'; +import { ViewOnlyLinksService } from '@osf/shared/services'; import { NodeData, PaginatedViewOnlyLinksModel } from '@shared/models'; import { @@ -34,7 +34,7 @@ import { ViewOnlyLinkStateModel } from './view-only-link.model'; }) @Injectable() export class ViewOnlyLinkState { - private readonly viewOnlyLinksService = inject(VIEW_ONLY_LINKS_SERVICE); + private readonly viewOnlyLinksService = inject(ViewOnlyLinksService); @Action(GetResourceDetails) getResourceDetails(ctx: StateContext, action: GetResourceDetails) { @@ -44,7 +44,11 @@ export class ViewOnlyLinkState { resourceDetails: { ...state.resourceDetails, isLoading: true, error: null }, }); - return this.viewOnlyLinksService.getResourceById(action.projectId).pipe( + if (!action.resourceType) { + return; + } + + return this.viewOnlyLinksService.getResourceById(action.resourceId, action.resourceType).pipe( map((response) => response?.data as NodeData), tap((details) => { const updatedDetails = { @@ -72,7 +76,11 @@ export class ViewOnlyLinkState { viewOnlyLinks: { ...state.viewOnlyLinks, isLoading: true, error: null }, }); - return this.viewOnlyLinksService.getViewOnlyLinksData(action.projectId).pipe( + if (!action.resourceType) { + return; + } + + return this.viewOnlyLinksService.getViewOnlyLinksData(action.resourceId, action.resourceType).pipe( map((response) => response), tap((links) => { ctx.patchState({ @@ -95,7 +103,11 @@ export class ViewOnlyLinkState { viewOnlyLinks: { ...state.viewOnlyLinks, isLoading: true, error: null }, }); - return this.viewOnlyLinksService.createViewOnlyLink(action.projectId, action.payload).pipe( + if (!action.resourceType) { + return; + } + + return this.viewOnlyLinksService.createViewOnlyLink(action.resourceId, action.resourceType, action.payload).pipe( tap((data: PaginatedViewOnlyLinksModel) => { ctx.patchState({ viewOnlyLinks: { @@ -120,7 +132,11 @@ export class ViewOnlyLinkState { viewOnlyLinks: { ...state.viewOnlyLinks, isLoading: true, error: null }, }); - return this.viewOnlyLinksService.deleteLink(action.projectId, action.linkId).pipe( + if (!action.resourceType) { + return; + } + + return this.viewOnlyLinksService.deleteLink(action.resourceId, action.resourceType, action.linkId).pipe( tap(() => { ctx.setState( patch({ diff --git a/src/app/shared/tokens/analytics.token.ts b/src/app/shared/tokens/analytics.token.ts deleted file mode 100644 index c7ba59851..000000000 --- a/src/app/shared/tokens/analytics.token.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -import { IAnalyticsService } from '@osf/shared/models'; - -export const ANALYTICS_SERVICE = new InjectionToken('ANALYTICS_SERVICE'); diff --git a/src/app/shared/tokens/contributors.token.ts b/src/app/shared/tokens/contributors.token.ts deleted file mode 100644 index f8026988c..000000000 --- a/src/app/shared/tokens/contributors.token.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -import { IContributorsService } from '@osf/shared/models'; - -export const CONTRIBUTORS_SERVICE = new InjectionToken('CONTRIBUTORS_SERVICE'); diff --git a/src/app/shared/tokens/index.ts b/src/app/shared/tokens/index.ts index 02405922b..82d99a590 100644 --- a/src/app/shared/tokens/index.ts +++ b/src/app/shared/tokens/index.ts @@ -1,4 +1 @@ -export * from './analytics.token'; -export * from './contributors.token'; export * from './subjects.token'; -export * from './view-only-links.token'; diff --git a/src/app/shared/tokens/view-only-links.token.ts b/src/app/shared/tokens/view-only-links.token.ts deleted file mode 100644 index 517cc1543..000000000 --- a/src/app/shared/tokens/view-only-links.token.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -import { IViewOnlyLinksService } from '@osf/shared/models'; - -export const VIEW_ONLY_LINKS_SERVICE = new InjectionToken('VIEW_ONLY_LINKS_SERVICE'); From ce4a51e2ca2bf6443dea5c202fa9f0a80d189539 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 7 Jul 2025 17:39:36 +0300 Subject: [PATCH 7/9] feat(contributors): updated routes --- src/app/app.routes.ts | 101 +++++++----------- .../nav-menu/nav-menu.component.html | 6 +- .../components/nav-menu/nav-menu.component.ts | 11 +- .../request-access.component.ts | 4 +- src/app/core/constants/nav-items.constant.ts | 17 +-- src/app/features/meetings/meetings.routes.ts | 27 +++++ .../features/registries/registries.routes.ts | 21 ---- .../registry-overview.component.ts | 29 ++--- src/app/features/registry/registry.routes.ts | 25 ++++- .../registry-overview.selectors.ts | 7 +- .../registry-overview.state.ts | 6 +- .../developer-app-details.component.html | 4 +- .../data-resources.component.html | 8 +- .../resource-card/resource-card.component.ts | 2 +- src/assets/i18n/en.json | 1 + 15 files changed, 128 insertions(+), 141 deletions(-) create mode 100644 src/app/features/meetings/meetings.routes.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 0899c6cf8..c87d8166f 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -2,8 +2,6 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; -import { RegistryOverviewState } from '@osf/features/registry/store/registry-overview'; - import { MyProfileResourceFiltersOptionsState } from './features/my-profile/components/filters/store'; import { MyProfileResourceFiltersState } from './features/my-profile/components/my-profile-resource-filters/store'; import { MyProfileState } from './features/my-profile/store'; @@ -22,53 +20,44 @@ export const routes: Routes = [ redirectTo: 'home', }, { - path: 'sign-up', - loadComponent: () => - import('./features/auth/pages/sign-up/sign-up.component').then((mod) => mod.SignUpComponent), - data: { skipBreadcrumbs: true }, - }, - { - path: 'forgot-password', - loadComponent: () => - import('./features/auth/pages/forgot-password/forgot-password.component').then( - (mod) => mod.ForgotPasswordComponent - ), + path: 'home', + loadComponent: () => import('./features/home/home.component').then((mod) => mod.HomeComponent), data: { skipBreadcrumbs: true }, }, { - path: 'reset-password', + path: 'home-logged-out', loadComponent: () => - import('./features/auth/pages/reset-password/reset-password.component').then( - (mod) => mod.ResetPasswordComponent + import('@osf/features/home/pages/home-logged-out/home-logged-out.component').then( + (mod) => mod.HomeLoggedOutComponent ), data: { skipBreadcrumbs: true }, }, { - path: 'home', + path: 'confirm/:userId/:token', loadComponent: () => import('./features/home/home.component').then((mod) => mod.HomeComponent), data: { skipBreadcrumbs: true }, }, { - path: 'home-logged-out', + path: 'sign-up', loadComponent: () => - import('@osf/features/home/pages/home-logged-out/home-logged-out.component').then( - (mod) => mod.HomeLoggedOutComponent - ), + import('./features/auth/pages/sign-up/sign-up.component').then((mod) => mod.SignUpComponent), data: { skipBreadcrumbs: true }, }, { - path: 'support', - loadComponent: () => import('./features/support/support.component').then((mod) => mod.SupportComponent), - }, - { - path: 'terms-of-use', + path: 'forgot-password', loadComponent: () => - import('./features/static/terms-of-use/terms-of-use.component').then((mod) => mod.TermsOfUseComponent), + import('./features/auth/pages/forgot-password/forgot-password.component').then( + (mod) => mod.ForgotPasswordComponent + ), + data: { skipBreadcrumbs: true }, }, { - path: 'privacy-policy', + path: 'reset-password', loadComponent: () => - import('./features/static/privacy-policy/privacy-policy.component').then((mod) => mod.PrivacyPolicyComponent), + import('./features/auth/pages/reset-password/reset-password.component').then( + (mod) => mod.ResetPasswordComponent + ), + data: { skipBreadcrumbs: true }, }, { path: 'collections', @@ -76,24 +65,7 @@ export const routes: Routes = [ }, { path: 'meetings', - loadComponent: () => import('./features/meetings/meetings.component').then((mod) => mod.MeetingsComponent), - children: [ - { - path: '', - pathMatch: 'full', - loadComponent: () => - import('@osf/features/meetings/pages/meetings-landing/meetings-landing.component').then( - (mod) => mod.MeetingsLandingComponent - ), - }, - { - path: ':id', - loadComponent: () => - import('@osf/features/meetings/pages/meeting-details/meeting-details.component').then( - (mod) => mod.MeetingDetailsComponent - ), - }, - ], + loadChildren: () => import('./features/meetings/meetings.routes').then((mod) => mod.meetingsRoutes), }, { path: 'my-projects', @@ -104,11 +76,6 @@ export const routes: Routes = [ path: 'my-projects/:id', loadChildren: () => import('./features/project/project.routes').then((mod) => mod.projectRoutes), }, - { - path: 'registries', - loadChildren: () => import('./features/registries/registries.routes').then((mod) => mod.registriesRoutes), - }, - { path: 'settings', loadChildren: () => import('./features/settings/settings.routes').then((mod) => mod.settingsRoutes), @@ -134,9 +101,22 @@ export const routes: Routes = [ loadChildren: () => import('./features/institutions/institutions.routes').then((r) => r.routes), }, { - path: 'confirm/:userId/:token', - loadComponent: () => import('./features/home/home.component').then((mod) => mod.HomeComponent), - data: { skipBreadcrumbs: true }, + path: 'registries', + loadChildren: () => import('./features/registries/registries.routes').then((mod) => mod.registriesRoutes), + }, + { + path: 'registries/:id', + loadChildren: () => import('./features/registry/registry.routes').then((mod) => mod.registryRoutes), + }, + { + path: 'terms-of-use', + loadComponent: () => + import('./features/static/terms-of-use/terms-of-use.component').then((mod) => mod.TermsOfUseComponent), + }, + { + path: 'privacy-policy', + loadComponent: () => + import('./features/static/privacy-policy/privacy-policy.component').then((mod) => mod.PrivacyPolicyComponent), }, { path: 'forbidden', @@ -145,20 +125,11 @@ export const routes: Routes = [ data: { skipBreadcrumbs: true }, }, { - path: 'request-access/:projectId', + path: 'request-access/:id', loadComponent: () => import('./core/components/request-access/request-access.component').then((mod) => mod.RequestAccessComponent), data: { skipBreadcrumbs: true }, }, - { - path: 'registries', - loadChildren: () => import('./features/registries/registries.routes').then((mod) => mod.registriesRoutes), - }, - { - path: 'registries/my-registrations/:registrationId', - loadChildren: () => import('./features/registry/registry.routes').then((mod) => mod.registryRoutes), - providers: [provideStates([RegistryOverviewState])], - }, { path: '**', loadComponent: () => diff --git a/src/app/core/components/nav-menu/nav-menu.component.html b/src/app/core/components/nav-menu/nav-menu.component.html index b6f8fd12c..981df4707 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.html +++ b/src/app/core/components/nav-menu/nav-menu.component.html @@ -24,12 +24,12 @@ }
- @if (item.label === 'navigation.myProjects' && isProjectRoute()) { + @if (item.label === 'navigation.myProjects' && !isRegistryRoute() && isProjectRoute()) {
this.currentRoute().projectId); - protected readonly isProjectRoute = computed(() => !!this.currentProjectId()); + protected readonly currentResourceId = computed(() => this.currentRoute().resourceId); + protected readonly isProjectRoute = computed(() => !!this.currentResourceId()); protected readonly isCollectionsRoute = computed(() => this.currentRoute().isCollectionsWithId); protected readonly isRegistryRoute = computed(() => this.currentRoute().isRegistryRoute); @@ -71,15 +71,14 @@ export class NavMenuComponent { const url = this.router.url; const urlSegments = url.split('/').filter((segment) => segment); - const projectId = this.route.firstChild?.snapshot.params['id'] || null; + const resourceId = this.route.firstChild?.snapshot.params['id'] || null; const section = this.route.firstChild?.firstChild?.snapshot.url[0]?.path || 'overview'; const isCollectionsWithId = urlSegments[0] === 'collections' && urlSegments[1] && urlSegments[1] !== ''; - const isRegistryRoute = - urlSegments[0] === 'registries' && urlSegments[1] === 'my-registrations' && !!urlSegments[2]; + const isRegistryRoute = urlSegments[0] === 'registries' && !!urlSegments[2]; return { - projectId, + resourceId, section, isCollectionsWithId, isRegistryRoute, diff --git a/src/app/core/components/request-access/request-access.component.ts b/src/app/core/components/request-access/request-access.component.ts index 0881a2fc9..2c8c10206 100644 --- a/src/app/core/components/request-access/request-access.component.ts +++ b/src/app/core/components/request-access/request-access.component.ts @@ -27,7 +27,7 @@ export class RequestAccessComponent { comment = model(''); private readonly route = inject(ActivatedRoute); - private readonly projectId = toSignal(this.route?.params.pipe(map((params) => params['projectId'])) ?? of(undefined)); + private readonly id = toSignal(this.route?.params.pipe(map((params) => params['id'])) ?? of(undefined)); private readonly requestAccessService = inject(RequestAccessService); private readonly router = inject(Router); @@ -36,7 +36,7 @@ export class RequestAccessComponent { requestAccess() { this.loaderService.show(); - this.requestAccessService.requestAccessToProject(this.projectId(), this.comment()).subscribe({ + this.requestAccessService.requestAccessToProject(this.id(), this.comment()).subscribe({ next: () => { this.loaderService.hide(); this.router.navigate(['/']); diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index aad780130..e8746fb5b 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -15,13 +15,6 @@ export const NAV_ITEMS: NavItem[] = [ icon: 'search', useExactMatch: true, }, - // [NS] TODO: Hidden until development - // { - // path: '/support', - // label: 'navigation.support', - // icon: 'support', - // useExactMatch: true, - // }, { path: '/my-projects', label: 'navigation.myProjects', @@ -83,7 +76,6 @@ export const NAV_ITEMS: NavItem[] = [ icon: 'institutions', useExactMatch: true, }, - { path: '/collections', label: 'navigation.collections', @@ -135,14 +127,6 @@ export const NAV_ITEMS: NavItem[] = [ }, ], }, - - //[NS] TODO: Hidden until development - // { - // path: '/donate', - // label: 'navigation.donate', - // icon: 'donate', - // useExactMatch: true, - // }, ]; export const PROJECT_MENU_ITEMS: MenuItem[] = [ @@ -188,6 +172,7 @@ export const REGISTRATION_MENU_ITEMS: MenuItem[] = [ { label: 'navigation.registration.resources', routerLink: 'resources' }, { label: 'navigation.registration.wiki', routerLink: 'wiki' }, { label: 'navigation.registration.components', routerLink: 'components' }, + { label: 'navigation.registration.contributors', routerLink: 'contributors' }, { label: 'navigation.registration.links', routerLink: 'links' }, { label: 'navigation.registration.analytics', routerLink: 'analytics' }, ], diff --git a/src/app/features/meetings/meetings.routes.ts b/src/app/features/meetings/meetings.routes.ts new file mode 100644 index 000000000..fd4a398d0 --- /dev/null +++ b/src/app/features/meetings/meetings.routes.ts @@ -0,0 +1,27 @@ +import { Routes } from '@angular/router'; + +import { MeetingsComponent } from './meetings.component'; + +export const meetingsRoutes: Routes = [ + { + path: '', + component: MeetingsComponent, + children: [ + { + path: '', + pathMatch: 'full', + loadComponent: () => + import('@osf/features/meetings/pages/meetings-landing/meetings-landing.component').then( + (mod) => mod.MeetingsLandingComponent + ), + }, + { + path: ':id', + loadComponent: () => + import('@osf/features/meetings/pages/meeting-details/meeting-details.component').then( + (mod) => mod.MeetingDetailsComponent + ), + }, + ], + }, +]; diff --git a/src/app/features/registries/registries.routes.ts b/src/app/features/registries/registries.routes.ts index 1d04accf5..a77a2d0e9 100644 --- a/src/app/features/registries/registries.routes.ts +++ b/src/app/features/registries/registries.routes.ts @@ -4,12 +4,10 @@ import { Routes } from '@angular/router'; import { RegistriesComponent } from '@osf/features/registries/registries.component'; import { RegistriesState } from '@osf/features/registries/store'; -import { ResourceType } from '@osf/shared/enums'; import { ContributorsState, SubjectsState } from '@osf/shared/stores'; import { SUBJECTS_SERVICE } from '@osf/shared/tokens'; import { ModerationState } from '../moderation/store'; -import { AnalyticsState } from '../project/analytics/store'; import { LicensesHandlers, ProjectsHandlers, ProvidersHandlers, SubjectsHandlers } from './store/handlers'; import { LicensesService, RegistrationSubjectsService } from './services'; @@ -76,25 +74,6 @@ export const registriesRoutes: Routes = [ }, ], }, - { - path: ':id', - children: [ - { - path: 'contributors', - loadComponent: () => - import('../project/contributors/contributors.component').then((mod) => mod.ContributorsComponent), - data: { resourceType: ResourceType.Registration }, - providers: [provideStates([ContributorsState])], - }, - { - path: 'analytics', - loadComponent: () => - import('../project/analytics/analytics.component').then((mod) => mod.AnalyticsComponent), - data: { resourceType: ResourceType.Registration }, - providers: [provideStates([AnalyticsState])], - }, - ], - }, ], }, ]; diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts index a27538908..3213c51fd 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts @@ -7,24 +7,25 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { GetBookmarksCollectionId } from '@osf/features/collections/store'; import { OverviewToolbarComponent } from '@osf/features/project/overview/components'; -import { RegistryRevisionsComponent, RegistryStatusesComponent } from '@osf/features/registry/components'; -import { MapViewSchemaBlock } from '@osf/features/registry/mappers'; -import { RegistrationQuestions } from '@osf/features/registry/models'; -import { - GetRegistryById, - GetRegistryInstitutions, - GetRegistrySubjects, - RegistryOverviewSelectors, -} from '@osf/features/registry/store/registry-overview'; import { DataResourcesComponent, LoadingSpinnerComponent, ResourceMetadataComponent, SubHeaderComponent, -} from '@shared/components'; -import { ResourceType } from '@shared/enums'; -import { MapRegistryOverview } from '@shared/mappers'; -import { ToolbarResource } from '@shared/models'; +} from '@osf/shared/components'; +import { ResourceType } from '@osf/shared/enums'; +import { MapRegistryOverview } from '@osf/shared/mappers'; +import { ToolbarResource } from '@osf/shared/models'; + +import { RegistryRevisionsComponent, RegistryStatusesComponent } from '../../components'; +import { MapViewSchemaBlock } from '../../mappers'; +import { RegistrationQuestions } from '../../models'; +import { + GetRegistryById, + GetRegistryInstitutions, + GetRegistrySubjects, + RegistryOverviewSelectors, +} from '../../store/registry-overview'; @Component({ selector: 'osf-registry-overview', @@ -107,7 +108,7 @@ export class RegistryOverviewComponent { constructor() { this.route.parent?.params.subscribe((params) => { - const id = params['registrationId']; + const id = params['id']; if (id) { this.actions.getRegistryById(id); this.actions.getSubjects(id); diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index 68ac8e1bb..574fa893d 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -1,5 +1,13 @@ +import { provideStates } from '@ngxs/store'; + import { Routes } from '@angular/router'; +import { ResourceType } from '@osf/shared/enums'; +import { ContributorsState, ViewOnlyLinkState } from '@osf/shared/stores'; + +import { AnalyticsState } from '../project/analytics/store'; + +import { RegistryOverviewState } from './store/registry-overview'; import { RegistryComponent } from './registry.component'; export const registryRoutes: Routes = [ @@ -14,7 +22,22 @@ export const registryRoutes: Routes = [ }, { path: 'overview', - loadComponent: () => import('@osf/features/registry/pages').then((c) => c.RegistryOverviewComponent), + loadComponent: () => + import('./pages/registry-overview/registry-overview.component').then((c) => c.RegistryOverviewComponent), + providers: [provideStates([RegistryOverviewState])], + }, + { + path: 'contributors', + loadComponent: () => + import('../project/contributors/contributors.component').then((mod) => mod.ContributorsComponent), + data: { resourceType: ResourceType.Registration }, + providers: [provideStates([ContributorsState, ViewOnlyLinkState])], + }, + { + path: 'analytics', + loadComponent: () => import('../project/analytics/analytics.component').then((mod) => mod.AnalyticsComponent), + data: { resourceType: ResourceType.Registration }, + providers: [provideStates([AnalyticsState])], }, ], }, diff --git a/src/app/features/registry/store/registry-overview/registry-overview.selectors.ts b/src/app/features/registry/store/registry-overview/registry-overview.selectors.ts index 0d7798449..bfac7b37c 100644 --- a/src/app/features/registry/store/registry-overview/registry-overview.selectors.ts +++ b/src/app/features/registry/store/registry-overview/registry-overview.selectors.ts @@ -1,8 +1,9 @@ import { Selector } from '@ngxs/store'; -import { RegistryInstitution, RegistryOverview, RegistrySubject } from '@osf/features/registry/models'; -import { RegistrySchemaBlock } from '@osf/features/registry/models/registry-schema-block.model'; -import { RegistryOverviewState, RegistryOverviewStateModel } from '@osf/features/registry/store/registry-overview'; +import { RegistryInstitution, RegistryOverview, RegistrySchemaBlock, RegistrySubject } from '../../models'; + +import { RegistryOverviewStateModel } from './registry-overview.model'; +import { RegistryOverviewState } from './registry-overview.state'; export class RegistryOverviewSelectors { @Selector([RegistryOverviewState]) diff --git a/src/app/features/registry/store/registry-overview/registry-overview.state.ts b/src/app/features/registry/store/registry-overview/registry-overview.state.ts index 92678450d..9f0c47952 100644 --- a/src/app/features/registry/store/registry-overview/registry-overview.state.ts +++ b/src/app/features/registry/store/registry-overview/registry-overview.state.ts @@ -5,7 +5,8 @@ import { catchError } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; -import { RegistryOverviewService } from '@osf/features/registry/services'; +import { RegistryOverviewService } from '../../services'; + import { GetRegistryById, GetRegistryInstitutions, @@ -13,8 +14,7 @@ import { GetSchemaBlocks, MakePublic, WithdrawRegistration, -} from '@osf/features/registry/store/registry-overview'; - +} from './registry-overview.actions'; import { RegistryOverviewStateModel } from './registry-overview.model'; @Injectable() diff --git a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.html b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.html index 746951d9a..c74d99e72 100644 --- a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.html +++ b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.html @@ -2,8 +2,8 @@ diff --git a/src/app/shared/components/data-resources/data-resources.component.html b/src/app/shared/components/data-resources/data-resources.component.html index 08df6fdea..ec31469bb 100644 --- a/src/app/shared/components/data-resources/data-resources.component.html +++ b/src/app/shared/components/data-resources/data-resources.component.html @@ -7,7 +7,7 @@ }

{{ 'resourceCard.resources.data' | translate }}

- + @if (hasAnalyticCode()) { code-resource } @else { @@ -15,7 +15,7 @@ }

{{ 'resourceCard.resources.analyticCode' | translate }}

- + @if (hasMaterials()) { materials-resource } @else { @@ -23,7 +23,7 @@ }

{{ 'resourceCard.resources.materials' | translate }}

- + @if (hasPapers()) { papers-resource } @else { @@ -31,7 +31,7 @@ }

{{ 'resourceCard.resources.papers' | translate }}

- + @if (hasSupplements()) { supplements-resource } @else { diff --git a/src/app/shared/components/resource-card/resource-card.component.ts b/src/app/shared/components/resource-card/resource-card.component.ts index e867f5726..1815c8af7 100644 --- a/src/app/shared/components/resource-card/resource-card.component.ts +++ b/src/app/shared/components/resource-card/resource-card.component.ts @@ -80,7 +80,7 @@ export class ResourceCardComponent { if (item.resourceType === ResourceType.Registration) { const parts = item.id.split('/'); const uri = parts[parts.length - 1]; - this.router.navigate(['/registries/my-registrations', uri]); + this.router.navigate(['/registries', uri]); } } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index bf9c2ae84..4b49b8d7b 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -137,6 +137,7 @@ "resources": "Resources", "wiki": "Wiki", "components": "Components", + "contributors": "Contributors", "links": "Links", "analytics": "Analytics" }, From 989d20ff4a572603863cd19d0c7fd684f5dd3bc3 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 7 Jul 2025 18:48:42 +0300 Subject: [PATCH 8/9] feat(contributors): updated nav menu --- .../nav-menu/nav-menu.component.html | 8 +- .../components/nav-menu/nav-menu.component.ts | 33 +----- src/app/core/constants/nav-items.constant.ts | 109 ++++++++---------- 3 files changed, 59 insertions(+), 91 deletions(-) diff --git a/src/app/core/components/nav-menu/nav-menu.component.html b/src/app/core/components/nav-menu/nav-menu.component.html index 981df4707..558eabef3 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.html +++ b/src/app/core/components/nav-menu/nav-menu.component.html @@ -1,13 +1,11 @@