diff --git a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.html b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.html index b0b2971b5..9fdbc7573 100644 --- a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.html +++ b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.html @@ -7,13 +7,15 @@

{{ 'project.metadata.addMetadata.publishedText' | translate } @else {

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

} - - + @if (showEditButton()) { + + + } } diff --git a/src/app/features/metadata/components/metadata-license/metadata-license.component.html b/src/app/features/metadata/components/metadata-license/metadata-license.component.html index ec313d5de..092b81ff8 100644 --- a/src/app/features/metadata/components/metadata-license/metadata-license.component.html +++ b/src/app/features/metadata/components/metadata-license/metadata-license.component.html @@ -2,7 +2,7 @@

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

- @if (!hideEditLicense()) { + @if (!readonly()) { (); - hideEditLicense = input(false); + readonly = input(false); license = input(null); } diff --git a/src/app/features/metadata/components/metadata-tags/metadata-tags.component.html b/src/app/features/metadata/components/metadata-tags/metadata-tags.component.html index 730cab374..24aced840 100644 --- a/src/app/features/metadata/components/metadata-tags/metadata-tags.component.html +++ b/src/app/features/metadata/components/metadata-tags/metadata-tags.component.html @@ -2,6 +2,19 @@

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

- + @if (readonly()) { +
+ @for (tag of tags(); track tag) { + + } +
+ } @else { + + }
diff --git a/src/app/features/metadata/components/metadata-tags/metadata-tags.component.ts b/src/app/features/metadata/components/metadata-tags/metadata-tags.component.ts index 512a77c9e..33df80c93 100644 --- a/src/app/features/metadata/components/metadata-tags/metadata-tags.component.ts +++ b/src/app/features/metadata/components/metadata-tags/metadata-tags.component.ts @@ -1,19 +1,29 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Card } from 'primeng/card'; +import { Tag } from 'primeng/tag'; -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core'; +import { Router } from '@angular/router'; import { TagsInputComponent } from '@osf/shared/components/tags-input/tags-input.component'; @Component({ selector: 'osf-metadata-tags', - imports: [Card, TranslatePipe, TagsInputComponent], + imports: [Card, TranslatePipe, TagsInputComponent, Tag], templateUrl: './metadata-tags.component.html', styleUrl: './metadata-tags.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class MetadataTagsComponent { tags = input([]); + readonly = input(false); + tagsChanged = output(); + + private router = inject(Router); + + tagClicked(tag: string) { + this.router.navigate(['/search'], { queryParams: { search: tag } }); + } } diff --git a/src/app/features/metadata/mappers/metadata.mapper.ts b/src/app/features/metadata/mappers/metadata.mapper.ts index 23e7af32a..b4fbf9111 100644 --- a/src/app/features/metadata/mappers/metadata.mapper.ts +++ b/src/app/features/metadata/mappers/metadata.mapper.ts @@ -27,6 +27,7 @@ export class MetadataMapper { })), provider: response.embeds?.provider?.data.id, public: response.attributes.public, + currentUserPermissions: response.attributes.current_user_permissions, }; } diff --git a/src/app/features/metadata/metadata.component.html b/src/app/features/metadata/metadata.component.html index becf23494..55e98a3ee 100644 --- a/src/app/features/metadata/metadata.component.html +++ b/src/app/features/metadata/metadata.component.html @@ -1,6 +1,6 @@
- + @@ -32,31 +38,39 @@
- + } - +
diff --git a/src/app/features/metadata/metadata.component.ts b/src/app/features/metadata/metadata.component.ts index 662c7f80a..05439abfd 100644 --- a/src/app/features/metadata/metadata.component.ts +++ b/src/app/features/metadata/metadata.component.ts @@ -21,7 +21,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { MetadataTabsComponent, SubHeaderComponent } from '@osf/shared/components'; -import { MetadataResourceEnum, ResourceType } from '@osf/shared/enums'; +import { MetadataResourceEnum, ResourceType, UserPermissions } from '@osf/shared/enums'; import { IS_MEDIUM } from '@osf/shared/helpers'; import { MetadataTabsModel, SubjectModel } from '@osf/shared/models'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; @@ -191,6 +191,18 @@ export class MetadataComponent implements OnInit { bibliographicContributors = computed(() => this.contributors().filter((contributor) => contributor.isBibliographic)); + hasWriteAccess = computed(() => { + const metadata = this.metadata(); + if (!metadata) return false; + return metadata.currentUserPermissions.includes(UserPermissions.Write); + }); + + hasAdminAccess = computed(() => { + const metadata = this.metadata(); + if (!metadata) return false; + return metadata.currentUserPermissions.includes(UserPermissions.Admin); + }); + constructor() { effect(() => { const records = this.cedarRecords(); diff --git a/src/app/features/metadata/models/metadata-json-api.model.ts b/src/app/features/metadata/models/metadata-json-api.model.ts index 0f8c235bd..ab1497b9e 100644 --- a/src/app/features/metadata/models/metadata-json-api.model.ts +++ b/src/app/features/metadata/models/metadata-json-api.model.ts @@ -1,4 +1,5 @@ import { ApiData, InstitutionsJsonApiResponse, LicenseDataJsonApi, LicenseRecordJsonApi } from '@osf/shared/models'; +import { UserPermissions } from '@shared/enums'; export interface MetadataJsonApiResponse { data: MetadataJsonApi; @@ -17,6 +18,7 @@ export interface MetadataAttributesJsonApi { category?: string; node_license?: LicenseRecordJsonApi; public?: boolean; + current_user_permissions: UserPermissions[]; } interface MetadataEmbedsJsonApi { diff --git a/src/app/features/metadata/models/metadata.model.ts b/src/app/features/metadata/models/metadata.model.ts index e7ad0f8cb..98b015c89 100644 --- a/src/app/features/metadata/models/metadata.model.ts +++ b/src/app/features/metadata/models/metadata.model.ts @@ -1,4 +1,5 @@ import { Identifier, Institution, LicenseModel } from '@osf/shared/models'; +import { UserPermissions } from '@shared/enums'; export interface Metadata { id: string; @@ -20,6 +21,7 @@ export interface Metadata { year: string; }; public?: boolean; + currentUserPermissions: UserPermissions[]; } export interface CustomItemMetadataRecord { diff --git a/src/app/features/project/overview/project-overview.component.html b/src/app/features/project/overview/project-overview.component.html index 8d62be1f7..238841f18 100644 --- a/src/app/features/project/overview/project-overview.component.html +++ b/src/app/features/project/overview/project-overview.component.html @@ -17,7 +17,7 @@ [isCollectionsRoute]="isCollectionsRoute()" [currentResource]="currentResource()" [projectDescription]="project.description" - [canEdit]="isAdmin" + [canEdit]="isAdmin()" />
@@ -47,7 +47,7 @@ }
- @if (isAdmin || canWrite) { + @if (canWrite()) { } @@ -57,8 +57,8 @@ [areComponentsLoading]="areComponentsLoading()" /> - - + +
@@ -67,7 +67,7 @@ [currentResource]="resourceOverview()" (customCitationUpdated)="onCustomCitationUpdated($event)" [isCollectionsRoute]="isCollectionsRoute()" - [canEdit]="canWrite" + [canEdit]="canWrite()" /> diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index b769f0168..b62da3d7f 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -175,13 +175,13 @@ export class ProjectOverviewComponent implements OnInit { userPermissions = computed(() => this.currentProject()?.currentUserPermissions || []); hasViewOnly = computed(() => hasViewOnlyParam(this.router)); - get isAdmin(): boolean { + isAdmin = computed(() => { return this.userPermissions().includes(UserPermissions.Admin); - } + }); - get canWrite(): boolean { + canWrite = computed(() => { return this.userPermissions().includes(UserPermissions.Write); - } + }); resourceOverview = computed(() => { const project = this.currentProject(); diff --git a/src/app/features/registry/components/registry-revisions/registry-revisions.component.html b/src/app/features/registry/components/registry-revisions/registry-revisions.component.html index 7fd1d2b1a..ad7a78930 100644 --- a/src/app/features/registry/components/registry-revisions/registry-revisions.component.html +++ b/src/app/features/registry/components/registry-revisions/registry-revisions.component.html @@ -18,7 +18,7 @@ /> } - @if (!readonly()) { + @if (canEdit()) { @if (registryAcceptedUnapproved) { (); isSubmitting = input(false); isModeration = input(false); - readonly = input(false); + canEdit = input(false); openRevision = output(); readonly updateRegistration = output(); diff --git a/src/app/features/registry/components/registry-statuses/registry-statuses.component.html b/src/app/features/registry/components/registry-statuses/registry-statuses.component.html index 2fe915bd8..2e43324b4 100644 --- a/src/app/features/registry/components/registry-statuses/registry-statuses.component.html +++ b/src/app/features/registry/components/registry-statuses/registry-statuses.component.html @@ -12,8 +12,8 @@

{{ 'registry.overview.statuses.' + registry()?.status + 'registry.overview.statuses.' + registry()?.status + '.long' | translate: { embargoEndDate: embargoEndDate } }}

- @if (!readonly()) { - @if (canWithdraw) { + @if (canEdit()) { + @if (canWithdraw()) { {{ 'registry.overview.statuses.' + registry()?.status + styleClass="w-full mt-2" > } - @if (registry()?.status === RegistryStatus.Embargo) { + @if (isEmbargo()) { (); - readonly = input(false); + canEdit = input(false); isModeration = input(false); readonly RegistryStatus = RegistryStatus; @@ -36,13 +36,13 @@ export class RegistryStatusesComponent { readonly customConfirmationService = inject(CustomConfirmationService); readonly actions = createDispatchMap({ makePublic: MakePublic }); - get canWithdraw(): boolean { + canWithdraw = computed(() => { return this.registry()?.reviewsState === RegistrationReviewStates.Accepted && !this.isModeration(); - } + }); - get isEmbargo(): boolean { + isEmbargo = computed(() => { return this.registry()?.status === RegistryStatus.Embargo; - } + }); get embargoEndDate() { const embargoEndDate = this.registry()?.embargoEndDate; diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.html b/src/app/features/registry/pages/registry-overview/registry-overview.component.html index a5b0867c6..d0ec326a6 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.html +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.html @@ -17,7 +17,7 @@ } @@ -32,7 +32,7 @@ } @else {
- @if (!schemaResponse()?.isOriginalResponse && !isInitialState && !hasViewOnly()) { + @if (!schemaResponse()?.isOriginalResponse && !isInitialState) {
@@ -70,7 +70,7 @@

@@ -82,7 +82,7 @@

(continueUpdate)="onContinueUpdateRegistration()" [isModeration]="isModeration" [isSubmitting]="isSchemaResponseLoading()" - [readonly]="hasNoPermissions() || hasViewOnly()" + [canEdit]="hasAdminAccess()" >

@@ -134,7 +134,7 @@

{{ section.title }}

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 92a9c56e9..bfe6e1654 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 @@ -31,7 +31,7 @@ import { SubHeaderComponent, ViewOnlyLinkMessageComponent, } from '@osf/shared/components'; -import { RegistrationReviewStates, ResourceType, RevisionReviewStates } from '@osf/shared/enums'; +import { RegistrationReviewStates, ResourceType, RevisionReviewStates, UserPermissions } from '@osf/shared/enums'; import { hasViewOnlyParam, toCamelCase } from '@osf/shared/helpers'; import { MapRegistryOverview } from '@osf/shared/mappers'; import { SchemaResponse, ToolbarResource } from '@osf/shared/models'; @@ -194,9 +194,17 @@ export class RegistryOverviewComponent { revisionId: string | null = null; isModeration = false; - userPermissions = computed(() => this.registry()?.currentUserPermissions || []); hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + canEdit = computed(() => { + const registry = this.registry(); + if (!registry) return false; + return ( + registry.currentUserPermissions.includes(UserPermissions.Admin) || + registry.currentUserPermissions.includes(UserPermissions.Write) + ); + }); + get isInitialState(): boolean { return this.registry()?.reviewsState === RegistrationReviewStates.Initial; } diff --git a/src/app/features/registry/pages/registry-resources/registry-resources.component.html b/src/app/features/registry/pages/registry-resources/registry-resources.component.html index 87516d8c7..d03ab0cab 100644 --- a/src/app/features/registry/pages/registry-resources/registry-resources.component.html +++ b/src/app/features/registry/pages/registry-resources/registry-resources.component.html @@ -1,6 +1,6 @@ {{ resourceName }}

-
- + @if (canEdit()) { +
+ - -
+ +
+ } } diff --git a/src/app/features/registry/pages/registry-resources/registry-resources.component.ts b/src/app/features/registry/pages/registry-resources/registry-resources.component.ts index ad5392f15..31373db97 100644 --- a/src/app/features/registry/pages/registry-resources/registry-resources.component.ts +++ b/src/app/features/registry/pages/registry-resources/registry-resources.component.ts @@ -7,12 +7,14 @@ import { DialogService } from 'primeng/dynamicdialog'; import { filter, finalize, switchMap, take } from 'rxjs'; -import { ChangeDetectionStrategy, Component, DestroyRef, HostBinding, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, DestroyRef, HostBinding, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute } from '@angular/router'; +import { GetResourceMetadata, MetadataSelectors } from '@osf/features/metadata/store'; import { IconComponent, LoadingSpinnerComponent, SubHeaderComponent } from '@osf/shared/components'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; +import { ResourceType, UserPermissions } from '@shared/enums'; import { AddResourceDialogComponent, EditResourceDialogComponent } from '../../components'; import { RegistryResource } from '../../models'; @@ -43,24 +45,33 @@ export class RegistryResourcesComponent { readonly resources = select(RegistryResourcesSelectors.getResources); readonly isResourcesLoading = select(RegistryResourcesSelectors.isResourcesLoading); readonly currentResource = select(RegistryResourcesSelectors.getCurrentResource); + readonly registry = select(MetadataSelectors.getResourceMetadata); - registryId = ''; + registryId = this.route.snapshot.parent?.params['id']; isAddingResource = signal(false); doiDomain = 'https://doi.org/'; private readonly actions = createDispatchMap({ + fetchRegistryData: GetResourceMetadata, getResources: GetRegistryResources, addResource: AddRegistryResource, deleteResource: DeleteResource, }); + canEdit = computed(() => { + const registry = this.registry(); + if (!registry) return false; + + return registry.currentUserPermissions.includes(UserPermissions.Write); + }); + + addButtonVisible = computed(() => { + return !!this.registry()?.identifiers?.length && this.canEdit(); + }); + constructor() { - this.route.parent?.params.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => { - this.registryId = params['id']; - if (this.registryId) { - this.actions.getResources(this.registryId); - } - }); + this.actions.fetchRegistryData(this.registryId, ResourceType.Registration); + this.actions.getResources(this.registryId); } addResource() { diff --git a/src/app/shared/components/metadata-tabs/metadata-tabs.component.html b/src/app/shared/components/metadata-tabs/metadata-tabs.component.html index ab4d6984d..ac20b7f8f 100644 --- a/src/app/shared/components/metadata-tabs/metadata-tabs.component.html +++ b/src/app/shared/components/metadata-tabs/metadata-tabs.component.html @@ -21,7 +21,7 @@ [template]="selectedCedarTemplate()!" [existingRecord]="selectedCedarRecord()!" [readonly]="cedarFormReadonly()" - [showEditButton]="true" + [showEditButton]="canEdit()" (emitData)="onCedarFormSubmit($event)" (changeTemplate)="onCedarFormChangeTemplate()" (editMode)="onCedarFormEdit()" diff --git a/src/app/shared/components/metadata-tabs/metadata-tabs.component.ts b/src/app/shared/components/metadata-tabs/metadata-tabs.component.ts index e2831678f..1470dafbc 100644 --- a/src/app/shared/components/metadata-tabs/metadata-tabs.component.ts +++ b/src/app/shared/components/metadata-tabs/metadata-tabs.component.ts @@ -28,6 +28,7 @@ export class MetadataTabsComponent { selectedCedarTemplate = input.required(); selectedCedarRecord = input.required(); cedarFormReadonly = input(true); + canEdit = input(true); changeTab = output(); formSubmit = output(); cedarFormEdit = output(); diff --git a/src/app/shared/components/registration-card/registration-card.component.html b/src/app/shared/components/registration-card/registration-card.component.html index 1e8b6a990..11c0c6e64 100644 --- a/src/app/shared/components/registration-card/registration-card.component.html +++ b/src/app/shared/components/registration-card/registration-card.component.html @@ -55,23 +55,25 @@

@if (isDraft()) { - - - + @if (hasAdminAccess) { + + + + } } @else { (false); @@ -53,6 +52,10 @@ export class RegistrationCardComponent { createSchemaResponse: CreateSchemaResponse, }); + get hasAdminAccess(): boolean { + return this.registrationData().currentUserPermissions.includes(UserPermissions.Admin); + } + get isAccepted(): boolean { return this.registrationData().reviewsState === RegistrationReviewStates.Accepted; } @@ -83,7 +86,7 @@ export class RegistrationCardComponent { } get showButtons(): boolean { - return this.isRootRegistration && (this.isAccepted || this.isPending || this.isEmbargo); + return this.isRootRegistration && (this.isAccepted || this.isPending || this.isEmbargo) && this.hasAdminAccess; } updateRegistration(id: string): void { diff --git a/src/app/shared/mappers/registration/registration.mapper.ts b/src/app/shared/mappers/registration/registration.mapper.ts index c09961357..923e22363 100644 --- a/src/app/shared/mappers/registration/registration.mapper.ts +++ b/src/app/shared/mappers/registration/registration.mapper.ts @@ -71,6 +71,7 @@ export class RegistrationMapper { id: contributor.id, fullName: contributor.embeds?.users?.data.attributes.full_name, })) || [], + currentUserPermissions: registration.attributes.current_user_permissions, }; } @@ -98,6 +99,7 @@ export class RegistrationMapper { fullName: contributor.embeds.users.data.attributes.full_name, })) || [], rootParentId: registration.relationships.root?.data?.id, + currentUserPermissions: registration.attributes.current_user_permissions, }; } diff --git a/src/app/shared/models/registration/registration-card.model.ts b/src/app/shared/models/registration/registration-card.model.ts index 4e33569b2..625b4995d 100644 --- a/src/app/shared/models/registration/registration-card.model.ts +++ b/src/app/shared/models/registration/registration-card.model.ts @@ -1,4 +1,4 @@ -import { RegistrationReviewStates, RegistryStatus, RevisionReviewStates } from '@osf/shared/enums'; +import { RegistrationReviewStates, RegistryStatus, RevisionReviewStates, UserPermissions } from '@osf/shared/enums'; import { ContributorModel } from '../contributors'; @@ -22,4 +22,5 @@ export interface RegistrationCard { hasPapers?: boolean; hasSupplements?: boolean; rootParentId?: string | null; + currentUserPermissions: UserPermissions[]; } diff --git a/src/app/shared/models/registration/registration-json-api.model.ts b/src/app/shared/models/registration/registration-json-api.model.ts index 5e6d75b01..a02d6f8e8 100644 --- a/src/app/shared/models/registration/registration-json-api.model.ts +++ b/src/app/shared/models/registration/registration-json-api.model.ts @@ -1,4 +1,4 @@ -import { RegistrationReviewStates, RevisionReviewStates } from '@osf/shared/enums'; +import { RegistrationReviewStates, RevisionReviewStates, UserPermissions } from '@osf/shared/enums'; import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '../common'; import { LicenseRecordJsonApi } from '../licenses-json-api.model'; @@ -42,6 +42,7 @@ export interface DraftRegistrationAttributesJsonApi { tags: string[]; title: string; public?: boolean; + current_user_permissions: UserPermissions[]; } export interface RegistrationAttributesJsonApi { @@ -64,6 +65,7 @@ export interface RegistrationAttributesJsonApi { pending_registration_approval: boolean; pending_embargo_approval: boolean; pending_embargo_termination_approval: boolean; + current_user_permissions: UserPermissions[]; } export interface DraftRegistrationRelationshipsJsonApi {