From a6c6978bb555fe1ba7850206f90025d9ac9ecdf2 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Tue, 30 Sep 2025 19:14:28 +0300 Subject: [PATCH 1/5] fix(moderation): add registration moderation guard --- src/app/app.routes.ts | 6 ++++ .../components/nav-menu/nav-menu.component.ts | 3 +- .../guards/registration-moderation.guard.ts | 35 +++++++++++++++++++ .../features/registries/registries.routes.ts | 3 +- .../mappers/registration-provider.mapper.ts | 1 + .../registration/registration-node.mapper.ts | 1 + .../shared/models/provider/provider.model.ts | 1 + .../provider/registry-provider.model.ts | 1 + .../registration-provider.actions.ts | 2 +- .../registration-provider.state.ts | 32 ++++++++--------- 10 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 src/app/core/guards/registration-moderation.guard.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 2e933f203..9d92791e7 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -157,6 +157,12 @@ export const routes: Routes = [ import('./core/components/request-access/request-access.component').then((mod) => mod.RequestAccessComponent), data: { skipBreadcrumbs: true }, }, + { + path: 'not-found', + loadComponent: () => + import('./core/components/page-not-found/page-not-found.component').then((mod) => mod.PageNotFoundComponent), + data: { skipBreadcrumbs: true }, + }, { path: ':id/files/:provider/:fileId', loadComponent: () => diff --git a/src/app/core/components/nav-menu/nav-menu.component.ts b/src/app/core/components/nav-menu/nav-menu.component.ts index e4f8d4f3e..496ef7c72 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.ts @@ -59,7 +59,8 @@ export class NavMenuComponent { preprintReviewsPageVisible: this.canUserViewReviews(), registrationModerationPageVisible: this.provider()?.type === CurrentResourceType.Registrations && - this.provider()?.permissions?.includes(ReviewPermissions.ViewSubmissions), + this.provider()?.permissions?.includes(ReviewPermissions.ViewSubmissions) && + !!this.provider()?.reviewsWorkflow, collectionModerationPageVisible: this.provider()?.type === CurrentResourceType.Collections && this.provider()?.permissions?.includes(ReviewPermissions.ViewSubmissions), diff --git a/src/app/core/guards/registration-moderation.guard.ts b/src/app/core/guards/registration-moderation.guard.ts new file mode 100644 index 000000000..b9286fa5b --- /dev/null +++ b/src/app/core/guards/registration-moderation.guard.ts @@ -0,0 +1,35 @@ +import { Store } from '@ngxs/store'; + +import { map, switchMap, take } from 'rxjs'; + +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; + +import { GetRegistryProvider, RegistrationProviderSelectors } from '@osf/shared/stores/registration-provider'; + +export const registrationModerationGuard: CanActivateFn = (route) => { + const store = inject(Store); + const router = inject(Router); + + const provider = store.selectSnapshot(RegistrationProviderSelectors.getBrandedProvider); + + if (provider?.reviewsWorkflow) { + return true; + } + const id = route.params['providerId']; + return store.dispatch(new GetRegistryProvider(id)).pipe( + switchMap(() => { + return store.select(RegistrationProviderSelectors.getBrandedProvider).pipe( + take(1), + map((provider) => { + if (!provider?.reviewsWorkflow) { + router.navigate(['/not-found']); + return false; + } + + return true; + }) + ); + }) + ); +}; diff --git a/src/app/features/registries/registries.routes.ts b/src/app/features/registries/registries.routes.ts index f52964f32..db6c48175 100644 --- a/src/app/features/registries/registries.routes.ts +++ b/src/app/features/registries/registries.routes.ts @@ -2,6 +2,7 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; +import { registrationModerationGuard } from '@core/guards/registration-moderation.guard'; import { authGuard } from '@osf/core/guards'; import { RegistriesComponent } from '@osf/features/registries/registries.component'; import { RegistriesState } from '@osf/features/registries/store'; @@ -43,7 +44,7 @@ export const registriesRoutes: Routes = [ }, { path: ':providerId/moderation', - canActivate: [authGuard], + canActivate: [authGuard, registrationModerationGuard], loadChildren: () => import('@osf/features/moderation/registry-moderation.routes').then((c) => c.registryModerationRoutes), }, diff --git a/src/app/shared/mappers/registration-provider.mapper.ts b/src/app/shared/mappers/registration-provider.mapper.ts index 78b9f8dc3..884cd4708 100644 --- a/src/app/shared/mappers/registration-provider.mapper.ts +++ b/src/app/shared/mappers/registration-provider.mapper.ts @@ -34,6 +34,7 @@ export class RegistrationProviderMapper { } : null, iri: response.links.iri, + reviewsWorkflow: response.attributes.reviews_workflow, }; } } diff --git a/src/app/shared/mappers/registration/registration-node.mapper.ts b/src/app/shared/mappers/registration/registration-node.mapper.ts index f168986bc..458cf93ee 100644 --- a/src/app/shared/mappers/registration/registration-node.mapper.ts +++ b/src/app/shared/mappers/registration/registration-node.mapper.ts @@ -85,6 +85,7 @@ export class RegistrationNodeMapper { name: provider.attributes.name, permissions: provider.attributes.permissions, type: CurrentResourceType.Registrations, + reviewsWorkflow: provider.attributes.reviews_workflow, }; } } diff --git a/src/app/shared/models/provider/provider.model.ts b/src/app/shared/models/provider/provider.model.ts index 88dc359c1..68b9a860b 100644 --- a/src/app/shared/models/provider/provider.model.ts +++ b/src/app/shared/models/provider/provider.model.ts @@ -5,6 +5,7 @@ export interface ProviderShortInfoModel { name: string; type: CurrentResourceType; permissions?: ReviewPermissions[]; + reviewsWorkflow?: string; } export interface BaseProviderModel { diff --git a/src/app/shared/models/provider/registry-provider.model.ts b/src/app/shared/models/provider/registry-provider.model.ts index 3c2f82ec6..c42f193e3 100644 --- a/src/app/shared/models/provider/registry-provider.model.ts +++ b/src/app/shared/models/provider/registry-provider.model.ts @@ -8,4 +8,5 @@ export interface RegistryProviderDetails { permissions: ReviewPermissions[]; brand: Brand | null; iri: string; + reviewsWorkflow: string; } diff --git a/src/app/shared/stores/registration-provider/registration-provider.actions.ts b/src/app/shared/stores/registration-provider/registration-provider.actions.ts index 8fe3f37d1..158e0e52e 100644 --- a/src/app/shared/stores/registration-provider/registration-provider.actions.ts +++ b/src/app/shared/stores/registration-provider/registration-provider.actions.ts @@ -3,7 +3,7 @@ const stateName = '[Registry Provider Search]'; export class GetRegistryProvider { static readonly type = `${stateName} Get Registry Provider`; - constructor(public providerName: string) {} + constructor(public providerId: string) {} } export class ClearRegistryProvider { diff --git a/src/app/shared/stores/registration-provider/registration-provider.state.ts b/src/app/shared/stores/registration-provider/registration-provider.state.ts index 82394aadd..393adbaea 100644 --- a/src/app/shared/stores/registration-provider/registration-provider.state.ts +++ b/src/app/shared/stores/registration-provider/registration-provider.state.ts @@ -1,5 +1,4 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { patch } from '@ngxs/store/operators'; import { catchError, of, tap } from 'rxjs'; @@ -30,14 +29,14 @@ export class RegistrationProviderState { const state = ctx.getState(); const currentProvider = state.currentBrandedProvider.data; - - if (currentProvider?.name === action.providerName) { + if (currentProvider && currentProvider?.id === action.providerId) { ctx.dispatch( new SetCurrentProvider({ - id: currentProvider.id, - name: currentProvider.name, + id: currentProvider?.id, + name: currentProvider?.name, type: CurrentResourceType.Registrations, - permissions: currentProvider.permissions, + permissions: currentProvider?.permissions, + reviewsWorkflow: currentProvider?.reviewsWorkflow, }) ); @@ -51,17 +50,15 @@ export class RegistrationProviderState { }, }); - return this.registrationProvidersService.getProviderBrand(action.providerName).pipe( + return this.registrationProvidersService.getProviderBrand(action.providerId).pipe( tap((provider) => { - ctx.setState( - patch({ - currentBrandedProvider: patch({ - data: provider, - isLoading: false, - error: null, - }), - }) - ); + ctx.patchState({ + currentBrandedProvider: { + data: provider, + isLoading: false, + error: null, + }, + }); ctx.dispatch( new SetCurrentProvider({ @@ -69,6 +66,7 @@ export class RegistrationProviderState { name: provider.name, type: CurrentResourceType.Registrations, permissions: provider.permissions, + reviewsWorkflow: provider.reviewsWorkflow, }) ); }), @@ -78,6 +76,6 @@ export class RegistrationProviderState { @Action(ClearRegistryProvider) clearRegistryProvider(ctx: StateContext) { - ctx.setState(patch({ ...REGISTRIES_PROVIDER_SEARCH_STATE_DEFAULTS })); + ctx.patchState({ ...REGISTRIES_PROVIDER_SEARCH_STATE_DEFAULTS }); } } From 0eb6fa857b77822d151e96c1075022ac4f7e3081 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Wed, 1 Oct 2025 14:19:17 +0300 Subject: [PATCH 2/5] fix(files): file menu updates --- src/app/core/guards/is-file.guard.ts | 5 ++- src/app/core/guards/is-project.guard.ts | 4 +-- src/app/core/guards/is-registry.guard.ts | 4 +-- .../file-detail/file-detail.component.ts | 2 +- .../files/pages/files/files.component.html | 1 - .../files/pages/files/files.component.spec.ts | 1 - .../files/pages/files/files.component.ts | 36 ++++++++++++++----- .../features/metadata/metadata.component.ts | 2 +- .../files-widget/files-widget.component.ts | 11 ++++-- .../files-control.component.html | 1 - .../files-tree/files-tree.component.html | 13 +------ .../files-tree/files-tree.component.ts | 9 ++--- 12 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/app/core/guards/is-file.guard.ts b/src/app/core/guards/is-file.guard.ts index f05adf254..e2e5e38ed 100644 --- a/src/app/core/guards/is-file.guard.ts +++ b/src/app/core/guards/is-file.guard.ts @@ -19,14 +19,13 @@ export const isFileGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) => } const currentResource = store.selectSnapshot(CurrentResourceSelectors.getCurrentResource); - if (currentResource && currentResource.id === id) { if (currentResource.type === CurrentResourceType.Files) { if (isMetadataPath) { return true; } if (currentResource.parentId) { - router.navigate(['/', currentResource.parentId, 'files', id]); + router.navigate(['/', currentResource.parentId, 'files', id], { queryParamsHandling: 'preserve' }); return false; } } @@ -46,7 +45,7 @@ export const isFileGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) => return true; } if (resource.parentId) { - router.navigate(['/', resource.parentId, 'files', id]); + router.navigate(['/', resource.parentId, 'files', id], { queryParamsHandling: 'preserve' }); return false; } } diff --git a/src/app/core/guards/is-project.guard.ts b/src/app/core/guards/is-project.guard.ts index 804d80322..d1eb1d6fb 100644 --- a/src/app/core/guards/is-project.guard.ts +++ b/src/app/core/guards/is-project.guard.ts @@ -24,7 +24,7 @@ export const isProjectGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) if (currentResource && !id.startsWith(currentResource.id)) { if (currentResource.type === CurrentResourceType.Projects && currentResource.parentId) { - router.navigate(['/', currentResource.parentId, 'files', id]); + router.navigate(['/', currentResource.parentId, 'files', id], { queryParamsHandling: 'preserve' }); return true; } @@ -53,7 +53,7 @@ export const isProjectGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) } if (resource.type === CurrentResourceType.Projects && resource.parentId) { - router.navigate(['/', resource.parentId, 'files', id]); + router.navigate(['/', resource.parentId, 'files', id], { queryParamsHandling: 'preserve' }); return true; } diff --git a/src/app/core/guards/is-registry.guard.ts b/src/app/core/guards/is-registry.guard.ts index 44a8628c0..63dfe4a28 100644 --- a/src/app/core/guards/is-registry.guard.ts +++ b/src/app/core/guards/is-registry.guard.ts @@ -24,7 +24,7 @@ export const isRegistryGuard: CanMatchFn = (route: Route, segments: UrlSegment[] if (currentResource && !id.startsWith(currentResource.id)) { if (currentResource.type === CurrentResourceType.Registrations && currentResource.parentId) { - router.navigate(['/', currentResource.parentId, 'files', id]); + router.navigate(['/', currentResource.parentId, 'files', id], { queryParamsHandling: 'preserve' }); return true; } @@ -53,7 +53,7 @@ export const isRegistryGuard: CanMatchFn = (route: Route, segments: UrlSegment[] } if (resource.type === CurrentResourceType.Registrations && resource.parentId) { - router.navigate(['/', resource.parentId, 'files', id]); + router.navigate(['/', resource.parentId, 'files', id], { queryParamsHandling: 'preserve' }); return true; } diff --git a/src/app/features/files/pages/file-detail/file-detail.component.ts b/src/app/features/files/pages/file-detail/file-detail.component.ts index 0d7d79740..6b4209ee5 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.ts @@ -304,7 +304,7 @@ export class FileDetailComponent { copyToClipboard(embedHtml: string): void { this.clipboard.copy(embedHtml); - this.toastService.showSuccess('files.toast.copiedToClipboard'); + this.toastService.showSuccess('files.toast.detail.copiedToClipboard'); } deleteEntry(link: string): void { diff --git a/src/app/features/files/pages/files/files.component.html b/src/app/features/files/pages/files/files.component.html index 5500aa4c6..229824a20 100644 --- a/src/app/features/files/pages/files/files.component.html +++ b/src/app/features/files/pages/files/files.component.html @@ -127,7 +127,6 @@ [isLoading]="isFilesLoading()" [actions]="filesTreeActions" [viewOnly]="!canEdit()" - [viewOnlyDownloadable]="isViewOnlyDownloadable()" [supportUpload]="canUploadFiles()" [resourceId]="resourceId()" [provider]="provider()" diff --git a/src/app/features/files/pages/files/files.component.spec.ts b/src/app/features/files/pages/files/files.component.spec.ts index 8ba77eae6..d10166785 100644 --- a/src/app/features/files/pages/files/files.component.spec.ts +++ b/src/app/features/files/pages/files/files.component.spec.ts @@ -120,7 +120,6 @@ describe('Component: Files', () => { 'isLoading', 'actions', 'viewOnly', - 'viewOnlyDownloadable', 'resourceId', 'provider', 'storage', diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index a055b7aff..61ec08c9f 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -59,7 +59,7 @@ import { } from '@osf/features/files/store'; import { ALL_SORT_OPTIONS, FILE_SIZE_LIMIT } from '@osf/shared/constants'; import { FileMenuType, ResourceType, SupportedFeature, UserPermissions } from '@osf/shared/enums'; -import { hasViewOnlyParam, IS_MEDIUM } from '@osf/shared/helpers'; +import { getViewOnlyParamFromUrl, hasViewOnlyParam, IS_MEDIUM } from '@osf/shared/helpers'; import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores'; import { FilesTreeComponent, @@ -184,7 +184,21 @@ export class FilesComponent { readonly allowedMenuActions = computed(() => { const provider = this.provider(); const supportedFeatures = this.supportedFeatures()[provider] || []; - return this.mapMenuActions(supportedFeatures); + const hasViewOnly = this.hasViewOnly(); + const isRegistration = this.resourceType() === ResourceType.Registration; + const menuMap = this.mapMenuActions(supportedFeatures); + + const result: Record = { ...menuMap }; + + if (hasViewOnly || isRegistration) { + const allowed = new Set([FileMenuType.Download, FileMenuType.Embed, FileMenuType.Share]); + + (Object.keys(result) as FileMenuType[]).forEach((key) => { + result[key] = allowed.has(key) && menuMap[key]; + }); + } + + return result; }); readonly rootFoldersOptions = computed(() => { @@ -211,15 +225,16 @@ export class FilesComponent { (permission) => permission === UserPermissions.Admin || permission === UserPermissions.Write ); - return !details.isRegistration && hasAdminOrWrite; + return hasAdminOrWrite; }); - readonly isViewOnlyDownloadable = computed( - () => this.allowedMenuActions()[FileMenuType.Download] && this.resourceType() === ResourceType.Registration - ); + readonly isRegistration = computed(() => this.resourceType() === ResourceType.Registration); canUploadFiles = computed( - () => this.supportedFeatures()[this.provider()]?.includes(SupportedFeature.AddUpdateFiles) && this.canEdit() + () => + this.supportedFeatures()[this.provider()]?.includes(SupportedFeature.AddUpdateFiles) && + this.canEdit() && + !this.isRegistration() ); isButtonDisabled = computed(() => this.fileIsUploading() || this.isFilesLoading()); @@ -492,7 +507,12 @@ export class FilesComponent { } navigateToFile(file: OsfFile) { - const url = this.router.createUrlTree([file.guid]).toString(); + let url = file.links?.html ?? ''; + const viewOnlyParam = this.hasViewOnly(); + if (viewOnlyParam) { + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}view_only=${getViewOnlyParamFromUrl(this.router.url)}`; + } window.open(url, '_blank'); } diff --git a/src/app/features/metadata/metadata.component.ts b/src/app/features/metadata/metadata.component.ts index c9bed5269..a3ffe6340 100644 --- a/src/app/features/metadata/metadata.component.ts +++ b/src/app/features/metadata/metadata.component.ts @@ -307,7 +307,7 @@ export class MetadataComponent implements OnInit { .subscribe(() => { this.updateSelectedCedarRecord(selectedRecord.id!); this.cedarFormReadonly.set(true); - this.toastService.showSuccess(this.translateService.instant('files.detail.toast.cedarUpdated')); + this.toastService.showSuccess('files.detail.toast.cedarUpdated'); }); } } diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.ts index ccd0da344..3097d9dd0 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.ts +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.ts @@ -31,7 +31,7 @@ import { SetFilesIsLoading, } from '@osf/features/files/store'; import { FilesTreeComponent, SelectComponent } from '@osf/shared/components'; -import { Primitive } from '@osf/shared/helpers'; +import { getViewOnlyParamFromUrl, hasViewOnlyParam, Primitive } from '@osf/shared/helpers'; import { ConfiguredAddonModel, FileLabelModel, @@ -91,6 +91,8 @@ export class FilesWidgetComponent { return []; }); + readonly hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + private readonly actions = createDispatchMap({ getFiles: GetFiles, setCurrentFolder: SetCurrentFolder, @@ -218,7 +220,12 @@ export class FilesWidgetComponent { } navigateToFile(file: OsfFile) { - const url = this.router.createUrlTree([file.guid]).toString(); + let url = file.links?.html ?? ''; + const viewOnlyParam = this.hasViewOnly(); + if (viewOnlyParam) { + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}view_only=${getViewOnlyParamFromUrl(this.router.url)}`; + } window.open(url, '_blank'); } diff --git a/src/app/features/registries/components/files-control/files-control.component.html b/src/app/features/registries/components/files-control/files-control.component.html index 41f62a227..172236d64 100644 --- a/src/app/features/registries/components/files-control/files-control.component.html +++ b/src/app/features/registries/components/files-control/files-control.component.html @@ -58,7 +58,6 @@ [isLoading]="isFilesLoading()" [actions]="filesTreeActions" [viewOnly]="filesViewOnly()" - [viewOnlyDownloadable]="true" [resourceId]="projectId()" [provider]="provider()" (folderIsOpening)="folderIsOpening($event)" diff --git a/src/app/shared/components/files-tree/files-tree.component.html b/src/app/shared/components/files-tree/files-tree.component.html index 48086e274..1e437042d 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -71,7 +71,7 @@ {{ file.dateModified | date: 'MMM d, y hh:mm a' }} - @if (!viewOnly() && !viewOnlyDownloadable()) { + @if (isSomeFileActionAllowed) {
- } @else if (viewOnly() && viewOnlyDownloadable()) { -
- -
} } diff --git a/src/app/shared/components/files-tree/files-tree.component.ts b/src/app/shared/components/files-tree/files-tree.component.ts index 72bdfa333..d52fb6ea9 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -3,7 +3,6 @@ import { select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { PrimeTemplate } from 'primeng/api'; -import { Button } from 'primeng/button'; import { PaginatorState } from 'primeng/paginator'; import { Tree, TreeNodeDropEvent } from 'primeng/tree'; @@ -57,7 +56,6 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp FileMenuComponent, StopPropagationDirective, CustomPaginatorComponent, - Button, ], templateUrl: './files-tree.component.html', styleUrl: './files-tree.component.scss', @@ -84,7 +82,6 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { resourceId = input.required(); actions = input.required(); viewOnly = input(true); - viewOnlyDownloadable = input(false); provider = input(); allowedMenuActions = input({} as FileMenuFlags); supportUpload = input(true); @@ -104,6 +101,10 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { readonly FileMenuType = FileMenuType; + get isSomeFileActionAllowed(): boolean { + return Object.keys(this.allowedMenuActions()).length > 0; + } + readonly nodes = computed(() => { const currentFolder = this.currentFolder(); const files = this.files(); @@ -389,7 +390,7 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { copyToClipboard(embedHtml: string): void { this.clipboard.copy(embedHtml); - this.toastService.showSuccess('files.toast.copiedToClipboard'); + this.toastService.showSuccess('files.toast.detail.copiedToClipboard'); } async dropNode(event: TreeNodeDropEvent) { From afb91b5c0778c97089af0edda95e917b3f9f5773 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Wed, 1 Oct 2025 17:09:52 +0300 Subject: [PATCH 3/5] feat(parent-card): work on parent card --- .../overview-components.component.html | 10 +++---- .../overview-components.component.scss | 25 ------------------ .../overview-parent-project.component.html | 26 +++++++++++++++++++ .../overview-parent-project.component.scss | 0 .../overview-parent-project.component.spec.ts | 22 ++++++++++++++++ .../overview-parent-project.component.ts | 20 ++++++++++++++ .../models/project-overview.models.ts | 1 + .../overview/project-overview.component.html | 2 ++ .../overview/project-overview.component.ts | 3 ++- .../services/project-overview.service.ts | 13 ++++++++++ .../store/project-overview.actions.ts | 4 +-- .../overview/store/project-overview.model.ts | 6 +++++ .../overview/store/project-overview.state.ts | 26 +++++++++++++++++-- src/assets/i18n/en.json | 1 + src/styles/components/nodes.scss | 25 ++++++++++++++++++ src/styles/styles.scss | 1 + 16 files changed, 150 insertions(+), 35 deletions(-) create mode 100644 src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.html create mode 100644 src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.scss create mode 100644 src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.spec.ts create mode 100644 src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.ts create mode 100644 src/styles/components/nodes.scss diff --git a/src/app/features/project/overview/components/overview-components/overview-components.component.html b/src/app/features/project/overview/components/overview-components/overview-components.component.html index 72656da99..2111b5897 100644 --- a/src/app/features/project/overview/components/overview-components/overview-components.component.html +++ b/src/app/features/project/overview/components/overview-components/overview-components.component.html @@ -1,4 +1,4 @@ -
+

{{ 'project.overview.components.title' | translate }}

@@ -11,7 +11,7 @@

{{ 'project.overview.components.title' | translate }}

}
-
+
@if (isComponentsLoading()) { } @else { @@ -21,7 +21,7 @@

{{ 'project.overview.components.title' | translate }}

-
+
-
+
diff --git a/src/app/features/project/overview/components/overview-components/overview-components.component.scss b/src/app/features/project/overview/components/overview-components/overview-components.component.scss index 1b65f7af8..e69de29bb 100644 --- a/src/app/features/project/overview/components/overview-components/overview-components.component.scss +++ b/src/app/features/project/overview/components/overview-components/overview-components.component.scss @@ -1,25 +0,0 @@ -@use "/styles/mixins" as mix; - -.component { - border: 1px solid var(--grey-2); - border-radius: mix.rem(12px); - color: var(--dark-blue-1); - - &-description { - border-radius: mix.rem(24px); - } - - &-container { - overflow-y: auto; - max-height: mix.rem(335px); - } - - &-wrapper { - border: 1px solid var(--grey-2); - border-radius: mix.rem(12px); - } - - &-title { - color: var(--dark-blue-1); - } -} diff --git a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.html b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.html new file mode 100644 index 000000000..387a6f871 --- /dev/null +++ b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.html @@ -0,0 +1,26 @@ +
+
+

{{ 'project.overview.parentProject.title' | translate }}

+
+ +
+ @if (isLoading()) { + + } @else { + + +
+ + +
+ +
+
+ } +
+
diff --git a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.scss b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.spec.ts b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.spec.ts new file mode 100644 index 000000000..81bbf3d6d --- /dev/null +++ b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OverviewParentProjectComponent } from './overview-parent-project.component'; + +describe.skip('OverviewParentProjectComponent', () => { + let component: OverviewParentProjectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OverviewParentProjectComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(OverviewParentProjectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.ts b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.ts new file mode 100644 index 000000000..c709d4ac7 --- /dev/null +++ b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.ts @@ -0,0 +1,20 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Skeleton } from 'primeng/skeleton'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { ContributorsListComponent, IconComponent, TruncatedTextComponent } from '@osf/shared/components'; + +@Component({ + selector: 'osf-overview-parent-project', + imports: [Skeleton, TranslatePipe, IconComponent, TruncatedTextComponent, ContributorsListComponent], + templateUrl: './overview-parent-project.component.html', + styleUrl: './overview-parent-project.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OverviewParentProjectComponent { + isLoading = input(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + project = input.required(); +} diff --git a/src/app/features/project/overview/models/project-overview.models.ts b/src/app/features/project/overview/models/project-overview.models.ts index 4390b4020..962893f5e 100644 --- a/src/app/features/project/overview/models/project-overview.models.ts +++ b/src/app/features/project/overview/models/project-overview.models.ts @@ -55,6 +55,7 @@ export interface ProjectOverview { rootFolder: string; iri: string; }; + parentId?: string; rootParentId?: string; } diff --git a/src/app/features/project/overview/project-overview.component.html b/src/app/features/project/overview/project-overview.component.html index 0800aad51..83853a7b5 100644 --- a/src/app/features/project/overview/project-overview.component.html +++ b/src/app/features/project/overview/project-overview.component.html @@ -57,6 +57,8 @@ [areComponentsLoading]="areComponentsLoading()" /> + + @if (!hasViewOnly()) { diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index 468c8e2a1..188aef3cd 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -58,6 +58,7 @@ import { } from '@shared/components'; import { DataciteService } from '@shared/services/datacite/datacite.service'; +import { OverviewParentProjectComponent } from './components/overview-parent-project/overview-parent-project.component'; import { FilesWidgetComponent, LinkedResourcesComponent, @@ -97,7 +98,7 @@ import { RouterLink, FilesWidgetComponent, ViewOnlyLinkMessageComponent, - ViewOnlyLinkMessageComponent, + OverviewParentProjectComponent, ], providers: [DatePipe], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/project/overview/services/project-overview.service.ts b/src/app/features/project/overview/services/project-overview.service.ts index c2610de69..f2a3c71c0 100644 --- a/src/app/features/project/overview/services/project-overview.service.ts +++ b/src/app/features/project/overview/services/project-overview.service.ts @@ -37,6 +37,7 @@ export class ProjectOverviewService { 'license', 'storage', 'preprints', + 'parent', ], 'fields[institutions]': 'assets,description,name', 'fields[preprints]': 'title,date_created', @@ -143,4 +144,16 @@ export class ProjectOverviewService { .get>(`${this.apiUrl}/nodes/${projectId}/children/`, params) .pipe(map((response) => response.data.map((item) => ComponentsMapper.fromGetComponentResponse(item)))); } + + getParentProject(projectId: string): Observable { + const params: Record = {}; + return this.jsonApiService + .get(`${this.apiUrl}/nodes/${projectId}/parent/`, params) + .pipe( + map((response) => ({ + project: ProjectOverviewMapper.fromGetProjectResponse(response.data), + meta: response.meta, + })) + ); + } } diff --git a/src/app/features/project/overview/store/project-overview.actions.ts b/src/app/features/project/overview/store/project-overview.actions.ts index eb6d523b6..2c819c59d 100644 --- a/src/app/features/project/overview/store/project-overview.actions.ts +++ b/src/app/features/project/overview/store/project-overview.actions.ts @@ -73,8 +73,8 @@ export class GetComponents { constructor(public projectId: string) {} } -export class GetComponentsTree { - static readonly type = '[Project Overview] Get Components Tree'; +export class GetParentProject { + static readonly type = '[Project Overview] Get Parent Project'; constructor(public projectId: string) {} } diff --git a/src/app/features/project/overview/store/project-overview.model.ts b/src/app/features/project/overview/store/project-overview.model.ts index cfd19a209..ee522bc05 100644 --- a/src/app/features/project/overview/store/project-overview.model.ts +++ b/src/app/features/project/overview/store/project-overview.model.ts @@ -7,6 +7,7 @@ export interface ProjectOverviewStateModel { components: AsyncStateModel; isAnonymous: boolean; duplicatedProject: BaseNodeModel | null; + parentProject: AsyncStateModel; } export const PROJECT_OVERVIEW_DEFAULTS: ProjectOverviewStateModel = { @@ -24,4 +25,9 @@ export const PROJECT_OVERVIEW_DEFAULTS: ProjectOverviewStateModel = { }, isAnonymous: false, duplicatedProject: null, + parentProject: { + data: null, + isLoading: false, + error: null, + }, }; diff --git a/src/app/features/project/overview/store/project-overview.state.ts b/src/app/features/project/overview/store/project-overview.state.ts index 8679a60d5..3b8ddf688 100644 --- a/src/app/features/project/overview/store/project-overview.state.ts +++ b/src/app/features/project/overview/store/project-overview.state.ts @@ -5,7 +5,6 @@ import { catchError, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/shared/helpers'; -import { ProjectsService } from '@osf/shared/services/projects.service'; import { ResourceType } from '@shared/enums'; import { ProjectOverviewService } from '../services'; @@ -18,6 +17,7 @@ import { DuplicateProject, ForkResource, GetComponents, + GetParentProject, GetProjectById, SetProjectCustomCitation, UpdateProjectPublicStatus, @@ -31,7 +31,6 @@ import { PROJECT_OVERVIEW_DEFAULTS, ProjectOverviewStateModel } from './project- @Injectable() export class ProjectOverviewState { projectOverviewService = inject(ProjectOverviewService); - projectsService = inject(ProjectsService); @Action(GetProjectById) getProjectById(ctx: StateContext, action: GetProjectById) { @@ -255,4 +254,27 @@ export class ProjectOverviewState { catchError((error) => handleSectionError(ctx, 'components', error)) ); } + + @Action(GetParentProject) + getParentProject(ctx: StateContext, action: GetParentProject) { + const state = ctx.getState(); + ctx.patchState({ + parentProject: { + ...state.parentProject, + isLoading: true, + }, + }); + return this.projectOverviewService.getParentProject(action.projectId).pipe( + tap((response) => { + ctx.patchState({ + parentProject: { + data: response.project, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'parentProject', error)) + ); + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 8a21847ac..e2f6e05c3 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -645,6 +645,7 @@ "linkProjectsButton": "Link Projects", "noComponentsMessage": "Add components to organize your project." }, + "parentProject": "Parent Project", "linkedProjects": { "title": "Linked Projects", "noLinkedProjectsMessage": "Link your project." diff --git a/src/styles/components/nodes.scss b/src/styles/components/nodes.scss new file mode 100644 index 000000000..caee55b75 --- /dev/null +++ b/src/styles/components/nodes.scss @@ -0,0 +1,25 @@ +@use "/styles/mixins" as mix; + +.node { + border: 1px solid var(--grey-2); + border-radius: mix.rem(12px); + color: var(--dark-blue-1); + + &-description { + border-radius: mix.rem(24px); + } + + &-container { + overflow-y: auto; + max-height: mix.rem(335px); + } + + &-wrapper { + border: 1px solid var(--grey-2); + border-radius: mix.rem(12px); + } + + &-title { + color: var(--dark-blue-1); + } +} diff --git a/src/styles/styles.scss b/src/styles/styles.scss index eeb37aec1..f192f1d5d 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -10,6 +10,7 @@ @use "./components/md-editor"; @use "./components/preprints"; @use "./components/collections"; +@use "./components/nodes"; @use "./overrides/button"; @use "./overrides/button-toggle"; From 83fe0bc26480499e9d6730afd16fae638c8a69e2 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Thu, 2 Oct 2025 17:35:04 +0300 Subject: [PATCH 4/5] feat(parent): revert --- .husky/pre-push | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.husky/pre-push b/.husky/pre-push index 0ca23b22c..5a281490a 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,19 +1,19 @@ -# # npm run build +# npm run build -# npm run test:coverage || { -# printf "\n\nERROR: Testing errors or coverage issues are found." -# printf "\n\nIn the future this will block your ability to push to github until it is resolved." -# printf "\n\nThe same pipeline runs via GitHub actions." -# printf "\n\nYou are seeing this error because code was added without test coverage." -# # printf "\n\n Please address them before proceeding.\n\n\n\n" -# # exit 1 -# } +npm run test:coverage || { + printf "\n\nERROR: Testing errors or coverage issues are found." + printf "\n\nIn the future this will block your ability to push to github until it is resolved." + printf "\n\nThe same pipeline runs via GitHub actions." + printf "\n\nYou are seeing this error because code was added without test coverage." + # printf "\n\n Please address them before proceeding.\n\n\n\n" + # exit 1 +} -# npm run test:check-coverage-thresholds || { -# printf "\n\nERROR: Coverage thresholds are not met." -# printf "\n\nIn the future this will block your ability to push to github until it is resolved." -# printf "\n\nThe same pipeline runs via GitHub actions." -# printf "\n\nYou are seeing this error because test coverage increased without updating the jest.config.js thresholds." -# #printf "\n\nPlease address them before proceeding.\n\n\n\n" -# # exit 1 -# } +npm run test:check-coverage-thresholds || { + printf "\n\nERROR: Coverage thresholds are not met." + printf "\n\nIn the future this will block your ability to push to github until it is resolved." + printf "\n\nThe same pipeline runs via GitHub actions." + printf "\n\nYou are seeing this error because test coverage increased without updating the jest.config.js thresholds." + #printf "\n\nPlease address them before proceeding.\n\n\n\n" + # exit 1 +} From cb7b897ea70f010dfe9c6def1126ef4038f2bba3 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Fri, 3 Oct 2025 15:09:38 +0300 Subject: [PATCH 5/5] feat(parent): add parent project --- .../overview-components.component.html | 2 +- .../overview-parent-project.component.html | 49 ++++++++++++++---- .../overview-parent-project.component.ts | 51 +++++++++++++++++-- .../mappers/project-overview.mapper.ts | 1 + .../models/project-overview.models.ts | 6 +++ .../overview/project-overview.component.html | 5 +- .../overview/project-overview.component.ts | 26 ++++++---- .../services/project-overview.service.ts | 20 ++++---- .../store/project-overview.selectors.ts | 10 ++++ 9 files changed, 133 insertions(+), 37 deletions(-) diff --git a/src/app/features/project/overview/components/overview-components/overview-components.component.html b/src/app/features/project/overview/components/overview-components/overview-components.component.html index c43b701c8..44c640ea2 100644 --- a/src/app/features/project/overview/components/overview-components/overview-components.component.html +++ b/src/app/features/project/overview/components/overview-components/overview-components.component.html @@ -17,7 +17,7 @@

{{ 'project.overview.components.title' | translate }}

} @else { @if (components().length) { @for (component of components(); track component.id) { -
+

diff --git a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.html b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.html index 387a6f871..46a5d262b 100644 --- a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.html +++ b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.html @@ -1,25 +1,52 @@
-

{{ 'project.overview.parentProject.title' | translate }}

+

{{ 'project.overview.parentProject' | translate }}

@if (isLoading()) { } @else { - +
+
+

+ + {{ project().title }} +

+ @if (isCurrentUserContributor()) { +
+ + - + } +
-
- +
+
+ @if (project().description) { + + }
}
diff --git a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.ts b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.ts index c709d4ac7..d24232ed1 100644 --- a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.ts +++ b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.ts @@ -1,20 +1,63 @@ +import { select } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; +import { Button } from 'primeng/button'; +import { Menu } from 'primeng/menu'; import { Skeleton } from 'primeng/skeleton'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; +import { Router } from '@angular/router'; +import { UserSelectors } from '@core/store/user'; import { ContributorsListComponent, IconComponent, TruncatedTextComponent } from '@osf/shared/components'; +import { ProjectOverview } from '../../models'; + @Component({ selector: 'osf-overview-parent-project', - imports: [Skeleton, TranslatePipe, IconComponent, TruncatedTextComponent, ContributorsListComponent], + imports: [Skeleton, TranslatePipe, IconComponent, TruncatedTextComponent, Button, Menu, ContributorsListComponent], templateUrl: './overview-parent-project.component.html', styleUrl: './overview-parent-project.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class OverviewParentProjectComponent { isLoading = input(false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - project = input.required(); + project = input.required(); + + private router = inject(Router); + currentUser = select(UserSelectors.getCurrentUser); + + menuItems = [ + { + label: 'project.overview.actions.manageContributors', + action: 'manageContributors', + }, + { + label: 'project.overview.actions.settings', + action: 'settings', + }, + ]; + + get isCurrentUserContributor() { + return () => { + const userId = this.currentUser()?.id; + return userId ? this.project()?.contributors.some((contributor) => contributor.userId === userId) : false; + }; + } + + handleMenuAction(action: string): void { + const projectId = this.project()?.id; + if (!projectId) { + return; + } + switch (action) { + case 'manageContributors': + this.router.navigate([projectId, 'contributors']); + break; + case 'settings': + this.router.navigate([projectId, 'settings']); + break; + } + } } diff --git a/src/app/features/project/overview/mappers/project-overview.mapper.ts b/src/app/features/project/overview/mappers/project-overview.mapper.ts index 88de48c72..2bd0c45c2 100644 --- a/src/app/features/project/overview/mappers/project-overview.mapper.ts +++ b/src/app/features/project/overview/mappers/project-overview.mapper.ts @@ -70,6 +70,7 @@ export class ProjectOverviewMapper { iri: response.links?.iri, }, rootParentId: response.relationships?.root?.data?.id, + parentId: response.relationships?.parent?.data?.id, } as ProjectOverview; } } diff --git a/src/app/features/project/overview/models/project-overview.models.ts b/src/app/features/project/overview/models/project-overview.models.ts index 962893f5e..8fedc266b 100644 --- a/src/app/features/project/overview/models/project-overview.models.ts +++ b/src/app/features/project/overview/models/project-overview.models.ts @@ -185,6 +185,12 @@ export interface ProjectOverviewGetResponseJsonApi { type: string; }; }; + parent?: { + data: { + id: string; + type: string; + }; + }; }; links: { iri: string; diff --git a/src/app/features/project/overview/project-overview.component.html b/src/app/features/project/overview/project-overview.component.html index 9f1653746..36e5cf2a1 100644 --- a/src/app/features/project/overview/project-overview.component.html +++ b/src/app/features/project/overview/project-overview.component.html @@ -56,8 +56,9 @@ [components]="components()" [areComponentsLoading]="areComponentsLoading()" /> - - + @if (parentProject()) { + + } @if (!hasViewOnly()) { diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index 87f1d7358..e2e1e16b7 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -72,6 +72,7 @@ import { SUBMISSION_REVIEW_STATUS_OPTIONS } from './constants'; import { ClearProjectOverview, GetComponents, + GetParentProject, GetProjectById, ProjectOverviewSelectors, SetProjectCustomCitation, @@ -132,6 +133,8 @@ export class ProjectOverviewComponent implements OnInit { hasWriteAccess = select(ProjectOverviewSelectors.hasWriteAccess); hasAdminAccess = select(ProjectOverviewSelectors.hasAdminAccess); isWikiEnabled = select(ProjectOverviewSelectors.isWikiEnabled); + parentProject = select(ProjectOverviewSelectors.getParentProject); + isParentProjectLoading = select(ProjectOverviewSelectors.getParentProjectLoading); private readonly actions = createDispatchMap({ getProject: GetProjectById, @@ -151,6 +154,7 @@ export class ProjectOverviewComponent implements OnInit { getRootFolders: GetRootFolders, getConfiguredStorageAddons: GetConfiguredStorageAddons, getSubjects: FetchSelectedSubjects, + getParentProject: GetParentProject, }); readonly activityPageSize = 5; @@ -220,15 +224,6 @@ export class ProjectOverviewComponent implements OnInit { }; }); - private readonly effectMetaTags = effect(() => { - if (!this.isProjectLoading()) { - const metaTagsData = this.metaTagsData(); - if (metaTagsData) { - this.metaTags.updateMetaTags(metaTagsData, this.destroyRef); - } - } - }); - private readonly metaTagsData = computed(() => { const project = this.currentProject(); if (!project) return null; @@ -262,6 +257,15 @@ export class ProjectOverviewComponent implements OnInit { this.setupCleanup(); this.setupProjectEffects(); this.setupRouteChangeListener(); + + effect(() => { + if (!this.isProjectLoading()) { + const metaTagsData = this.metaTagsData(); + if (metaTagsData) { + this.metaTags.updateMetaTags(metaTagsData, this.destroyRef); + } + } + }); } onCustomCitationUpdated(citation: string): void { @@ -337,6 +341,10 @@ export class ProjectOverviewComponent implements OnInit { const rootParentId = currentProject.rootParentId ?? currentProject.id; this.actions.getComponentsTree(rootParentId, currentProject.id, ResourceType.Project); this.actions.getSubjects(currentProject.id, ResourceType.Project); + const parentProjectId = currentProject.parentId; + if (parentProjectId) { + this.actions.getParentProject(parentProjectId); + } } }); effect(() => { diff --git a/src/app/features/project/overview/services/project-overview.service.ts b/src/app/features/project/overview/services/project-overview.service.ts index f2a3c71c0..9fbe5357f 100644 --- a/src/app/features/project/overview/services/project-overview.service.ts +++ b/src/app/features/project/overview/services/project-overview.service.ts @@ -37,7 +37,6 @@ export class ProjectOverviewService { 'license', 'storage', 'preprints', - 'parent', ], 'fields[institutions]': 'assets,description,name', 'fields[preprints]': 'title,date_created', @@ -146,14 +145,15 @@ export class ProjectOverviewService { } getParentProject(projectId: string): Observable { - const params: Record = {}; - return this.jsonApiService - .get(`${this.apiUrl}/nodes/${projectId}/parent/`, params) - .pipe( - map((response) => ({ - project: ProjectOverviewMapper.fromGetProjectResponse(response.data), - meta: response.meta, - })) - ); + const params: Record = { + 'embed[]': ['bibliographic_contributors'], + 'fields[users]': 'family_name,full_name,given_name,middle_name', + }; + return this.jsonApiService.get(`${this.apiUrl}/nodes/${projectId}/`, params).pipe( + map((response) => ({ + project: ProjectOverviewMapper.fromGetProjectResponse(response.data), + meta: response.meta, + })) + ); } } diff --git a/src/app/features/project/overview/store/project-overview.selectors.ts b/src/app/features/project/overview/store/project-overview.selectors.ts index babe51c21..6cc9489a5 100644 --- a/src/app/features/project/overview/store/project-overview.selectors.ts +++ b/src/app/features/project/overview/store/project-overview.selectors.ts @@ -75,4 +75,14 @@ export class ProjectOverviewSelectors { static isWikiEnabled(state: ProjectOverviewStateModel): boolean { return !!state.project.data?.wikiEnabled; } + + @Selector([ProjectOverviewState]) + static getParentProject(state: ProjectOverviewStateModel) { + return state.parentProject.data; + } + + @Selector([ProjectOverviewState]) + static getParentProjectLoading(state: ProjectOverviewStateModel) { + return state.parentProject.isLoading; + } }