From 452387a831ee06d5ace1c714d2ac526139bab466 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Wed, 16 Jul 2025 00:05:30 +0300 Subject: [PATCH 1/3] feat(admin-users): added inst registrations page --- .../admin-institutions/constants/index.ts | 1 + .../project-table-columns.constant.ts | 2 +- .../registration-table-columns.constant.ts | 79 ++++++++ .../admin-institutions/mappers/index.ts | 1 + ...ution-registration-to-table-data.mapper.ts | 40 ++++ .../institution-registrations.mapper.ts | 57 ++++++ .../admin-institutions/models/index.ts | 2 + .../models/institution-registration.model.ts | 16 ++ ...tution-registrations-query-params.model.ts | 5 + .../institutions-registrations.component.html | 14 ++ .../institutions-registrations.component.scss | 3 + .../institutions-registrations.component.ts | 173 +++++++++++++++++- .../services/institutions-admin.service.ts | 154 ++++++++++++---- .../store/institutions-admin.actions.ts | 11 ++ .../store/institutions-admin.model.ts | 2 + .../store/institutions-admin.selectors.ts | 21 +++ .../store/institutions-admin.state.ts | 31 +++- src/assets/i18n/en.json | 7 + 18 files changed, 578 insertions(+), 41 deletions(-) create mode 100644 src/app/features/admin-institutions/constants/registration-table-columns.constant.ts create mode 100644 src/app/features/admin-institutions/mappers/institution-registration-to-table-data.mapper.ts create mode 100644 src/app/features/admin-institutions/mappers/institution-registrations.mapper.ts create mode 100644 src/app/features/admin-institutions/models/institution-registration.model.ts create mode 100644 src/app/features/admin-institutions/models/institution-registrations-query-params.model.ts diff --git a/src/app/features/admin-institutions/constants/index.ts b/src/app/features/admin-institutions/constants/index.ts index 9044772a9..7410fa967 100644 --- a/src/app/features/admin-institutions/constants/index.ts +++ b/src/app/features/admin-institutions/constants/index.ts @@ -1,4 +1,5 @@ export * from './admin-table-columns.constant'; export * from './department-options.constant'; export * from './project-table-columns.constant'; +export * from './registration-table-columns.constant'; export * from './resource-tab-option.constant'; diff --git a/src/app/features/admin-institutions/constants/project-table-columns.constant.ts b/src/app/features/admin-institutions/constants/project-table-columns.constant.ts index 51628fe50..b1d7be4a6 100644 --- a/src/app/features/admin-institutions/constants/project-table-columns.constant.ts +++ b/src/app/features/admin-institutions/constants/project-table-columns.constant.ts @@ -4,7 +4,7 @@ export const projectTableColumns: TableColumn[] = [ { field: 'title', header: 'adminInstitutions.projects.title', - sortable: true, + sortable: false, isLink: true, linkTarget: '_blank', }, diff --git a/src/app/features/admin-institutions/constants/registration-table-columns.constant.ts b/src/app/features/admin-institutions/constants/registration-table-columns.constant.ts new file mode 100644 index 000000000..2a6ec07ec --- /dev/null +++ b/src/app/features/admin-institutions/constants/registration-table-columns.constant.ts @@ -0,0 +1,79 @@ +import { TableColumn } from '@osf/features/admin-institutions/models'; + +export const registrationTableColumns: TableColumn[] = [ + { + field: 'title', + header: 'adminInstitutions.projects.title', + sortable: false, + isLink: true, + linkTarget: '_blank', + }, + { + field: 'link', + header: 'adminInstitutions.projects.link', + sortable: false, + isLink: true, + linkTarget: '_blank', + }, + { + field: 'dateCreated', + header: 'adminInstitutions.projects.dateCreated', + sortable: true, + dateFormat: 'dd/MM/yyyy', + }, + { + field: 'dateModified', + header: 'adminInstitutions.projects.dateModified', + sortable: true, + dateFormat: 'dd/MM/yyyy', + }, + { + field: 'doi', + header: 'adminInstitutions.registrations.doi', + sortable: false, + isLink: true, + linkTarget: '_blank', + }, + { + field: 'storageLocation', + header: 'adminInstitutions.registrations.storageLocation', + sortable: false, + }, + { + field: 'totalDataStored', + header: 'adminInstitutions.projects.totalDataStored', + sortable: false, + }, + { + field: 'contributorName', + header: 'adminInstitutions.projects.contributorName', + sortable: true, + isLink: true, + linkTarget: '_blank', + }, + { + field: 'views', + header: 'adminInstitutions.projects.views', + sortable: false, + }, + { + field: 'resourceType', + header: 'adminInstitutions.projects.resourceType', + sortable: false, + }, + { + field: 'license', + header: 'adminInstitutions.projects.license', + sortable: false, + }, + { + field: 'funderName', + header: 'adminInstitutions.registrations.funderName', + sortable: false, + }, + { + field: 'registrationSchema', + header: 'adminInstitutions.registrations.registrationSchema', + sortable: false, + }, +]; diff --git a/src/app/features/admin-institutions/mappers/index.ts b/src/app/features/admin-institutions/mappers/index.ts index 20dbd3dfa..88c77ca06 100644 --- a/src/app/features/admin-institutions/mappers/index.ts +++ b/src/app/features/admin-institutions/mappers/index.ts @@ -1,6 +1,7 @@ export { mapInstitutionDepartment, mapInstitutionDepartments } from './institution-departments.mapper'; export { mapProjectToTableCellData } from './institution-project-to-table-data.mapper'; export { mapInstitutionProjects } from './institution-projects.mapper'; +export { mapInstitutionRegistrations } from './institution-registrations.mapper'; export { mapIndexCardResults } from './institution-summary-index.mapper'; export { mapInstitutionSummaryMetrics } from './institution-summary-metrics.mapper'; export { mapUserToTableCellData } from './institution-user-to-table-data.mapper'; diff --git a/src/app/features/admin-institutions/mappers/institution-registration-to-table-data.mapper.ts b/src/app/features/admin-institutions/mappers/institution-registration-to-table-data.mapper.ts new file mode 100644 index 000000000..1e8b0a679 --- /dev/null +++ b/src/app/features/admin-institutions/mappers/institution-registration-to-table-data.mapper.ts @@ -0,0 +1,40 @@ +import { InstitutionRegistration, TableCellData, TableCellLink } from '../models'; + +export function mapRegistrationToTableData(registration: InstitutionRegistration): TableCellData { + return { + id: registration.id, + title: { + text: registration.title, + url: registration.link, + target: '_blank', + } as TableCellLink, + link: { + text: registration.link.split('/').pop() || registration.link, + url: registration.link, + target: '_blank', + } as TableCellLink, + dateCreated: registration.dateCreated, + dateModified: registration.dateModified, + doi: registration.doi + ? ({ + text: registration.doi?.split('org/')[1], + url: registration.doi, + target: '_blank', + } as TableCellLink) + : '-', + storageLocation: registration.storageLocation || '-', + totalDataStored: registration.totalDataStored || '-', + contributorName: registration.contributorName + ? ({ + text: registration.contributorName, + url: `https://osf.io/${registration.contributorName}`, + target: '_blank', + } as TableCellLink) + : '-', + views: registration.views || '-', + resourceType: registration.resourceType || '-', + license: registration.license || '-', + funderName: registration.funderName || '-', + registrationSchema: registration.registrationSchema || '-', + }; +} diff --git a/src/app/features/admin-institutions/mappers/institution-registrations.mapper.ts b/src/app/features/admin-institutions/mappers/institution-registrations.mapper.ts new file mode 100644 index 000000000..901f61c68 --- /dev/null +++ b/src/app/features/admin-institutions/mappers/institution-registrations.mapper.ts @@ -0,0 +1,57 @@ +import { + Affiliation, + IncludedItem, + IndexCard, + InstitutionRegistration, + InstitutionRegistrationsJsonApi, + SearchResult, +} from '../models'; + +export function mapInstitutionRegistrations(response: InstitutionRegistrationsJsonApi): InstitutionRegistration[] { + if (!response.included) { + return []; + } + + const searchResults = response.included.filter( + (item: IncludedItem): item is SearchResult => item.type === 'search-result' + ); + const indexCards = response.included.filter((item: IncludedItem): item is IndexCard => item.type === 'index-card'); + const registrations: InstitutionRegistration[] = []; + + searchResults.forEach((result: SearchResult) => { + const indexCardId = result.relationships?.indexCard?.data?.id; + if (indexCardId) { + const indexCard = indexCards.find((card: IndexCard) => card.id === indexCardId); + if (indexCard && indexCard.attributes) { + const metadata = indexCard.attributes.resourceMetadata; + + if (metadata) { + registrations.push({ + id: metadata['@id'] || indexCard.id, + title: metadata.title?.[0]?.['@value'] || '', + link: metadata['@id'] || '', + dateCreated: metadata.dateCreated?.[0]?.['@value'] || '', + dateModified: metadata.dateModified?.[0]?.['@value'] || '', + doi: metadata.identifier?.[0]?.['@value'] || '', + storageLocation: metadata.storageRegion?.[0]?.prefLabel?.[0]?.['@value'] || '', + totalDataStored: metadata.storageByteCount?.[0]?.['@value'] || '', + contributorName: metadata.creator?.[0]?.name?.[0]?.['@value'] || '', + views: metadata.usage?.[0]?.viewCount?.[0]?.['@value'] + ? parseInt(metadata.usage[0].viewCount[0]['@value']) + : undefined, + resourceType: metadata.resourceType?.[0]?.['@id'] || '', + license: metadata.rights?.[0]?.name?.[0]?.['@value'] || '', + funderName: + metadata.affiliation + ?.map((aff: Affiliation) => aff.name?.[0]?.['@value']) + .filter((value): value is string => Boolean(value)) + .join(', ') || '', + registrationSchema: metadata.subject?.[0]?.prefLabel?.[0]?.['@value'] || '', + }); + } + } + } + }); + + return registrations; +} diff --git a/src/app/features/admin-institutions/models/index.ts b/src/app/features/admin-institutions/models/index.ts index c9859791c..ac181316f 100644 --- a/src/app/features/admin-institutions/models/index.ts +++ b/src/app/features/admin-institutions/models/index.ts @@ -5,7 +5,9 @@ export * from './institution-project.model'; export * from './institution-project.model'; export * from './institution-projects-json-api.model'; export * from './institution-projects-query-params.model'; +export * from './institution-registration.model'; export * from './institution-registrations-json-api.model'; +export * from './institution-registrations-query-params.model'; export * from './institution-search-filter.model'; export * from './institution-summary-metric.model'; export * from './institution-summary-metrics-json-api.model'; diff --git a/src/app/features/admin-institutions/models/institution-registration.model.ts b/src/app/features/admin-institutions/models/institution-registration.model.ts new file mode 100644 index 000000000..ebf7ceee4 --- /dev/null +++ b/src/app/features/admin-institutions/models/institution-registration.model.ts @@ -0,0 +1,16 @@ +export interface InstitutionRegistration { + id: string; + title: string; + link: string; + dateCreated: string; + dateModified: string; + doi?: string; + storageLocation: string; + totalDataStored?: string; + contributorName: string; + views?: number; + resourceType: string; + license?: string; + funderName?: string; + registrationSchema?: string; +} diff --git a/src/app/features/admin-institutions/models/institution-registrations-query-params.model.ts b/src/app/features/admin-institutions/models/institution-registrations-query-params.model.ts new file mode 100644 index 000000000..96b8d5297 --- /dev/null +++ b/src/app/features/admin-institutions/models/institution-registrations-query-params.model.ts @@ -0,0 +1,5 @@ +export interface InstitutionRegistrationsQueryParams { + size?: number; + cursor?: string; + sort?: string; +} diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html index e69de29bb..3a3c8c57f 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html @@ -0,0 +1,14 @@ + +
+

{{ totalCount() }} {{ 'adminInstitutions.registrations.totalRegistrations' | translate }}

+
+
diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.scss b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.scss index e69de29bb..eab134e2c 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.scss +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.scss @@ -0,0 +1,3 @@ +.title { + color: var(--pr-blue-1); +} diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts index 5dbe432ab..2f1e14b56 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts @@ -1,10 +1,177 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { parseQueryFilterParams } from '@core/helpers'; +import { AdminTableComponent } from '@osf/features/admin-institutions/components'; +import { registrationTableColumns } from '@osf/features/admin-institutions/constants'; +import { mapRegistrationToTableData } from '@osf/features/admin-institutions/mappers/institution-registration-to-table-data.mapper'; +import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store'; +import { SortOrder } from '@shared/enums'; +import { QueryParams } from '@shared/models'; +import { InstitutionsSearchSelectors } from '@shared/stores'; + +import { FetchRegistrations } from '../../store/institutions-admin.actions'; + +import { environment } from 'src/environments/environment'; + +interface RegistrationQueryParams { + size?: number; + sort?: string; + cursor?: string; +} @Component({ selector: 'osf-institutions-registrations', - imports: [], + imports: [CommonModule, AdminTableComponent, TranslatePipe], templateUrl: './institutions-registrations.component.html', styleUrl: './institutions-registrations.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class InstitutionsRegistrationsComponent {} +export class InstitutionsRegistrationsComponent implements OnInit { + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + + private readonly actions = createDispatchMap({ + fetchRegistrations: FetchRegistrations, + }); + + institution = select(InstitutionsSearchSelectors.getInstitution); + registrations = select(InstitutionsAdminSelectors.getRegistrations); + totalCount = select(InstitutionsAdminSelectors.getRegistrationsTotalCount); + isLoading = select(InstitutionsAdminSelectors.getRegistrationsLoading); + registrationsLinks = select(InstitutionsAdminSelectors.getRegistrationsLinks); + + tableColumns = signal(registrationTableColumns); + reportsLink = 'https://drive.google.com/drive/folders/1_aFmeJwLp5xBS3-8clZ4xA9L3UFxdzDd'; + + currentPageSize = signal(10); + currentSort = signal('-dateModified'); + currentCursor = signal(''); + + private queryParams = signal({}); + + tableData = computed(() => { + const registrationsData = this.registrations(); + return registrationsData.map(mapRegistrationToTableData); + }); + + downloadLink = computed(() => { + const institution = this.institution(); + const queryParams = this.queryParams(); + + if (!institution?.iris?.length) { + return ''; + } + + const institutionIris = institution.iris.join(','); + const baseUrl = `${environment.shareDomainUrl}/index-card-search`; + const params = new URLSearchParams({ + 'cardSearchFilter[affiliation][]': institutionIris, + 'cardSearchFilter[resourceType]': 'Registration', + 'cardSearchFilter[accessService]': environment.webUrl, + 'page[size]': String(queryParams.size || this.currentPageSize()), + sort: queryParams.sort || this.currentSort(), + }); + + if (queryParams.cursor) { + params.append('page[cursor]', queryParams.cursor); + } + + return `${baseUrl}?${params.toString()}`; + }); + + constructor() { + this.setupQueryParamsEffect(); + } + + ngOnInit() { + this.loadRegistrations(); + } + + onSortChange(params: QueryParams): void { + this.updateQueryParams({ + sort: + params.sortColumn && params.sortOrder + ? params.sortOrder === SortOrder.Desc + ? `-${params.sortColumn}` + : params.sortColumn + : undefined, + }); + } + + onLinkPageChange(link: string): void { + const url = new URL(link); + const cursor = url.searchParams.get('page[cursor]') || ''; + this.updateQueryParams({ cursor }); + } + + private setupQueryParamsEffect(): void { + effect(() => { + this.route.queryParams.pipe(takeUntilDestroyed()).subscribe((params) => { + const queryParams = parseQueryFilterParams(params); + this.queryParams.set({ + size: queryParams.size, + sort: + queryParams.sortColumn && queryParams.sortOrder + ? queryParams.sortOrder === SortOrder.Desc + ? `-${queryParams.sortColumn}` + : queryParams.sortColumn + : undefined, + cursor: params['cursor'] || '', + }); + this.currentPageSize.set(queryParams.size || 10); + this.currentSort.set( + queryParams.sortColumn && queryParams.sortOrder + ? queryParams.sortOrder === SortOrder.Desc + ? `-${queryParams.sortColumn}` + : queryParams.sortColumn + : '-dateModified' + ); + this.currentCursor.set(params['cursor'] || ''); + }); + }); + } + + private loadRegistrations(): void { + const institution = this.institution(); + const institutionId = this.route.parent?.snapshot.params['institution-id']; + + if (!institutionId || !institution?.iris?.length) { + return; + } + + this.actions.fetchRegistrations( + institutionId, + institution.iris, + this.currentPageSize(), + this.currentSort(), + this.currentCursor() + ); + } + + private updateQueryParams(params: RegistrationQueryParams): void { + const queryParams: Record = {}; + + if (params.sort) { + queryParams['sort'] = params.sort; + } + if (params.cursor) { + queryParams['cursor'] = params.cursor; + } + if (params.size) { + queryParams['size'] = params.size.toString(); + } + + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: 'merge', + }); + } +} diff --git a/src/app/features/admin-institutions/services/institutions-admin.service.ts b/src/app/features/admin-institutions/services/institutions-admin.service.ts index 9dd5eedb2..a85e1de69 100644 --- a/src/app/features/admin-institutions/services/institutions-admin.service.ts +++ b/src/app/features/admin-institutions/services/institutions-admin.service.ts @@ -11,6 +11,7 @@ import { mapIndexCardResults, mapInstitutionDepartments, mapInstitutionProjects, + mapInstitutionRegistrations, mapInstitutionSummaryMetrics, mapInstitutionUsers, sendMessageRequestMapper, @@ -20,6 +21,7 @@ import { InstitutionDepartmentsJsonApi, InstitutionIndexValueSearchJsonApi, InstitutionProject, + InstitutionRegistration, InstitutionRegistrationsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetrics, @@ -92,44 +94,90 @@ export class InstitutionsAdminService { ); } - fetchProjects( - institutionId: string, - institutionIris: string[], - pageSize = 10, - sort = '-dateModified', - cursor = '' - ): Observable<{ - projects: InstitutionProject[]; - totalCount: number; - links?: PaginationLinksModel; - }> { - const url = `${environment.shareDomainUrl}/index-card-search`; - let params: Record = {}; - - const affiliationParam = institutionIris.join(','); - - params = { - 'cardSearchFilter[affiliation][]': affiliationParam, - 'cardSearchFilter[resourceType]': 'Project', - 'cardSearchFilter[accessService]': environment.webUrl, - 'page[cursor]': cursor, - 'page[size]': pageSize.toString(), - sort, - }; + fetchProjects(institutionId: string, iris: string[], pageSize = 10, sort = '-dateModified', cursor = '') { + return this.fetchIndexCards('Project', iris, pageSize, sort, cursor); + } - return this.jsonApiService.get(url, params).pipe( - map((response: InstitutionRegistrationsJsonApi) => { - const projects = mapInstitutionProjects(response); - const links = response.data.relationships.searchResultPage.links; - return { - projects, - totalCount: response.data.attributes.totalResultCount, - links, - }; - }) - ); + fetchRegistrations(institutionId: string, iris: string[], pageSize = 10, sort = '-dateModified', cursor = '') { + return this.fetchIndexCards('Registration', iris, pageSize, sort, cursor); } + // fetchProjects( + // institutionId: string, + // institutionIris: string[], + // pageSize = 10, + // sort = '-dateModified', + // cursor = '' + // ): Observable<{ + // projects: InstitutionProject[]; + // totalCount: number; + // links?: PaginationLinksModel; + // }> { + // const url = `${environment.shareDomainUrl}/index-card-search`; + // let params: Record = {}; + // + // const affiliationParam = institutionIris.join(','); + // + // params = { + // 'cardSearchFilter[affiliation][]': affiliationParam, + // 'cardSearchFilter[resourceType]': 'Project', + // 'cardSearchFilter[accessService]': environment.webUrl, + // 'page[cursor]': cursor, + // 'page[size]': pageSize.toString(), + // sort, + // }; + // + // return this.jsonApiService.get(url, params).pipe( + // map((response: InstitutionRegistrationsJsonApi) => { + // const projects = mapInstitutionProjects(response); + // const links = response.data.relationships.searchResultPage.links; + // return { + // projects, + // totalCount: response.data.attributes.totalResultCount, + // links, + // }; + // }) + // ); + // } + // + // fetchRegistrations( + // institutionId: string, + // institutionIris: string[], + // pageSize = 10, + // sort = '-dateModified', + // cursor = '' + // ): Observable<{ + // registrations: InstitutionRegistration[]; + // totalCount: number; + // links?: PaginationLinksModel; + // }> { + // const url = `${environment.shareDomainUrl}/index-card-search`; + // let params: Record = {}; + // + // const affiliationParam = institutionIris.join(','); + // + // params = { + // 'cardSearchFilter[affiliation][]': affiliationParam, + // 'cardSearchFilter[resourceType]': 'Registration', + // 'cardSearchFilter[accessService]': environment.webUrl, + // 'page[cursor]': cursor, + // 'page[size]': pageSize.toString(), + // sort, + // }; + // + // return this.jsonApiService.get(url, params).pipe( + // map((response: InstitutionRegistrationsJsonApi) => { + // const registrations = mapInstitutionRegistrations(response); + // const links = response.data.relationships.searchResultPage.links; + // return { + // registrations, + // totalCount: response.data.attributes.totalResultCount, + // links, + // }; + // }) + // ); + // } + fetchIndexValueSearch( institutionId: string, valueSearchPropertyPath: string, @@ -153,4 +201,40 @@ export class InstitutionsAdminService { return this.jsonApiService.post(`${this.hardcodedUrl}/institutions/messages/`, payload); } + + private fetchIndexCards( + resourceType: 'Project' | 'Registration', + institutionIris: string[], + pageSize = 10, + sort = '-dateModified', + cursor = '' + ): Observable<{ + items: InstitutionProject[] | InstitutionRegistration[]; + totalCount: number; + links?: PaginationLinksModel; + }> { + const url = `${environment.shareDomainUrl}/index-card-search`; + const affiliationParam = institutionIris.join(','); + + const params: Record = { + 'cardSearchFilter[affiliation][]': affiliationParam, + 'cardSearchFilter[resourceType]': resourceType, // ← різниця + 'cardSearchFilter[accessService]': environment.webUrl, + 'page[cursor]': cursor, + 'page[size]': pageSize.toString(), + sort, + }; + + return this.jsonApiService.get(url, params).pipe( + map((res) => { + const mapper = resourceType === 'Project' ? mapInstitutionProjects : mapInstitutionRegistrations; + + return { + items: mapper(res), + totalCount: res.data.attributes.totalResultCount, + links: res.data.relationships.searchResultPage.links, + }; + }) + ); + } } diff --git a/src/app/features/admin-institutions/store/institutions-admin.actions.ts b/src/app/features/admin-institutions/store/institutions-admin.actions.ts index a44bd7457..c04efa7c3 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.actions.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.actions.ts @@ -49,6 +49,17 @@ export class FetchProjects { ) {} } +export class FetchRegistrations { + static readonly type = '[InstitutionsAdmin] Fetch Registrations'; + constructor( + public institutionId: string, + public institutionIris: string[], + public pageSize = 10, + public sort = '-dateModified', + public cursor = '' + ) {} +} + export class SendUserMessage { static readonly type = '[InstitutionsAdmin] Send User Message'; constructor( diff --git a/src/app/features/admin-institutions/store/institutions-admin.model.ts b/src/app/features/admin-institutions/store/institutions-admin.model.ts index 473fcbd94..10f163f9c 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.model.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.model.ts @@ -3,6 +3,7 @@ import { AsyncStateModel, AsyncStateWithLinksModel, AsyncStateWithTotalCount } f import { InstitutionDepartment, InstitutionProject, + InstitutionRegistration, InstitutionSearchFilter, InstitutionSummaryMetrics, InstitutionUser, @@ -17,6 +18,7 @@ export interface InstitutionsAdminModel { searchResults: AsyncStateModel; users: AsyncStateWithTotalCount; projects: AsyncStateWithLinksModel; + registrations: AsyncStateWithLinksModel; sendMessage: AsyncStateModel; selectedInstitutionId: string | null; currentSearchPropertyPath: string | null; diff --git a/src/app/features/admin-institutions/store/institutions-admin.selectors.ts b/src/app/features/admin-institutions/store/institutions-admin.selectors.ts index 91959090f..898982f2d 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.selectors.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.selectors.ts @@ -5,6 +5,7 @@ import { PaginationLinksModel } from '@shared/models'; import { InstitutionDepartment, InstitutionProject, + InstitutionRegistration, InstitutionSearchFilter, InstitutionSummaryMetrics, InstitutionUser, @@ -140,6 +141,26 @@ export class InstitutionsAdminSelectors { return state.projects.links; } + @Selector([InstitutionsAdminState]) + static getRegistrations(state: InstitutionsAdminModel): InstitutionRegistration[] { + return state.registrations.data; + } + + @Selector([InstitutionsAdminState]) + static getRegistrationsLoading(state: InstitutionsAdminModel): boolean { + return state.registrations.isLoading; + } + + @Selector([InstitutionsAdminState]) + static getRegistrationsTotalCount(state: InstitutionsAdminModel): number { + return state.registrations.totalCount; + } + + @Selector([InstitutionsAdminState]) + static getRegistrationsLinks(state: InstitutionsAdminModel): PaginationLinksModel | undefined { + return state.registrations.links; + } + @Selector([InstitutionsAdminState]) static getSendMessageResponse(state: InstitutionsAdminModel): SendMessageResponseJsonApi | null { return state.sendMessage.data; diff --git a/src/app/features/admin-institutions/store/institutions-admin.state.ts b/src/app/features/admin-institutions/store/institutions-admin.state.ts index dfb3b636f..491c63d4d 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.state.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.state.ts @@ -6,7 +6,7 @@ import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@core/handlers'; -import { InstitutionSummaryMetrics } from '../models'; +import { InstitutionProject, InstitutionRegistration, InstitutionSummaryMetrics } from '../models'; import { InstitutionsAdminService } from '../services/institutions-admin.service'; import { @@ -16,6 +16,7 @@ import { FetchInstitutionSummaryMetrics, FetchInstitutionUsers, FetchProjects, + FetchRegistrations, FetchStorageRegionSearch, SendUserMessage, } from './institutions-admin.actions'; @@ -31,6 +32,7 @@ import { InstitutionsAdminModel } from './institutions-admin.model'; searchResults: { data: [], isLoading: false, error: null }, users: { data: [], totalCount: 0, isLoading: false, error: null }, projects: { data: [], totalCount: 0, isLoading: false, error: null, links: undefined }, + registrations: { data: [], totalCount: 0, isLoading: false, error: null, links: undefined }, sendMessage: { data: null, isLoading: false, error: null }, selectedInstitutionId: null, currentSearchPropertyPath: null, @@ -161,7 +163,7 @@ export class InstitutionsAdminState { tap((response) => { ctx.patchState({ projects: { - data: response.projects, + data: response.items as InstitutionProject[], totalCount: response.totalCount, isLoading: false, error: null, @@ -173,6 +175,31 @@ export class InstitutionsAdminState { ); } + @Action(FetchRegistrations) + fetchRegistrations(ctx: StateContext, action: FetchRegistrations) { + const state = ctx.getState(); + ctx.patchState({ + registrations: { ...state.registrations, isLoading: true, error: null }, + }); + + return this.institutionsAdminService + .fetchRegistrations(action.institutionId, action.institutionIris, action.pageSize, action.sort, action.cursor) + .pipe( + tap((response) => { + ctx.patchState({ + registrations: { + data: response.items as InstitutionRegistration[], + totalCount: response.totalCount, + isLoading: false, + error: null, + links: response.links, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'registrations', error)) + ); + } + @Action(SendUserMessage) sendUserMessage(ctx: StateContext, action: SendUserMessage) { const state = ctx.getState(); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index ff1f3bbc1..fe13e2e4c 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2164,6 +2164,13 @@ "addOns": "Add-ons", "funderName": "Funder Name", "totalProjects": "Total Projects" + }, + "registrations": { + "doi": "DOI", + "storageLocation": "Storage Location", + "funderName": "Funder Name", + "registrationSchema": "Registration Schema", + "totalRegistrations": "Total Registrations" } } } From ffd9a167c113b70bd27e0b080fe78669dd0277a0 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Wed, 16 Jul 2025 13:37:40 +0300 Subject: [PATCH 2/3] feat(admin-users): added inst registration and preprints pages --- .../admin-institutions/constants/index.ts | 1 + .../preprints-table-columns.constant.ts | 58 ++++++ .../project-table-columns.constant.ts | 2 +- .../registration-table-columns.constant.ts | 4 +- .../helpers/extract-path-after-domain.ts | 4 + .../admin-institutions/helpers/index.ts | 1 + .../admin-institutions/mappers/index.ts | 2 + ...stitution-preprint-to-table-data.mapper.ts | 37 ++++ .../mappers/institution-preprints.mapper.ts | 40 ++++ ...ution-registration-to-table-data.mapper.ts | 5 +- .../admin-institutions/models/index.ts | 1 + .../models/institution-preprint.model.ts | 13 ++ .../institutions-preprints.component.html | 26 +++ .../institutions-preprints.component.scss | 3 + .../institutions-preprints.component.ts | 179 +++++++++++++++++- .../institutions-projects.component.html | 49 +++-- .../institutions-projects.component.ts | 5 +- .../institutions-registrations.component.html | 38 ++-- .../institutions-registrations.component.ts | 124 ++++++------ .../institutions-users.component.html | 90 +++++---- .../institutions-users.component.ts | 4 +- .../services/institutions-admin.service.ts | 101 +++------- .../store/institutions-admin.actions.ts | 11 ++ .../store/institutions-admin.model.ts | 2 + .../store/institutions-admin.selectors.ts | 21 ++ .../store/institutions-admin.state.ts | 29 ++- .../institutions/institutions.component.scss | 3 + .../institutions/institutions.component.ts | 1 + src/assets/i18n/en.json | 16 +- 29 files changed, 639 insertions(+), 231 deletions(-) create mode 100644 src/app/features/admin-institutions/constants/preprints-table-columns.constant.ts create mode 100644 src/app/features/admin-institutions/helpers/extract-path-after-domain.ts create mode 100644 src/app/features/admin-institutions/helpers/index.ts create mode 100644 src/app/features/admin-institutions/mappers/institution-preprint-to-table-data.mapper.ts create mode 100644 src/app/features/admin-institutions/mappers/institution-preprints.mapper.ts create mode 100644 src/app/features/admin-institutions/models/institution-preprint.model.ts create mode 100644 src/app/features/institutions/institutions.component.scss diff --git a/src/app/features/admin-institutions/constants/index.ts b/src/app/features/admin-institutions/constants/index.ts index 7410fa967..7c1360d02 100644 --- a/src/app/features/admin-institutions/constants/index.ts +++ b/src/app/features/admin-institutions/constants/index.ts @@ -1,5 +1,6 @@ export * from './admin-table-columns.constant'; export * from './department-options.constant'; +export * from './preprints-table-columns.constant'; export * from './project-table-columns.constant'; export * from './registration-table-columns.constant'; export * from './resource-tab-option.constant'; diff --git a/src/app/features/admin-institutions/constants/preprints-table-columns.constant.ts b/src/app/features/admin-institutions/constants/preprints-table-columns.constant.ts new file mode 100644 index 000000000..a17b765d7 --- /dev/null +++ b/src/app/features/admin-institutions/constants/preprints-table-columns.constant.ts @@ -0,0 +1,58 @@ +import { TableColumn } from '@osf/features/admin-institutions/models'; + +export const preprintsTableColumns: TableColumn[] = [ + { + field: 'title', + header: 'adminInstitutions.projects.title', + isLink: true, + linkTarget: '_blank', + }, + { + field: 'link', + header: 'adminInstitutions.projects.link', + sortable: false, + isLink: true, + linkTarget: '_blank', + }, + { + field: 'dateCreated', + header: 'adminInstitutions.projects.dateCreated', + sortable: true, + dateFormat: 'dd/MM/yyyy', + }, + { + field: 'dateModified', + header: 'adminInstitutions.projects.dateModified', + sortable: true, + dateFormat: 'dd/MM/yyyy', + }, + { + field: 'doi', + header: 'adminInstitutions.projects.doi', + isLink: true, + linkTarget: '_blank', + sortable: false, + }, + { + field: 'license', + header: 'adminInstitutions.projects.license', + sortable: false, + }, + { + field: 'contributorName', + header: 'adminInstitutions.projects.contributorName', + sortable: true, + isLink: true, + linkTarget: '_blank', + }, + { + field: 'viewsLast30Days', + header: 'adminInstitutions.projects.views', + sortable: false, + }, + { + field: 'downloadsLast30Days', + header: 'adminInstitutions.preprints.downloadsLastDays', + sortable: false, + }, +]; diff --git a/src/app/features/admin-institutions/constants/project-table-columns.constant.ts b/src/app/features/admin-institutions/constants/project-table-columns.constant.ts index b1d7be4a6..51628fe50 100644 --- a/src/app/features/admin-institutions/constants/project-table-columns.constant.ts +++ b/src/app/features/admin-institutions/constants/project-table-columns.constant.ts @@ -4,7 +4,7 @@ export const projectTableColumns: TableColumn[] = [ { field: 'title', header: 'adminInstitutions.projects.title', - sortable: false, + sortable: true, isLink: true, linkTarget: '_blank', }, diff --git a/src/app/features/admin-institutions/constants/registration-table-columns.constant.ts b/src/app/features/admin-institutions/constants/registration-table-columns.constant.ts index 2a6ec07ec..b301d7174 100644 --- a/src/app/features/admin-institutions/constants/registration-table-columns.constant.ts +++ b/src/app/features/admin-institutions/constants/registration-table-columns.constant.ts @@ -29,14 +29,14 @@ export const registrationTableColumns: TableColumn[] = [ }, { field: 'doi', - header: 'adminInstitutions.registrations.doi', + header: 'adminInstitutions.projects.doi', sortable: false, isLink: true, linkTarget: '_blank', }, { field: 'storageLocation', - header: 'adminInstitutions.registrations.storageLocation', + header: 'adminInstitutions.projects.storageLocation', sortable: false, }, { diff --git a/src/app/features/admin-institutions/helpers/extract-path-after-domain.ts b/src/app/features/admin-institutions/helpers/extract-path-after-domain.ts new file mode 100644 index 000000000..019c44bb6 --- /dev/null +++ b/src/app/features/admin-institutions/helpers/extract-path-after-domain.ts @@ -0,0 +1,4 @@ +export function extractPathAfterDomain(url: string): string { + const parsedUrl = new URL(url); + return parsedUrl.pathname.replace(/^\/+/, ''); +} diff --git a/src/app/features/admin-institutions/helpers/index.ts b/src/app/features/admin-institutions/helpers/index.ts new file mode 100644 index 000000000..779fca02f --- /dev/null +++ b/src/app/features/admin-institutions/helpers/index.ts @@ -0,0 +1 @@ +export * from './extract-path-after-domain'; diff --git a/src/app/features/admin-institutions/mappers/index.ts b/src/app/features/admin-institutions/mappers/index.ts index 88c77ca06..ea84875b8 100644 --- a/src/app/features/admin-institutions/mappers/index.ts +++ b/src/app/features/admin-institutions/mappers/index.ts @@ -1,6 +1,8 @@ export { mapInstitutionDepartment, mapInstitutionDepartments } from './institution-departments.mapper'; +export { mapPreprintToTableData } from './institution-preprint-to-table-data.mapper'; export { mapProjectToTableCellData } from './institution-project-to-table-data.mapper'; export { mapInstitutionProjects } from './institution-projects.mapper'; +export { mapRegistrationToTableData } from './institution-registration-to-table-data.mapper'; export { mapInstitutionRegistrations } from './institution-registrations.mapper'; export { mapIndexCardResults } from './institution-summary-index.mapper'; export { mapInstitutionSummaryMetrics } from './institution-summary-metrics.mapper'; diff --git a/src/app/features/admin-institutions/mappers/institution-preprint-to-table-data.mapper.ts b/src/app/features/admin-institutions/mappers/institution-preprint-to-table-data.mapper.ts new file mode 100644 index 000000000..de1426c7a --- /dev/null +++ b/src/app/features/admin-institutions/mappers/institution-preprint-to-table-data.mapper.ts @@ -0,0 +1,37 @@ +import { extractPathAfterDomain } from '@osf/features/admin-institutions/helpers'; + +import { InstitutionPreprint, TableCellData, TableCellLink } from '../models'; + +export function mapPreprintToTableData(preprint: InstitutionPreprint): TableCellData { + return { + id: preprint.id, + title: { + text: preprint.title, + url: preprint.link, + target: '_blank', + } as TableCellLink, + link: { + text: preprint.link.split('/').pop() || preprint.link, + url: preprint.link, + target: '_blank', + } as TableCellLink, + dateCreated: preprint.dateCreated, + dateModified: preprint.dateModified, + doi: preprint.doi + ? ({ + text: extractPathAfterDomain(preprint.doi), + url: preprint.doi, + } as TableCellLink) + : '-', + license: preprint.license || '-', + contributorName: preprint.contributorName + ? ({ + text: preprint.contributorName, + url: `https://osf.io/${preprint.contributorName}`, + target: '_blank', + } as TableCellLink) + : '-', + viewsLast30Days: preprint.viewsLast30Days || '-', + downloadsLast30Days: preprint.downloadsLast30Days || '-', + }; +} diff --git a/src/app/features/admin-institutions/mappers/institution-preprints.mapper.ts b/src/app/features/admin-institutions/mappers/institution-preprints.mapper.ts new file mode 100644 index 000000000..ba9e972f1 --- /dev/null +++ b/src/app/features/admin-institutions/mappers/institution-preprints.mapper.ts @@ -0,0 +1,40 @@ +import { IncludedItem, IndexCard, InstitutionPreprint, InstitutionRegistrationsJsonApi, SearchResult } from '../models'; + +export function mapInstitutionPreprints(response: InstitutionRegistrationsJsonApi): InstitutionPreprint[] { + if (!response.included) { + return []; + } + + const searchResults = response.included.filter( + (item: IncludedItem): item is SearchResult => item.type === 'search-result' + ); + const indexCards = response.included.filter((item: IncludedItem): item is IndexCard => item.type === 'index-card'); + + const preprints: InstitutionPreprint[] = []; + + searchResults.forEach((result: SearchResult) => { + const indexCardId = result.relationships?.indexCard?.data?.id; + if (indexCardId) { + const indexCard = indexCards.find((card: IndexCard) => card.id === indexCardId); + if (indexCard && indexCard.attributes) { + const metadata = indexCard.attributes.resourceMetadata; + + if (metadata) { + preprints.push({ + id: metadata['@id'] || indexCard.id, + title: metadata.title?.[0]?.['@value'] || '', + link: metadata['@id'] || '', + dateCreated: metadata.dateCreated?.[0]?.['@value'] || '', + dateModified: metadata.dateModified?.[0]?.['@value'] || '', + doi: metadata.identifier?.[0]?.['@value'] || '', + contributorName: metadata.creator?.[0]?.name?.[0]?.['@value'] || '', + license: metadata.rights?.[0]?.name?.[0]?.['@value'] || '', + registrationSchema: metadata.subject?.[0]?.prefLabel?.[0]?.['@value'] || '', + }); + } + } + } + }); + + return preprints; +} diff --git a/src/app/features/admin-institutions/mappers/institution-registration-to-table-data.mapper.ts b/src/app/features/admin-institutions/mappers/institution-registration-to-table-data.mapper.ts index 1e8b0a679..1ca7b345d 100644 --- a/src/app/features/admin-institutions/mappers/institution-registration-to-table-data.mapper.ts +++ b/src/app/features/admin-institutions/mappers/institution-registration-to-table-data.mapper.ts @@ -1,3 +1,5 @@ +import { extractPathAfterDomain } from '@osf/features/admin-institutions/helpers'; + import { InstitutionRegistration, TableCellData, TableCellLink } from '../models'; export function mapRegistrationToTableData(registration: InstitutionRegistration): TableCellData { @@ -17,9 +19,8 @@ export function mapRegistrationToTableData(registration: InstitutionRegistration dateModified: registration.dateModified, doi: registration.doi ? ({ - text: registration.doi?.split('org/')[1], + text: extractPathAfterDomain(registration.doi), url: registration.doi, - target: '_blank', } as TableCellLink) : '-', storageLocation: registration.storageLocation || '-', diff --git a/src/app/features/admin-institutions/models/index.ts b/src/app/features/admin-institutions/models/index.ts index ac181316f..fc9989a54 100644 --- a/src/app/features/admin-institutions/models/index.ts +++ b/src/app/features/admin-institutions/models/index.ts @@ -1,6 +1,7 @@ export * from './institution-department.model'; export * from './institution-departments-json-api.model'; export * from './institution-index-value-search-json-api.model'; +export * from './institution-preprint.model'; export * from './institution-project.model'; export * from './institution-project.model'; export * from './institution-projects-json-api.model'; diff --git a/src/app/features/admin-institutions/models/institution-preprint.model.ts b/src/app/features/admin-institutions/models/institution-preprint.model.ts new file mode 100644 index 000000000..ad6d3c410 --- /dev/null +++ b/src/app/features/admin-institutions/models/institution-preprint.model.ts @@ -0,0 +1,13 @@ +export interface InstitutionPreprint { + id: string; + title: string; + link: string; + dateCreated: string; + dateModified: string; + doi?: string; + license?: string; + contributorName: string; + viewsLast30Days?: number; + downloadsLast30Days?: number; + registrationSchema?: string; +} diff --git a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html index e69de29bb..ddf31efc3 100644 --- a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html +++ b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html @@ -0,0 +1,26 @@ +@if (isLoading()) { +
+ +
+} @else if (tableData().length > 0) { + +
+

+ {{ totalCount() }} {{ 'adminInstitutions.preprints.totalPreprints' | translate | lowercase }} +

+
+
+} @else { +
+

{{ 'adminInstitutions.preprints.noData' | translate }}

+
+} diff --git a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.scss b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.scss index e69de29bb..eab134e2c 100644 --- a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.scss +++ b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.scss @@ -0,0 +1,3 @@ +.title { + color: var(--pr-blue-1); +} diff --git a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts index 493f5ce8b..836a0f9af 100644 --- a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts @@ -1,10 +1,183 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, untracked } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Params, Router } from '@angular/router'; + +import { parseQueryFilterParams } from '@core/helpers'; +import { AdminTableComponent } from '@osf/features/admin-institutions/components'; +import { preprintsTableColumns } from '@osf/features/admin-institutions/constants'; +import { mapPreprintToTableData } from '@osf/features/admin-institutions/mappers'; +import { InstitutionProjectsQueryParamsModel, TableCellData } from '@osf/features/admin-institutions/models'; +import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store'; +import { LoadingSpinnerComponent } from '@osf/shared/components'; +import { TABLE_PARAMS } from '@shared/constants'; +import { SortOrder } from '@shared/enums'; +import { Institution, QueryParams } from '@shared/models'; +import { InstitutionsSearchSelectors } from '@shared/stores'; + +import { FetchPreprints } from '../../store/institutions-admin.actions'; + +import { environment } from 'src/environments/environment'; + +interface PreprintQueryParams { + size?: number; + sort?: string; + cursor?: string; +} @Component({ selector: 'osf-institutions-preprints', - imports: [], + imports: [CommonModule, AdminTableComponent, TranslatePipe, LoadingSpinnerComponent], templateUrl: './institutions-preprints.component.html', styleUrl: './institutions-preprints.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class InstitutionsPreprintsComponent {} +export class InstitutionsPreprintsComponent { + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + + private readonly actions = createDispatchMap({ + fetchPreprints: FetchPreprints, + }); + + private institutionId = ''; + + institution = select(InstitutionsSearchSelectors.getInstitution); + preprints = select(InstitutionsAdminSelectors.getPreprints); + totalCount = select(InstitutionsAdminSelectors.getPreprintsTotalCount); + isLoading = select(InstitutionsAdminSelectors.getPreprintsLoading); + preprintsLinks = select(InstitutionsAdminSelectors.getPreprintsLinks); + + tableColumns = signal(preprintsTableColumns); + reportsLink = 'https://drive.google.com/drive/folders/1_aFmeJwLp5xBS3-8clZ4xA9L3UFxdzDd'; + + queryParams = toSignal(this.route.queryParams); + currentPageSize = signal(TABLE_PARAMS.rows); + currentSort = signal('-dateModified'); + sortField = signal('-dateModified'); + sortOrder = signal(1); + + currentCursor = signal(''); + + tableData = computed(() => { + const preprintsData = this.preprints(); + return preprintsData.map(mapPreprintToTableData) as TableCellData[]; + }); + + downloadLink = computed(() => { + const institution = this.institution(); + const queryParams = this.queryParams(); + + if (!institution?.iris?.length) { + return ''; + } + + const institutionIris = institution.iris.join(','); + const baseUrl = `${environment.shareDomainUrl}/index-card-search`; + let params = new URLSearchParams(); + if (queryParams) { + params = new URLSearchParams({ + 'cardSearchFilter[affiliation][]': institutionIris, + 'cardSearchFilter[resourceType]': 'Preprint', + 'cardSearchFilter[accessService]': environment.webUrl, + 'page[size]': String(queryParams['size'] || this.currentPageSize()), + sort: queryParams['sort'] || this.currentSort(), + }); + } + + if (queryParams && queryParams['cursor']) { + params.append('page[cursor]', queryParams['cursor']); + } + + return `${baseUrl}?${params.toString()}`; + }); + + constructor() { + this.setupQueryParamsEffect(); + } + + onSortChange(params: QueryParams): void { + this.updateQueryParams({ + sort: + params.sortColumn && params.sortOrder + ? params.sortOrder === SortOrder.Desc + ? `-${params.sortColumn}` + : params.sortColumn + : undefined, + }); + } + + onLinkPageChange(link: string): void { + const url = new URL(link); + const cursor = url.searchParams.get('page[cursor]') || ''; + this.updateQueryParams({ cursor }); + } + + private setupQueryParamsEffect(): void { + effect(() => { + const institutionId = this.route.parent?.snapshot.params['institution-id']; + const rawQueryParams = this.queryParams(); + if (!rawQueryParams && !institutionId) return; + + this.institutionId = institutionId; + const parsedQueryParams = this.parseQueryParams(rawQueryParams as Params); + + this.updateComponentState(parsedQueryParams); + + const sortField = parsedQueryParams.sortColumn; + const sortOrder = parsedQueryParams.sortOrder; + const sortParam = sortOrder === SortOrder.Desc ? `-${sortField}` : sortField; + const cursor = parsedQueryParams.cursor; + const size = parsedQueryParams.size; + + const institution = this.institution() as Institution; + const institutionIris = institution.iris || []; + + this.actions.fetchPreprints(this.institutionId, institutionIris, size, sortParam, cursor); + }); + } + + private parseQueryParams(params: Params): InstitutionProjectsQueryParamsModel { + const parsed = parseQueryFilterParams(params); + return { + ...parsed, + cursor: params['cursor'] || '', + }; + } + + private updateComponentState(params: InstitutionProjectsQueryParamsModel): void { + untracked(() => { + this.currentPageSize.set(params.size); + + if (params.sortColumn) { + this.sortField.set(params.sortColumn); + const order = params.sortOrder === SortOrder.Desc ? -1 : 1; + this.sortOrder.set(order); + } + }); + } + + private updateQueryParams(params: PreprintQueryParams): void { + const queryParams: Record = {}; + + if (params.sort) { + queryParams['sort'] = params.sort; + } + if (params.cursor) { + queryParams['cursor'] = params.cursor; + } + if (params.size) { + queryParams['size'] = params.size.toString(); + } + + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: 'merge', + }); + } +} diff --git a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html index 7924855a6..85b3ad42c 100644 --- a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html +++ b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html @@ -1,21 +1,30 @@ - -
-

