From 9b7c64b41629e3ff2a234fd34e5565f275a8b448 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 16 Sep 2025 18:16:40 +0300 Subject: [PATCH 1/5] fix(global-search): Fixed correct order of index-card --- .../shared/services/global-search.service.ts | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/app/shared/services/global-search.service.ts b/src/app/shared/services/global-search.service.ts index 74c5434aa..b82177089 100644 --- a/src/app/shared/services/global-search.service.ts +++ b/src/app/shared/services/global-search.service.ts @@ -55,9 +55,9 @@ export class GlobalSearchService { const options: FilterOption[] = []; let nextUrl: string | undefined; - const searchResultItems = response - .included!.filter((item): item is SearchResultJsonApi => item.type === 'search-result') - .sort((a, b) => Number(a.id.at(-1)) - Number(b.id.at(-1))); + const searchResultItems = response.included!.filter( + (item): item is SearchResultJsonApi => item.type === 'search-result' + ); const filterOptionItems = response.included!.filter((item): item is FilterOptionItem => item.type === 'index-card'); options.push(...mapFilterOptions(searchResultItems, filterOptionItems)); @@ -73,13 +73,19 @@ export class GlobalSearchService { } private handleResourcesRawResponse(response: IndexCardSearchResponseJsonApi): ResourcesData { - const searchResultItems = response - .included!.filter((item): item is SearchResultJsonApi => item.type === 'search-result') - .sort((a, b) => Number(a.id.at(-1)) - Number(b.id.at(-1))); + const searchResultIds = response.data.relationships.searchResultPage.data.map((obj) => obj.id); - const indexCardItems = response.included!.filter((item) => item.type === 'index-card') as IndexCardDataJsonApi[]; - const indexCardItemsCorrectOrder = searchResultItems.map((searchResult) => { - return indexCardItems.find((indexCard) => indexCard.id === searchResult.relationships.indexCard.data.id)!; + const searchResultItems = searchResultIds.map( + (searchResultId) => + response.included!.find( + (item): item is SearchResultJsonApi => item.type === 'search-result' && searchResultId === item.id + )! + ); + const indexCardItems = searchResultItems.map((searchResult) => { + return response.included!.find( + (item): item is IndexCardDataJsonApi => + item.type === 'index-card' && item.id === searchResult.relationships.indexCard.data.id + )!; }); const relatedPropertyPathItems = response.included!.filter( (item): item is RelatedPropertyPathItem => item.type === 'related-property-path' @@ -88,7 +94,7 @@ export class GlobalSearchService { const appliedFilters: AppliedFilter[] = response.data?.attributes?.cardSearchFilter || []; return { - resources: indexCardItemsCorrectOrder.map((item) => MapResources(item)), + resources: indexCardItems.map((item) => MapResources(item)), filters: CombinedFilterMapper(appliedFilters, relatedPropertyPathItems), count: response.data.attributes.totalResultCount, self: response.data.links.self, From a60f4060773a2e7a76c613c47fc5ee7aecb0d8be Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 16 Sep 2025 18:18:16 +0300 Subject: [PATCH 2/5] fix(institution-dashboard): Showing contributors sorted by permissions --- .../admin-table/admin-table.component.html | 46 +++++++++++++------ .../admin-table/admin-table.component.ts | 14 +++--- .../preprints-table-columns.constant.ts | 1 + .../project-table-columns.constant.ts | 1 + .../registration-table-columns.constant.ts | 1 + ...stitution-preprint-to-table-data.mapper.ts | 21 ++++----- ...nstitution-project-to-table-data.mapper.ts | 44 ++++++++++++++---- ...ution-registration-to-table-data.mapper.ts | 21 ++++----- .../institution-user-to-table-data.mapper.ts | 2 - .../admin-institutions/models/table.model.ts | 5 +- .../institutions-projects.component.ts | 12 +++-- .../institutions-registrations.component.ts | 3 +- .../resource-card/resource-card.component.ts | 27 ++--------- src/app/shared/helpers/index.ts | 1 + .../sort-contributors-by-permissions.ts | 13 ++++++ .../institutions/institutions.mapper.ts | 2 +- .../shared/mappers/search/search.mapper.ts | 4 ++ .../index-card-search-json-api.models.ts | 9 +++- .../shared/models/search/resource.model.ts | 9 +++- .../global-search/global-search.state.ts | 2 +- 20 files changed, 144 insertions(+), 94 deletions(-) create mode 100644 src/app/shared/helpers/sort-contributors-by-permissions.ts diff --git a/src/app/features/admin-institutions/components/admin-table/admin-table.component.html b/src/app/features/admin-institutions/components/admin-table/admin-table.component.html index 587a466a8..927105683 100644 --- a/src/app/features/admin-institutions/components/admin-table/admin-table.component.html +++ b/src/app/features/admin-institutions/components/admin-table/admin-table.component.html @@ -110,37 +110,57 @@ @for (col of columns; track col.field) {
- @if (col.isLink && isLink(rowData[col.field])) { + @let currentColumnField = rowData[col.field]; + @if (col.isLink && isLink(currentColumnField)) { @if (col.dateFormat) { - {{ getCellValue(rowData[col.field]) | date: col.dateFormat }} + {{ getCellValue(currentColumnField) | date: col.dateFormat }} } @else { - {{ getCellValue(rowData[col.field]) }} + {{ getCellValue(currentColumnField) }} } + } @else if (col.isLink && col.isArray && isLinkArray(currentColumnField)) { +
+ @for (link of currentColumnField; track $index) { + + {{ link.text }}{{ $last ? '' : ',' }} + + @if (col.showIcon) { + + } + } +
} @else { - @if (col.dateFormat && rowData[col.field]) { - {{ getCellValue(rowData[col.field]) | date: col.dateFormat }} - } @else if (!col.dateFormat && rowData[col.field]) { - {{ getCellValue(rowData[col.field]) }} + @if (col.dateFormat && currentColumnField) { + {{ getCellValue(currentColumnField) | date: col.dateFormat }} + } @else if (!col.dateFormat && currentColumnField) { + {{ getCellValue(currentColumnField) }} } @else { - {{ rowData[col.field] ?? '-' }} + {{ currentColumnField ?? '-' }} } } - @if (col.showIcon) { + @if (col.showIcon && !col.isArray) { }
diff --git a/src/app/features/admin-institutions/components/admin-table/admin-table.component.ts b/src/app/features/admin-institutions/components/admin-table/admin-table.component.ts index ef7b5b72e..8650fe738 100644 --- a/src/app/features/admin-institutions/components/admin-table/admin-table.component.ts +++ b/src/app/features/admin-institutions/components/admin-table/admin-table.component.ts @@ -121,10 +121,11 @@ export class AdminTableComponent { } } - onIconClick(rowData: TableCellData, column: TableColumn): void { + onIconClick(rowData: TableCellData, column: TableColumn, arrayIndex?: number): void { if (column.iconAction) { this.iconClicked.emit({ rowData, + arrayIndex, column, action: column.iconAction, }); @@ -135,6 +136,10 @@ export class AdminTableComponent { return value !== null && value !== undefined && typeof value === 'object' && 'text' in value && 'url' in value; } + isLinkArray(value: unknown): value is TableCellLink[] { + return Array.isArray(value) && value.every((v) => v && typeof v === 'object' && 'url' in v); + } + getCellValue(value: string | number | TableCellLink | undefined): string { if (this.isLink(value)) { return this.translateService.instant(value.text); @@ -152,11 +157,4 @@ export class AdminTableComponent { } return ''; } - - getLinkTarget(value: string | number | TableCellLink | undefined, column: TableColumn): string { - if (this.isLink(value)) { - return value.target || column.linkTarget || '_self'; - } - return column.linkTarget || '_self'; - } } 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 index 970a40ff1..859a85cba 100644 --- a/src/app/features/admin-institutions/constants/preprints-table-columns.constant.ts +++ b/src/app/features/admin-institutions/constants/preprints-table-columns.constant.ts @@ -39,6 +39,7 @@ export const preprintsTableColumns: TableColumn[] = [ field: 'contributorName', header: 'adminInstitutions.projects.contributorName', isLink: true, + isArray: true, linkTarget: '_blank', }, { 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 653abf8d5..1e8820b82 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 @@ -46,6 +46,7 @@ export const projectTableColumns: TableColumn[] = [ header: 'adminInstitutions.projects.contributorName', isLink: true, linkTarget: '_blank', + isArray: true, showIcon: true, iconClass: 'fa-solid fa-comment text-primary', iconTooltip: 'adminInstitutions.institutionUsers.sendMessage', 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 ff7577636..3fce5dd02 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 @@ -45,6 +45,7 @@ export const registrationTableColumns: TableColumn[] = [ field: 'contributorName', header: 'adminInstitutions.projects.contributorName', isLink: true, + isArray: true, linkTarget: '_blank', }, { 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 index 81973bcb9..539aef6ea 100644 --- 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 @@ -1,7 +1,8 @@ +import { getSortedContributorsByPermissions } from '@shared/helpers'; import { ResourceModel } from '@shared/models'; import { extractPathAfterDomain } from '../helpers'; -import { TableCellData, TableCellLink } from '../models'; +import { TableCellData } from '../models'; export function mapPreprintResourceToTableData(preprint: ResourceModel): TableCellData { return { @@ -9,24 +10,20 @@ export function mapPreprintResourceToTableData(preprint: ResourceModel): TableCe link: { text: preprint.absoluteUrl.split('/').pop() || preprint.absoluteUrl, url: preprint.absoluteUrl, - target: '_blank', - } as TableCellLink, + }, dateCreated: preprint.dateCreated, dateModified: preprint.dateModified, doi: preprint.doi[0] - ? ({ + ? { text: extractPathAfterDomain(preprint.doi[0]), url: preprint.doi[0], - } as TableCellLink) + } : '-', license: preprint.license?.name || '-', - contributorName: preprint.creators[0] - ? ({ - text: preprint.creators[0].name, - url: preprint.creators[0].absoluteUrl, - target: '_blank', - } as TableCellLink) - : '-', + contributorName: getSortedContributorsByPermissions(preprint)?.map((creator) => ({ + text: creator.name.trim(), + url: creator.absoluteUrl, + })), viewsLast30Days: preprint.viewsCount || '-', downloadsLast30Days: preprint.downloadCount || '-', }; diff --git a/src/app/features/admin-institutions/mappers/institution-project-to-table-data.mapper.ts b/src/app/features/admin-institutions/mappers/institution-project-to-table-data.mapper.ts index 1e5e9bfd7..706b0aa47 100644 --- a/src/app/features/admin-institutions/mappers/institution-project-to-table-data.mapper.ts +++ b/src/app/features/admin-institutions/mappers/institution-project-to-table-data.mapper.ts @@ -1,29 +1,27 @@ +import { getSortedContributorsByPermissions } from '@shared/helpers'; import { ResourceModel } from '@shared/models'; import { extractPathAfterDomain } from '../helpers'; -import { TableCellData, TableCellLink } from '../models'; +import { TableCellData } from '../models'; -export function mapProjectResourceToTableCellData(project: ResourceModel): TableCellData { +export function mapProjectResourceToTableCellData(project: ResourceModel, currentInstitutionId: string): TableCellData { return { title: project.title, link: { url: project.absoluteUrl, text: project.absoluteUrl.split('/').pop() || project.absoluteUrl, - } as TableCellLink, + }, dateCreated: project.dateCreated!, dateModified: project.dateModified!, doi: project.doi[0] - ? ({ + ? { text: extractPathAfterDomain(project.doi[0]), url: project.doi[0], - } as TableCellLink) + } : '-', storageLocation: project.storageRegion || '-', totalDataStored: project.storageByteCount ? `${(+project.storageByteCount / (1024 * 1024)).toFixed(1)} MB` : '0 B', - creator: { - url: project.creators[0].absoluteUrl || '#', - text: project.creators[0].name || '-', - } as TableCellLink, + creator: mapCreators(project, currentInstitutionId), views: project.viewsCount || '-', resourceType: project.resourceNature || '-', license: project.license?.name || '-', @@ -31,3 +29,31 @@ export function mapProjectResourceToTableCellData(project: ResourceModel): Table funderName: project.funders?.[0]?.name || '-', }; } + +function mapCreators(project: ResourceModel, currentInstitutionId: string) { + const creatorsRoles = project.qualifiedAttribution.map((qa) => { + let role; + if (qa.hadRole.includes('admin')) { + role = 'Administrator'; + } else if (qa.hadRole.includes('write')) { + role = 'Read + Write'; + } else { + role = 'Read'; + } + return { + id: qa.agentId, + role, + }; + }); + + return getSortedContributorsByPermissions(project) + ?.filter((creator) => creator.affiliationAbsoluteUrl === currentInstitutionId) + ?.map((creator) => { + const name = creator.name.trim(); + const role = creatorsRoles.find((cr) => cr.id === creator.absoluteUrl)!.role; + return { + text: `${name} (${role})`, + url: creator.absoluteUrl, + }; + }); +} 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 8dbd650cf..7733c1e3e 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,7 +1,8 @@ import { extractPathAfterDomain } from '@osf/features/admin-institutions/helpers'; +import { getSortedContributorsByPermissions } from '@shared/helpers'; import { ResourceModel } from '@shared/models'; -import { TableCellData, TableCellLink } from '../models'; +import { TableCellData } from '../models'; export function mapRegistrationResourceToTableData(registration: ResourceModel): TableCellData { return { @@ -9,27 +10,23 @@ export function mapRegistrationResourceToTableData(registration: ResourceModel): link: { text: registration.absoluteUrl.split('/').pop() || registration.absoluteUrl, url: registration.absoluteUrl, - target: '_blank', - } as TableCellLink, + }, dateCreated: registration.dateCreated, dateModified: registration.dateModified, doi: registration.doi[0] - ? ({ + ? { text: extractPathAfterDomain(registration.doi[0]), url: registration.doi[0], - } as TableCellLink) + } : '-', storageLocation: registration.storageRegion || '-', totalDataStored: registration.storageByteCount ? `${(+registration.storageByteCount / (1024 * 1024)).toFixed(1)} MB` : '0 B', - contributorName: registration.creators[0] - ? ({ - text: registration.creators[0].name, - url: registration.creators[0].absoluteUrl, - target: '_blank', - } as TableCellLink) - : '-', + contributorName: getSortedContributorsByPermissions(registration)?.map((creator) => ({ + text: creator.name.trim(), + url: creator.absoluteUrl, + })), views: registration.viewsCount || '-', resourceType: registration.resourceNature || '-', license: registration.license?.name || '-', diff --git a/src/app/features/admin-institutions/mappers/institution-user-to-table-data.mapper.ts b/src/app/features/admin-institutions/mappers/institution-user-to-table-data.mapper.ts index 96e293d8a..de47c279a 100644 --- a/src/app/features/admin-institutions/mappers/institution-user-to-table-data.mapper.ts +++ b/src/app/features/admin-institutions/mappers/institution-user-to-table-data.mapper.ts @@ -10,13 +10,11 @@ export function mapUserToTableCellData(user: InstitutionUser): TableCellData { userLink: { text: user.userId, url: `${environment.webUrl}/${user.userId}`, - target: '_blank', }, orcidId: user.orcidId ? { text: user.orcidId, url: `https://orcid.org/${user.orcidId}`, - target: '_blank', } : '-', publicProjects: user.publicProjects, diff --git a/src/app/features/admin-institutions/models/table.model.ts b/src/app/features/admin-institutions/models/table.model.ts index c5045189e..c2d6150d2 100644 --- a/src/app/features/admin-institutions/models/table.model.ts +++ b/src/app/features/admin-institutions/models/table.model.ts @@ -5,6 +5,7 @@ export interface TableColumn { sortField?: string; isLink?: boolean; linkTarget?: '_blank' | '_self'; + isArray?: boolean; showIcon?: boolean; iconClass?: string; iconTooltip?: string; @@ -15,13 +16,13 @@ export interface TableColumn { export interface TableCellLink { text: string; url: string; - target?: '_blank' | '_self'; } -export type TableCellData = Record; +export type TableCellData = Record; export interface TableIconClickEvent { rowData: TableCellData; + arrayIndex?: number; column: TableColumn; action: string; } 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 36939b6f5..9b2fb2901 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 @@ -87,7 +87,9 @@ export class InstitutionsProjectsComponent implements OnInit, OnDestroy { currentUser = select(UserSelectors.getCurrentUser); tableData = computed(() => - this.resources().map((resource: ResourceModel): TableCellData => mapProjectResourceToTableCellData(resource)) + this.resources().map( + (resource: ResourceModel): TableCellData => mapProjectResourceToTableCellData(resource, this.institution().iri) + ) ); sortParam = computed(() => { @@ -149,11 +151,11 @@ export class InstitutionsProjectsComponent implements OnInit, OnDestroy { filter((value) => !!value), takeUntilDestroyed(this.destroyRef) ) - .subscribe((data: ContactDialogData) => this.sendEmailToUser(event.rowData, data)); + .subscribe((data: ContactDialogData) => this.sendEmailToUser(event, data)); } - private sendEmailToUser(userRowData: TableCellData, emailData: ContactDialogData): void { - const userId = (userRowData['creator'] as TableCellLink).url.split('/').pop() || ''; + private sendEmailToUser(event: TableIconClickEvent, emailData: ContactDialogData): void { + const userId = (event.rowData['creator'] as TableCellLink[])[event.arrayIndex ?? 0].url.split('/').pop() || ''; if (emailData.selectedOption === ContactOption.SendMessage) { this.actions @@ -167,7 +169,7 @@ export class InstitutionsProjectsComponent implements OnInit, OnDestroy { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => this.toastService.showSuccess('adminInstitutions.institutionUsers.messageSent')); } else { - const projectId = (userRowData['title'] as TableCellLink).url.split('/').pop() || ''; + const projectId = (event.rowData['link'] as TableCellLink).url.split('/').pop() || ''; this.actions .requestProjectAccess({ 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 bec708727..daee016f4 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 @@ -26,7 +26,6 @@ import { registrationTableColumns } from '../../constants'; import { DownloadType } from '../../enums'; import { downloadResults } from '../../helpers'; import { mapRegistrationResourceToTableData } from '../../mappers/institution-registration-to-table-data.mapper'; -import { TableCellData } from '../../models'; import { InstitutionsAdminSelectors } from '../../store'; @Component({ @@ -64,7 +63,7 @@ export class InstitutionsRegistrationsComponent implements OnInit, OnDestroy { nextLink = select(GlobalSearchSelectors.getNext); previousLink = select(GlobalSearchSelectors.getPrevious); - tableData = computed(() => this.resources().map(mapRegistrationResourceToTableData) as TableCellData[]); + tableData = computed(() => this.resources().map(mapRegistrationResourceToTableData)); sortParam = computed(() => { const sortField = this.sortField(); diff --git a/src/app/shared/components/resource-card/resource-card.component.ts b/src/app/shared/components/resource-card/resource-card.component.ts index 0534ddabc..fe763fea6 100644 --- a/src/app/shared/components/resource-card/resource-card.component.ts +++ b/src/app/shared/components/resource-card/resource-card.component.ts @@ -13,14 +13,8 @@ import { getPreprintDocumentType } from '@osf/features/preprints/helpers'; import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { CardLabelTranslationKeys } from '@osf/shared/constants'; import { ResourceType } from '@osf/shared/enums'; -import { IS_XSMALL } from '@osf/shared/helpers'; -import { - AbsoluteUrlName, - IsContainedBy, - QualifiedAttribution, - ResourceModel, - UserRelatedCounts, -} from '@osf/shared/models'; +import { getSortedContributorsByPermissions, IS_XSMALL } from '@osf/shared/helpers'; +import { ResourceModel, UserRelatedCounts } from '@osf/shared/models'; import { ResourceCardService } from '@osf/shared/services'; import { DataResourcesComponent } from '../data-resources/data-resources.component'; @@ -98,9 +92,9 @@ export class ResourceCardComponent { return resource.affiliations; } } else if (resource.creators) { - return this.getSortedContributors(resource); + return getSortedContributorsByPermissions(resource); } else if (resource.isContainedBy?.creators) { - return this.getSortedContributors(resource.isContainedBy); + return getSortedContributorsByPermissions(resource.isContainedBy); } return []; @@ -168,17 +162,4 @@ export class ResourceCardComponent { this.userRelatedCounts.set(res); }); } - - private getSortedContributors(base: ResourceModel | IsContainedBy) { - const objectOrder = Object.fromEntries( - base.qualifiedAttribution.map((item: QualifiedAttribution) => [item.agentId, item.order]) - ); - return base.creators - ?.map((item: AbsoluteUrlName) => ({ - name: item.name, - absoluteUrl: item.absoluteUrl, - index: objectOrder[item.absoluteUrl], - })) - .sort((a: { index: number }, b: { index: number }) => a.index - b.index); - } } diff --git a/src/app/shared/helpers/index.ts b/src/app/shared/helpers/index.ts index 901677ae5..a6327db25 100644 --- a/src/app/shared/helpers/index.ts +++ b/src/app/shared/helpers/index.ts @@ -15,6 +15,7 @@ export * from './password.helper'; export * from './path-join.helper'; export * from './remove-nullable.helper'; export * from './search-pref-to-json-api-query-params.helper'; +export * from './sort-contributors-by-permissions'; export * from './state-error.handler'; export * from './types.helper'; export * from './url-param.helper'; diff --git a/src/app/shared/helpers/sort-contributors-by-permissions.ts b/src/app/shared/helpers/sort-contributors-by-permissions.ts new file mode 100644 index 000000000..cb9e8b730 --- /dev/null +++ b/src/app/shared/helpers/sort-contributors-by-permissions.ts @@ -0,0 +1,13 @@ +import { Creator, IsContainedBy, QualifiedAttribution, ResourceModel } from '@shared/models'; + +export function getSortedContributorsByPermissions(base: ResourceModel | IsContainedBy) { + const objectOrder = Object.fromEntries( + base.qualifiedAttribution.map((item: QualifiedAttribution) => [item.agentId, item.order]) + ); + return base.creators + ?.map((item: Creator) => ({ + ...item, + index: objectOrder[item.absoluteUrl], + })) + .sort((a: { index: number }, b: { index: number }) => a.index - b.index); +} diff --git a/src/app/shared/mappers/institutions/institutions.mapper.ts b/src/app/shared/mappers/institutions/institutions.mapper.ts index 1c5ddb58e..53ee7f8e1 100644 --- a/src/app/shared/mappers/institutions/institutions.mapper.ts +++ b/src/app/shared/mappers/institutions/institutions.mapper.ts @@ -17,7 +17,7 @@ export class InstitutionsMapper { type: data.type, name: data.attributes.name, description: data.attributes.description, - iri: data.attributes.iri, + iri: data.links.iri, rorIri: data.attributes.ror_iri, iris: data.attributes.iris, assets: data.attributes.assets, diff --git a/src/app/shared/mappers/search/search.mapper.ts b/src/app/shared/mappers/search/search.mapper.ts index 59099d6d5..4a7fd7bca 100644 --- a/src/app/shared/mappers/search/search.mapper.ts +++ b/src/app/shared/mappers/search/search.mapper.ts @@ -27,6 +27,7 @@ export function MapResources(indexCardData: IndexCardDataJsonApi): ResourceModel creators: (resourceMetadata.creator ?? []).map((creator) => ({ absoluteUrl: creator?.['@id'], name: creator?.name?.[0]?.['@value'], + affiliationAbsoluteUrl: creator.affiliation?.[0]?.['@id'] ?? null, })), affiliations: (resourceMetadata.affiliation ?? []).map((affiliation) => ({ absoluteUrl: affiliation?.['@id'], @@ -36,6 +37,7 @@ export function MapResources(indexCardData: IndexCardDataJsonApi): ResourceModel qualifiedAttribution: (resourceMetadata.qualifiedAttribution ?? []).map((qualifiedAttribution) => ({ agentId: qualifiedAttribution?.agent?.[0]?.['@id'], order: +qualifiedAttribution?.['osf:order']?.[0]?.['@value'], + hadRole: qualifiedAttribution.hadRole?.[0]?.['@id'], })), identifiers: (resourceMetadata.identifier ?? []).map((obj) => obj['@value']), provider: (resourceMetadata.publisher ?? null)?.map((publisher) => ({ @@ -77,10 +79,12 @@ export function MapResources(indexCardData: IndexCardDataJsonApi): ResourceModel creators: (isContainedBy?.creator ?? []).map((creator) => ({ absoluteUrl: creator?.['@id'], name: creator?.name?.[0]?.['@value'], + affiliationAbsoluteUrl: creator.affiliation?.[0]?.['@id'] ?? null, })), qualifiedAttribution: (isContainedBy?.qualifiedAttribution ?? []).map((qualifiedAttribution) => ({ agentId: qualifiedAttribution?.agent?.[0]?.['@id'], order: +qualifiedAttribution?.['osf:order']?.[0]?.['@value'], + hadRole: qualifiedAttribution.hadRole?.[0]?.['@id'], })), }))[0], statedConflictOfInterest: resourceMetadata.statedConflictOfInterest?.[0]?.['@value'], diff --git a/src/app/shared/models/search/index-card-search-json-api.models.ts b/src/app/shared/models/search/index-card-search-json-api.models.ts index 5f4c3154e..cf8087208 100644 --- a/src/app/shared/models/search/index-card-search-json-api.models.ts +++ b/src/app/shared/models/search/index-card-search-json-api.models.ts @@ -9,6 +9,7 @@ export type IndexCardSearchResponseJsonApi = JsonApiResponse< }; relationships: { searchResultPage: { + data: { id: string }[]; links: { first: { href: string; @@ -64,7 +65,7 @@ interface ResourceMetadataJsonApi { dateModified: { '@value': string }[]; dateWithdrawn: { '@value': string }[]; - creator: MetadataField[]; + creator: Creator[]; hasVersion: MetadataField[]; identifier: { '@value': string }[]; publisher: MetadataField[]; @@ -111,9 +112,13 @@ interface Usage { downloadCount: { '@value': string }[]; } +interface Creator extends MetadataField { + affiliation: MetadataField[]; +} + interface IsContainedBy extends MetadataField { funder: MetadataField[]; - creator: MetadataField[]; + creator: Creator[]; rights: MetadataField[]; qualifiedAttribution: QualifiedAttribution[]; } diff --git a/src/app/shared/models/search/resource.model.ts b/src/app/shared/models/search/resource.model.ts index 155061f5f..a436b7069 100644 --- a/src/app/shared/models/search/resource.model.ts +++ b/src/app/shared/models/search/resource.model.ts @@ -16,7 +16,7 @@ export interface ResourceModel { dateWithdrawn?: Date; doi: string[]; - creators: AbsoluteUrlName[]; + creators: Creator[]; identifiers: string[]; provider?: AbsoluteUrlName; license?: AbsoluteUrlName; @@ -46,7 +46,7 @@ export interface ResourceModel { export interface IsContainedBy extends AbsoluteUrlName { funders: AbsoluteUrlName[]; - creators: AbsoluteUrlName[]; + creators: Creator[]; license?: AbsoluteUrlName; qualifiedAttribution: QualifiedAttribution[]; } @@ -54,6 +54,11 @@ export interface IsContainedBy extends AbsoluteUrlName { export interface QualifiedAttribution { agentId: string; order: number; + hadRole: string; +} + +export interface Creator extends AbsoluteUrlName { + affiliationAbsoluteUrl: StringOrNull; } export interface AbsoluteUrlName { diff --git a/src/app/shared/stores/global-search/global-search.state.ts b/src/app/shared/stores/global-search/global-search.state.ts index 82342dffe..78126bc54 100644 --- a/src/app/shared/stores/global-search/global-search.state.ts +++ b/src/app/shared/stores/global-search/global-search.state.ts @@ -315,7 +315,7 @@ export class GlobalSearchState { filtersParams['page[size]'] = '10'; const sortBy = state.sortBy; - const sortParam = sortBy.includes('date') || sortBy.includes('relevance') ? 'sort' : 'sort[integer-value]'; + const sortParam = sortBy.includes('count') && !sortBy.includes('relevance') ? 'sort[integer-value]' : 'sort'; filtersParams[sortParam] = sortBy; Object.entries(state.defaultFilterValues).forEach(([key, value]) => { From 37a7105fa1fcc49a4894ccd64b1fb9233d44197c Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 16 Sep 2025 18:35:28 +0300 Subject: [PATCH 3/5] fix(institution-dashboard): Showing contributors sorted by permissions --- .../preprints-table-columns.constant.ts | 2 +- .../registration-table-columns.constant.ts | 2 +- .../mappers/creators.mapper.ts | 30 ++++++++++++++++++ ...stitution-preprint-to-table-data.mapper.ts | 10 +++--- ...nstitution-project-to-table-data.mapper.ts | 31 ++----------------- ...ution-registration-to-table-data.mapper.ts | 13 ++++---- .../institutions-preprints.component.ts | 8 +++-- .../institutions-registrations.component.ts | 10 ++++-- .../moderators-list.component.ts | 16 +++++----- .../moderation/services/moderators.service.ts | 12 +++++-- .../store/moderators/moderators.state.ts | 3 +- 11 files changed, 77 insertions(+), 60 deletions(-) create mode 100644 src/app/features/admin-institutions/mappers/creators.mapper.ts 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 index 859a85cba..cbd0852b3 100644 --- a/src/app/features/admin-institutions/constants/preprints-table-columns.constant.ts +++ b/src/app/features/admin-institutions/constants/preprints-table-columns.constant.ts @@ -36,7 +36,7 @@ export const preprintsTableColumns: TableColumn[] = [ header: 'adminInstitutions.projects.license', }, { - field: 'contributorName', + field: 'creator', header: 'adminInstitutions.projects.contributorName', isLink: true, isArray: true, 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 3fce5dd02..f7ca752d5 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 @@ -42,7 +42,7 @@ export const registrationTableColumns: TableColumn[] = [ sortField: 'storageByteCount', }, { - field: 'contributorName', + field: 'creator', header: 'adminInstitutions.projects.contributorName', isLink: true, isArray: true, diff --git a/src/app/features/admin-institutions/mappers/creators.mapper.ts b/src/app/features/admin-institutions/mappers/creators.mapper.ts new file mode 100644 index 000000000..48a14125d --- /dev/null +++ b/src/app/features/admin-institutions/mappers/creators.mapper.ts @@ -0,0 +1,30 @@ +import { getSortedContributorsByPermissions } from '@shared/helpers'; +import { ResourceModel } from '@shared/models'; + +export function mapCreators(project: ResourceModel, currentInstitutionId: string) { + const creatorsRoles = project.qualifiedAttribution.map((qa) => { + let role; + if (qa.hadRole.includes('admin')) { + role = 'Administrator'; + } else if (qa.hadRole.includes('write')) { + role = 'Read + Write'; + } else { + role = 'Read'; + } + return { + id: qa.agentId, + role, + }; + }); + + return getSortedContributorsByPermissions(project) + ?.filter((creator) => creator.affiliationAbsoluteUrl === currentInstitutionId) + ?.map((creator) => { + const name = creator.name.trim(); + const role = creatorsRoles.find((cr) => cr.id === creator.absoluteUrl)!.role; + return { + text: `${name} (${role})`, + url: creator.absoluteUrl, + }; + }); +} 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 index 539aef6ea..78247d03a 100644 --- 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 @@ -1,10 +1,11 @@ -import { getSortedContributorsByPermissions } from '@shared/helpers'; import { ResourceModel } from '@shared/models'; import { extractPathAfterDomain } from '../helpers'; import { TableCellData } from '../models'; -export function mapPreprintResourceToTableData(preprint: ResourceModel): TableCellData { +import { mapCreators } from './creators.mapper'; + +export function mapPreprintResourceToTableData(preprint: ResourceModel, currentInstitutionId: string): TableCellData { return { title: preprint.title, link: { @@ -20,10 +21,7 @@ export function mapPreprintResourceToTableData(preprint: ResourceModel): TableCe } : '-', license: preprint.license?.name || '-', - contributorName: getSortedContributorsByPermissions(preprint)?.map((creator) => ({ - text: creator.name.trim(), - url: creator.absoluteUrl, - })), + creator: mapCreators(preprint, currentInstitutionId), viewsLast30Days: preprint.viewsCount || '-', downloadsLast30Days: preprint.downloadCount || '-', }; diff --git a/src/app/features/admin-institutions/mappers/institution-project-to-table-data.mapper.ts b/src/app/features/admin-institutions/mappers/institution-project-to-table-data.mapper.ts index 706b0aa47..0a32b1a02 100644 --- a/src/app/features/admin-institutions/mappers/institution-project-to-table-data.mapper.ts +++ b/src/app/features/admin-institutions/mappers/institution-project-to-table-data.mapper.ts @@ -1,9 +1,10 @@ -import { getSortedContributorsByPermissions } from '@shared/helpers'; import { ResourceModel } from '@shared/models'; import { extractPathAfterDomain } from '../helpers'; import { TableCellData } from '../models'; +import { mapCreators } from './creators.mapper'; + export function mapProjectResourceToTableCellData(project: ResourceModel, currentInstitutionId: string): TableCellData { return { title: project.title, @@ -29,31 +30,3 @@ export function mapProjectResourceToTableCellData(project: ResourceModel, curren funderName: project.funders?.[0]?.name || '-', }; } - -function mapCreators(project: ResourceModel, currentInstitutionId: string) { - const creatorsRoles = project.qualifiedAttribution.map((qa) => { - let role; - if (qa.hadRole.includes('admin')) { - role = 'Administrator'; - } else if (qa.hadRole.includes('write')) { - role = 'Read + Write'; - } else { - role = 'Read'; - } - return { - id: qa.agentId, - role, - }; - }); - - return getSortedContributorsByPermissions(project) - ?.filter((creator) => creator.affiliationAbsoluteUrl === currentInstitutionId) - ?.map((creator) => { - const name = creator.name.trim(); - const role = creatorsRoles.find((cr) => cr.id === creator.absoluteUrl)!.role; - return { - text: `${name} (${role})`, - url: creator.absoluteUrl, - }; - }); -} 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 7733c1e3e..5116c73df 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,10 +1,14 @@ import { extractPathAfterDomain } from '@osf/features/admin-institutions/helpers'; -import { getSortedContributorsByPermissions } from '@shared/helpers'; import { ResourceModel } from '@shared/models'; import { TableCellData } from '../models'; -export function mapRegistrationResourceToTableData(registration: ResourceModel): TableCellData { +import { mapCreators } from './creators.mapper'; + +export function mapRegistrationResourceToTableData( + registration: ResourceModel, + currentInstitutionId: string +): TableCellData { return { title: registration.title, link: { @@ -23,10 +27,7 @@ export function mapRegistrationResourceToTableData(registration: ResourceModel): totalDataStored: registration.storageByteCount ? `${(+registration.storageByteCount / (1024 * 1024)).toFixed(1)} MB` : '0 B', - contributorName: getSortedContributorsByPermissions(registration)?.map((creator) => ({ - text: creator.name.trim(), - url: creator.absoluteUrl, - })), + creator: mapCreators(registration, currentInstitutionId), views: registration.viewsCount || '-', resourceType: registration.resourceNature || '-', license: registration.license?.name || '-', 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 e7e155022..da2e9a868 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 @@ -8,7 +8,7 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, OnDestroy, OnInit, signal } from '@angular/core'; import { ResourceType, SortOrder } from '@osf/shared/enums'; -import { PaginationLinksModel, SearchFilters } from '@osf/shared/models'; +import { PaginationLinksModel, ResourceModel, SearchFilters } from '@osf/shared/models'; import { FetchResources, FetchResourcesByLink, @@ -62,7 +62,11 @@ export class InstitutionsPreprintsComponent implements OnInit, OnDestroy { nextLink = select(GlobalSearchSelectors.getNext); previousLink = select(GlobalSearchSelectors.getPrevious); - tableData = computed(() => this.resources().map(mapPreprintResourceToTableData) as TableCellData[]); + tableData = computed(() => + this.resources().map( + (resource: ResourceModel): TableCellData => mapPreprintResourceToTableData(resource, this.institution().iri) + ) + ); sortParam = computed(() => { const sortField = this.sortField(); 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 daee016f4..a48f36e43 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 @@ -7,8 +7,9 @@ import { Button } from 'primeng/button'; import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, OnDestroy, OnInit, signal } from '@angular/core'; +import { TableCellData } from '@osf/features/admin-institutions/models'; import { ResourceType, SortOrder } from '@osf/shared/enums'; -import { PaginationLinksModel, SearchFilters } from '@osf/shared/models'; +import { PaginationLinksModel, ResourceModel, SearchFilters } from '@osf/shared/models'; import { ClearFilterSearchResults, FetchResources, @@ -63,8 +64,11 @@ export class InstitutionsRegistrationsComponent implements OnInit, OnDestroy { nextLink = select(GlobalSearchSelectors.getNext); previousLink = select(GlobalSearchSelectors.getPrevious); - tableData = computed(() => this.resources().map(mapRegistrationResourceToTableData)); - + tableData = computed(() => + this.resources().map( + (resource: ResourceModel): TableCellData => mapRegistrationResourceToTableData(resource, this.institution().iri) + ) + ); sortParam = computed(() => { const sortField = this.sortField(); const sortOrder = this.sortOrder(); diff --git a/src/app/features/moderation/components/moderators-list/moderators-list.component.ts b/src/app/features/moderation/components/moderators-list/moderators-list.component.ts index 9e963da8c..7f37baf80 100644 --- a/src/app/features/moderation/components/moderators-list/moderators-list.component.ts +++ b/src/app/features/moderation/components/moderators-list/moderators-list.component.ts @@ -5,7 +5,7 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { DialogService } from 'primeng/dynamicdialog'; -import { debounceTime, distinctUntilChanged, filter, forkJoin, map, of, skip } from 'rxjs'; +import { debounceTime, distinctUntilChanged, filter, forkJoin, map, of } from 'rxjs'; import { ChangeDetectionStrategy, @@ -92,12 +92,6 @@ export class ModeratorsListComponent implements OnInit { constructor() { effect(() => { this.moderators.set(JSON.parse(JSON.stringify(this.initialModerators()))); - - if (this.isModeratorsLoading()) { - this.searchControl.disable(); - } else { - this.searchControl.enable(); - } }); } @@ -199,7 +193,11 @@ export class ModeratorsListComponent implements OnInit { private setSearchSubscription() { this.searchControl.valueChanges - .pipe(skip(1), debounceTime(500), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) - .subscribe((res) => this.actions.updateSearchValue(res ?? null)); + .pipe(debounceTime(500), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) + .subscribe((res) => { + if (!res) res = null; + this.actions.updateSearchValue(res); + this.actions.loadModerators(this.providerId(), this.resourceType()); + }); } } diff --git a/src/app/features/moderation/services/moderators.service.ts b/src/app/features/moderation/services/moderators.service.ts index ee9a78dc8..f2e99f256 100644 --- a/src/app/features/moderation/services/moderators.service.ts +++ b/src/app/features/moderation/services/moderators.service.ts @@ -5,6 +5,7 @@ import { inject, Injectable } from '@angular/core'; import { ResourceType } from '@osf/shared/enums'; import { JsonApiResponse, PaginatedData, ResponseJsonApi, UserDataJsonApi } from '@osf/shared/models'; import { JsonApiService } from '@osf/shared/services'; +import { StringOrNull } from '@shared/helpers'; import { AddModeratorType } from '../enums'; import { ModerationMapper } from '../mappers'; @@ -25,11 +26,18 @@ export class ModeratorsService { [ResourceType.Preprint, 'providers/preprints'], ]); - getModerators(resourceId: string, resourceType: ResourceType): Observable { + getModerators( + resourceId: string, + resourceType: ResourceType, + searchValue: StringOrNull + ): Observable { const baseUrl = `${this.apiUrl}/${this.urlMap.get(resourceType)}/${resourceId}/moderators/`; + const params = { + searchValue, + }; return this.jsonApiService - .get(baseUrl) + .get(baseUrl, params) .pipe(map((response) => response.data.map((moderator) => ModerationMapper.fromModeratorResponse(moderator)))); } diff --git a/src/app/features/moderation/store/moderators/moderators.state.ts b/src/app/features/moderation/store/moderators/moderators.state.ts index 9733135af..e6ee46550 100644 --- a/src/app/features/moderation/store/moderators/moderators.state.ts +++ b/src/app/features/moderation/store/moderators/moderators.state.ts @@ -40,7 +40,8 @@ export class ModeratorsState { moderators: { ...state.moderators, isLoading: true, error: null }, }); - return this.moderatorsService.getModerators(action.resourceId, action.resourceType).pipe( + const searchValue = state.moderators.searchValue; + return this.moderatorsService.getModerators(action.resourceId, action.resourceType, searchValue).pipe( tap((moderators: ModeratorModel[]) => { ctx.patchState({ moderators: { From 277610f25c6e85c4b76986282b367bdb293f48cd Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 16 Sep 2025 18:48:41 +0300 Subject: [PATCH 4/5] fix(moderators-search): Fixed searching moderators --- src/app/features/moderation/services/moderators.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/features/moderation/services/moderators.service.ts b/src/app/features/moderation/services/moderators.service.ts index f2e99f256..ed4c73ee7 100644 --- a/src/app/features/moderation/services/moderators.service.ts +++ b/src/app/features/moderation/services/moderators.service.ts @@ -33,9 +33,10 @@ export class ModeratorsService { ): Observable { const baseUrl = `${this.apiUrl}/${this.urlMap.get(resourceType)}/${resourceId}/moderators/`; - const params = { - searchValue, - }; + const params: Record = {}; + if (searchValue) { + params['filter[full_name]'] = searchValue; + } return this.jsonApiService .get(baseUrl, params) .pipe(map((response) => response.data.map((moderator) => ModerationMapper.fromModeratorResponse(moderator)))); From cd54a1aaba59ad41aa74199e8e448d40c3e65cbd Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 22 Sep 2025 16:59:46 +0300 Subject: [PATCH 5/5] fix(institution-summary): Improved charts logic --- .../institutions-summary.component.html | 148 ++++++----- .../institutions-summary.component.ts | 249 ++++++++++-------- src/app/features/admin-institutions/routes.ts | 4 + .../bar-chart/bar-chart.component.html | 19 +- .../bar-chart/bar-chart.component.ts | 30 +-- .../doughnut-chart.component.html | 19 +- .../doughnut-chart.component.ts | 29 +- 7 files changed, 261 insertions(+), 237 deletions(-) diff --git a/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.html b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.html index 0f657dde5..750c68efb 100644 --- a/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.html +++ b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.html @@ -11,79 +11,93 @@
-
- -
+ @if (departmentLabels().length > 0) { +
+ +
+ } -
- -
+ @if (projectsLabels().length > 0) { +
+ +
+ } -
- -
+ @if (registrationsLabels().length > 0) { +
+ +
+ } -
- -
+ @if (osfProjectsLabels().length > 0) { +
+ +
+ } -
- -
+ @if (licenceLabels().length > 0) { +
+ +
+ } -
- -
+ @if (addonLabels().length > 0) { +
+ +
+ } -
- -
+ @if (storageLabels().length > 0) { +
+ +
+ }
} diff --git a/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.ts b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.ts index 150fa0883..af580466a 100644 --- a/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.ts @@ -2,7 +2,7 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { ChangeDetectionStrategy, Component, effect, inject, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, inject, OnInit, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { BarChartComponent, LoadingSpinnerComponent, StatisticCardComponent } from '@shared/components'; @@ -45,26 +45,26 @@ export class InstitutionsSummaryComponent implements OnInit { rightsSearch = select(InstitutionsAdminSelectors.getSearchResults); rightsLoading = select(InstitutionsAdminSelectors.getSearchResultsLoading); - departmentLabels: string[] = []; - departmentDataset: DatasetInput[] = []; + departmentLabels = signal([]); + departmentDataset = signal([]); - projectsLabels: string[] = []; - projectDataset: DatasetInput[] = []; + projectsLabels = signal([]); + projectDataset = signal([]); - registrationsLabels: string[] = []; - registrationsDataset: DatasetInput[] = []; + registrationsLabels = signal([]); + registrationsDataset = signal([]); - osfProjectsLabels: string[] = []; - osfProjectsDataset: DatasetInput[] = []; + osfProjectsLabels = signal([]); + osfProjectsDataset = signal([]); - storageLabels: string[] = []; - storageDataset: DatasetInput[] = []; + storageLabels = signal([]); + storageDataset = signal([]); - licenceLabels: string[] = []; - licenceDataset: DatasetInput[] = []; + licenceLabels = signal([]); + licenceDataset = signal([]); - addonLabels: string[] = []; - addonDataset: DatasetInput[] = []; + addonLabels = signal([]); + addonDataset = signal([]); private readonly actions = createDispatchMap({ fetchDepartments: FetchInstitutionDepartments, @@ -75,10 +75,12 @@ export class InstitutionsSummaryComponent implements OnInit { }); constructor() { - effect(() => { - this.setStatisticSummaryData(); - this.setChartData(); - }); + this.setStatisticSummaryDataEffect(); + this.setDepartmentsEffect(); + this.setSummaryMetricsEffect(); + this.setStorageEffect(); + this.setLicenseEffect(); + this.setAddonsEffect(); } ngOnInit(): void { @@ -93,102 +95,131 @@ export class InstitutionsSummaryComponent implements OnInit { } } - private setStatisticSummaryData(): void { - const summary = this.summaryMetrics(); - - if (summary) { - this.statisticsData = [ - { - label: 'adminInstitutions.summary.totalUsers', - value: summary.userCount, - }, - { - label: 'adminInstitutions.summary.totalMonthlyLoggedInUsers', - value: summary.monthlyLoggedInUserCount, - }, - { - label: 'adminInstitutions.summary.totalMonthlyActiveUsers', - value: summary.monthlyActiveUserCount, - }, - { - label: 'adminInstitutions.summary.osfPublicAndPrivateProjects', - value: summary.publicProjectCount + summary.privateProjectCount, - }, - { - label: 'adminInstitutions.summary.osfPublicAndEmbargoedRegistrations', - value: summary.publicRegistrationCount + summary.embargoedRegistrationCount, - }, - { - label: 'adminInstitutions.summary.osfPreprints', - value: summary.publishedPreprintCount, - }, - { - label: 'adminInstitutions.summary.totalPublicFileCount', - value: summary.publicFileCount, - }, - { - label: 'adminInstitutions.summary.totalStorageInGb', - value: this.convertBytesToGB(summary.storageByteCount), - }, - ]; - } + private setStatisticSummaryDataEffect(): void { + effect(() => { + const summary = this.summaryMetrics(); + + if (summary) { + this.statisticsData = [ + { + label: 'adminInstitutions.summary.totalUsers', + value: summary.userCount, + }, + { + label: 'adminInstitutions.summary.totalMonthlyLoggedInUsers', + value: summary.monthlyLoggedInUserCount, + }, + { + label: 'adminInstitutions.summary.totalMonthlyActiveUsers', + value: summary.monthlyActiveUserCount, + }, + { + label: 'adminInstitutions.summary.osfPublicAndPrivateProjects', + value: summary.publicProjectCount + summary.privateProjectCount, + }, + { + label: 'adminInstitutions.summary.osfPublicAndEmbargoedRegistrations', + value: summary.publicRegistrationCount + summary.embargoedRegistrationCount, + }, + { + label: 'adminInstitutions.summary.osfPreprints', + value: summary.publishedPreprintCount, + }, + { + label: 'adminInstitutions.summary.totalPublicFileCount', + value: summary.publicFileCount, + }, + { + label: 'adminInstitutions.summary.totalStorageInGb', + value: this.convertBytesToGB(summary.storageByteCount), + }, + ]; + } + }); } - private setChartData(): void { - const departments = this.departments(); - const summary = this.summaryMetrics(); - const storage = this.storageRegionSearch(); - const licenses = this.rightsSearch(); - const addons = this.hasOsfAddonSearch(); - - this.departmentLabels = departments.map((item) => item.name || ''); - this.departmentDataset = [{ label: '', data: departments.map((item) => item.numberOfUsers) }]; - - this.projectsLabels = ['resourceCard.labels.publicProjects', 'adminInstitutions.summary.privateProjects'].map( - (el) => this.translateService.instant(el) - ); - this.projectDataset = [{ label: '', data: [summary.publicProjectCount, summary.privateProjectCount] }]; - - this.registrationsLabels = [ - 'resourceCard.labels.publicRegistrations', - 'adminInstitutions.summary.embargoedRegistrations', - ].map((el) => this.translateService.instant(el)); - this.registrationsDataset = [ - { label: '', data: [summary.publicRegistrationCount, summary.embargoedRegistrationCount] }, - ]; - - this.osfProjectsLabels = [ - 'adminInstitutions.summary.publicRegistrations', - 'adminInstitutions.summary.embargoedRegistrations', - 'adminInstitutions.summary.publicProjects', - 'adminInstitutions.summary.privateProjects', - 'common.search.tabs.preprints', - ].map((el) => this.translateService.instant(el)); - this.osfProjectsDataset = [ - { - label: '', - data: [ - summary.publicRegistrationCount, - summary.embargoedRegistrationCount, - summary.publicProjectCount, - summary.privateProjectCount, - summary.publishedPreprintCount, - ], - }, - ]; - - this.storageLabels = storage.map((result) => result.label); - this.storageDataset = [{ label: '', data: storage.map((result) => +result.value) }]; - - this.licenceLabels = licenses.map((result) => result.label); - this.licenceDataset = [{ label: '', data: licenses.map((result) => +result.value) }]; - - this.addonLabels = addons.map((result) => result.label); - this.addonDataset = [{ label: '', data: addons.map((result) => +result.value) }]; + private setAddonsEffect(): void { + effect(() => { + const addons = this.hasOsfAddonSearch(); + + this.addonLabels.set(addons.map((result) => result.label)); + this.addonDataset.set([{ label: '', data: addons.map((result) => +result.value) }]); + }); } private convertBytesToGB(bytes: number): string { const gb = bytes / (1024 * 1024 * 1024); return gb.toFixed(1); } + + private setDepartmentsEffect() { + effect(() => { + const departments = this.departments(); + + this.departmentLabels.set(departments.map((item) => item.name || '')); + this.departmentDataset.set([{ label: '', data: departments.map((item) => item.numberOfUsers) }]); + }); + } + + private setSummaryMetricsEffect() { + effect(() => { + const summary = this.summaryMetrics(); + + this.projectsLabels.set( + ['resourceCard.labels.publicProjects', 'adminInstitutions.summary.privateProjects'].map((el) => + this.translateService.instant(el) + ) + ); + this.projectDataset.set([{ label: '', data: [summary.publicProjectCount, summary.privateProjectCount] }]); + + this.registrationsLabels.set( + ['resourceCard.labels.publicRegistrations', 'adminInstitutions.summary.embargoedRegistrations'].map((el) => + this.translateService.instant(el) + ) + ); + this.registrationsDataset.set([ + { label: '', data: [summary.publicRegistrationCount, summary.embargoedRegistrationCount] }, + ]); + + this.osfProjectsLabels.set( + [ + 'adminInstitutions.summary.publicRegistrations', + 'adminInstitutions.summary.embargoedRegistrations', + 'adminInstitutions.summary.publicProjects', + 'adminInstitutions.summary.privateProjects', + 'common.search.tabs.preprints', + ].map((el) => this.translateService.instant(el)) + ); + this.osfProjectsDataset.set([ + { + label: '', + data: [ + summary.publicRegistrationCount, + summary.embargoedRegistrationCount, + summary.publicProjectCount, + summary.privateProjectCount, + summary.publishedPreprintCount, + ], + }, + ]); + }); + } + + private setStorageEffect() { + effect(() => { + const storage = this.storageRegionSearch(); + + this.storageLabels.set(storage.map((result) => result.label)); + this.storageDataset.set([{ label: '', data: storage.map((result) => +result.value) }]); + }); + } + + private setLicenseEffect() { + effect(() => { + const licenses = this.rightsSearch(); + + this.licenceLabels.set(licenses.map((result) => result.label)); + this.licenceDataset.set([{ label: '', data: licenses.map((result) => +result.value) }]); + }); + } } diff --git a/src/app/features/admin-institutions/routes.ts b/src/app/features/admin-institutions/routes.ts index 9d697be2e..d64d488c0 100644 --- a/src/app/features/admin-institutions/routes.ts +++ b/src/app/features/admin-institutions/routes.ts @@ -32,18 +32,22 @@ export const routes: Routes = [ { path: 'users', component: InstitutionsUsersComponent, + data: { scrollToTop: false }, }, { path: 'projects', component: InstitutionsProjectsComponent, + data: { scrollToTop: false }, }, { path: 'registrations', component: InstitutionsRegistrationsComponent, + data: { scrollToTop: false }, }, { path: 'preprints', component: InstitutionsPreprintsComponent, + data: { scrollToTop: false }, }, ], }, diff --git a/src/app/shared/components/bar-chart/bar-chart.component.html b/src/app/shared/components/bar-chart/bar-chart.component.html index 66f2adbd6..99a45b351 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.html +++ b/src/app/shared/components/bar-chart/bar-chart.component.html @@ -5,24 +5,29 @@

{{ title() | translate }}

@if (isLoading()) { } @else { -
-
- +
+
+ @if (data() && options() && labels().length) { + + }
@if (showExpandedSection()) { -
+
- +

{{ title() | translate }}

-
+
@for (label of labels(); let i = $index; track i) {
  • -
    +
    {{ label }}
    @if (datasets().length) { diff --git a/src/app/shared/components/bar-chart/bar-chart.component.ts b/src/app/shared/components/bar-chart/bar-chart.component.ts index e47946b32..297f98779 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.ts +++ b/src/app/shared/components/bar-chart/bar-chart.component.ts @@ -4,16 +4,7 @@ import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'pr import { ChartModule } from 'primeng/chart'; import { isPlatformBrowser } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - inject, - input, - OnInit, - PLATFORM_ID, - signal, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input, OnInit, PLATFORM_ID, signal } from '@angular/core'; import { PIE_CHART_PALETTE } from '@osf/shared/constants'; import { DatasetInput } from '@osf/shared/models'; @@ -49,17 +40,13 @@ export class BarChartComponent implements OnInit { orientation = input<'horizontal' | 'vertical'>('horizontal'); showExpandedSection = input(false); - options = signal({}); - data = signal({} as ChartData); + options = signal(null); + data = signal(null); platformId = inject(PLATFORM_ID); - cd = inject(ChangeDetectorRef); + readonly PIE_CHART_PALETTE = PIE_CHART_PALETTE; ngOnInit() { - this.initChart(); - } - - initChart() { if (isPlatformBrowser(this.platformId)) { const documentStyle = getComputedStyle(document.documentElement); const textColorSecondary = documentStyle.getPropertyValue('--dark-blue-1'); @@ -69,8 +56,6 @@ export class BarChartComponent implements OnInit { this.setChartData(defaultBackgroundColor, defaultBorderColor); this.setChartOptions(textColorSecondary, surfaceBorder); - - this.cd.markForCheck(); } } @@ -88,15 +73,12 @@ export class BarChartComponent implements OnInit { }); } - getColor(index: number): string { - return PIE_CHART_PALETTE[index % PIE_CHART_PALETTE.length]; - } - private setChartOptions(textColorSecondary: string, surfaceBorder: string) { this.options.set({ indexAxis: this.orientation() === 'horizontal' ? 'y' : 'x', maintainAspectRatio: false, - aspectRatio: 0.8, + aspectRatio: 0.9, + responsive: true, plugins: { legend: { display: this.showLegend(), diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.html b/src/app/shared/components/doughnut-chart/doughnut-chart.component.html index eefcdeccb..e8c487bfc 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.html +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.html @@ -5,24 +5,29 @@

    {{ title() | translate }}

    @if (isLoading()) { } @else { -
    -
    - +
    +
    + @if (data() && options() && labels().length) { + + }
    @if (showExpandedSection()) { -
    +
    - +

    {{ title() | translate }}

    -
    +
    @for (label of labels(); let i = $index; track i) {
  • -
    +
    {{ label }}
    @if (datasets().length) { diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts b/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts index 718d71c16..88b4f6755 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts @@ -4,16 +4,7 @@ import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'pr import { ChartModule } from 'primeng/chart'; import { isPlatformBrowser } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - inject, - input, - OnInit, - PLATFORM_ID, - signal, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input, OnInit, PLATFORM_ID, signal } from '@angular/core'; import { PIE_CHART_PALETTE } from '@osf/shared/constants'; import { DatasetInput } from '@osf/shared/models'; @@ -39,7 +30,6 @@ import { ChartData, ChartOptions } from 'chart.js'; }) export class DoughnutChartComponent implements OnInit { private readonly platformId = inject(PLATFORM_ID); - private readonly cd = inject(ChangeDetectorRef); isLoading = input(false); title = input(''); @@ -48,26 +38,18 @@ export class DoughnutChartComponent implements OnInit { showLegend = input(false); showExpandedSection = input(false); - options = signal({}); - data = signal({} as ChartData); + options = signal | null>(null); + data = signal(null); - ngOnInit() { - this.initChart(); - } + readonly PIE_CHART_PALETTE = PIE_CHART_PALETTE; - initChart() { + ngOnInit() { if (isPlatformBrowser(this.platformId)) { this.setChartData(); this.setChartOptions(); - - this.cd.markForCheck(); } } - getColor(index: number): string { - return PIE_CHART_PALETTE[index % PIE_CHART_PALETTE.length]; - } - private setChartData() { const chartDatasets = this.datasets().map((dataset) => ({ label: dataset.label, @@ -84,6 +66,7 @@ export class DoughnutChartComponent implements OnInit { private setChartOptions() { this.options.set({ + cutout: '60%', maintainAspectRatio: true, responsive: true, plugins: {