diff --git a/src/app/features/admin-institutions/constants/index.ts b/src/app/features/admin-institutions/constants/index.ts index 9044772a9..7c1360d02 100644 --- a/src/app/features/admin-institutions/constants/index.ts +++ b/src/app/features/admin-institutions/constants/index.ts @@ -1,4 +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/registration-table-columns.constant.ts b/src/app/features/admin-institutions/constants/registration-table-columns.constant.ts new file mode 100644 index 000000000..b301d7174 --- /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.projects.doi', + sortable: false, + isLink: true, + linkTarget: '_blank', + }, + { + field: 'storageLocation', + header: 'adminInstitutions.projects.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/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 20dbd3dfa..ea84875b8 100644 --- a/src/app/features/admin-institutions/mappers/index.ts +++ b/src/app/features/admin-institutions/mappers/index.ts @@ -1,6 +1,9 @@ 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'; export { mapUserToTableCellData } from './institution-user-to-table-data.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 new file mode 100644 index 000000000..1ca7b345d --- /dev/null +++ b/src/app/features/admin-institutions/mappers/institution-registration-to-table-data.mapper.ts @@ -0,0 +1,41 @@ +import { extractPathAfterDomain } from '@osf/features/admin-institutions/helpers'; + +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: extractPathAfterDomain(registration.doi), + url: registration.doi, + } 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-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 c9859791c..b7ddff2c9 100644 --- a/src/app/features/admin-institutions/models/index.ts +++ b/src/app/features/admin-institutions/models/index.ts @@ -1,11 +1,15 @@ +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'; +export * from './institution-preprint.model'; 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-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/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-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..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 @@ -1,10 +1,181 @@ -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 { + 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'; +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'; @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: IndexSearchQueryParamsModel): 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 e69de29bb..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 @@ -0,0 +1,26 @@ +@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.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..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 @@ -1,10 +1,179 @@ -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 { registrationTableColumns } from '@osf/features/admin-institutions/constants'; +import { mapRegistrationToTableData } from '@osf/features/admin-institutions/mappers'; +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'; +import { SortOrder } from '@shared/enums'; +import { Institution, QueryParams } from '@shared/models'; +import { InstitutionsSearchSelectors } from '@shared/stores'; + +import { FetchRegistrations } from '../../store/institutions-admin.actions'; + +import { environment } from 'src/environments/environment'; @Component({ selector: 'osf-institutions-registrations', - imports: [], + imports: [CommonModule, AdminTableComponent, TranslatePipe, LoadingSpinnerComponent], templateUrl: './institutions-registrations.component.html', styleUrl: './institutions-registrations.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class InstitutionsRegistrationsComponent {} +export class InstitutionsRegistrationsComponent { + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + + private readonly actions = createDispatchMap({ + fetchRegistrations: FetchRegistrations, + }); + + private institutionId = ''; + + 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'; + + queryParams = toSignal(this.route.queryParams); + currentPageSize = signal(TABLE_PARAMS.rows); + currentSort = signal('-dateModified'); + sortField = signal('-dateModified'); + sortOrder = signal(1); + + tableData = computed(() => { + const registrationsData = this.registrations(); + return registrationsData.map(mapRegistrationToTableData) 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]': 'Registration', + '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.fetchRegistrations(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: IndexSearchQueryParamsModel): 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-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 9dd5eedb2..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'; @@ -11,6 +12,7 @@ import { mapIndexCardResults, mapInstitutionDepartments, mapInstitutionProjects, + mapInstitutionRegistrations, mapInstitutionSummaryMetrics, mapInstitutionUsers, sendMessageRequestMapper, @@ -19,7 +21,9 @@ import { InstitutionDepartment, InstitutionDepartmentsJsonApi, InstitutionIndexValueSearchJsonApi, + InstitutionPreprint, InstitutionProject, + InstitutionRegistration, InstitutionRegistrationsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetrics, @@ -92,42 +96,16 @@ 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(','); + fetchProjects(institutionId: string, iris: string[], pageSize = 10, sort = '-dateModified', cursor = '') { + return this.fetchIndexCards('Project', iris, pageSize, sort, cursor); + } - params = { - 'cardSearchFilter[affiliation][]': affiliationParam, - 'cardSearchFilter[resourceType]': 'Project', - 'cardSearchFilter[accessService]': environment.webUrl, - 'page[cursor]': cursor, - 'page[size]': pageSize.toString(), - sort, - }; + fetchRegistrations(institutionId: string, iris: string[], pageSize = 10, sort = '-dateModified', cursor = '') { + return this.fetchIndexCards('Registration', 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, - }; - }) - ); + fetchPreprints(institutionId: string, iris: string[], pageSize = 10, sort = '-dateModified', cursor = '') { + return this.fetchIndexCards('Preprint', iris, pageSize, sort, cursor); } fetchIndexValueSearch( @@ -153,4 +131,53 @@ export class InstitutionsAdminService { return this.jsonApiService.post(`${this.hardcodedUrl}/institutions/messages/`, payload); } + + private fetchIndexCards( + resourceType: 'Project' | 'Registration' | 'Preprint', + institutionIris: string[], + pageSize = 10, + sort = '-dateModified', + cursor = '' + ): Observable<{ + items: InstitutionProject[] | InstitutionRegistration[] | InstitutionPreprint[]; + 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) => { + 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), + 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..0d74548e7 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,28 @@ 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 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 473fcbd94..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,7 +2,9 @@ import { AsyncStateModel, AsyncStateWithLinksModel, AsyncStateWithTotalCount } f import { InstitutionDepartment, + InstitutionPreprint, InstitutionProject, + InstitutionRegistration, InstitutionSearchFilter, InstitutionSummaryMetrics, InstitutionUser, @@ -17,6 +19,8 @@ export interface InstitutionsAdminModel { searchResults: AsyncStateModel; 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 91959090f..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,7 +4,9 @@ import { PaginationLinksModel } from '@shared/models'; import { InstitutionDepartment, + InstitutionPreprint, InstitutionProject, + InstitutionRegistration, InstitutionSearchFilter, InstitutionSummaryMetrics, InstitutionUser, @@ -140,6 +142,46 @@ 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 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 dfb3b636f..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 { InstitutionSummaryMetrics } from '../models'; +import { InstitutionPreprint, InstitutionProject, InstitutionRegistration, InstitutionSummaryMetrics } from '../models'; import { InstitutionsAdminService } from '../services/institutions-admin.service'; import { @@ -15,7 +15,9 @@ import { FetchInstitutionSearchResults, FetchInstitutionSummaryMetrics, FetchInstitutionUsers, + FetchPreprints, FetchProjects, + FetchRegistrations, FetchStorageRegionSearch, SendUserMessage, } from './institutions-admin.actions'; @@ -31,6 +33,8 @@ 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 }, + preprints: { data: [], totalCount: 0, isLoading: false, error: null, links: undefined }, sendMessage: { data: null, isLoading: false, error: null }, selectedInstitutionId: null, currentSearchPropertyPath: null, @@ -161,7 +165,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 +177,56 @@ 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(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 cdc50bc64..56a20cb47 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2183,7 +2183,8 @@ "hasOrcid": "Has ORCID", "sendMessage": "Send message", "osfLink": "OSF Link", - "orcid": "ORCID" + "orcid": "ORCID", + "noData": "No users found" }, "projects": { "title": "Title", @@ -2199,7 +2200,19 @@ "license": "License", "addOns": "Add-ons", "funderName": "Funder Name", - "totalProjects": "Total Projects" + "totalProjects": "Total Projects", + "noData": "No projects found" + }, + "registrations": { + "funderName": "Funder Name", + "registrationSchema": "Registration Schema", + "totalRegistrations": "Total Registrations", + "noData": "No registrations found" + }, + "preprints": { + "totalPreprints": "Total Preprints", + "downloadsLastDays": "Downloads (last 30 days)", + "noData": "No preprints found" } } }