@if (col.sortable) {
@@ -98,10 +102,18 @@
[target]="getLinkTarget(rowData[col.field], col)"
class="text-primary no-underline hover:underline"
>
- {{ getCellValueWithFormatting(rowData[col.field], col) }}
+ @if (col.dateFormat) {
+ {{ getCellValue(rowData[col.field]) | date: col.dateFormat }}
+ } @else {
+ {{ getCellValue(rowData[col.field]) }}
+ }
} @else {
- {{ getCellValueWithFormatting(rowData[col.field], col) }}
+ @if (col.dateFormat) {
+ {{ getCellValue(rowData[col.field]) | date: col.dateFormat }}
+ } @else {
+ {{ getCellValue(rowData[col.field]) }}
+ }
}
@if (col.showIcon) {
@@ -121,13 +133,39 @@
-@if (enablePagination() && totalCount() > pageSize()) {
-
-
+@if (isNextPreviousPagination()) {
+
+ @if (firstLink() && prevLink()) {
+
+ }
+
+
+
+
+
+
+} @else {
+ @if (enablePagination() && totalCount() > pageSize()) {
+
+
+
+ }
}
diff --git a/src/app/features/admin-institutions/components/admin-table/admin-table.component.scss b/src/app/features/admin-institutions/components/admin-table/admin-table.component.scss
index ba786a529..5b95ea1f7 100644
--- a/src/app/features/admin-institutions/components/admin-table/admin-table.component.scss
+++ b/src/app/features/admin-institutions/components/admin-table/admin-table.component.scss
@@ -1,5 +1,5 @@
.p-06 {
- padding: 0.6rem;
+ padding: 0.625rem;
}
.hover-group {
@@ -15,6 +15,11 @@
}
}
+.download-button {
+ --p-button-outlined-info-border-color: var(--grey-2);
+ --p-button-padding-y: 0.625rem;
+}
+
.child-button-0-padding {
--p-button-padding-y: 0;
--p-button-icon-only-width: max-content;
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 e5a90116c..90856d99a 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
@@ -8,6 +8,7 @@ import { PaginatorState } from 'primeng/paginator';
import { TableModule } from 'primeng/table';
import { Tooltip } from 'primeng/tooltip';
+import { DatePipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@@ -18,6 +19,7 @@ import {
TableIconClickEvent,
} from '@osf/features/admin-institutions/models';
import { CustomPaginatorComponent } from '@osf/shared/components';
+import { StopPropagationDirective } from '@shared/directives';
import { SortOrder } from '@shared/enums';
import { QueryParams } from '@shared/models';
@@ -33,6 +35,8 @@ import { QueryParams } from '@shared/models';
TranslatePipe,
Button,
Menu,
+ StopPropagationDirective,
+ DatePipe,
],
templateUrl: './admin-table.component.html',
styleUrl: './admin-table.component.scss',
@@ -41,6 +45,8 @@ import { QueryParams } from '@shared/models';
export class AdminTableComponent {
private readonly translateService = inject(TranslateService);
+ private userInitiatedSort = false;
+
tableColumns = input.required
();
tableData = input.required();
@@ -53,9 +59,22 @@ export class AdminTableComponent {
sortField = input('');
sortOrder = input(1);
+ isNextPreviousPagination = input(false);
+
+ paginationLinks = input<
+ | {
+ first?: { href: string };
+ next?: { href: string };
+ prev?: { href: string };
+ last?: { href: string };
+ }
+ | undefined
+ >();
+
pageChanged = output();
sortChanged = output();
iconClicked = output();
+ linkPageChanged = output();
downloadLink = input('');
reportsLink = input('');
@@ -70,17 +89,17 @@ export class AdminTableComponent {
{
label: 'CSV',
icon: 'fa fa-file-csv',
- link: this.createUrl(baseUrl, 'csv'),
+ link: this.createUrl(baseUrl, 'text/csv'),
},
{
label: 'TSV',
icon: 'fa fa-file-alt',
- link: this.createUrl(baseUrl, 'tsv'),
+ link: this.createUrl(baseUrl, 'text/tab-separated-values'),
},
{
label: 'JSON',
icon: 'fa fa-file-code',
- link: this.createUrl(baseUrl, 'json'),
+ link: this.createUrl(baseUrl, 'application/json'),
},
];
});
@@ -99,6 +118,10 @@ export class AdminTableComponent {
sortColumn = computed(() => this.sortField());
currentSortOrder = computed(() => this.sortOrder());
+ firstLink = computed(() => this.paginationLinks()?.first?.href || '');
+ prevLink = computed(() => this.paginationLinks()?.prev?.href || '');
+ nextLink = computed(() => this.paginationLinks()?.next?.href || '');
+
constructor() {
effect(() => {
const columns = this.tableColumns();
@@ -116,13 +139,20 @@ export class AdminTableComponent {
this.pageChanged.emit(event);
}
+ onHeaderClick(column: TableColumn): void {
+ if (column.sortable) {
+ this.userInitiatedSort = true;
+ }
+ }
+
onSort(event: SortEvent): void {
- if (event.field) {
+ if (event.field && this.userInitiatedSort) {
this.sortChanged.emit({
sortColumn: event.field,
sortOrder: event.order === -1 ? SortOrder.Desc : SortOrder.Asc,
} as QueryParams);
}
+ this.userInitiatedSort = false;
}
onIconClick(rowData: TableCellData, column: TableColumn): void {
@@ -146,36 +176,12 @@ export class AdminTableComponent {
return this.translateService.instant(String(value)) || '';
}
- getCellValueWithFormatting(value: string | number | TableCellLink | undefined, column: TableColumn): string {
- if (this.isLink(value)) {
- return this.translateService.instant(value.text);
- }
-
- const stringValue = String(value);
-
- if (column.dateFormat && stringValue) {
- return this.formatDate(stringValue, column.dateFormat);
- }
-
- return this.translateService.instant(stringValue) || '';
- }
-
- private formatDate(value: string, format: string): string {
- if (format === 'yyyy-mm-to-mm/yyyy') {
- const yearMonthRegex = /^(\d{4})-(\d{2})$/;
- const match = value.match(yearMonthRegex);
-
- if (match) {
- const [, year, month] = match;
- return `${month}/${year}`;
- }
- }
-
- return value;
+ switchPage(link: string) {
+ this.linkPageChanged.emit(link);
}
- private createUrl(baseUrl: string, format: string): string {
- return `${baseUrl}?format=${format}`;
+ private createUrl(baseUrl: string, mediaType: string): string {
+ return `${baseUrl}&acceptMediatype=${encodeURIComponent(mediaType)}`;
}
getLinkUrl(value: string | number | TableCellLink | undefined): string {
diff --git a/src/app/features/admin-institutions/constants/admin-table-columns.constant.ts b/src/app/features/admin-institutions/constants/admin-table-columns.constant.ts
index 9eea223d4..12e0d68b8 100644
--- a/src/app/features/admin-institutions/constants/admin-table-columns.constant.ts
+++ b/src/app/features/admin-institutions/constants/admin-table-columns.constant.ts
@@ -21,19 +21,19 @@ export const userTableColumns: TableColumn[] = [
field: 'monthLastLogin',
header: 'adminInstitutions.institutionUsers.lastLogin',
sortable: true,
- dateFormat: 'yyyy-mm-to-mm/yyyy',
+ dateFormat: 'MM/yyyy',
},
{
field: 'monthLastActive',
header: 'adminInstitutions.institutionUsers.lastActive',
sortable: true,
- dateFormat: 'yyyy-mm-to-mm/yyyy',
+ dateFormat: 'MM/yyyy',
},
{
field: 'accountCreationDate',
header: 'adminInstitutions.institutionUsers.accountCreated',
sortable: true,
- dateFormat: 'yyyy-mm-to-mm/yyyy',
+ dateFormat: 'MM/yyyy',
},
{ field: 'publicRegistrationCount', header: 'adminInstitutions.summary.publicRegistrations', sortable: true },
{ field: 'embargoedRegistrationCount', header: 'adminInstitutions.summary.embargoedRegistrations', sortable: true },
diff --git a/src/app/features/admin-institutions/constants/index.ts b/src/app/features/admin-institutions/constants/index.ts
index 2d8e951ea..9044772a9 100644
--- a/src/app/features/admin-institutions/constants/index.ts
+++ b/src/app/features/admin-institutions/constants/index.ts
@@ -1,3 +1,4 @@
export * from './admin-table-columns.constant';
export * from './department-options.constant';
+export * from './project-table-columns.constant';
export * from './resource-tab-option.constant';
diff --git a/src/app/features/admin-institutions/constants/project-table-columns.constant.ts b/src/app/features/admin-institutions/constants/project-table-columns.constant.ts
new file mode 100644
index 000000000..51628fe50
--- /dev/null
+++ b/src/app/features/admin-institutions/constants/project-table-columns.constant.ts
@@ -0,0 +1,77 @@
+import { TableColumn } from '@osf/features/admin-institutions/models';
+
+export const projectTableColumns: TableColumn[] = [
+ {
+ field: 'title',
+ header: 'adminInstitutions.projects.title',
+ sortable: true,
+ 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,
+ },
+ {
+ field: 'storageLocation',
+ header: 'adminInstitutions.projects.storageLocation',
+ sortable: false,
+ },
+ {
+ field: 'totalDataStored',
+ header: 'adminInstitutions.projects.totalDataStored',
+ sortable: false,
+ },
+ {
+ field: 'creator',
+ 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: 'addOns',
+ header: 'adminInstitutions.projects.addOns',
+ sortable: false,
+ },
+ {
+ field: 'funderName',
+ header: 'adminInstitutions.projects.funderName',
+ sortable: false,
+ },
+];
diff --git a/src/app/features/admin-institutions/mappers/index.ts b/src/app/features/admin-institutions/mappers/index.ts
index 5b8786253..20dbd3dfa 100644
--- a/src/app/features/admin-institutions/mappers/index.ts
+++ b/src/app/features/admin-institutions/mappers/index.ts
@@ -1,4 +1,8 @@
export { mapInstitutionDepartment, mapInstitutionDepartments } from './institution-departments.mapper';
+export { mapProjectToTableCellData } from './institution-project-to-table-data.mapper';
+export { mapInstitutionProjects } from './institution-projects.mapper';
+export { mapIndexCardResults } from './institution-summary-index.mapper';
export { mapInstitutionSummaryMetrics } from './institution-summary-metrics.mapper';
export { mapUserToTableCellData } from './institution-user-to-table-data.mapper';
export { mapInstitutionUsers } from './institution-users.mapper';
+export { sendMessageRequestMapper } from './send-message-request.mapper';
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
new file mode 100644
index 000000000..22411579c
--- /dev/null
+++ b/src/app/features/admin-institutions/mappers/institution-project-to-table-data.mapper.ts
@@ -0,0 +1,28 @@
+import { InstitutionProject, TableCellData, TableCellLink } from '@osf/features/admin-institutions/models';
+
+export function mapProjectToTableCellData(project: InstitutionProject): TableCellData {
+ return {
+ title: {
+ url: project.id,
+ text: project.title,
+ } as TableCellLink,
+ link: {
+ url: project.id,
+ text: project.identifier || project.id,
+ } as TableCellLink,
+ dateCreated: project.dateCreated,
+ dateModified: project.dateModified,
+ doi: '-',
+ storageLocation: project.storageRegion || '-',
+ totalDataStored: project.storageByteCount ? `${(project.storageByteCount / (1024 * 1024)).toFixed(1)} MB` : '0 B',
+ creator: {
+ url: project.creator || '#',
+ text: project.creator || '-',
+ } as TableCellLink,
+ views: project.viewCount?.toString() || '-',
+ resourceType: project.resourceType,
+ license: project.rights || '-',
+ addOns: '-',
+ funderName: '-',
+ };
+}
diff --git a/src/app/features/admin-institutions/mappers/institution-projects.mapper.ts b/src/app/features/admin-institutions/mappers/institution-projects.mapper.ts
new file mode 100644
index 000000000..8b108d6d1
--- /dev/null
+++ b/src/app/features/admin-institutions/mappers/institution-projects.mapper.ts
@@ -0,0 +1,65 @@
+import {
+ Affiliation,
+ IncludedItem,
+ IndexCard,
+ InstitutionProject,
+ InstitutionRegistrationsJsonApi,
+ SearchResult,
+} from '../models';
+
+export function mapInstitutionProjects(response: InstitutionRegistrationsJsonApi): InstitutionProject[] {
+ 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 projects: InstitutionProject[] = [];
+
+ 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) {
+ projects.push({
+ id: metadata['@id'] || indexCard.id,
+ title: metadata.title?.[0]?.['@value'] || '',
+ creator: metadata.creator?.[0]?.name?.[0]?.['@value'] || '',
+ dateCreated: metadata.dateCreated?.[0]?.['@value'] || '',
+ dateModified: metadata.dateModified?.[0]?.['@value'] || '',
+ resourceType: metadata.resourceType?.[0]?.['@id'] || '',
+ accessService: metadata.accessService?.[0]?.['@id'] || '',
+ publisher: metadata.publisher?.[0]?.name?.[0]?.['@value'] || '',
+ identifier: metadata.identifier?.[0]?.['@value'] || '',
+ storageByteCount: metadata.storageByteCount?.[0]?.['@value']
+ ? parseInt(metadata.storageByteCount[0]['@value'])
+ : undefined,
+ storageRegion: metadata.storageRegion?.[0]?.prefLabel?.[0]?.['@value'] || undefined,
+ affiliation:
+ metadata.affiliation
+ ?.map((aff: Affiliation) => aff.name?.[0]?.['@value'])
+ .filter((value): value is string => Boolean(value)) || [],
+ description: metadata.description?.[0]?.['@value'] || undefined,
+ rights: metadata.rights?.[0]?.name?.[0]?.['@value'] || undefined,
+ subject: metadata.subject?.[0]?.prefLabel?.[0]?.['@value'] || undefined,
+ viewCount: metadata.usage?.[0]?.viewCount?.[0]?.['@value']
+ ? parseInt(metadata.usage[0].viewCount[0]['@value'])
+ : undefined,
+ downloadCount: metadata.usage?.[0]?.downloadCount?.[0]?.['@value']
+ ? parseInt(metadata.usage[0].downloadCount[0]['@value'])
+ : undefined,
+ hasVersion: metadata.hasVersion ? metadata.hasVersion.length > 0 : false,
+ supplements: metadata.supplements ? metadata.supplements.length > 0 : false,
+ });
+ }
+ }
+ }
+ });
+
+ return projects;
+}
diff --git a/src/app/features/admin-institutions/models/index.ts b/src/app/features/admin-institutions/models/index.ts
index fe82ed31a..c9859791c 100644
--- a/src/app/features/admin-institutions/models/index.ts
+++ b/src/app/features/admin-institutions/models/index.ts
@@ -1,6 +1,11 @@
export * from './institution-department.model';
export * from './institution-departments-json-api.model';
export * from './institution-index-value-search-json-api.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-registrations-json-api.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-project.model.ts b/src/app/features/admin-institutions/models/institution-project.model.ts
new file mode 100644
index 000000000..d07d4eeaa
--- /dev/null
+++ b/src/app/features/admin-institutions/models/institution-project.model.ts
@@ -0,0 +1,21 @@
+export interface InstitutionProject {
+ id: string;
+ title: string;
+ creator: string;
+ dateCreated: string;
+ dateModified: string;
+ resourceType: string;
+ accessService: string;
+ publisher: string;
+ identifier: string;
+ storageByteCount?: number;
+ storageRegion?: string;
+ affiliation?: string[];
+ description?: string;
+ rights?: string;
+ subject?: string;
+ viewCount?: number;
+ downloadCount?: number;
+ hasVersion?: boolean;
+ supplements?: boolean;
+}
diff --git a/src/app/features/admin-institutions/models/institution-projects-json-api.model.ts b/src/app/features/admin-institutions/models/institution-projects-json-api.model.ts
new file mode 100644
index 000000000..0528b8d9a
--- /dev/null
+++ b/src/app/features/admin-institutions/models/institution-projects-json-api.model.ts
@@ -0,0 +1,50 @@
+export interface IncludedItem {
+ id: string;
+ type: 'related-property-path' | 'search-result' | 'index-card';
+ attributes?: Record;
+ relationships?: Record;
+ links?: Record;
+}
+
+export interface SearchResult extends IncludedItem {
+ type: 'search-result';
+ relationships?: {
+ indexCard?: {
+ data?: {
+ id: string;
+ };
+ };
+ };
+}
+
+export interface IndexCard extends IncludedItem {
+ type: 'index-card';
+ attributes?: {
+ resourceMetadata?: ResourceMetadata;
+ };
+}
+
+export interface ResourceMetadata {
+ '@id'?: string;
+ title?: { '@value': string }[];
+ creator?: { name?: { '@value': string }[] }[];
+ dateCreated?: { '@value': string }[];
+ dateModified?: { '@value': string }[];
+ resourceType?: { '@id': string }[];
+ accessService?: { '@id': string }[];
+ publisher?: { name?: { '@value': string }[] }[];
+ identifier?: { '@value': string }[];
+ storageByteCount?: { '@value': string }[];
+ storageRegion?: { prefLabel?: { '@value': string }[] }[];
+ affiliation?: { name?: { '@value': string }[] }[];
+ description?: { '@value': string }[];
+ rights?: { name?: { '@value': string }[] }[];
+ subject?: { prefLabel?: { '@value': string }[] }[];
+ usage?: { viewCount?: { '@value': string }[]; downloadCount?: { '@value': string }[] }[];
+ hasVersion?: unknown[];
+ supplements?: unknown[];
+}
+
+export interface Affiliation {
+ name?: { '@value': string }[];
+}
diff --git a/src/app/features/admin-institutions/models/institution-projects-query-params.model.ts b/src/app/features/admin-institutions/models/institution-projects-query-params.model.ts
new file mode 100644
index 000000000..13b269bd9
--- /dev/null
+++ b/src/app/features/admin-institutions/models/institution-projects-query-params.model.ts
@@ -0,0 +1,5 @@
+import { QueryParams } from '@shared/models';
+
+export interface InstitutionProjectsQueryParamsModel extends QueryParams {
+ cursor?: string;
+}
diff --git a/src/app/features/admin-institutions/models/institution-registrations-json-api.model.ts b/src/app/features/admin-institutions/models/institution-registrations-json-api.model.ts
new file mode 100644
index 000000000..ba9fab66e
--- /dev/null
+++ b/src/app/features/admin-institutions/models/institution-registrations-json-api.model.ts
@@ -0,0 +1,45 @@
+export interface InstitutionRegistrationsJsonApi {
+ data: {
+ id: string;
+ type: 'index-card-search';
+ attributes: {
+ totalResultCount: number;
+ cardSearchFilter: {
+ filterType: { '@id': string };
+ propertyPathKey: string;
+ propertyPathSet: Record[];
+ filterValueSet: Record[];
+ }[];
+ };
+ relationships: {
+ relatedProperties: {
+ data: {
+ id: string;
+ type: 'related-property-path';
+ }[];
+ };
+ searchResultPage: {
+ data: {
+ id: string;
+ type: 'search-result';
+ }[];
+ links?: {
+ first?: { href: string };
+ next?: { href: string };
+ prev?: { href: string };
+ last?: { href: string };
+ };
+ };
+ };
+ links: {
+ self: string;
+ };
+ };
+ included: {
+ id: string;
+ type: 'related-property-path' | 'search-result' | 'index-card';
+ attributes?: Record;
+ relationships?: Record;
+ links?: Record;
+ }[];
+}
diff --git a/src/app/features/admin-institutions/models/table.model.ts b/src/app/features/admin-institutions/models/table.model.ts
index 542df0b1f..787796466 100644
--- a/src/app/features/admin-institutions/models/table.model.ts
+++ b/src/app/features/admin-institutions/models/table.model.ts
@@ -8,7 +8,7 @@ export interface TableColumn {
iconClass?: string;
iconTooltip?: string;
iconAction?: string;
- dateFormat?: 'yyyy-mm-to-mm/yyyy' | 'default';
+ dateFormat?: 'MM/yyyy' | 'dd/MM/yyyy' | 'shortDate' | 'mediumDate' | 'longDate' | 'fullDate' | string;
}
export interface TableCellLink {
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 e69de29bb..7924855a6 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
@@ -0,0 +1,21 @@
+
+
+
{{ totalCount() }} {{ 'adminInstitutions.projects.totalProjects' | translate }}
+
+
diff --git a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.scss b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.scss
index e69de29bb..eab134e2c 100644
--- a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.scss
+++ b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.scss
@@ -0,0 +1,3 @@
+.title {
+ color: var(--pr-blue-1);
+}
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 066666964..ba83d069d 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
@@ -1,10 +1,194 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { createDispatchMap, select } from '@ngxs/store';
+
+import { TranslatePipe } from '@ngx-translate/core';
+
+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 { 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 { TABLE_PARAMS } from '@shared/constants';
+import { SortOrder } from '@shared/enums';
+import { Institution, QueryParams } from '@shared/models';
+import { InstitutionsSearchSelectors } from '@shared/stores';
+
+import { InstitutionProject, InstitutionProjectsQueryParamsModel, TableCellData } from '../../models';
+
+import { environment } from 'src/environments/environment';
@Component({
selector: 'osf-institutions-projects',
- imports: [],
+ imports: [AdminTableComponent, TranslatePipe],
templateUrl: './institutions-projects.component.html',
styleUrl: './institutions-projects.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class InstitutionsProjectsComponent {}
+export class InstitutionsProjectsComponent {
+ private readonly route = inject(ActivatedRoute);
+ private readonly router = inject(Router);
+
+ private readonly actions = createDispatchMap({
+ fetchProjects: FetchProjects,
+ });
+
+ institutionId = '';
+ 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);
+
+ sortField = signal('-dateModified');
+ sortOrder = signal(1);
+
+ tableColumns = projectTableColumns;
+
+ projects = select(InstitutionsAdminSelectors.getProjects);
+ totalCount = select(InstitutionsAdminSelectors.getProjectsTotalCount);
+ isLoading = select(InstitutionsAdminSelectors.getProjectsLoading);
+ projectsLinks = select(InstitutionsAdminSelectors.getProjectsLinks);
+ institution = select(InstitutionsSearchSelectors.getInstitution);
+
+ tableData = computed(() => {
+ return this.projects().map((project: InstitutionProject): TableCellData => mapProjectToTableCellData(project));
+ });
+
+ downloadUrl = computed(() => {
+ const baseUrl = `${environment.shareDomainUrl}/index-card-search`;
+ const institution = this.institution() as Institution;
+ const institutionIris = institution.iris || [];
+ const affiliationParam = institutionIris.join(',');
+
+ const params = new URLSearchParams({
+ 'cardSearchFilter[affiliation][]': affiliationParam,
+ 'cardSearchFilter[resourceType]': 'Project',
+ 'cardSearchFilter[accessService]': environment.webUrl,
+ 'page[cursor]': '',
+ 'page[size]': '10000',
+ sort: this.sortField(),
+ withFileName: 'projects-search-results',
+ 'fields[Project]':
+ 'title,dateCreated,dateModified,sameAs,storageRegion.prefLabel,storageByteCount,creator.name,usage.viewCount,resourceNature.displayLabel,rights.name,hasOsfAddon.prefLabel,funder.name',
+ });
+
+ return `${baseUrl}?${params.toString()}`;
+ });
+
+ constructor() {
+ this.setupQueryParamsEffect();
+ }
+
+ onSortChange(params: QueryParams): void {
+ this.sortField.set(params.sortColumn || '-dateModified');
+ this.sortOrder.set(params.sortOrder || 1);
+
+ this.updateQueryParams({
+ sortColumn: params.sortColumn || '-dateModified',
+ sortOrder: params.sortOrder || 1,
+ cursor: '',
+ });
+ }
+
+ onLinkPageChange(linkUrl: string): void {
+ if (!linkUrl) return;
+
+ const cursor = this.extractCursorFromUrl(linkUrl);
+
+ this.updateQueryParams({
+ cursor: 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.fetchProjects(this.institutionId, institutionIris, size, sortParam, cursor);
+ });
+ }
+
+ private updateQueryParams(updates: Partial): void {
+ const queryParams: Record = {};
+ const current = this.route.snapshot.queryParams;
+
+ const same =
+ (updates.page?.toString() ?? current['page']) === current['page'] &&
+ (updates.size?.toString() ?? current['size']) === current['size'] &&
+ (updates.sortColumn ?? current['sortColumn']) === current['sortColumn'] &&
+ (updates.sortOrder?.toString() ?? current['sortOrder']) === current['sortOrder'] &&
+ (updates.cursor ?? current['cursor']) === current['cursor'];
+
+ if (same) return;
+
+ if ('page' in updates) {
+ queryParams['page'] = updates.page!.toString();
+ }
+ if ('size' in updates) {
+ queryParams['size'] = updates.size!.toString();
+ }
+ if ('sortColumn' in updates) {
+ queryParams['sortColumn'] = updates.sortColumn || undefined;
+ }
+ if ('sortOrder' in updates) {
+ queryParams['sortOrder'] = updates.sortOrder?.toString() || undefined;
+ }
+ if ('cursor' in updates) {
+ queryParams['cursor'] = updates.cursor || undefined;
+ }
+
+ this.router.navigate([], {
+ relativeTo: this.route,
+ queryParams,
+ queryParamsHandling: 'merge',
+ });
+ }
+
+ private parseQueryParams(params: Params): InstitutionProjectsQueryParamsModel {
+ const parsed = parseQueryFilterParams(params);
+ return {
+ ...parsed,
+ cursor: params['cursor'] || '',
+ };
+ }
+
+ private updateComponentState(params: InstitutionProjectsQueryParamsModel): void {
+ untracked(() => {
+ this.currentPage.set(params.page);
+ this.currentPageSize.set(params.size);
+ this.first.set((params.page - 1) * params.size);
+
+ if (params.sortColumn) {
+ this.sortField.set(params.sortColumn);
+ const order = params.sortOrder === SortOrder.Desc ? -1 : 1;
+ this.sortOrder.set(order);
+ }
+ });
+ }
+
+ private extractCursorFromUrl(url: string): string {
+ const urlObj = new URL(url);
+ return urlObj.searchParams.get('page[cursor]') || '';
+ }
+}
diff --git a/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.scss b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.scss
index f865d569d..f39417f43 100644
--- a/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.scss
+++ b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.scss
@@ -1,7 +1,9 @@
+@use "assets/styles/variables" as var;
+
.width-25 {
width: calc(25% - 1.5rem);
- @media (max-width: 576px) {
+ @media (max-width: var.$breakpoint-sm) {
width: calc(50% - 0.5rem);
}
}
@@ -9,7 +11,11 @@
.width-33 {
width: calc(33% - 1rem);
- @media (max-width: 576px) {
+ @media (max-width: var.$breakpoint-lg) {
+ width: calc(50% - 1rem);
+ }
+
+ @media (max-width: var.$breakpoint-sm) {
width: 100%;
}
}
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 5943efff7..8710a5fa3 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,46 +1,45 @@
-
-
-
-
{{ amountText() }}
-
+
+
+
{{ amountText() }}
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
-
-
+
+
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 ae7802897..c93d770ba 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
@@ -70,6 +70,7 @@ export class InstitutionsUsersComponent implements OnInit {
});
institutionId = '';
+ reportsLink = 'https://drive.google.com/drive/folders/1_aFmeJwLp5xBS3-8clZ4xA9L3UFxdzDd';
queryParams = toSignal(this.route.queryParams);
currentPage = signal(1);
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 4310e1b2b..9dd5eedb2 100644
--- a/src/app/features/admin-institutions/services/institutions-admin.service.ts
+++ b/src/app/features/admin-institutions/services/institutions-admin.service.ts
@@ -1,16 +1,26 @@
-import { catchError, map, Observable, of } from 'rxjs';
+import { catchError, Observable, of } from 'rxjs';
+import { map } from 'rxjs/operators';
import { inject, Injectable } from '@angular/core';
import { JsonApiService } from '@core/services';
-import { mapIndexCardResults } from '@osf/features/admin-institutions/mappers/institution-summary-index.mapper';
-import { sendMessageRequestMapper } from '@osf/features/admin-institutions/mappers/send-message-request.mapper';
import { departmens, summaryMetrics, users } from '@osf/features/admin-institutions/services/mock';
+import { PaginationLinksModel } from '@shared/models';
+import {
+ mapIndexCardResults,
+ mapInstitutionDepartments,
+ mapInstitutionProjects,
+ mapInstitutionSummaryMetrics,
+ mapInstitutionUsers,
+ sendMessageRequestMapper,
+} from '../mappers';
import {
InstitutionDepartment,
InstitutionDepartmentsJsonApi,
InstitutionIndexValueSearchJsonApi,
+ InstitutionProject,
+ InstitutionRegistrationsJsonApi,
InstitutionSearchFilter,
InstitutionSummaryMetrics,
InstitutionSummaryMetricsJsonApi,
@@ -20,11 +30,6 @@ import {
SendMessageResponseJsonApi,
} from '../models';
-import {
- mapInstitutionDepartments,
- mapInstitutionSummaryMetrics,
- mapInstitutionUsers,
-} from 'src/app/features/admin-institutions/mappers';
import { environment } from 'src/environments/environment';
@Injectable({
@@ -87,6 +92,44 @@ export class InstitutionsAdminService {
);
}
+ fetchProjects(
+ institutionId: string,
+ institutionIris: string[],
+ pageSize = 10,
+ sort = '-dateModified',
+ cursor = ''
+ ): Observable<{
+ projects: InstitutionProject[];
+ totalCount: number;
+ links?: PaginationLinksModel;
+ }> {
+ const url = `${environment.shareDomainUrl}/index-card-search`;
+ let params: Record = {};
+
+ const affiliationParam = institutionIris.join(',');
+
+ params = {
+ 'cardSearchFilter[affiliation][]': affiliationParam,
+ 'cardSearchFilter[resourceType]': 'Project',
+ 'cardSearchFilter[accessService]': environment.webUrl,
+ 'page[cursor]': cursor,
+ 'page[size]': pageSize.toString(),
+ sort,
+ };
+
+ 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,
+ };
+ })
+ );
+ }
+
fetchIndexValueSearch(
institutionId: string,
valueSearchPropertyPath: string,
@@ -108,9 +151,6 @@ export class InstitutionsAdminService {
sendMessage(request: SendMessageRequest): Observable {
const payload = sendMessageRequestMapper(request);
- return this.jsonApiService.post(
- `${this.hardcodedUrl}/users/${request.userId}/messages/`,
- payload
- );
+ return this.jsonApiService.post(`${this.hardcodedUrl}/institutions/messages/`, payload);
}
}
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 06cea18c0..a44bd7457 100644
--- a/src/app/features/admin-institutions/store/institutions-admin.actions.ts
+++ b/src/app/features/admin-institutions/store/institutions-admin.actions.ts
@@ -38,6 +38,17 @@ export class FetchInstitutionUsers {
) {}
}
+export class FetchProjects {
+ static readonly type = '[InstitutionsAdmin] Fetch Projects';
+ 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 472df7cb3..473fcbd94 100644
--- a/src/app/features/admin-institutions/store/institutions-admin.model.ts
+++ b/src/app/features/admin-institutions/store/institutions-admin.model.ts
@@ -1,7 +1,8 @@
-import { AsyncStateModel, AsyncStateWithTotalCount } from '@shared/models';
+import { AsyncStateModel, AsyncStateWithLinksModel, AsyncStateWithTotalCount } from '@shared/models';
import {
InstitutionDepartment,
+ InstitutionProject,
InstitutionSearchFilter,
InstitutionSummaryMetrics,
InstitutionUser,
@@ -15,6 +16,7 @@ export interface InstitutionsAdminModel {
storageRegionSearch: AsyncStateModel;
searchResults: AsyncStateModel;
users: AsyncStateWithTotalCount;
+ projects: 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 07ec7e9ef..91959090f 100644
--- a/src/app/features/admin-institutions/store/institutions-admin.selectors.ts
+++ b/src/app/features/admin-institutions/store/institutions-admin.selectors.ts
@@ -1,7 +1,10 @@
import { Selector } from '@ngxs/store';
+import { PaginationLinksModel } from '@shared/models';
+
import {
InstitutionDepartment,
+ InstitutionProject,
InstitutionSearchFilter,
InstitutionSummaryMetrics,
InstitutionUser,
@@ -117,6 +120,26 @@ export class InstitutionsAdminSelectors {
return state.users.totalCount;
}
+ @Selector([InstitutionsAdminState])
+ static getProjects(state: InstitutionsAdminModel): InstitutionProject[] {
+ return state.projects.data;
+ }
+
+ @Selector([InstitutionsAdminState])
+ static getProjectsLoading(state: InstitutionsAdminModel): boolean {
+ return state.projects.isLoading;
+ }
+
+ @Selector([InstitutionsAdminState])
+ static getProjectsTotalCount(state: InstitutionsAdminModel): number {
+ return state.projects.totalCount;
+ }
+
+ @Selector([InstitutionsAdminState])
+ static getProjectsLinks(state: InstitutionsAdminModel): PaginationLinksModel | undefined {
+ return state.projects.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 478ad1072..dfb3b636f 100644
--- a/src/app/features/admin-institutions/store/institutions-admin.state.ts
+++ b/src/app/features/admin-institutions/store/institutions-admin.state.ts
@@ -15,6 +15,7 @@ import {
FetchInstitutionSearchResults,
FetchInstitutionSummaryMetrics,
FetchInstitutionUsers,
+ FetchProjects,
FetchStorageRegionSearch,
SendUserMessage,
} from './institutions-admin.actions';
@@ -29,6 +30,7 @@ import { InstitutionsAdminModel } from './institutions-admin.model';
storageRegionSearch: { data: [], isLoading: false, error: null },
searchResults: { data: [], isLoading: false, error: null },
users: { data: [], totalCount: 0, isLoading: false, error: null },
+ projects: { data: [], totalCount: 0, isLoading: false, error: null, links: undefined },
sendMessage: { data: null, isLoading: false, error: null },
selectedInstitutionId: null,
currentSearchPropertyPath: null,
@@ -146,6 +148,31 @@ export class InstitutionsAdminState {
);
}
+ @Action(FetchProjects)
+ fetchProjects(ctx: StateContext, action: FetchProjects) {
+ const state = ctx.getState();
+ ctx.patchState({
+ projects: { ...state.projects, isLoading: true, error: null },
+ });
+
+ return this.institutionsAdminService
+ .fetchProjects(action.institutionId, action.institutionIris, action.pageSize, action.sort, action.cursor)
+ .pipe(
+ tap((response) => {
+ ctx.patchState({
+ projects: {
+ data: response.projects,
+ totalCount: response.totalCount,
+ isLoading: false,
+ error: null,
+ links: response.links,
+ },
+ });
+ }),
+ catchError((error) => handleSectionError(ctx, 'projects', error))
+ );
+ }
+
@Action(SendUserMessage)
sendUserMessage(ctx: StateContext, action: SendUserMessage) {
const state = ctx.getState();
diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts
index 9e24c8269..3b9d4951c 100644
--- a/src/app/shared/models/index.ts
+++ b/src/app/shared/models/index.ts
@@ -23,6 +23,7 @@ export * from './metadata-field.model';
export * from './nodes/create-project-form.model';
export * from './nodes/nodes-json-api.model';
export * from './paginated-data.model';
+export * from './pagination-links.model';
export * from './project-metadata-update-payload.model';
export * from './query-params.model';
export * from './registration';
diff --git a/src/app/shared/models/pagination-links.model.ts b/src/app/shared/models/pagination-links.model.ts
new file mode 100644
index 000000000..055dc3918
--- /dev/null
+++ b/src/app/shared/models/pagination-links.model.ts
@@ -0,0 +1,9 @@
+export interface PaginationLinksModel {
+ first?: LinkModel;
+ next?: LinkModel;
+ prev?: LinkModel;
+}
+
+export interface LinkModel {
+ href: string;
+}
diff --git a/src/app/shared/models/store/async-state-with-links.model.ts b/src/app/shared/models/store/async-state-with-links.model.ts
new file mode 100644
index 000000000..a741fa71e
--- /dev/null
+++ b/src/app/shared/models/store/async-state-with-links.model.ts
@@ -0,0 +1,5 @@
+import { AsyncStateWithTotalCount, PaginationLinksModel } from '@shared/models';
+
+export interface AsyncStateWithLinksModel extends AsyncStateWithTotalCount {
+ links?: PaginationLinksModel;
+}
diff --git a/src/app/shared/models/store/index.ts b/src/app/shared/models/store/index.ts
index 3b5469b1b..f129cbcc6 100644
--- a/src/app/shared/models/store/index.ts
+++ b/src/app/shared/models/store/index.ts
@@ -1,2 +1,3 @@
export type { AsyncStateModel } from './async-state.model';
+export type { AsyncStateWithLinksModel } from './async-state-with-links.model';
export type { AsyncStateWithTotalCount } from './async-state-with-total-count.model';
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 0a21a5087..ff1f3bbc1 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -2148,6 +2148,22 @@
"sendMessage": "Send message",
"osfLink": "OSF Link",
"orcid": "ORCID"
+ },
+ "projects": {
+ "title": "Title",
+ "link": "Link",
+ "dateCreated": "Created Date",
+ "dateModified": "Modified Date",
+ "doi": "DOI",
+ "storageLocation": "Storage Location",
+ "totalDataStored": "Total Data Stored on OSF",
+ "contributorName": "Contributor Name",
+ "views": "Views (last 30 days)",
+ "resourceType": "Resource Type",
+ "license": "License",
+ "addOns": "Add-ons",
+ "funderName": "Funder Name",
+ "totalProjects": "Total Projects"
}
}
}
diff --git a/src/assets/styles/overrides/table.scss b/src/assets/styles/overrides/table.scss
index 2777bf16b..abb7e6a49 100644
--- a/src/assets/styles/overrides/table.scss
+++ b/src/assets/styles/overrides/table.scss
@@ -99,6 +99,12 @@ p-table {
}
}
+.institution-admin-table {
+ th {
+ min-width: 10rem;
+ }
+}
+
@media (max-width: var.$breakpoint-xl) {
.addon-table,
.my-projects-table {