{{ totalCount() }} {{ 'adminInstitutions.projects.totalProjects' | translate }}

+@if (isLoading()) { +
+
- +} @else if (tableData().length > 0) { + +
+

{{ totalCount() }} {{ 'adminInstitutions.projects.totalProjects' | translate }}

+
+
+} @else { +
+

{{ 'adminInstitutions.projects.noData' | translate }}

+
+} diff --git a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts index ba83d069d..778003d75 100644 --- a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts @@ -12,6 +12,7 @@ import { projectTableColumns } from '@osf/features/admin-institutions/constants' import { mapProjectToTableCellData } from '@osf/features/admin-institutions/mappers'; import { FetchProjects } from '@osf/features/admin-institutions/store/institutions-admin.actions'; import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store/institutions-admin.selectors'; +import { LoadingSpinnerComponent } from '@osf/shared/components'; import { TABLE_PARAMS } from '@shared/constants'; import { SortOrder } from '@shared/enums'; import { Institution, QueryParams } from '@shared/models'; @@ -23,7 +24,7 @@ import { environment } from 'src/environments/environment'; @Component({ selector: 'osf-institutions-projects', - imports: [AdminTableComponent, TranslatePipe], + imports: [AdminTableComponent, TranslatePipe, LoadingSpinnerComponent], templateUrl: './institutions-projects.component.html', styleUrl: './institutions-projects.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -40,7 +41,6 @@ export class InstitutionsProjectsComponent { reportsLink = 'https://drive.google.com/drive/folders/1_aFmeJwLp5xBS3-8clZ4xA9L3UFxdzDd'; queryParams = toSignal(this.route.queryParams); - currentPage = signal(1); currentPageSize = signal(TABLE_PARAMS.rows); first = signal(0); @@ -175,7 +175,6 @@ export class InstitutionsProjectsComponent { private updateComponentState(params: InstitutionProjectsQueryParamsModel): void { untracked(() => { - this.currentPage.set(params.page); this.currentPageSize.set(params.size); this.first.set((params.page - 1) * params.size); diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html index 3a3c8c57f..586d48b27 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html @@ -1,14 +1,26 @@ - -
-

{{ totalCount() }} {{ 'adminInstitutions.registrations.totalRegistrations' | translate }}

+@if (isLoading()) { +
+
- +} @else if (tableData().length > 0) { + +
+

+ {{ totalCount() }} {{ 'adminInstitutions.registrations.totalRegistrations' | translate | lowercase }} +

+
+
+} @else { +
+

{{ 'adminInstitutions.registrations.noData' | translate }}

+
+} diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts index 2f1e14b56..a82afc51e 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts @@ -3,17 +3,20 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, effect, inject, OnInit, signal } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, untracked } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Params, Router } from '@angular/router'; import { parseQueryFilterParams } from '@core/helpers'; import { AdminTableComponent } from '@osf/features/admin-institutions/components'; import { registrationTableColumns } from '@osf/features/admin-institutions/constants'; -import { mapRegistrationToTableData } from '@osf/features/admin-institutions/mappers/institution-registration-to-table-data.mapper'; +import { mapRegistrationToTableData } from '@osf/features/admin-institutions/mappers'; +import { InstitutionProjectsQueryParamsModel, TableCellData } from '@osf/features/admin-institutions/models'; import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store'; +import { LoadingSpinnerComponent } from '@osf/shared/components'; +import { TABLE_PARAMS } from '@shared/constants'; import { SortOrder } from '@shared/enums'; -import { QueryParams } from '@shared/models'; +import { Institution, QueryParams } from '@shared/models'; import { InstitutionsSearchSelectors } from '@shared/stores'; import { FetchRegistrations } from '../../store/institutions-admin.actions'; @@ -28,12 +31,12 @@ interface RegistrationQueryParams { @Component({ selector: 'osf-institutions-registrations', - imports: [CommonModule, AdminTableComponent, TranslatePipe], + imports: [CommonModule, AdminTableComponent, TranslatePipe, LoadingSpinnerComponent], templateUrl: './institutions-registrations.component.html', styleUrl: './institutions-registrations.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class InstitutionsRegistrationsComponent implements OnInit { +export class InstitutionsRegistrationsComponent { private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); @@ -41,6 +44,8 @@ export class InstitutionsRegistrationsComponent implements OnInit { fetchRegistrations: FetchRegistrations, }); + private institutionId = ''; + institution = select(InstitutionsSearchSelectors.getInstitution); registrations = select(InstitutionsAdminSelectors.getRegistrations); totalCount = select(InstitutionsAdminSelectors.getRegistrationsTotalCount); @@ -50,15 +55,15 @@ export class InstitutionsRegistrationsComponent implements OnInit { tableColumns = signal(registrationTableColumns); reportsLink = 'https://drive.google.com/drive/folders/1_aFmeJwLp5xBS3-8clZ4xA9L3UFxdzDd'; - currentPageSize = signal(10); + queryParams = toSignal(this.route.queryParams); + currentPageSize = signal(TABLE_PARAMS.rows); currentSort = signal('-dateModified'); - currentCursor = signal(''); - - private queryParams = signal({}); + sortField = signal('-dateModified'); + sortOrder = signal(1); tableData = computed(() => { const registrationsData = this.registrations(); - return registrationsData.map(mapRegistrationToTableData); + return registrationsData.map(mapRegistrationToTableData) as TableCellData[]; }); downloadLink = computed(() => { @@ -71,16 +76,19 @@ export class InstitutionsRegistrationsComponent implements OnInit { const institutionIris = institution.iris.join(','); const baseUrl = `${environment.shareDomainUrl}/index-card-search`; - const params = new URLSearchParams({ - 'cardSearchFilter[affiliation][]': institutionIris, - 'cardSearchFilter[resourceType]': 'Registration', - 'cardSearchFilter[accessService]': environment.webUrl, - 'page[size]': String(queryParams.size || this.currentPageSize()), - sort: queryParams.sort || this.currentSort(), - }); + let params = new URLSearchParams(); + if (queryParams) { + params = new URLSearchParams({ + 'cardSearchFilter[affiliation][]': institutionIris, + 'cardSearchFilter[resourceType]': 'Registration', + 'cardSearchFilter[accessService]': environment.webUrl, + 'page[size]': String(queryParams['size'] || this.currentPageSize()), + sort: queryParams['sort'] || this.currentSort(), + }); + } - if (queryParams.cursor) { - params.append('page[cursor]', queryParams.cursor); + if (queryParams && queryParams['cursor']) { + params.append('page[cursor]', queryParams['cursor']); } return `${baseUrl}?${params.toString()}`; @@ -90,10 +98,6 @@ export class InstitutionsRegistrationsComponent implements OnInit { this.setupQueryParamsEffect(); } - ngOnInit() { - this.loadRegistrations(); - } - onSortChange(params: QueryParams): void { this.updateQueryParams({ sort: @@ -113,46 +117,46 @@ export class InstitutionsRegistrationsComponent implements OnInit { private setupQueryParamsEffect(): void { effect(() => { - this.route.queryParams.pipe(takeUntilDestroyed()).subscribe((params) => { - const queryParams = parseQueryFilterParams(params); - this.queryParams.set({ - size: queryParams.size, - sort: - queryParams.sortColumn && queryParams.sortOrder - ? queryParams.sortOrder === SortOrder.Desc - ? `-${queryParams.sortColumn}` - : queryParams.sortColumn - : undefined, - cursor: params['cursor'] || '', - }); - this.currentPageSize.set(queryParams.size || 10); - this.currentSort.set( - queryParams.sortColumn && queryParams.sortOrder - ? queryParams.sortOrder === SortOrder.Desc - ? `-${queryParams.sortColumn}` - : queryParams.sortColumn - : '-dateModified' - ); - this.currentCursor.set(params['cursor'] || ''); - }); + const institutionId = this.route.parent?.snapshot.params['institution-id']; + const rawQueryParams = this.queryParams(); + if (!rawQueryParams && !institutionId) return; + + this.institutionId = institutionId; + const parsedQueryParams = this.parseQueryParams(rawQueryParams as Params); + + this.updateComponentState(parsedQueryParams); + + const sortField = parsedQueryParams.sortColumn; + const sortOrder = parsedQueryParams.sortOrder; + const sortParam = sortOrder === SortOrder.Desc ? `-${sortField}` : sortField; + const cursor = parsedQueryParams.cursor; + const size = parsedQueryParams.size; + + const institution = this.institution() as Institution; + const institutionIris = institution.iris || []; + + this.actions.fetchRegistrations(this.institutionId, institutionIris, size, sortParam, cursor); }); } - private loadRegistrations(): void { - const institution = this.institution(); - const institutionId = this.route.parent?.snapshot.params['institution-id']; + private parseQueryParams(params: Params): InstitutionProjectsQueryParamsModel { + const parsed = parseQueryFilterParams(params); + return { + ...parsed, + cursor: params['cursor'] || '', + }; + } - if (!institutionId || !institution?.iris?.length) { - return; - } + private updateComponentState(params: InstitutionProjectsQueryParamsModel): void { + untracked(() => { + this.currentPageSize.set(params.size); - this.actions.fetchRegistrations( - institutionId, - institution.iris, - this.currentPageSize(), - this.currentSort(), - this.currentCursor() - ); + if (params.sortColumn) { + this.sortField.set(params.sortColumn); + const order = params.sortOrder === SortOrder.Desc ? -1 : 1; + this.sortOrder.set(order); + } + }); } private updateQueryParams(params: RegistrationQueryParams): void { diff --git a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html index 8710a5fa3..cd2a4694b 100644 --- a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html +++ b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html @@ -1,45 +1,55 @@ - -
-

{{ amountText() }}

+@if (isLoading()) { +
+
- -
-
- - - +} @else if (tableData().length > 0) { + +
+

{{ amountText() }}

-
- +
+
+ + + +
+ +
+ +
+ +} @else { +
+

{{ 'adminInstitutions.institutionUsers.noData' | translate }}

- +} diff --git a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts index c93d770ba..f6f6e2618 100644 --- a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts @@ -35,7 +35,7 @@ import { SendUserMessage, } from '@osf/features/admin-institutions/store/institutions-admin.actions'; import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store/institutions-admin.selectors'; -import { SelectComponent } from '@osf/shared/components'; +import { LoadingSpinnerComponent, SelectComponent } from '@osf/shared/components'; import { TABLE_PARAMS } from '@shared/constants'; import { SortOrder } from '@shared/enums'; import { QueryParams } from '@shared/models'; @@ -51,7 +51,7 @@ import { @Component({ selector: 'osf-institutions-users', - imports: [AdminTableComponent, FormsModule, SelectComponent, CheckboxModule, TranslatePipe], + imports: [AdminTableComponent, FormsModule, SelectComponent, CheckboxModule, TranslatePipe, LoadingSpinnerComponent], templateUrl: './institutions-users.component.html', styleUrl: './institutions-users.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/admin-institutions/services/institutions-admin.service.ts b/src/app/features/admin-institutions/services/institutions-admin.service.ts index a85e1de69..80a69cbfd 100644 --- a/src/app/features/admin-institutions/services/institutions-admin.service.ts +++ b/src/app/features/admin-institutions/services/institutions-admin.service.ts @@ -4,6 +4,7 @@ import { map } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@core/services'; +import { mapInstitutionPreprints } from '@osf/features/admin-institutions/mappers/institution-preprints.mapper'; import { departmens, summaryMetrics, users } from '@osf/features/admin-institutions/services/mock'; import { PaginationLinksModel } from '@shared/models'; @@ -20,6 +21,7 @@ import { InstitutionDepartment, InstitutionDepartmentsJsonApi, InstitutionIndexValueSearchJsonApi, + InstitutionPreprint, InstitutionProject, InstitutionRegistration, InstitutionRegistrationsJsonApi, @@ -102,81 +104,9 @@ export class InstitutionsAdminService { return this.fetchIndexCards('Registration', iris, pageSize, sort, cursor); } - // fetchProjects( - // institutionId: string, - // institutionIris: string[], - // pageSize = 10, - // sort = '-dateModified', - // cursor = '' - // ): Observable<{ - // projects: InstitutionProject[]; - // totalCount: number; - // links?: PaginationLinksModel; - // }> { - // const url = `${environment.shareDomainUrl}/index-card-search`; - // let params: Record = {}; - // - // const affiliationParam = institutionIris.join(','); - // - // params = { - // 'cardSearchFilter[affiliation][]': affiliationParam, - // 'cardSearchFilter[resourceType]': 'Project', - // 'cardSearchFilter[accessService]': environment.webUrl, - // 'page[cursor]': cursor, - // 'page[size]': pageSize.toString(), - // sort, - // }; - // - // return this.jsonApiService.get(url, params).pipe( - // map((response: InstitutionRegistrationsJsonApi) => { - // const projects = mapInstitutionProjects(response); - // const links = response.data.relationships.searchResultPage.links; - // return { - // projects, - // totalCount: response.data.attributes.totalResultCount, - // links, - // }; - // }) - // ); - // } - // - // fetchRegistrations( - // institutionId: string, - // institutionIris: string[], - // pageSize = 10, - // sort = '-dateModified', - // cursor = '' - // ): Observable<{ - // registrations: InstitutionRegistration[]; - // totalCount: number; - // links?: PaginationLinksModel; - // }> { - // const url = `${environment.shareDomainUrl}/index-card-search`; - // let params: Record = {}; - // - // const affiliationParam = institutionIris.join(','); - // - // params = { - // 'cardSearchFilter[affiliation][]': affiliationParam, - // 'cardSearchFilter[resourceType]': 'Registration', - // 'cardSearchFilter[accessService]': environment.webUrl, - // 'page[cursor]': cursor, - // 'page[size]': pageSize.toString(), - // sort, - // }; - // - // return this.jsonApiService.get(url, params).pipe( - // map((response: InstitutionRegistrationsJsonApi) => { - // const registrations = mapInstitutionRegistrations(response); - // const links = response.data.relationships.searchResultPage.links; - // return { - // registrations, - // totalCount: response.data.attributes.totalResultCount, - // links, - // }; - // }) - // ); - // } + fetchPreprints(institutionId: string, iris: string[], pageSize = 10, sort = '-dateModified', cursor = '') { + return this.fetchIndexCards('Preprint', iris, pageSize, sort, cursor); + } fetchIndexValueSearch( institutionId: string, @@ -203,13 +133,13 @@ export class InstitutionsAdminService { } private fetchIndexCards( - resourceType: 'Project' | 'Registration', + resourceType: 'Project' | 'Registration' | 'Preprint', institutionIris: string[], pageSize = 10, sort = '-dateModified', cursor = '' ): Observable<{ - items: InstitutionProject[] | InstitutionRegistration[]; + items: InstitutionProject[] | InstitutionRegistration[] | InstitutionPreprint[]; totalCount: number; links?: PaginationLinksModel; }> { @@ -218,7 +148,7 @@ export class InstitutionsAdminService { const params: Record = { 'cardSearchFilter[affiliation][]': affiliationParam, - 'cardSearchFilter[resourceType]': resourceType, // ← різниця + 'cardSearchFilter[resourceType]': resourceType, 'cardSearchFilter[accessService]': environment.webUrl, 'page[cursor]': cursor, 'page[size]': pageSize.toString(), @@ -227,7 +157,20 @@ export class InstitutionsAdminService { return this.jsonApiService.get(url, params).pipe( map((res) => { - const mapper = resourceType === 'Project' ? mapInstitutionProjects : mapInstitutionRegistrations; + let mapper: ( + response: InstitutionRegistrationsJsonApi + ) => InstitutionProject[] | InstitutionRegistration[] | InstitutionPreprint[]; + switch (resourceType) { + case 'Registration': + mapper = mapInstitutionRegistrations; + break; + case 'Project': + mapper = mapInstitutionProjects; + break; + default: + mapper = mapInstitutionPreprints; + break; + } return { items: mapper(res), diff --git a/src/app/features/admin-institutions/store/institutions-admin.actions.ts b/src/app/features/admin-institutions/store/institutions-admin.actions.ts index c04efa7c3..0d74548e7 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.actions.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.actions.ts @@ -60,6 +60,17 @@ export class FetchRegistrations { ) {} } +export class FetchPreprints { + static readonly type = '[InstitutionsAdmin] Fetch Preprints'; + constructor( + public institutionId: string, + public institutionIris: string[], + public pageSize = 10, + public sort = '-dateModified', + public cursor = '' + ) {} +} + export class SendUserMessage { static readonly type = '[InstitutionsAdmin] Send User Message'; constructor( diff --git a/src/app/features/admin-institutions/store/institutions-admin.model.ts b/src/app/features/admin-institutions/store/institutions-admin.model.ts index 10f163f9c..fc3588d2b 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.model.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.model.ts @@ -2,6 +2,7 @@ import { AsyncStateModel, AsyncStateWithLinksModel, AsyncStateWithTotalCount } f import { InstitutionDepartment, + InstitutionPreprint, InstitutionProject, InstitutionRegistration, InstitutionSearchFilter, @@ -19,6 +20,7 @@ export interface InstitutionsAdminModel { users: AsyncStateWithTotalCount; projects: AsyncStateWithLinksModel; registrations: AsyncStateWithLinksModel; + preprints: AsyncStateWithLinksModel; sendMessage: AsyncStateModel; selectedInstitutionId: string | null; currentSearchPropertyPath: string | null; diff --git a/src/app/features/admin-institutions/store/institutions-admin.selectors.ts b/src/app/features/admin-institutions/store/institutions-admin.selectors.ts index 898982f2d..6ab035c26 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.selectors.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.selectors.ts @@ -4,6 +4,7 @@ import { PaginationLinksModel } from '@shared/models'; import { InstitutionDepartment, + InstitutionPreprint, InstitutionProject, InstitutionRegistration, InstitutionSearchFilter, @@ -161,6 +162,26 @@ export class InstitutionsAdminSelectors { return state.registrations.links; } + @Selector([InstitutionsAdminState]) + static getPreprints(state: InstitutionsAdminModel): InstitutionPreprint[] { + return state.preprints.data; + } + + @Selector([InstitutionsAdminState]) + static getPreprintsLoading(state: InstitutionsAdminModel): boolean { + return state.preprints.isLoading; + } + + @Selector([InstitutionsAdminState]) + static getPreprintsTotalCount(state: InstitutionsAdminModel): number { + return state.preprints.totalCount; + } + + @Selector([InstitutionsAdminState]) + static getPreprintsLinks(state: InstitutionsAdminModel): PaginationLinksModel | undefined { + return state.preprints.links; + } + @Selector([InstitutionsAdminState]) static getSendMessageResponse(state: InstitutionsAdminModel): SendMessageResponseJsonApi | null { return state.sendMessage.data; diff --git a/src/app/features/admin-institutions/store/institutions-admin.state.ts b/src/app/features/admin-institutions/store/institutions-admin.state.ts index 491c63d4d..312a54c52 100644 --- a/src/app/features/admin-institutions/store/institutions-admin.state.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.state.ts @@ -6,7 +6,7 @@ import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@core/handlers'; -import { InstitutionProject, InstitutionRegistration, InstitutionSummaryMetrics } from '../models'; +import { InstitutionPreprint, InstitutionProject, InstitutionRegistration, InstitutionSummaryMetrics } from '../models'; import { InstitutionsAdminService } from '../services/institutions-admin.service'; import { @@ -15,6 +15,7 @@ import { FetchInstitutionSearchResults, FetchInstitutionSummaryMetrics, FetchInstitutionUsers, + FetchPreprints, FetchProjects, FetchRegistrations, FetchStorageRegionSearch, @@ -33,6 +34,7 @@ import { InstitutionsAdminModel } from './institutions-admin.model'; users: { data: [], totalCount: 0, isLoading: false, error: null }, projects: { data: [], totalCount: 0, isLoading: false, error: null, links: undefined }, registrations: { data: [], totalCount: 0, isLoading: false, error: null, links: undefined }, + preprints: { data: [], totalCount: 0, isLoading: false, error: null, links: undefined }, sendMessage: { data: null, isLoading: false, error: null }, selectedInstitutionId: null, currentSearchPropertyPath: null, @@ -200,6 +202,31 @@ export class InstitutionsAdminState { ); } + @Action(FetchPreprints) + fetchPreprints(ctx: StateContext, action: FetchPreprints) { + const state = ctx.getState(); + ctx.patchState({ + preprints: { ...state.preprints, isLoading: true, error: null }, + }); + + return this.institutionsAdminService + .fetchPreprints(action.institutionId, action.institutionIris, action.pageSize, action.sort, action.cursor) + .pipe( + tap((response) => { + ctx.patchState({ + preprints: { + data: response.items as InstitutionPreprint[], + totalCount: response.totalCount, + isLoading: false, + error: null, + links: response.links, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'preprints', error)) + ); + } + @Action(SendUserMessage) sendUserMessage(ctx: StateContext, action: SendUserMessage) { const state = ctx.getState(); diff --git a/src/app/features/institutions/institutions.component.scss b/src/app/features/institutions/institutions.component.scss new file mode 100644 index 000000000..5f81e6c60 --- /dev/null +++ b/src/app/features/institutions/institutions.component.scss @@ -0,0 +1,3 @@ +:host { + flex: 1; +} diff --git a/src/app/features/institutions/institutions.component.ts b/src/app/features/institutions/institutions.component.ts index 0d0687196..f89bfbc89 100644 --- a/src/app/features/institutions/institutions.component.ts +++ b/src/app/features/institutions/institutions.component.ts @@ -5,6 +5,7 @@ import { RouterOutlet } from '@angular/router'; selector: 'osf-institutions', imports: [RouterOutlet], templateUrl: './institutions.component.html', + styleUrl: './institutions.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class InstitutionsComponent {} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index fe13e2e4c..cd5d3b09d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2147,7 +2147,8 @@ "hasOrcid": "Has ORCID", "sendMessage": "Send message", "osfLink": "OSF Link", - "orcid": "ORCID" + "orcid": "ORCID", + "noData": "No users found" }, "projects": { "title": "Title", @@ -2163,14 +2164,19 @@ "license": "License", "addOns": "Add-ons", "funderName": "Funder Name", - "totalProjects": "Total Projects" + "totalProjects": "Total Projects", + "noData": "No projects found" }, "registrations": { - "doi": "DOI", - "storageLocation": "Storage Location", "funderName": "Funder Name", "registrationSchema": "Registration Schema", - "totalRegistrations": "Total Registrations" + "totalRegistrations": "Total Registrations", + "noData": "No registrations found" + }, + "preprints": { + "totalPreprints": "Total Preprints", + "downloadsLastDays": "Downloads (last 30 days)", + "noData": "No preprints found" } } } From 60e027b3199f800571b7bd3bae9b6bb779509e0f Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Wed, 16 Jul 2025 17:09:20 +0300 Subject: [PATCH 3/3] feat(admin-users): added inst registration and preprints pages --- .../models/index-search-query-params.model.ts | 5 +++++ .../features/admin-institutions/models/index.ts | 1 + .../institutions-preprints.component.ts | 14 ++++++-------- .../institutions-registrations.component.ts | 14 ++++++-------- 4 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 src/app/features/admin-institutions/models/index-search-query-params.model.ts diff --git a/src/app/features/admin-institutions/models/index-search-query-params.model.ts b/src/app/features/admin-institutions/models/index-search-query-params.model.ts new file mode 100644 index 000000000..e15b990ee --- /dev/null +++ b/src/app/features/admin-institutions/models/index-search-query-params.model.ts @@ -0,0 +1,5 @@ +export interface IndexSearchQueryParamsModel { + size?: number; + sort?: string; + cursor?: string; +} diff --git a/src/app/features/admin-institutions/models/index.ts b/src/app/features/admin-institutions/models/index.ts index fc9989a54..b7ddff2c9 100644 --- a/src/app/features/admin-institutions/models/index.ts +++ b/src/app/features/admin-institutions/models/index.ts @@ -1,3 +1,4 @@ +export * from './index-search-query-params.model'; export * from './institution-department.model'; export * from './institution-departments-json-api.model'; export * from './institution-index-value-search-json-api.model'; diff --git a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts index 836a0f9af..1ec7d9b47 100644 --- a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts @@ -11,7 +11,11 @@ import { parseQueryFilterParams } from '@core/helpers'; import { AdminTableComponent } from '@osf/features/admin-institutions/components'; import { preprintsTableColumns } from '@osf/features/admin-institutions/constants'; import { mapPreprintToTableData } from '@osf/features/admin-institutions/mappers'; -import { InstitutionProjectsQueryParamsModel, TableCellData } from '@osf/features/admin-institutions/models'; +import { + IndexSearchQueryParamsModel, + InstitutionProjectsQueryParamsModel, + TableCellData, +} from '@osf/features/admin-institutions/models'; import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store'; import { LoadingSpinnerComponent } from '@osf/shared/components'; import { TABLE_PARAMS } from '@shared/constants'; @@ -23,12 +27,6 @@ import { FetchPreprints } from '../../store/institutions-admin.actions'; import { environment } from 'src/environments/environment'; -interface PreprintQueryParams { - size?: number; - sort?: string; - cursor?: string; -} - @Component({ selector: 'osf-institutions-preprints', imports: [CommonModule, AdminTableComponent, TranslatePipe, LoadingSpinnerComponent], @@ -161,7 +159,7 @@ export class InstitutionsPreprintsComponent { }); } - private updateQueryParams(params: PreprintQueryParams): void { + private updateQueryParams(params: IndexSearchQueryParamsModel): void { const queryParams: Record = {}; if (params.sort) { diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts index a82afc51e..1fff480de 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts @@ -11,7 +11,11 @@ import { parseQueryFilterParams } from '@core/helpers'; import { AdminTableComponent } from '@osf/features/admin-institutions/components'; import { registrationTableColumns } from '@osf/features/admin-institutions/constants'; import { mapRegistrationToTableData } from '@osf/features/admin-institutions/mappers'; -import { InstitutionProjectsQueryParamsModel, TableCellData } from '@osf/features/admin-institutions/models'; +import { + IndexSearchQueryParamsModel, + InstitutionProjectsQueryParamsModel, + TableCellData, +} from '@osf/features/admin-institutions/models'; import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store'; import { LoadingSpinnerComponent } from '@osf/shared/components'; import { TABLE_PARAMS } from '@shared/constants'; @@ -23,12 +27,6 @@ import { FetchRegistrations } from '../../store/institutions-admin.actions'; import { environment } from 'src/environments/environment'; -interface RegistrationQueryParams { - size?: number; - sort?: string; - cursor?: string; -} - @Component({ selector: 'osf-institutions-registrations', imports: [CommonModule, AdminTableComponent, TranslatePipe, LoadingSpinnerComponent], @@ -159,7 +157,7 @@ export class InstitutionsRegistrationsComponent { }); } - private updateQueryParams(params: RegistrationQueryParams): void { + private updateQueryParams(params: IndexSearchQueryParamsModel): void { const queryParams: Record = {}; if (params.sort) {