From 0b212f4472a375e2a1e95c29e5d81f880a8a6a17 Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Fri, 11 Apr 2025 13:02:55 +0300 Subject: [PATCH 1/4] fix(api-service): fixes for json-api service --- .../core/services/json-api/json-api.entity.ts | 9 ++---- .../services/json-api/json-api.service.ts | 29 ++++++------------ src/app/features/home/dashboard.service.ts | 28 ++++++++--------- .../models/raw-models/ProjectItem.entity.ts | 30 +++++++++++++------ 4 files changed, 46 insertions(+), 50 deletions(-) diff --git a/src/app/core/services/json-api/json-api.entity.ts b/src/app/core/services/json-api/json-api.entity.ts index 31e660911..ce7b6d27a 100644 --- a/src/app/core/services/json-api/json-api.entity.ts +++ b/src/app/core/services/json-api/json-api.entity.ts @@ -1,9 +1,6 @@ -export interface JsonApiResponse { - data: T; -} - -export interface JsonApiArrayResponse { - data: T[]; +export interface JsonApiResponse { + data: Data; + included: Included; } export interface ApiData { diff --git a/src/app/core/services/json-api/json-api.service.ts b/src/app/core/services/json-api/json-api.service.ts index ea639e05b..b147f7718 100644 --- a/src/app/core/services/json-api/json-api.service.ts +++ b/src/app/core/services/json-api/json-api.service.ts @@ -1,11 +1,6 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; -import { map, Observable } from 'rxjs'; -import { - JsonApiArrayResponse, - JsonApiResponse, -} from '@core/services/json-api/json-api.entity'; - +import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) @@ -13,22 +8,16 @@ export class JsonApiService { http: HttpClient = inject(HttpClient); get(url: string, params?: Record): Observable { - return this.http - .get>(url, { params: this.buildHttpParams(params) }) - .pipe(map((response) => response.data)); - } - - getArray(url: string, params?: Record): Observable { + const token = 'ENTER_VALID_PAT'; const headers = new HttpHeaders({ - Authorization: 'ENTER_VALID_PAT', + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.api+json', }); - return this.http - .get>(url, { - params: this.buildHttpParams(params), - headers: headers, - }) - .pipe(map((response) => response.data)); + return this.http.get(url, { + params: this.buildHttpParams(params), + headers: headers, + }); } private buildHttpParams(params?: Record): HttpParams { @@ -40,7 +29,7 @@ export class JsonApiService { if (Array.isArray(value)) { value.forEach((item) => { - httpParams = httpParams.append(`${key}[]`, item); // Handles arrays + httpParams = httpParams.append(`${key}`, item); // Handles arrays }); } else { httpParams = httpParams.set(key, value as string); diff --git a/src/app/features/home/dashboard.service.ts b/src/app/features/home/dashboard.service.ts index 3662e239a..7ab9a3148 100644 --- a/src/app/features/home/dashboard.service.ts +++ b/src/app/features/home/dashboard.service.ts @@ -5,6 +5,7 @@ import { Project } from '@osf/features/home/models/project.entity'; import { mapProjectUStoProject } from '@osf/features/home/mappers/dashboard.mapper'; import { ProjectItem } from '@osf/features/home/models/raw-models/ProjectItem.entity'; import { environment } from '../../../environments/environment'; +import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; @Injectable({ providedIn: 'root', @@ -21,11 +22,10 @@ export class DashboardService { }; return this.jsonApiService - .getArray( - `${environment.apiUrl}/sparse/users/${userId}/nodes/`, - params, - ) - .pipe(map((projects) => projects.map(mapProjectUStoProject))); + .get< + JsonApiResponse + >(`${environment.apiUrl}/sparse/users/${userId}/nodes/`, params) + .pipe(map((response) => response.data.map(mapProjectUStoProject))); } getNoteworthy(): Observable { @@ -36,11 +36,10 @@ export class DashboardService { }; return this.jsonApiService - .getArray( - `${environment.apiUrl}/nodes/${projectId}/linked_nodes`, - params, - ) - .pipe(map((projects) => projects.map(mapProjectUStoProject))); + .get< + JsonApiResponse + >(`${environment.apiUrl}/nodes/${projectId}/linked_nodes`, params) + .pipe(map((response) => response.data.map(mapProjectUStoProject))); } getMostPopular(): Observable { @@ -51,10 +50,9 @@ export class DashboardService { }; return this.jsonApiService - .getArray( - `${environment.apiUrl}/nodes/${projectId}/linked_nodes`, - params, - ) - .pipe(map((projects) => projects.map(mapProjectUStoProject))); + .get< + JsonApiResponse + >(`${environment.apiUrl}/nodes/${projectId}/linked_nodes`, params) + .pipe(map((response) => response.data.map(mapProjectUStoProject))); } } diff --git a/src/app/features/home/models/raw-models/ProjectItem.entity.ts b/src/app/features/home/models/raw-models/ProjectItem.entity.ts index af1f3a2c8..ceab31895 100644 --- a/src/app/features/home/models/raw-models/ProjectItem.entity.ts +++ b/src/app/features/home/models/raw-models/ProjectItem.entity.ts @@ -1,10 +1,22 @@ -import {ProjectUS} from '@osf/features/home/models/raw-models/projectUS.entity'; -import {ApiData, JsonApiArrayResponse, JsonApiResponse} from '@core/services/json-api/json-api.entity'; -import {BibliographicContributorUS} from '@osf/features/home/models/raw-models/bibliographicContributorUS.entity'; -import {UserUS} from '@osf/features/home/models/raw-models/userUS.entity'; +import { ProjectUS } from '@osf/features/home/models/raw-models/projectUS.entity'; +import { + ApiData, + JsonApiResponse, +} from '@core/services/json-api/json-api.entity'; +import { BibliographicContributorUS } from '@osf/features/home/models/raw-models/bibliographicContributorUS.entity'; +import { UserUS } from '@osf/features/home/models/raw-models/userUS.entity'; -export type ProjectItem = ApiData> - }>> -}> +export type ProjectItem = ApiData< + ProjectUS, + { + bibliographic_contributors: JsonApiResponse< + ApiData< + BibliographicContributorUS, + { + users: JsonApiResponse, null>; + } + >[], + null + >; + } +>; From 29087a3260016ea11df6eff688e83f06f751ddc3 Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Fri, 11 Apr 2025 13:06:26 +0300 Subject: [PATCH 2/4] fix(api-service): params fix --- src/app/features/home/dashboard.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/home/dashboard.service.ts b/src/app/features/home/dashboard.service.ts index 7ab9a3148..d995d3f71 100644 --- a/src/app/features/home/dashboard.service.ts +++ b/src/app/features/home/dashboard.service.ts @@ -16,7 +16,7 @@ export class DashboardService { getProjects(): Observable { const userId = 'ENTER_VALID_USER_ID'; const params = { - embed: ['bibliographic_contributors', 'parent', 'root'], + 'embed[]': ['bibliographic_contributors', 'parent', 'root'], page: 1, sort: '-last_logged', }; From cb1c998b3a5edef6e5cba99ed6433f17817f20b6 Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Fri, 11 Apr 2025 17:56:51 +0300 Subject: [PATCH 3/4] chore(search): creator-filter --- src/app/app.config.ts | 3 +- src/app/app.module.ts | 3 +- .../core/services/json-api/json-api.entity.ts | 1 + .../services/json-api/json-api.service.ts | 3 +- src/app/features/home/dashboard.service.ts | 6 +- src/app/features/search/data.ts | 640 +++++++++--------- .../features/search/mappers/search.mapper.ts | 52 ++ .../raw-models/resource-response.model.ts | 73 ++ .../search/models/resource-type.enum.ts | 2 +- .../features/search/models/resource.entity.ts | 5 +- src/app/features/search/search.component.html | 12 +- src/app/features/search/search.component.ts | 64 +- src/app/features/search/search.service.ts | 56 ++ src/app/features/search/store/index.ts | 4 + .../features/search/store/search.actions.ts | 3 + src/app/features/search/store/search.model.ts | 5 + .../features/search/store/search.selectors.ts | 11 + src/app/features/search/store/search.state.ts | 37 + .../resource-card.component.html | 36 +- .../resource-card.component.scss | 1 + .../creators/creators-filter.component.html | 16 + .../creators/creators-filter.component.scss | 12 + .../creators/creators-filter.component.ts | 71 ++ .../mappers/creators/creators.mappers.ts | 9 + .../models/creator/creator-item.entity.ts | 4 + .../models/creator/creator.entity.ts | 4 + .../resource-filters.component.html | 8 +- .../resource-filters.component.scss | 11 - .../resource-filters.component.ts | 36 +- .../resource-filters.service.ts | 46 ++ .../store/resource-filters.state.ts | 2 + .../resources/resources.component.ts | 2 +- .../entities/metadata-field.inteface.ts | 6 + src/assets/styles/overrides/select.scss | 18 + src/environments/environment.development.ts | 1 + src/environments/environment.ts | 1 + 36 files changed, 832 insertions(+), 432 deletions(-) create mode 100644 src/app/features/search/mappers/search.mapper.ts create mode 100644 src/app/features/search/models/raw-models/resource-response.model.ts create mode 100644 src/app/features/search/search.service.ts create mode 100644 src/app/features/search/store/index.ts create mode 100644 src/app/features/search/store/search.actions.ts create mode 100644 src/app/features/search/store/search.model.ts create mode 100644 src/app/features/search/store/search.selectors.ts create mode 100644 src/app/features/search/store/search.state.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.html create mode 100644 src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.scss create mode 100644 src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.ts create mode 100644 src/app/shared/components/resources/resource-filters/mappers/creators/creators.mappers.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/creator/creator-item.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/creator/creator.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/resource-filters.service.ts create mode 100644 src/app/shared/entities/metadata-field.inteface.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 012c16ab5..2a7cc4273 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -11,13 +11,14 @@ import { ConfirmationService } from 'primeng/api'; import { AuthState } from '@core/store/auth'; import { HomeState } from '@core/store/home'; import { ResourceFiltersState } from '@shared/components/resources/resource-filters/store/resource-filters.state'; +import { SearchState } from '@osf/features/search/store'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideStore( - [AuthState, HomeState, ResourceFiltersState], + [AuthState, HomeState, ResourceFiltersState, SearchState], withNgxsReduxDevtoolsPlugin({ disabled: false }), ), providePrimeNG({ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 27de9234f..8c61ec438 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -2,8 +2,9 @@ import { NgModule } from '@angular/core'; import { NgxsModule } from '@ngxs/store'; import { AuthState } from '@core/store/auth'; import { HomeState } from '@core/store/home'; +import { SearchState } from '@osf/features/search/store'; @NgModule({ - imports: [NgxsModule.forRoot([AuthState, HomeState])], + imports: [NgxsModule.forRoot([AuthState, HomeState, SearchState])], }) export class AppModule {} diff --git a/src/app/core/services/json-api/json-api.entity.ts b/src/app/core/services/json-api/json-api.entity.ts index ce7b6d27a..34f3e3805 100644 --- a/src/app/core/services/json-api/json-api.entity.ts +++ b/src/app/core/services/json-api/json-api.entity.ts @@ -7,4 +7,5 @@ export interface ApiData { id: string; attributes: Attributes; embeds: Embeds; + type: string; } diff --git a/src/app/core/services/json-api/json-api.service.ts b/src/app/core/services/json-api/json-api.service.ts index b147f7718..46420d1d0 100644 --- a/src/app/core/services/json-api/json-api.service.ts +++ b/src/app/core/services/json-api/json-api.service.ts @@ -8,7 +8,8 @@ export class JsonApiService { http: HttpClient = inject(HttpClient); get(url: string, params?: Record): Observable { - const token = 'ENTER_VALID_PAT'; + const token = + 'UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm'; const headers = new HttpHeaders({ Authorization: `Bearer ${token}`, Accept: 'application/vnd.api+json', diff --git a/src/app/features/home/dashboard.service.ts b/src/app/features/home/dashboard.service.ts index d995d3f71..3560f9855 100644 --- a/src/app/features/home/dashboard.service.ts +++ b/src/app/features/home/dashboard.service.ts @@ -14,7 +14,7 @@ export class DashboardService { jsonApiService = inject(JsonApiService); getProjects(): Observable { - const userId = 'ENTER_VALID_USER_ID'; + const userId = 'k9p2t'; const params = { 'embed[]': ['bibliographic_contributors', 'parent', 'root'], page: 1, @@ -31,7 +31,7 @@ export class DashboardService { getNoteworthy(): Observable { const projectId = 'pf5z9'; const params = { - embed: 'bibliographic_contributors', + 'embed[]': 'bibliographic_contributors', 'page[size]': 5, }; @@ -45,7 +45,7 @@ export class DashboardService { getMostPopular(): Observable { const projectId = 'kvw3y'; const params = { - embed: 'bibliographic_contributors', + 'embed[]': 'bibliographic_contributors', 'page[size]': 5, }; diff --git a/src/app/features/search/data.ts b/src/app/features/search/data.ts index 90e6230f1..fcf48fa5a 100644 --- a/src/app/features/search/data.ts +++ b/src/app/features/search/data.ts @@ -1,320 +1,320 @@ -import { Resource } from '@osf/features/search/models/resource.entity'; -import { ResourceType } from '@osf/features/search/models/resource-type.enum'; - -export const resources: Resource[] = [ - { - id: 'https://osf.io/wrfgp', - resourceType: ResourceType.File, - dateCreated: new Date('2025-01-10'), - dateModified: new Date('2025-01-14'), - creators: [ - { - id: 'https://osf.io/a6t2x', - name: 'Lívia Rodrigues de Lima Pires', - }, - ], - fileName: 'dadosAnalisados.rds', - from: { - id: 'https://osf.io/e86jf', - name: 'Tese', - }, - }, - { - id: 'https://osf.io/4crzf', - resourceType: ResourceType.Project, - dateCreated: new Date('2025-01-15'), - dateModified: new Date('2025-01-18'), - creators: [ - { - id: 'https://osf.io/4fy2t', - name: 'Jane Simpson', - }, - { - id: 'https://osf.io/5jv47', - name: 'Wendy Francis', - }, - { - id: 'https://osf.io/6a5yb', - name: 'Daniel Wadsworth', - }, - { - id: 'https://osf.io/6g7nc', - name: 'Kristen Tulloch', - }, - { - id: 'https://osf.io/7a3tm', - name: 'Dr Tamara Sysak', - }, - { - id: 'https://osf.io/b8tvg', - name: 'PJ Humphreys', - }, - { - id: 'https://osf.io/n2hyv', - name: 'Alison Craswell', - }, - { - id: 'https://osf.io/qtnc8', - name: 'Apil Gurung', - }, - { - id: 'https://osf.io/qvuw6', - name: 'Karen Watson', - }, - { - id: 'https://osf.io/zwu3t', - name: 'Helen Szabo', - }, - ], - title: - 'Intergenerational community home sharing for older adults and university students: A scoping review protocol.', - description: - 'The objective of this scoping review is to review programs designed to facilitate matches between older adults residing within the community and university students. This protocol provides an overview of the topic, justification for and methods to be implemented in the conduct and reporting of the scoping review.', - }, - { - id: 'https://osf.io/pgmhr', - resourceType: ResourceType.File, - dateCreated: new Date('2025-01-17'), - dateModified: new Date('2025-01-17'), - creators: [{ id: 'https://osf.io/65nk7', name: 'Aline Miranda Ferreira' }], - fileName: 'Scoping review protocol.pdf', - license: 'MIT License', - from: { - id: 'https://osf.io/4wks9', - name: 'Instruments for Measuring Pain, Functionality, and Quality of Life Following Knee Fractures: A Scoping Review', - }, - }, - { - id: 'https://osf.io/f7h5v', - resourceType: ResourceType.File, - dateCreated: new Date('2025-01-29'), - dateModified: new Date('2025-01-29'), - creators: [{ id: 'https://osf.io/d7rtw', name: 'David P. Goldenberg' }], - fileName: 'PCB.zip', - from: { - id: 'https://osf.io/qs7r9', - name: 'Design Files', - }, - }, - { - id: 'https://osf.io/3ua54', - resourceType: ResourceType.Project, - dateCreated: new Date('2025-03-16'), - dateModified: new Date('2025-03-16'), - creators: [{ id: 'https://osf.io/uby48', name: 'Aadit Nair Sajeev' }], - title: - 'Unemployment of Locals in Kochi due to the Augmented Influx of Migrant Workers', - description: - 'This project page includes supplementals like interview transcript for a case study into the unemployment of locals due to increasing influx of migrant workers into the state.', - }, - { - id: 'https://osf.io/xy9qn', - resourceType: ResourceType.Project, - dateCreated: new Date('2025-03-16'), - dateModified: new Date('2025-03-16'), - creators: [ - { - id: 'https://osf.io/x87gf', - name: 'Doctor Jack Samuel Egerton MSci EngD', - }, - ], - title: 'Stone functions (tetration)', - }, - { - id: 'https://osf.io/cpm64', - resourceType: ResourceType.Registration, - dateCreated: new Date('2025-01-10'), - dateModified: new Date('2025-01-10'), - creators: [ - { - id: 'https://osf.io/4fy2t', - name: 'Simran Khanna', - }, - { - id: 'https://osf.io/5jv47', - name: 'Devika Shenoy', - }, - { - id: 'https://osf.io/6a5yb', - name: 'Steph Hendren', - }, - { - id: 'https://osf.io/6g7nc', - name: 'Christian Zirbes', - }, - { - id: 'https://osf.io/7a3tm', - name: 'Anthony Catanzano', - }, - { - id: 'https://osf.io/b8tvg', - name: 'Rachelle Shao', - }, - { - id: 'https://osf.io/n2hyv', - name: 'Sofiu Ogunbiyi', - }, - { - id: 'https://osf.io/qtnc8', - name: 'Evelyn Hunter', - }, - { - id: 'https://osf.io/qvuw6', - name: 'Katie Radulovacki', - }, - { - id: 'https://osf.io/zwu3t', - name: 'Muhamed Sanneh', - }, - ], - title: - 'A Scoping Review and Meta-analysis Exploring the Associations between Socioeconomic Identity and Management of Pediatric Extremity Fracture', - description: - 'The incidence and management of extremity fractures in pediatric patients can be influenced by social and financial deprivation. Previous studies have highlighted that the social determinants of health, such as socioeconomic status, race, and insurance type, are independently associated with the incidence of pediatric fractures . [1, 2] This underscores the need to understand how these factors specifically affect fracture management in pediatric populations.\n\nIn addition to incidence, socioeconomic status has been shown to impact the timing and type of treatment for fractures. Vazquez et al. demonstrated that adolescents from poor socioeconomic backgrounds were more likely to experience delayed surgical fixation of femoral fractures, which was associated with worse outcomes, including longer hospital stays and higher healthcare costs.[2] Similarly, Evans et al. reported that children from socially deprived areas had worse perceived function and pain outcomes after upper extremity fractures, even after receiving orthopedic treatment.[3] These findings suggest that social deprivation not only affects initial access to care but also influences recovery and long-term outcomes.\n\nThe proposed scoping review and meta-analysis will systematically evaluate the existing literature to map out the extent and nature of the impact of social and financial deprivation on extremity fracture management in pediatric patients. By including a wide range of sociodemographic variables and outcomes, this review aims to provide a comprehensive understanding of the disparities in fracture care. This will inform future research and policy-making to address these inequities and improve healthcare delivery for socially and economically disadvantaged pediatric populations.\n\n[1] Dy CJ, Lyman S, Do HT, Fabricant PD, Marx RG, Green DW. Socioeconomic factors are associated with frequency of repeat emergency department visits for pediatric closed fractures. J Pediatr Orthop. 2014;34(5):548-551. doi:10.1097/BPO.0000000000000143\n[2] Ramaesh R, Clement ND, Rennie L, Court-Brown C, Gaston MS. Social deprivation as a risk factor for fractures in childhood. Bone Joint J. 2015;97-B(2):240-245. doi:10.1302/0301-620X.97B2.34057\n[3] Vazquez S, Dominguez JF, Jacoby M, et al. Poor socioeconomic status is associated with delayed femoral fracture fixation in adolescent patients. Injury. 2023;54(12):111128. doi:10.1016/j.injury.2023.111128', - license: 'No license', - publisher: { - id: 'https://osf.io/registries/osf', - name: 'OSF Registries', - }, - registrationTemplate: 'Generalized Systematic Review Registration', - doi: 'https://doi.org/10.17605/OSF.IO/CPM64', - }, - { - id: 'https://osf.io/8tk45', - resourceType: ResourceType.Registration, - dateCreated: new Date('2025-03-06'), - dateModified: new Date('2025-03-06'), - creators: [ - { - id: 'https://osf.io/45kpt', - name: 'Laura A. King', - }, - { - id: 'https://osf.io/cuqrk', - name: 'Erica A. Holberg', - }, - ], - title: 'Consequentialist Criteria: Money x Effort (Between)', - description: - "This is the first of two studies aimed at testing whether the effect of amount of money raised for a good cause is present in within person comparisons, but null when utilizing a between person design. In previous studies, we have found that success in achieving an agent's morally good aim and high cost to agent for doing the right thing are salient to moral evaluators when the explicit contrast to failure or low cost is made (within person), but not particularly salient to moral evaluation when the explicit contrast is not drawn (between person). In this study, we are interested to determine the relative presence and strength of amount of money raised (a consequentialist criterion for moral evaluation) and effort put forth (a non-consequentialist moral criterion centered on the agent's will) when this is not explicitly contrasted because using a between-person design. In a pilot study, we found no effect of money upon moral goodness. There was an effect of money upon effort for the low condition ($50). We want to see how results are altered if we add an explicit factor for effort. Does amount of money have null effect upon perceived moral goodness? Does effort have a larger effect upon perceived moral goodness than amount of money raised? \n\nParticipants will read scenarios reflecting a 3 (money: extremely high vs. high vs. low) x 3 (effort: high vs. no mention vs. low) design. In all 9 scenarios, participants will rate the moral goodness of a person who raises money for and then runs in a 5K for a good cause, where the conditions vary as described above. They also will answer how likely it is that the target would undertake a similar volunteer commitment in the future.", - publisher: { - id: 'https://osf.io/registries/osf', - name: 'OSF Registries', - }, - registrationTemplate: 'OSF Preregistration', - license: 'No license', - doi: 'https://doi.org/10.17605/OSF.IO/8TK45', - }, - { - id: 'https://osf.io/wa6yf', - resourceType: ResourceType.File, - dateCreated: new Date('2025-01-14'), - dateModified: new Date('2025-01-14'), - creators: [ - { - id: 'https://osf.io/6nkxv', - name: 'Kari-Anne B. Næss', - }, - { - id: 'https://osf.io/b2g9q', - name: 'Frida Johanne Holmen ', - }, - { - id: 'https://osf.io/j6hd5', - name: 'Thormod Idsøe', - }, - ], - from: { - id: 'https://osf.io/tbzv6', - name: 'A Cross-sectional Study of Inference-making Comparing First- and Second Language Learners in Early Childhood Education and Care', - }, - fileName: 'Model_FH_prereg_140125.pdf', - license: 'No license', - }, - { - id: 'https://osf.io/4hg87', - resourceType: ResourceType.ProjectComponent, - dateCreated: new Date('2025-01-04'), - dateModified: new Date('2025-01-04'), - creators: [ - { - id: 'https://osf.io/2x5kc', - name: 'Bowen Wang-Kildegaard', - }, - ], - title: 'Dataset and Codes', - }, - { - id: 'https://osf.io/87vyr', - resourceType: ResourceType.Preprint, - dateCreated: new Date('2025-02-20'), - dateModified: new Date('2025-02-20'), - creators: [ - { - id: 'https://osf.io/2x5kc', - name: 'Eric L. Olson', - }, - ], - title: 'Evaluative Judgment Across Domains', - description: - 'Keberadaan suatu organisme pada suatu tempat dipengaruhi oleh faktor lingkungan dan makanan. Ketersediaan makanan dengan kualitas yang cocok dan kuantitas yang ukup bagi suatu organisme akan meningkatkan populasi cepat. Sebaliknya jika keadaan tersebut tidak mendukung maka akan dipastikan bahwa organisme tersebut akan menurun. Sedangkan faktor abiotik meliputi suhu, kelembaban, cahaya, curah hujan, dan angin. Suhu mempengaruhi aktivitas serangga serta perkembangannya. Serangga juga tertarik pada gelombang cahaya tertentu. Serangga ada yang menerima intensitas cahaya yang tinggi dan aktif pada siang hari (diurnal) dan serangga ada yang aktif menerima intensitas cahaya rendah pada malam hari (nokturnal). Metode yang digunakan yaitu metode Light trap. Hail yang didapatkan bahwa komponen lingkungan (biotik dan abiotik) akan mempengaruhi kelimpahan dan keanekaragaman spesies pada suatu tempat sehingga tingginya kelimpahan individu tiap jenis dapat dipakai untuk menilai kualitas suatu habitat. Sehingga tidak semua serangga yang aktif pada siang hari tidak dapat aktif di malam hari karena efek adanya sinar tergantung sepenuhnya pada kondisi temperature dan kelembaban disekitar.', - provider: { - id: 'https://osf.io/preprints/osf', - name: 'Open Science Framework', - }, - conflictOfInterestResponse: 'Author asserted no Conflict of Interest', - license: 'No License', - doi: 'https://doi.org/10.31227/osf.io/fcs5r', - }, - { - id: 'https://osf.io/87vyr', - resourceType: ResourceType.Preprint, - dateCreated: new Date('2025-02-20'), - dateModified: new Date('2025-02-20'), - creators: [ - { - id: 'https://osf.io/2x5kc', - name: 'Fitha Kaamiliyaa Hamka', - }, - ], - title: - 'Identifikasi Serangga Nokturnal di Bukit Samata Kabupaten Gowa, Sulawesi Selatan', - description: - 'Keberadaan suatu organisme pada suatu tempat dipengaruhi oleh faktor lingkungan dan makanan. Ketersediaan makanan dengan kualitas yang cocok dan kuantitas yang ukup bagi suatu organisme akan meningkatkan populasi cepat. Sebaliknya jika keadaan tersebut tidak mendukung maka akan dipastikan bahwa organisme tersebut akan menurun. Sedangkan faktor abiotik meliputi suhu, kelembaban, cahaya, curah hujan, dan angin. Suhu mempengaruhi aktivitas serangga serta perkembangannya. Serangga juga tertarik pada gelombang cahaya tertentu. Serangga ada yang menerima intensitas cahaya yang tinggi dan aktif pada siang hari (diurnal) dan serangga ada yang aktif menerima intensitas cahaya rendah pada malam hari (nokturnal). Metode yang digunakan yaitu metode Light trap. Hail yang didapatkan bahwa komponen lingkungan (biotik dan abiotik) akan mempengaruhi kelimpahan dan keanekaragaman spesies pada suatu tempat sehingga tingginya kelimpahan individu tiap jenis dapat dipakai untuk menilai kualitas suatu habitat. Sehingga tidak semua serangga yang aktif pada siang hari tidak dapat aktif di malam hari karena efek adanya sinar tergantung sepenuhnya pada kondisi temperature dan kelembaban disekitar.', - provider: { - id: 'https://osf.io/preprints/osf', - name: 'Open Science Framework', - }, - conflictOfInterestResponse: 'Author asserted no Conflict of Interest', - license: 'No License', - doi: 'https://doi.org/10.31234/osf.io/7mgkd', - }, - { - id: 'https://osf.io/cegxv', - resourceType: ResourceType.User, - title: 'Amelia Jamison', - publicProjects: 2, - publicRegistrations: 0, - publicPreprints: 0, - }, - { - id: 'https://osf.io/cegxv', - resourceType: ResourceType.User, - title: 'Cristal Alvarez', - publicProjects: 2, - publicRegistrations: 0, - publicPreprints: 0, - orcid: 'https://orcid.org/0000-0003-2697-7146', - }, - { - id: 'https://osf.io/cegxv', - resourceType: ResourceType.User, - title: 'Rohini Ganjoo', - publicProjects: 16, - publicRegistrations: 6, - publicPreprints: 3, - orcid: 'https://orcid.org/0000-0003-2697-7146', - employment: 'University of Turku', - education: 'University of Turku', - }, -]; +// import { Resource } from '@osf/features/search/models/resource.entity'; +// import { ResourceType } from '@osf/features/search/models/resource-type.enum'; +// +// export const resources: Resource[] = [ +// { +// id: 'https://osf.io/wrfgp', +// resourceType: ResourceType.File, +// dateCreated: new Date('2025-01-10'), +// dateModified: new Date('2025-01-14'), +// creators: [ +// { +// id: 'https://osf.io/a6t2x', +// name: 'Lívia Rodrigues de Lima Pires', +// }, +// ], +// fileName: 'dadosAnalisados.rds', +// from: { +// id: 'https://osf.io/e86jf', +// name: 'Tese', +// }, +// }, +// { +// id: 'https://osf.io/4crzf', +// resourceType: ResourceType.Project, +// dateCreated: new Date('2025-01-15'), +// dateModified: new Date('2025-01-18'), +// creators: [ +// { +// id: 'https://osf.io/4fy2t', +// name: 'Jane Simpson', +// }, +// { +// id: 'https://osf.io/5jv47', +// name: 'Wendy Francis', +// }, +// { +// id: 'https://osf.io/6a5yb', +// name: 'Daniel Wadsworth', +// }, +// { +// id: 'https://osf.io/6g7nc', +// name: 'Kristen Tulloch', +// }, +// { +// id: 'https://osf.io/7a3tm', +// name: 'Dr Tamara Sysak', +// }, +// { +// id: 'https://osf.io/b8tvg', +// name: 'PJ Humphreys', +// }, +// { +// id: 'https://osf.io/n2hyv', +// name: 'Alison Craswell', +// }, +// { +// id: 'https://osf.io/qtnc8', +// name: 'Apil Gurung', +// }, +// { +// id: 'https://osf.io/qvuw6', +// name: 'Karen Watson', +// }, +// { +// id: 'https://osf.io/zwu3t', +// name: 'Helen Szabo', +// }, +// ], +// title: +// 'Intergenerational community home sharing for older adults and university students: A scoping review protocol.', +// description: +// 'The objective of this scoping review is to review programs designed to facilitate matches between older adults residing within the community and university students. This protocol provides an overview of the topic, justification for and methods to be implemented in the conduct and reporting of the scoping review.', +// }, +// { +// id: 'https://osf.io/pgmhr', +// resourceType: ResourceType.File, +// dateCreated: new Date('2025-01-17'), +// dateModified: new Date('2025-01-17'), +// creators: [{ id: 'https://osf.io/65nk7', name: 'Aline Miranda Ferreira' }], +// fileName: 'Scoping review protocol.pdf', +// license: 'MIT License', +// from: { +// id: 'https://osf.io/4wks9', +// name: 'Instruments for Measuring Pain, Functionality, and Quality of Life Following Knee Fractures: A Scoping Review', +// }, +// }, +// { +// id: 'https://osf.io/f7h5v', +// resourceType: ResourceType.File, +// dateCreated: new Date('2025-01-29'), +// dateModified: new Date('2025-01-29'), +// creators: [{ id: 'https://osf.io/d7rtw', name: 'David P. Goldenberg' }], +// fileName: 'PCB.zip', +// from: { +// id: 'https://osf.io/qs7r9', +// name: 'Design Files', +// }, +// }, +// { +// id: 'https://osf.io/3ua54', +// resourceType: ResourceType.Project, +// dateCreated: new Date('2025-03-16'), +// dateModified: new Date('2025-03-16'), +// creators: [{ id: 'https://osf.io/uby48', name: 'Aadit Nair Sajeev' }], +// title: +// 'Unemployment of Locals in Kochi due to the Augmented Influx of Migrant Workers', +// description: +// 'This project page includes supplementals like interview transcript for a case study into the unemployment of locals due to increasing influx of migrant workers into the state.', +// }, +// { +// id: 'https://osf.io/xy9qn', +// resourceType: ResourceType.Project, +// dateCreated: new Date('2025-03-16'), +// dateModified: new Date('2025-03-16'), +// creators: [ +// { +// id: 'https://osf.io/x87gf', +// name: 'Doctor Jack Samuel Egerton MSci EngD', +// }, +// ], +// title: 'Stone functions (tetration)', +// }, +// { +// id: 'https://osf.io/cpm64', +// resourceType: ResourceType.Registration, +// dateCreated: new Date('2025-01-10'), +// dateModified: new Date('2025-01-10'), +// creators: [ +// { +// id: 'https://osf.io/4fy2t', +// name: 'Simran Khanna', +// }, +// { +// id: 'https://osf.io/5jv47', +// name: 'Devika Shenoy', +// }, +// { +// id: 'https://osf.io/6a5yb', +// name: 'Steph Hendren', +// }, +// { +// id: 'https://osf.io/6g7nc', +// name: 'Christian Zirbes', +// }, +// { +// id: 'https://osf.io/7a3tm', +// name: 'Anthony Catanzano', +// }, +// { +// id: 'https://osf.io/b8tvg', +// name: 'Rachelle Shao', +// }, +// { +// id: 'https://osf.io/n2hyv', +// name: 'Sofiu Ogunbiyi', +// }, +// { +// id: 'https://osf.io/qtnc8', +// name: 'Evelyn Hunter', +// }, +// { +// id: 'https://osf.io/qvuw6', +// name: 'Katie Radulovacki', +// }, +// { +// id: 'https://osf.io/zwu3t', +// name: 'Muhamed Sanneh', +// }, +// ], +// title: +// 'A Scoping Review and Meta-analysis Exploring the Associations between Socioeconomic Identity and Management of Pediatric Extremity Fracture', +// description: +// 'The incidence and management of extremity fractures in pediatric patients can be influenced by social and financial deprivation. Previous studies have highlighted that the social determinants of health, such as socioeconomic status, race, and insurance type, are independently associated with the incidence of pediatric fractures . [1, 2] This underscores the need to understand how these factors specifically affect fracture management in pediatric populations.\n\nIn addition to incidence, socioeconomic status has been shown to impact the timing and type of treatment for fractures. Vazquez et al. demonstrated that adolescents from poor socioeconomic backgrounds were more likely to experience delayed surgical fixation of femoral fractures, which was associated with worse outcomes, including longer hospital stays and higher healthcare costs.[2] Similarly, Evans et al. reported that children from socially deprived areas had worse perceived function and pain outcomes after upper extremity fractures, even after receiving orthopedic treatment.[3] These findings suggest that social deprivation not only affects initial access to care but also influences recovery and long-term outcomes.\n\nThe proposed scoping review and meta-analysis will systematically evaluate the existing literature to map out the extent and nature of the impact of social and financial deprivation on extremity fracture management in pediatric patients. By including a wide range of sociodemographic variables and outcomes, this review aims to provide a comprehensive understanding of the disparities in fracture care. This will inform future research and policy-making to address these inequities and improve healthcare delivery for socially and economically disadvantaged pediatric populations.\n\n[1] Dy CJ, Lyman S, Do HT, Fabricant PD, Marx RG, Green DW. Socioeconomic factors are associated with frequency of repeat emergency department visits for pediatric closed fractures. J Pediatr Orthop. 2014;34(5):548-551. doi:10.1097/BPO.0000000000000143\n[2] Ramaesh R, Clement ND, Rennie L, Court-Brown C, Gaston MS. Social deprivation as a risk factor for fractures in childhood. Bone Joint J. 2015;97-B(2):240-245. doi:10.1302/0301-620X.97B2.34057\n[3] Vazquez S, Dominguez JF, Jacoby M, et al. Poor socioeconomic status is associated with delayed femoral fracture fixation in adolescent patients. Injury. 2023;54(12):111128. doi:10.1016/j.injury.2023.111128', +// license: 'No license', +// publisher: { +// id: 'https://osf.io/registries/osf', +// name: 'OSF Registries', +// }, +// registrationTemplate: 'Generalized Systematic Review Registration', +// doi: 'https://doi.org/10.17605/OSF.IO/CPM64', +// }, +// { +// id: 'https://osf.io/8tk45', +// resourceType: ResourceType.Registration, +// dateCreated: new Date('2025-03-06'), +// dateModified: new Date('2025-03-06'), +// creators: [ +// { +// id: 'https://osf.io/45kpt', +// name: 'Laura A. King', +// }, +// { +// id: 'https://osf.io/cuqrk', +// name: 'Erica A. Holberg', +// }, +// ], +// title: 'Consequentialist Criteria: Money x Effort (Between)', +// description: +// "This is the first of two studies aimed at testing whether the effect of amount of money raised for a good cause is present in within person comparisons, but null when utilizing a between person design. In previous studies, we have found that success in achieving an agent's morally good aim and high cost to agent for doing the right thing are salient to moral evaluators when the explicit contrast to failure or low cost is made (within person), but not particularly salient to moral evaluation when the explicit contrast is not drawn (between person). In this study, we are interested to determine the relative presence and strength of amount of money raised (a consequentialist criterion for moral evaluation) and effort put forth (a non-consequentialist moral criterion centered on the agent's will) when this is not explicitly contrasted because using a between-person design. In a pilot study, we found no effect of money upon moral goodness. There was an effect of money upon effort for the low condition ($50). We want to see how results are altered if we add an explicit factor for effort. Does amount of money have null effect upon perceived moral goodness? Does effort have a larger effect upon perceived moral goodness than amount of money raised? \n\nParticipants will read scenarios reflecting a 3 (money: extremely high vs. high vs. low) x 3 (effort: high vs. no mention vs. low) design. In all 9 scenarios, participants will rate the moral goodness of a person who raises money for and then runs in a 5K for a good cause, where the conditions vary as described above. They also will answer how likely it is that the target would undertake a similar volunteer commitment in the future.", +// publisher: { +// id: 'https://osf.io/registries/osf', +// name: 'OSF Registries', +// }, +// registrationTemplate: 'OSF Preregistration', +// license: 'No license', +// doi: 'https://doi.org/10.17605/OSF.IO/8TK45', +// }, +// { +// id: 'https://osf.io/wa6yf', +// resourceType: ResourceType.File, +// dateCreated: new Date('2025-01-14'), +// dateModified: new Date('2025-01-14'), +// creators: [ +// { +// id: 'https://osf.io/6nkxv', +// name: 'Kari-Anne B. Næss', +// }, +// { +// id: 'https://osf.io/b2g9q', +// name: 'Frida Johanne Holmen ', +// }, +// { +// id: 'https://osf.io/j6hd5', +// name: 'Thormod Idsøe', +// }, +// ], +// from: { +// id: 'https://osf.io/tbzv6', +// name: 'A Cross-sectional Study of Inference-making Comparing First- and Second Language Learners in Early Childhood Education and Care', +// }, +// fileName: 'Model_FH_prereg_140125.pdf', +// license: 'No license', +// }, +// { +// id: 'https://osf.io/4hg87', +// resourceType: ResourceType.ProjectComponent, +// dateCreated: new Date('2025-01-04'), +// dateModified: new Date('2025-01-04'), +// creators: [ +// { +// id: 'https://osf.io/2x5kc', +// name: 'Bowen Wang-Kildegaard', +// }, +// ], +// title: 'Dataset and Codes', +// }, +// { +// id: 'https://osf.io/87vyr', +// resourceType: ResourceType.Preprint, +// dateCreated: new Date('2025-02-20'), +// dateModified: new Date('2025-02-20'), +// creators: [ +// { +// id: 'https://osf.io/2x5kc', +// name: 'Eric L. Olson', +// }, +// ], +// title: 'Evaluative Judgment Across Domains', +// description: +// 'Keberadaan suatu organisme pada suatu tempat dipengaruhi oleh faktor lingkungan dan makanan. Ketersediaan makanan dengan kualitas yang cocok dan kuantitas yang ukup bagi suatu organisme akan meningkatkan populasi cepat. Sebaliknya jika keadaan tersebut tidak mendukung maka akan dipastikan bahwa organisme tersebut akan menurun. Sedangkan faktor abiotik meliputi suhu, kelembaban, cahaya, curah hujan, dan angin. Suhu mempengaruhi aktivitas serangga serta perkembangannya. Serangga juga tertarik pada gelombang cahaya tertentu. Serangga ada yang menerima intensitas cahaya yang tinggi dan aktif pada siang hari (diurnal) dan serangga ada yang aktif menerima intensitas cahaya rendah pada malam hari (nokturnal). Metode yang digunakan yaitu metode Light trap. Hail yang didapatkan bahwa komponen lingkungan (biotik dan abiotik) akan mempengaruhi kelimpahan dan keanekaragaman spesies pada suatu tempat sehingga tingginya kelimpahan individu tiap jenis dapat dipakai untuk menilai kualitas suatu habitat. Sehingga tidak semua serangga yang aktif pada siang hari tidak dapat aktif di malam hari karena efek adanya sinar tergantung sepenuhnya pada kondisi temperature dan kelembaban disekitar.', +// provider: { +// id: 'https://osf.io/preprints/osf', +// name: 'Open Science Framework', +// }, +// conflictOfInterestResponse: 'Author asserted no Conflict of Interest', +// license: 'No License', +// doi: 'https://doi.org/10.31227/osf.io/fcs5r', +// }, +// { +// id: 'https://osf.io/87vyr', +// resourceType: ResourceType.Preprint, +// dateCreated: new Date('2025-02-20'), +// dateModified: new Date('2025-02-20'), +// creators: [ +// { +// id: 'https://osf.io/2x5kc', +// name: 'Fitha Kaamiliyaa Hamka', +// }, +// ], +// title: +// 'Identifikasi Serangga Nokturnal di Bukit Samata Kabupaten Gowa, Sulawesi Selatan', +// description: +// 'Keberadaan suatu organisme pada suatu tempat dipengaruhi oleh faktor lingkungan dan makanan. Ketersediaan makanan dengan kualitas yang cocok dan kuantitas yang ukup bagi suatu organisme akan meningkatkan populasi cepat. Sebaliknya jika keadaan tersebut tidak mendukung maka akan dipastikan bahwa organisme tersebut akan menurun. Sedangkan faktor abiotik meliputi suhu, kelembaban, cahaya, curah hujan, dan angin. Suhu mempengaruhi aktivitas serangga serta perkembangannya. Serangga juga tertarik pada gelombang cahaya tertentu. Serangga ada yang menerima intensitas cahaya yang tinggi dan aktif pada siang hari (diurnal) dan serangga ada yang aktif menerima intensitas cahaya rendah pada malam hari (nokturnal). Metode yang digunakan yaitu metode Light trap. Hail yang didapatkan bahwa komponen lingkungan (biotik dan abiotik) akan mempengaruhi kelimpahan dan keanekaragaman spesies pada suatu tempat sehingga tingginya kelimpahan individu tiap jenis dapat dipakai untuk menilai kualitas suatu habitat. Sehingga tidak semua serangga yang aktif pada siang hari tidak dapat aktif di malam hari karena efek adanya sinar tergantung sepenuhnya pada kondisi temperature dan kelembaban disekitar.', +// provider: { +// id: 'https://osf.io/preprints/osf', +// name: 'Open Science Framework', +// }, +// conflictOfInterestResponse: 'Author asserted no Conflict of Interest', +// license: 'No License', +// doi: 'https://doi.org/10.31234/osf.io/7mgkd', +// }, +// { +// id: 'https://osf.io/cegxv', +// resourceType: ResourceType.User, +// title: 'Amelia Jamison', +// publicProjects: 2, +// publicRegistrations: 0, +// publicPreprints: 0, +// }, +// { +// id: 'https://osf.io/cegxv', +// resourceType: ResourceType.User, +// title: 'Cristal Alvarez', +// publicProjects: 2, +// publicRegistrations: 0, +// publicPreprints: 0, +// orcid: 'https://orcid.org/0000-0003-2697-7146', +// }, +// { +// id: 'https://osf.io/cegxv', +// resourceType: ResourceType.User, +// title: 'Rohini Ganjoo', +// publicProjects: 16, +// publicRegistrations: 6, +// publicPreprints: 3, +// orcid: 'https://orcid.org/0000-0003-2697-7146', +// employment: 'University of Turku', +// education: 'University of Turku', +// }, +// ]; diff --git a/src/app/features/search/mappers/search.mapper.ts b/src/app/features/search/mappers/search.mapper.ts new file mode 100644 index 000000000..08318275b --- /dev/null +++ b/src/app/features/search/mappers/search.mapper.ts @@ -0,0 +1,52 @@ +import { ResourceItem } from '@osf/features/search/models/raw-models/resource-response.model'; +import { Resource } from '@osf/features/search/models/resource.entity'; +import { ResourceType } from '@osf/features/search/models/resource-type.enum'; +import { LinkItem } from '@osf/features/search/models/link-item.entity'; + +export function MapResources(rawItem: ResourceItem): Resource { + return { + id: rawItem['@id'], + resourceType: + ResourceType[ + rawItem?.resourceType[0]['@id'] as keyof typeof ResourceType + ], + dateCreated: rawItem?.dateCreated?.[0]?.['@value'] + ? new Date(rawItem?.dateCreated?.[0]?.['@value']) + : undefined, + dateModified: rawItem?.dateModified?.[0]?.['@value'] + ? new Date(rawItem?.dateModified?.[0]?.['@value']) + : undefined, + creators: (rawItem?.creator ?? []).map( + (creator) => + ({ + id: creator?.['@id'], + name: creator?.name?.[0]?.['@value'], + }) as LinkItem, + ), + fileName: rawItem?.fileName?.[0]?.['@value'], + title: rawItem?.title?.[0]?.['@value'] ?? rawItem?.name?.[0]?.['@value'], + description: rawItem?.description?.[0]?.['@value'], + from: { + id: rawItem?.isPartOf?.[0]?.['@id'], + name: rawItem?.isPartOf?.[0]?.title?.[0]?.['@value'], + }, + license: { + id: rawItem?.rights?.[0]?.['@id'], + name: rawItem?.rights?.[0]?.name?.[0]?.['@value'], + }, + provider: { + id: rawItem?.publisher?.[0]?.['@id'], + name: rawItem?.publisher?.[0]?.name?.[0]?.['@value'], + }, + registrationTemplate: rawItem?.conformsTo?.[0]?.title?.[0]?.['@value'], + doi: rawItem?.identifier?.[0]?.['@value'], + conflictOfInterestResponse: rawItem?.statedConflictOfInterest?.[0]?.['@id'], + }; +} + +// publicProjects?: number; +// publicRegistrations?: number; +// publicPreprints?: number; +// orcid?: string; +// employment?: string; +// education?: string; diff --git a/src/app/features/search/models/raw-models/resource-response.model.ts b/src/app/features/search/models/raw-models/resource-response.model.ts new file mode 100644 index 000000000..1b662be1a --- /dev/null +++ b/src/app/features/search/models/raw-models/resource-response.model.ts @@ -0,0 +1,73 @@ +import { MetadataField } from '@shared/entities/metadata-field.inteface'; + +export interface ResourceItem { + '@id': string; + accessService: MetadataField[]; + affiliation: MetadataField[]; + creator: Creator[]; + conformsTo: ConformsTo[]; + dateCopyrighted: { '@value': string }[]; + dateCreated: { '@value': string }[]; + dateModified: { '@value': string }[]; + description: { '@value': string }[]; + hasPreregisteredAnalysisPlan: { '@id': string }[]; + hasPreregisteredStudyDesign: { '@id': string }[]; + hostingInstitution: HostingInstitution[]; + identifier: { '@value': string }[]; + keyword: { '@value': string }[]; + publisher: MetadataField[]; + resourceNature: ResourceNature[]; + qualifiedAttribution: QualifiedAttribution[]; + resourceType: { '@id': string }[]; + title: { '@value': string }[]; + name: { '@value': string }[]; + fileName: { '@value': string }[]; + isPartOf: isPartOf[]; + isPartOfCollection: IsPartOfCollection[]; + rights: MetadataField[]; + statedConflictOfInterest: { '@id': string }[]; +} + +export interface Creator extends MetadataField { + affiliation: MetadataField[]; + sameAs: { '@id': string }[]; +} + +export interface HostingInstitution extends MetadataField { + sameAs: MetadataField[]; +} + +export interface QualifiedAttribution { + agent: { '@id': string }[]; + hadRole: { '@id': string }[]; +} + +export interface isPartOf extends MetadataField { + creator: Creator[]; + dateCopyright: { '@value': string }[]; + dateCreated: { '@value': string }[]; + publisher: MetadataField[]; + rights: MetadataField[]; + rightHolder: { '@value': string }[]; + sameAs: { '@id': string }[]; + title: { '@value': string }[]; +} + +export interface IsPartOfCollection { + '@id': string; + resourceNature: { '@id': string }[]; + title: { '@value': string }[]; +} + +export interface ResourceNature { + '@id': string; + displayLabel: { + '@language': string; + '@value': string; + }[]; +} + +export interface ConformsTo { + '@id': string; + title: { '@value': string }[]; +} diff --git a/src/app/features/search/models/resource-type.enum.ts b/src/app/features/search/models/resource-type.enum.ts index 4750fc6bd..61ac6ded6 100644 --- a/src/app/features/search/models/resource-type.enum.ts +++ b/src/app/features/search/models/resource-type.enum.ts @@ -5,5 +5,5 @@ export enum ResourceType { Registration, Preprint, ProjectComponent, - User, + Agent, } diff --git a/src/app/features/search/models/resource.entity.ts b/src/app/features/search/models/resource.entity.ts index 2898c17d6..fb8c6012d 100644 --- a/src/app/features/search/models/resource.entity.ts +++ b/src/app/features/search/models/resource.entity.ts @@ -11,11 +11,10 @@ export interface Resource { title?: string; description?: string; from?: LinkItem; - license?: string; - publisher?: LinkItem; + license?: LinkItem; + provider?: LinkItem; registrationTemplate?: string; doi?: string; - provider?: LinkItem; conflictOfInterestResponse?: string; publicProjects?: number; publicRegistrations?: number; diff --git a/src/app/features/search/search.component.html b/src/app/features/search/search.component.html index be9758979..e1c878e8b 100644 --- a/src/app/features/search/search.component.html +++ b/src/app/features/search/search.component.html @@ -37,7 +37,7 @@ > @@ -46,7 +46,7 @@ @@ -58,7 +58,7 @@ > @@ -70,7 +70,7 @@ > @@ -79,7 +79,7 @@ @@ -88,7 +88,7 @@ diff --git a/src/app/features/search/search.component.ts b/src/app/features/search/search.component.ts index 823ab02bd..ce172bf34 100644 --- a/src/app/features/search/search.component.ts +++ b/src/app/features/search/search.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, Component, - computed, + effect, inject, + OnInit, signal, } from '@angular/core'; import { SearchInputComponent } from '@shared/components/search-input/search-input.component'; @@ -18,8 +19,9 @@ import { TableModule } from 'primeng/table'; import { DataViewModule } from 'primeng/dataview'; import { ResourcesComponent } from '@shared/components/resources/resources.component'; import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; -import { Resource } from '@osf/features/search/models/resource.entity'; -import { resources } from '@osf/features/search/data'; +import { Store } from '@ngxs/store'; +import { GetResources, SearchSelectors } from '@osf/features/search/store'; +import { ResourceFiltersSelectors } from '@shared/components/resources/resource-filters/store'; @Component({ selector: 'osf-search', @@ -44,45 +46,33 @@ import { resources } from '@osf/features/search/data'; styleUrl: './search.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SearchComponent { +export class SearchComponent implements OnInit { + readonly #store = inject(Store); + protected searchValue = signal(''); - protected selectedTab = 0; protected readonly isMobile = toSignal(inject(IS_XSMALL)); + protected readonly resources = this.#store.selectSignal( + SearchSelectors.getResources, + ); + protected readonly creatorsFilter = this.#store.selectSignal( + ResourceFiltersSelectors.getCreator, + ); + + protected selectedTab = 0; + protected readonly ResourceTab = ResourceTab; - protected readonly resources = signal(resources); - protected readonly searchedResources = computed(() => { - const search = this.searchValue().toLowerCase(); - return this.resources().filter( - (resource: Resource) => - resource.title?.toLowerCase().includes(search) || - resource.fileName?.toLowerCase().includes(search) || - resource.description?.toLowerCase().includes(search) || - resource.creators - ?.map((p) => p.name.toLowerCase()) - .some((name) => name.includes(search)) || - resource.dateCreated - ?.toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - }) - .toLowerCase() - .includes(search) || - resource.dateModified - ?.toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - }) - .toLowerCase() - .includes(search) || - resource.from?.name.toLowerCase().includes(search), - ); - }); + constructor() { + effect(() => { + this.creatorsFilter(); + this.#store.dispatch(GetResources); + }); + } + + ngOnInit() { + this.#store.dispatch(GetResources); + } onTabChange(index: number): void { this.selectedTab = index; } - - protected readonly ResourceTab = ResourceTab; } diff --git a/src/app/features/search/search.service.ts b/src/app/features/search/search.service.ts new file mode 100644 index 000000000..b7c5e9b68 --- /dev/null +++ b/src/app/features/search/search.service.ts @@ -0,0 +1,56 @@ +import { inject, Injectable } from '@angular/core'; +import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { map, Observable } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { ResourceItem } from '@osf/features/search/models/raw-models/resource-response.model'; +import { + ApiData, + JsonApiResponse, +} from '@core/services/json-api/json-api.entity'; +import { MapResources } from '@osf/features/search/mappers/search.mapper'; +import { Resource } from '@osf/features/search/models/resource.entity'; + +@Injectable({ + providedIn: 'root', +}) +export class SearchService { + #jsonApiService = inject(JsonApiService); + + getResources(filters: Record): Observable { + const params: Record = { + 'cardSearchFilter[resourceType]': + 'Registration,RegistrationComponent,Project,ProjectComponent,Preprint,Agent,File', + 'cardSearchFilter[accessService]': 'https://staging4.osf.io/', + 'cardSearchText[*,creator.name,isContainedBy.creator.name]': '', + 'page[size]': '20', + sort: '-relevance', + ...filters, + }; + + return this.#jsonApiService + .get< + JsonApiResponse< + null, + ApiData<{ resourceMetadata: ResourceItem }, null>[] + > + >(`${environment.shareDomainUrl}/index-card-search`, params) + .pipe( + map((response) => + response.included + .filter((item) => item.type === 'index-card') + .map((item) => MapResources(item.attributes.resourceMetadata)), + ), + ); + } + + // getValueSearch(): Observable { + // const params = { + // 'cardSearchFilter[resourceType]': 'Registration,RegistrationComponent,Project,ProjectComponent,Preprint,Agent,File', + // 'cardSearchFilter[accessService]': 'https://staging4.osf.io/', + // 'cardSearchText[*,creator.name,isContainedBy.creator.name]': '', + // sort: '-relevance', + // }; + // + // return + // } +} diff --git a/src/app/features/search/store/index.ts b/src/app/features/search/store/index.ts new file mode 100644 index 000000000..c491f1685 --- /dev/null +++ b/src/app/features/search/store/index.ts @@ -0,0 +1,4 @@ +export * from './search.actions'; +export * from './search.model'; +export * from './search.selectors'; +export * from './search.state'; diff --git a/src/app/features/search/store/search.actions.ts b/src/app/features/search/store/search.actions.ts new file mode 100644 index 000000000..2fc54668c --- /dev/null +++ b/src/app/features/search/store/search.actions.ts @@ -0,0 +1,3 @@ +export class GetResources { + static readonly type = '[Search] Get Resources'; +} diff --git a/src/app/features/search/store/search.model.ts b/src/app/features/search/store/search.model.ts new file mode 100644 index 000000000..458dabfa6 --- /dev/null +++ b/src/app/features/search/store/search.model.ts @@ -0,0 +1,5 @@ +import { Resource } from '@osf/features/search/models/resource.entity'; + +export interface SearchStateModel { + resources: Resource[]; +} diff --git a/src/app/features/search/store/search.selectors.ts b/src/app/features/search/store/search.selectors.ts new file mode 100644 index 000000000..11bc27530 --- /dev/null +++ b/src/app/features/search/store/search.selectors.ts @@ -0,0 +1,11 @@ +import { SearchState } from '@osf/features/search/store/search.state'; +import { Selector } from '@ngxs/store'; +import { SearchStateModel } from '@osf/features/search/store/search.model'; +import { Resource } from '@osf/features/search/models/resource.entity'; + +export class SearchSelectors { + @Selector([SearchState]) + static getResources(state: SearchStateModel): Resource[] { + return state.resources; + } +} diff --git a/src/app/features/search/store/search.state.ts b/src/app/features/search/store/search.state.ts new file mode 100644 index 000000000..216c05568 --- /dev/null +++ b/src/app/features/search/store/search.state.ts @@ -0,0 +1,37 @@ +import { inject, Injectable } from '@angular/core'; +import { SearchService } from '@osf/features/search/search.service'; +import { SearchStateModel } from '@osf/features/search/store/search.model'; +import { Action, State, StateContext, Store } from '@ngxs/store'; +import { GetResources } from '@osf/features/search/store/search.actions'; +import { tap } from 'rxjs'; +import { ResourceFiltersSelectors } from '@shared/components/resources/resource-filters/store'; + +@Injectable() +@State({ + name: 'search', + defaults: { + resources: [], + }, +}) +export class SearchState { + searchService = inject(SearchService); + store = inject(Store); + + @Action(GetResources) + getResources(ctx: StateContext) { + const creatorParams: Record = {}; + + const creatorFilters = this.store.selectSignal( + ResourceFiltersSelectors.getCreator, + ); + if (creatorFilters()) { + creatorParams['cardSearchFilter[creator][]'] = creatorFilters(); + } + + return this.searchService.getResources(creatorParams).pipe( + tap((resources) => { + ctx.patchState({ resources: resources }); + }), + ); + } +} diff --git a/src/app/shared/components/resources/resource-card/resource-card.component.html b/src/app/shared/components/resources/resource-card/resource-card.component.html index e9b2b1f91..b6e5d955f 100644 --- a/src/app/shared/components/resources/resource-card/resource-card.component.html +++ b/src/app/shared/components/resources/resource-card/resource-card.component.html @@ -3,7 +3,11 @@
-

{{ ResourceType[item().resourceType] }}

+ @if (item().resourceType === ResourceType.Agent) { +

User

+ } @else { +

{{ ResourceType[item().resourceType] }}

+ }
@if (item().resourceType === ResourceType.File) { @@ -43,7 +47,7 @@
} - @if (item().from) { + @if (item().from?.id) {

From:

{{ @@ -91,10 +95,17 @@

Description: {{ item().description }}

} - @if (item().publisher) { + @if (item().provider?.id) { + +

Registration provider: 

+
{{ item().provider?.name }} + + } + + @if (item().license?.id) { -

Registration provider:

- {{ item().publisher?.name }} +

License: 

+ {{ item().license?.name }}
} @@ -104,21 +115,24 @@

} - @if (item().provider) { + @if (item().provider?.id) {

Provider: 

{{ item().provider?.name }}
} - @if (item().conflictOfInterestResponse) { + @if ( + item().conflictOfInterestResponse && + item().conflictOfInterestResponse === "no-conflict-of-interest" + ) {

- Conflict of Interest response: - {{ item().conflictOfInterestResponse }} + Conflict of Interest response: Author asserted no Conflict of + Interest

} - @if (item().resourceType !== ResourceType.User) { + @if (item().resourceType !== ResourceType.Agent) {

URL: 

{{ item().id }} @@ -132,7 +146,7 @@
} - @if (item().resourceType === ResourceType.User) { + @if (item().resourceType === ResourceType.Agent) {

Public projects: {{ item().publicProjects }}

Public registrations: {{ item().publicRegistrations }}

Public preprints: {{ item().publicPreprints }}

diff --git a/src/app/shared/components/resources/resource-card/resource-card.component.scss b/src/app/shared/components/resources/resource-card/resource-card.component.scss index 508b4f1b1..d7aae7722 100644 --- a/src/app/shared/components/resources/resource-card/resource-card.component.scss +++ b/src/app/shared/components/resources/resource-card/resource-card.component.scss @@ -13,6 +13,7 @@ font-size: 1.4rem; line-height: 1.7rem; color: var.$dark-blue-1; + padding-bottom: 4px; &:hover { text-decoration: underline; } diff --git a/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.html new file mode 100644 index 000000000..b08b4e9ae --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.html @@ -0,0 +1,16 @@ +
+
+

Filter creators by typing their name below

+ +
+
diff --git a/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.scss new file mode 100644 index 000000000..7b5b24e58 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.scss @@ -0,0 +1,12 @@ +:host { + .content-body { + display: flex; + flex-direction: column; + row-gap: 1.5rem; + padding-bottom: 1.5rem; + + p { + font-weight: 300; + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.ts new file mode 100644 index 000000000..e5e3f08ab --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.ts @@ -0,0 +1,71 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + OnInit, + signal, +} from '@angular/core'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { debounceTime, finalize, tap } from 'rxjs'; +import { Creator } from '@shared/components/resources/resource-filters/models/creator/creator.entity'; +import { ResourceFiltersService } from '@shared/components/resources/resource-filters/resource-filters.service'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Store } from '@ngxs/store'; +import { SetCreator } from '@shared/components/resources/resource-filters/store'; + +@Component({ + selector: 'osf-creators-filter', + imports: [Select, ReactiveFormsModule], + templateUrl: './creators-filter.component.html', + styleUrl: './creators-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CreatorsFilterComponent implements OnInit { + readonly #store = inject(Store); + readonly #resourceFiltersService = inject(ResourceFiltersService); + + protected searchCreatorsResults = signal([]); + protected creatorsOptions = computed(() => { + return this.searchCreatorsResults().map((creator) => ({ + label: creator.name, + value: creator.id, + })); + }); + protected creatorsLoading = signal(false); + + readonly creatorsGroup = new FormGroup({ + creator: new FormControl(''), + }); + + ngOnInit() { + if (this.creatorsGroup) { + this.creatorsGroup + ?.get('creator')! + .valueChanges.pipe( + debounceTime(500), + tap(() => this.creatorsLoading.set(true)), + finalize(() => this.creatorsLoading.set(false)), + ) + .subscribe((searchText) => { + console.log(searchText); + if (searchText && searchText !== '') { + this.#resourceFiltersService + .getCreators(searchText) + .subscribe((creators) => { + this.searchCreatorsResults.set(creators); + }); + } else { + this.searchCreatorsResults.set([]); + this.#store.dispatch(new SetCreator('')); + } + }); + } + } + + setCreator(event: SelectChangeEvent): void { + if ((event.originalEvent as PointerEvent).pointerId) { + this.#store.dispatch(new SetCreator(event.value)); + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/mappers/creators/creators.mappers.ts b/src/app/shared/components/resources/resource-filters/mappers/creators/creators.mappers.ts new file mode 100644 index 000000000..c8bed480d --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/mappers/creators/creators.mappers.ts @@ -0,0 +1,9 @@ +import { CreatorItem } from '@shared/components/resources/resource-filters/models/creator/creator-item.entity'; +import { Creator } from '@shared/components/resources/resource-filters/models/creator/creator.entity'; + +export function MapCreators(rawItem: CreatorItem): Creator { + return { + id: rawItem?.['@id'], + name: rawItem?.name?.[0]?.['@value'], + }; +} diff --git a/src/app/shared/components/resources/resource-filters/models/creator/creator-item.entity.ts b/src/app/shared/components/resources/resource-filters/models/creator/creator-item.entity.ts new file mode 100644 index 000000000..b69a75009 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/creator/creator-item.entity.ts @@ -0,0 +1,4 @@ +export interface CreatorItem { + '@id': string; + name: { '@value': string }[]; +} diff --git a/src/app/shared/components/resources/resource-filters/models/creator/creator.entity.ts b/src/app/shared/components/resources/resource-filters/models/creator/creator.entity.ts new file mode 100644 index 000000000..c4ffc7510 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/creator/creator.entity.ts @@ -0,0 +1,4 @@ +export interface Creator { + id: string; + name: string; +} diff --git a/src/app/shared/components/resources/resource-filters/resource-filters.component.html b/src/app/shared/components/resources/resource-filters/resource-filters.component.html index fc35123ae..f9de3010d 100644 --- a/src/app/shared/components/resources/resource-filters/resource-filters.component.html +++ b/src/app/shared/components/resources/resource-filters/resource-filters.component.html @@ -2,13 +2,7 @@ Creator -
-

Filter creators by typing their name below

- - [(ngModel)]="creator" [suggestions]="filteredCreators()" - (completeMethod)="setCreator($event)" - -
+
diff --git a/src/app/shared/components/resources/resource-filters/resource-filters.component.scss b/src/app/shared/components/resources/resource-filters/resource-filters.component.scss index 6b72be59f..96985d459 100644 --- a/src/app/shared/components/resources/resource-filters/resource-filters.component.scss +++ b/src/app/shared/components/resources/resource-filters/resource-filters.component.scss @@ -6,15 +6,4 @@ padding: 0 1.7rem 0 1.7rem; width: 30%; height: fit-content; - - .content-body { - display: flex; - flex-direction: column; - row-gap: 1.5rem; - padding-bottom: 1.5rem; - - p { - font-weight: 300; - } - } } diff --git a/src/app/shared/components/resources/resource-filters/resource-filters.component.ts b/src/app/shared/components/resources/resource-filters/resource-filters.component.ts index 4b17d3220..3bba3241c 100644 --- a/src/app/shared/components/resources/resource-filters/resource-filters.component.ts +++ b/src/app/shared/components/resources/resource-filters/resource-filters.component.ts @@ -1,23 +1,13 @@ -import { - ChangeDetectionStrategy, - Component, - computed, - inject, - model, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { Accordion, AccordionContent, AccordionHeader, AccordionPanel, } from 'primeng/accordion'; -import { - AutoComplete, - AutoCompleteCompleteEvent, - AutoCompleteModule, -} from 'primeng/autocomplete'; -import { Store } from '@ngxs/store'; -import { SetCreator } from '@shared/components/resources/resource-filters/store/resource-filters.actions'; +import { AutoComplete, AutoCompleteModule } from 'primeng/autocomplete'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CreatorsFilterComponent } from '@shared/components/resources/resource-filters/filters/creators/creators-filter.component'; @Component({ selector: 'osf-resource-filters', @@ -28,23 +18,11 @@ import { SetCreator } from '@shared/components/resources/resource-filters/store/ AccordionPanel, AutoComplete, AutoCompleteModule, + ReactiveFormsModule, + CreatorsFilterComponent, ], templateUrl: './resource-filters.component.html', styleUrl: './resource-filters.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ResourceFiltersComponent { - readonly #store = inject(Store); - - creator = model(''); - creators: string[] = []; - filteredCreators = computed(() => { - return this.creators.filter((creator) => - creator.toLowerCase().includes(this.creator().toLowerCase()), - ); - }); - - setCreator(event: AutoCompleteCompleteEvent) { - this.#store.dispatch(new SetCreator(event.query)); - } -} +export class ResourceFiltersComponent {} diff --git a/src/app/shared/components/resources/resource-filters/resource-filters.service.ts b/src/app/shared/components/resources/resource-filters/resource-filters.service.ts new file mode 100644 index 000000000..f9696688c --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/resource-filters.service.ts @@ -0,0 +1,46 @@ +import { inject, Injectable } from '@angular/core'; +import { map, Observable } from 'rxjs'; +import { + ApiData, + JsonApiResponse, +} from '@core/services/json-api/json-api.entity'; +import { environment } from '../../../../../environments/environment'; +import { CreatorItem } from '@shared/components/resources/resource-filters/models/creator/creator-item.entity'; +import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { MapCreators } from '@shared/components/resources/resource-filters/mappers/creators/creators.mappers'; +import { Creator } from '@shared/components/resources/resource-filters/models/creator/creator.entity'; + +@Injectable({ + providedIn: 'root', +}) +export class ResourceFiltersService { + #jsonApiService = inject(JsonApiService); + + getCreators(valueSearchText: string): Observable { + const params = { + 'cardSearchFilter[resourceType]': + 'Registration,RegistrationComponent,Project,ProjectComponent,Preprint,Agent,File', + 'cardSearchFilter[accessService]': 'https://staging4.osf.io/', + 'cardSearchText[*,creator.name,isContainedBy.creator.name]': '', + 'page[size]': '20', + sort: '-relevance', + valueSearchPropertyPath: 'creator', + valueSearchText: valueSearchText, + }; + + return this.#jsonApiService + .get< + JsonApiResponse< + null, + ApiData<{ resourceMetadata: CreatorItem }, null>[] + > + >(`${environment.shareDomainUrl}/index-value-search`, params) + .pipe( + map((response) => + response.included + .filter((item) => item.type === 'index-card') + .map((item) => MapCreators(item.attributes.resourceMetadata)), + ), + ); + } +} diff --git a/src/app/shared/components/resources/resource-filters/store/resource-filters.state.ts b/src/app/shared/components/resources/resource-filters/store/resource-filters.state.ts index 02b5f095b..0880cdf89 100644 --- a/src/app/shared/components/resources/resource-filters/store/resource-filters.state.ts +++ b/src/app/shared/components/resources/resource-filters/store/resource-filters.state.ts @@ -12,6 +12,7 @@ import { SetSubject, } from '@shared/components/resources/resource-filters/store/resource-filters.actions'; import { ResourceType } from '@osf/features/search/models/resource-type.enum'; +import { Injectable } from '@angular/core'; @State({ name: 'resourceFilters', @@ -27,6 +28,7 @@ import { ResourceType } from '@osf/features/search/models/resource-type.enum'; partOfCollection: '', }, }) +@Injectable() export class ResourceFiltersState { @Action(SetCreator) setCreator(ctx: StateContext, action: SetCreator) { diff --git a/src/app/shared/components/resources/resources.component.ts b/src/app/shared/components/resources/resources.component.ts index baeb7bfad..5e6106dd4 100644 --- a/src/app/shared/components/resources/resources.component.ts +++ b/src/app/shared/components/resources/resources.component.ts @@ -57,7 +57,7 @@ export class ResourcesComponent { case ResourceTab.Files: return resources.resourceType === ResourceType.File; case ResourceTab.Users: - return resources.resourceType === ResourceType.User; + return resources.resourceType === ResourceType.Agent; default: return true; } diff --git a/src/app/shared/entities/metadata-field.inteface.ts b/src/app/shared/entities/metadata-field.inteface.ts new file mode 100644 index 000000000..11e221696 --- /dev/null +++ b/src/app/shared/entities/metadata-field.inteface.ts @@ -0,0 +1,6 @@ +export interface MetadataField { + '@id': string; + identifier: { '@value': string }[]; + name: { '@value': string }[]; + resourceType: { '@id': string }[]; +} diff --git a/src/assets/styles/overrides/select.scss b/src/assets/styles/overrides/select.scss index eead30a9b..f499c533e 100644 --- a/src/assets/styles/overrides/select.scss +++ b/src/assets/styles/overrides/select.scss @@ -10,6 +10,10 @@ color: var.$dark-blue-1; } +.p-select-overlay { + min-width: 100%; +} + .p-select-label { @include mix.flex-align-center; width: 100%; @@ -54,3 +58,17 @@ } } } + +.filter { + .p-select { + //.p-select-dropdown { + // display: none; + //} + + .p-select-label { + font-size: 1.2rem; + font-weight: 400; + color: var.$dark-blue-1; + } + } +} diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 2d4be37a9..4fbee9975 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,4 +1,5 @@ export const environment = { production: false, apiUrl: 'https://api.staging4.osf.io/v2', + shareDomainUrl: 'https://staging-share.osf.io/trove', }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 2d4be37a9..4fbee9975 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,4 +1,5 @@ export const environment = { production: false, apiUrl: 'https://api.staging4.osf.io/v2', + shareDomainUrl: 'https://staging-share.osf.io/trove', }; From 338a35ab92f3d0a3a5f2d194619c6c9aaccdc21d Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Mon, 28 Apr 2025 17:27:00 +0300 Subject: [PATCH 4/4] chore(search): search api --- package.json | 1 + src/app/app.routes.ts | 5 +- .../components/header/header.component.html | 2 +- src/app/core/helpers/ngxs-states.constant.ts | 2 + .../core/services/json-api/json-api.entity.ts | 3 +- .../home-logged-out.component.spec.ts | 10 +- .../models/raw-models/ProjectItem.entity.ts | 8 +- src/app/features/search/data.ts | 320 ---------------- .../features/search/mappers/search.mapper.ts | 12 +- .../raw-models/index-card-search.model.ts | 27 ++ .../raw-models/resource-response.model.ts | 11 +- .../features/search/models/resource.entity.ts | 5 + .../search/models/resources-data.entity.ts | 9 + src/app/features/search/search.component.html | 116 +++--- src/app/features/search/search.component.scss | 36 ++ src/app/features/search/search.component.ts | 101 ++++- src/app/features/search/search.service.ts | 93 +++-- .../features/search/store/search.actions.ts | 30 ++ src/app/features/search/store/search.model.ts | 8 + .../features/search/store/search.selectors.ts | 36 ++ src/app/features/search/store/search.state.ts | 78 +++- .../helpers/get-resource-types.helper.ts | 18 + .../filter-chips/filter-chips.component.html | 141 +++++++ .../filter-chips/filter-chips.component.scss | 5 + .../filter-chips.component.spec.ts | 22 ++ .../filter-chips/filter-chips.component.ts | 68 ++++ .../mappers/user-counts.mapper.ts | 16 + .../models/user-counts-response.entity.ts | 44 +++ .../models/user-related-data-counts.entity.ts | 7 + .../resource-card.component.html | 144 ++++--- .../resource-card/resource-card.component.ts | 46 ++- .../resource-card/resource-card.service.ts | 24 ++ .../creators/creators-filter.component.html | 32 +- .../creators/creators-filter.component.scss | 12 - .../creators/creators-filter.component.ts | 107 ++++-- .../date-created-filter.component.html | 13 + .../date-created-filter.component.scss | 0 .../date-created-filter.component.spec.ts | 22 ++ .../date-created-filter.component.ts | 63 ++++ .../funder/funder-filter.component.html | 19 + .../funder/funder-filter.component.scss | 0 .../funder/funder-filter.component.spec.ts | 22 ++ .../filters/funder/funder-filter.component.ts | 87 +++++ .../institution-filter.component.html | 20 + .../institution-filter.component.scss | 5 + .../institution-filter.component.spec.ts | 22 ++ .../institution-filter.component.ts | 91 +++++ .../license-filter.component.html | 20 + .../license-filter.component.scss | 0 .../license-filter.component.spec.ts | 22 ++ .../license-filter.component.ts | 85 +++++ .../part-of-collection-filter.component.html | 19 + .../part-of-collection-filter.component.scss | 0 ...art-of-collection-filter.component.spec.ts | 22 ++ .../part-of-collection-filter.component.ts | 74 ++++ .../provider-filter.component.html | 20 + .../provider-filter.component.scss | 0 .../provider-filter.component.spec.ts | 22 ++ .../provider-filter.component.ts | 85 +++++ .../resource-type-filter.component.html | 20 + .../resource-type-filter.component.scss | 0 .../resource-type-filter.component.spec.ts | 22 ++ .../resource-type-filter.component.ts | 89 +++++ .../store/resource-filters-options.actions.ts | 42 +++ .../store/resource-filters-options.model.ts | 21 ++ .../resource-filters-options.selectors.ts | 69 ++++ .../store/resource-filters-options.state.ts | 137 +++++++ .../subject/subject-filter.component.html | 20 + .../subject/subject-filter.component.scss | 0 .../subject/subject-filter.component.spec.ts | 22 ++ .../subject/subject-filter.component.ts | 85 +++++ .../dateCreated/date-created.mapper.ts | 26 ++ .../mappers/funder/funder.mapper.ts | 29 ++ .../mappers/institution/institution.mapper.ts | 30 ++ .../mappers/license/license.mapper.ts | 29 ++ .../part-of-collection.mapper.ts | 30 ++ .../mappers/provider/provider.mapper.ts | 30 ++ .../resource-type/resource-type.mapper.ts | 30 ++ .../mappers/subject/subject.mapper.ts | 27 ++ .../models/dateCreated/date-created.entity.ts | 4 + .../dateCreated/date-index-card.entity.ts | 10 + .../resource-filters/models/filter-labels.ts | 11 + .../models/filter-type.enum.ts | 11 + .../models/funder/funder-filter.entity.ts | 5 + .../funder/funder-index-card-filter.entity.ts | 11 + .../funder-index-value-search.entity.ts | 4 + .../models/index-card-filter.entity.ts | 11 + .../models/index-value-search.entity.ts | 4 + .../institution/institution-filter.entity.ts | 5 + .../institution-index-card-filter.entity.ts | 11 + .../institution-index-value-search.entity.ts | 6 + .../models/license/license-filter.entity.ts | 5 + .../license-index-card-filter.entity.ts | 11 + .../license-index-value-search.entity.ts | 6 + .../part-of-collection-filter.entity.ts | 5 + ...-of-collection-index-card-filter.entity.ts | 11 + ...of-collection-index-value-search.entity.ts | 6 + .../models/provider/provider-filter.entity.ts | 5 + .../provider-index-card-filter.entity.ts | 11 + .../provider-index-value-search.entity.ts | 6 + .../resource-type-index-card-filter.entity.ts | 11 + ...resource-type-index-value-search.entity.ts | 6 + .../resource-type/resource-type.entity.ts | 5 + .../models/search-result-count.entity.ts | 15 + .../subject/subject-filter-response.entity.ts | 5 + .../models/subject/subject-filter.entity.ts | 5 + .../resource-filters.component.html | 205 ++++------ .../resource-filters.component.scss | 1 - .../resource-filters.component.ts | 354 +++++++++++++++++- .../resource-filters.service.ts | 231 +++++++++++- .../store/resource-filters.actions.ts | 44 ++- .../store/resource-filters.model.ts | 26 +- .../store/resource-filters.selectors.ts | 45 ++- .../store/resource-filters.state.ts | 132 ++++++- .../utils/add-filters-params.helper.ts | 38 ++ .../resources/resources.component.html | 91 ++++- .../resources/resources.component.scss | 22 ++ .../resources/resources.component.ts | 107 ++++-- src/assets/icons/source/close.svg | 2 +- src/assets/icons/source/code-colored.svg | 16 + src/assets/icons/source/data-colored.svg | 16 + src/assets/icons/source/materials-colored.svg | 16 + src/assets/icons/source/papers-colored.svg | 16 + .../icons/source/supplements-colored.svg | 19 + src/assets/icons/system/chevron-left.svg | 3 + src/assets/icons/system/chevron-right.svg | 3 + src/assets/icons/system/first.svg | 3 + src/assets/styles/_variables.scss | 6 + src/assets/styles/overrides/accordion.scss | 17 + src/assets/styles/overrides/chip.scss | 29 ++ src/assets/styles/overrides/dropdown.scss | 19 - src/assets/styles/overrides/paginator.scss | 47 +++ src/assets/styles/overrides/select.scss | 41 +- src/assets/styles/overrides/tabs.scss | 16 + src/assets/styles/styles.scss | 6 + 135 files changed, 4019 insertions(+), 848 deletions(-) delete mode 100644 src/app/features/search/data.ts create mode 100644 src/app/features/search/models/raw-models/index-card-search.model.ts create mode 100644 src/app/features/search/models/resources-data.entity.ts create mode 100644 src/app/features/search/utils/helpers/get-resource-types.helper.ts create mode 100644 src/app/shared/components/resources/filter-chips/filter-chips.component.html create mode 100644 src/app/shared/components/resources/filter-chips/filter-chips.component.scss create mode 100644 src/app/shared/components/resources/filter-chips/filter-chips.component.spec.ts create mode 100644 src/app/shared/components/resources/filter-chips/filter-chips.component.ts create mode 100644 src/app/shared/components/resources/resource-card/mappers/user-counts.mapper.ts create mode 100644 src/app/shared/components/resources/resource-card/models/user-counts-response.entity.ts create mode 100644 src/app/shared/components/resources/resource-card/models/user-related-data-counts.entity.ts create mode 100644 src/app/shared/components/resources/resource-card/resource-card.service.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.html create mode 100644 src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.scss create mode 100644 src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.spec.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.html create mode 100644 src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.scss create mode 100644 src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.spec.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.html create mode 100644 src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.scss create mode 100644 src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.spec.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.html create mode 100644 src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.scss create mode 100644 src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.spec.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.html create mode 100644 src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.scss create mode 100644 src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.spec.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.html create mode 100644 src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.scss create mode 100644 src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.spec.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.html create mode 100644 src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.scss create mode 100644 src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.spec.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.actions.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.model.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.state.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.html create mode 100644 src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.scss create mode 100644 src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.spec.ts create mode 100644 src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.ts create mode 100644 src/app/shared/components/resources/resource-filters/mappers/dateCreated/date-created.mapper.ts create mode 100644 src/app/shared/components/resources/resource-filters/mappers/funder/funder.mapper.ts create mode 100644 src/app/shared/components/resources/resource-filters/mappers/institution/institution.mapper.ts create mode 100644 src/app/shared/components/resources/resource-filters/mappers/license/license.mapper.ts create mode 100644 src/app/shared/components/resources/resource-filters/mappers/part-of-collection/part-of-collection.mapper.ts create mode 100644 src/app/shared/components/resources/resource-filters/mappers/provider/provider.mapper.ts create mode 100644 src/app/shared/components/resources/resource-filters/mappers/resource-type/resource-type.mapper.ts create mode 100644 src/app/shared/components/resources/resource-filters/mappers/subject/subject.mapper.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/dateCreated/date-created.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/dateCreated/date-index-card.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/filter-labels.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/filter-type.enum.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/funder/funder-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/funder/funder-index-card-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/funder/funder-index-value-search.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/index-card-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/index-value-search.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/institution/institution-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/institution/institution-index-card-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/institution/institution-index-value-search.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/license/license-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/license/license-index-card-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/license/license-index-value-search.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-card-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-value-search.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/provider/provider-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/provider/provider-index-card-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/provider/provider-index-value-search.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/resource-type/resource-type-index-card-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/resource-type/resource-type-index-value-search.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/resource-type/resource-type.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/search-result-count.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/subject/subject-filter-response.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/models/subject/subject-filter.entity.ts create mode 100644 src/app/shared/components/resources/resource-filters/utils/add-filters-params.helper.ts create mode 100644 src/assets/icons/source/code-colored.svg create mode 100644 src/assets/icons/source/data-colored.svg create mode 100644 src/assets/icons/source/materials-colored.svg create mode 100644 src/assets/icons/source/papers-colored.svg create mode 100644 src/assets/icons/source/supplements-colored.svg create mode 100644 src/assets/icons/system/chevron-left.svg create mode 100644 src/assets/icons/system/chevron-right.svg create mode 100644 src/assets/icons/system/first.svg create mode 100644 src/assets/styles/overrides/chip.scss diff --git a/package.json b/package.json index 7ed7471f5..f753a31bd 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "private": true, "dependencies": { + "@angular/animations": "^19.2.0", "@angular/cdk": "^19.2.1", "@angular/cli": "^19.2.0", "@angular/common": "^19.2.0", diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d5f9d31bc..92eefa145 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,6 +1,7 @@ import { Routes } from '@angular/router'; import { provideStates } from '@ngxs/store'; import { ResourceFiltersState } from '@shared/components/resources/resource-filters/store/resource-filters.state'; +import { ResourceFiltersOptionsState } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.state'; export const routes: Routes = [ { @@ -140,7 +141,9 @@ export const routes: Routes = [ import('./features/search/search.component').then( (mod) => mod.SearchComponent, ), - providers: [provideStates([ResourceFiltersState])], + providers: [ + provideStates([ResourceFiltersState, ResourceFiltersOptionsState]), + ], }, ], }, diff --git a/src/app/core/components/header/header.component.html b/src/app/core/components/header/header.component.html index f73896f19..7508423be 100644 --- a/src/app/core/components/header/header.component.html +++ b/src/app/core/components/header/header.component.html @@ -1,2 +1,2 @@ - + {{ authButtonText() }} diff --git a/src/app/core/helpers/ngxs-states.constant.ts b/src/app/core/helpers/ngxs-states.constant.ts index 404b90898..c0d346532 100644 --- a/src/app/core/helpers/ngxs-states.constant.ts +++ b/src/app/core/helpers/ngxs-states.constant.ts @@ -3,6 +3,7 @@ import { TokensState } from '@core/store/settings'; import { AddonsState } from '@core/store/settings/addons'; import { UserState } from '@core/store/user'; import { HomeState } from 'src/app/features/home/store'; +import { SearchState } from '@osf/features/search/store'; export const STATES = [ AuthState, @@ -10,4 +11,5 @@ export const STATES = [ AddonsState, UserState, HomeState, + SearchState, ]; diff --git a/src/app/core/services/json-api/json-api.entity.ts b/src/app/core/services/json-api/json-api.entity.ts index c19e4e5e1..f483ff96a 100644 --- a/src/app/core/services/json-api/json-api.entity.ts +++ b/src/app/core/services/json-api/json-api.entity.ts @@ -3,9 +3,10 @@ export interface JsonApiResponse { included?: Included; } -export interface ApiData { +export interface ApiData { id: string; attributes: Attributes; embeds: Embeds; type: string; + relationships: Relationships; } diff --git a/src/app/features/home/logged-out/home-logged-out.component.spec.ts b/src/app/features/home/logged-out/home-logged-out.component.spec.ts index 7f49c93e9..fb6846f89 100644 --- a/src/app/features/home/logged-out/home-logged-out.component.spec.ts +++ b/src/app/features/home/logged-out/home-logged-out.component.spec.ts @@ -1,17 +1,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { LoggedOutComponent } from './home-logged-out.component'; +import { HomeLoggedOutComponent } from './home-logged-out.component'; describe('LoggedOutComponent', () => { - let component: LoggedOutComponent; - let fixture: ComponentFixture; + let component: HomeLoggedOutComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [LoggedOutComponent], + imports: [HomeLoggedOutComponent], }).compileComponents(); - fixture = TestBed.createComponent(LoggedOutComponent); + fixture = TestBed.createComponent(HomeLoggedOutComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/features/home/models/raw-models/ProjectItem.entity.ts b/src/app/features/home/models/raw-models/ProjectItem.entity.ts index ceab31895..0d0bcfc7d 100644 --- a/src/app/features/home/models/raw-models/ProjectItem.entity.ts +++ b/src/app/features/home/models/raw-models/ProjectItem.entity.ts @@ -13,10 +13,12 @@ export type ProjectItem = ApiData< ApiData< BibliographicContributorUS, { - users: JsonApiResponse, null>; - } + users: JsonApiResponse, null>; + }, + null >[], null >; - } + }, + null >; diff --git a/src/app/features/search/data.ts b/src/app/features/search/data.ts deleted file mode 100644 index fcf48fa5a..000000000 --- a/src/app/features/search/data.ts +++ /dev/null @@ -1,320 +0,0 @@ -// import { Resource } from '@osf/features/search/models/resource.entity'; -// import { ResourceType } from '@osf/features/search/models/resource-type.enum'; -// -// export const resources: Resource[] = [ -// { -// id: 'https://osf.io/wrfgp', -// resourceType: ResourceType.File, -// dateCreated: new Date('2025-01-10'), -// dateModified: new Date('2025-01-14'), -// creators: [ -// { -// id: 'https://osf.io/a6t2x', -// name: 'Lívia Rodrigues de Lima Pires', -// }, -// ], -// fileName: 'dadosAnalisados.rds', -// from: { -// id: 'https://osf.io/e86jf', -// name: 'Tese', -// }, -// }, -// { -// id: 'https://osf.io/4crzf', -// resourceType: ResourceType.Project, -// dateCreated: new Date('2025-01-15'), -// dateModified: new Date('2025-01-18'), -// creators: [ -// { -// id: 'https://osf.io/4fy2t', -// name: 'Jane Simpson', -// }, -// { -// id: 'https://osf.io/5jv47', -// name: 'Wendy Francis', -// }, -// { -// id: 'https://osf.io/6a5yb', -// name: 'Daniel Wadsworth', -// }, -// { -// id: 'https://osf.io/6g7nc', -// name: 'Kristen Tulloch', -// }, -// { -// id: 'https://osf.io/7a3tm', -// name: 'Dr Tamara Sysak', -// }, -// { -// id: 'https://osf.io/b8tvg', -// name: 'PJ Humphreys', -// }, -// { -// id: 'https://osf.io/n2hyv', -// name: 'Alison Craswell', -// }, -// { -// id: 'https://osf.io/qtnc8', -// name: 'Apil Gurung', -// }, -// { -// id: 'https://osf.io/qvuw6', -// name: 'Karen Watson', -// }, -// { -// id: 'https://osf.io/zwu3t', -// name: 'Helen Szabo', -// }, -// ], -// title: -// 'Intergenerational community home sharing for older adults and university students: A scoping review protocol.', -// description: -// 'The objective of this scoping review is to review programs designed to facilitate matches between older adults residing within the community and university students. This protocol provides an overview of the topic, justification for and methods to be implemented in the conduct and reporting of the scoping review.', -// }, -// { -// id: 'https://osf.io/pgmhr', -// resourceType: ResourceType.File, -// dateCreated: new Date('2025-01-17'), -// dateModified: new Date('2025-01-17'), -// creators: [{ id: 'https://osf.io/65nk7', name: 'Aline Miranda Ferreira' }], -// fileName: 'Scoping review protocol.pdf', -// license: 'MIT License', -// from: { -// id: 'https://osf.io/4wks9', -// name: 'Instruments for Measuring Pain, Functionality, and Quality of Life Following Knee Fractures: A Scoping Review', -// }, -// }, -// { -// id: 'https://osf.io/f7h5v', -// resourceType: ResourceType.File, -// dateCreated: new Date('2025-01-29'), -// dateModified: new Date('2025-01-29'), -// creators: [{ id: 'https://osf.io/d7rtw', name: 'David P. Goldenberg' }], -// fileName: 'PCB.zip', -// from: { -// id: 'https://osf.io/qs7r9', -// name: 'Design Files', -// }, -// }, -// { -// id: 'https://osf.io/3ua54', -// resourceType: ResourceType.Project, -// dateCreated: new Date('2025-03-16'), -// dateModified: new Date('2025-03-16'), -// creators: [{ id: 'https://osf.io/uby48', name: 'Aadit Nair Sajeev' }], -// title: -// 'Unemployment of Locals in Kochi due to the Augmented Influx of Migrant Workers', -// description: -// 'This project page includes supplementals like interview transcript for a case study into the unemployment of locals due to increasing influx of migrant workers into the state.', -// }, -// { -// id: 'https://osf.io/xy9qn', -// resourceType: ResourceType.Project, -// dateCreated: new Date('2025-03-16'), -// dateModified: new Date('2025-03-16'), -// creators: [ -// { -// id: 'https://osf.io/x87gf', -// name: 'Doctor Jack Samuel Egerton MSci EngD', -// }, -// ], -// title: 'Stone functions (tetration)', -// }, -// { -// id: 'https://osf.io/cpm64', -// resourceType: ResourceType.Registration, -// dateCreated: new Date('2025-01-10'), -// dateModified: new Date('2025-01-10'), -// creators: [ -// { -// id: 'https://osf.io/4fy2t', -// name: 'Simran Khanna', -// }, -// { -// id: 'https://osf.io/5jv47', -// name: 'Devika Shenoy', -// }, -// { -// id: 'https://osf.io/6a5yb', -// name: 'Steph Hendren', -// }, -// { -// id: 'https://osf.io/6g7nc', -// name: 'Christian Zirbes', -// }, -// { -// id: 'https://osf.io/7a3tm', -// name: 'Anthony Catanzano', -// }, -// { -// id: 'https://osf.io/b8tvg', -// name: 'Rachelle Shao', -// }, -// { -// id: 'https://osf.io/n2hyv', -// name: 'Sofiu Ogunbiyi', -// }, -// { -// id: 'https://osf.io/qtnc8', -// name: 'Evelyn Hunter', -// }, -// { -// id: 'https://osf.io/qvuw6', -// name: 'Katie Radulovacki', -// }, -// { -// id: 'https://osf.io/zwu3t', -// name: 'Muhamed Sanneh', -// }, -// ], -// title: -// 'A Scoping Review and Meta-analysis Exploring the Associations between Socioeconomic Identity and Management of Pediatric Extremity Fracture', -// description: -// 'The incidence and management of extremity fractures in pediatric patients can be influenced by social and financial deprivation. Previous studies have highlighted that the social determinants of health, such as socioeconomic status, race, and insurance type, are independently associated with the incidence of pediatric fractures . [1, 2] This underscores the need to understand how these factors specifically affect fracture management in pediatric populations.\n\nIn addition to incidence, socioeconomic status has been shown to impact the timing and type of treatment for fractures. Vazquez et al. demonstrated that adolescents from poor socioeconomic backgrounds were more likely to experience delayed surgical fixation of femoral fractures, which was associated with worse outcomes, including longer hospital stays and higher healthcare costs.[2] Similarly, Evans et al. reported that children from socially deprived areas had worse perceived function and pain outcomes after upper extremity fractures, even after receiving orthopedic treatment.[3] These findings suggest that social deprivation not only affects initial access to care but also influences recovery and long-term outcomes.\n\nThe proposed scoping review and meta-analysis will systematically evaluate the existing literature to map out the extent and nature of the impact of social and financial deprivation on extremity fracture management in pediatric patients. By including a wide range of sociodemographic variables and outcomes, this review aims to provide a comprehensive understanding of the disparities in fracture care. This will inform future research and policy-making to address these inequities and improve healthcare delivery for socially and economically disadvantaged pediatric populations.\n\n[1] Dy CJ, Lyman S, Do HT, Fabricant PD, Marx RG, Green DW. Socioeconomic factors are associated with frequency of repeat emergency department visits for pediatric closed fractures. J Pediatr Orthop. 2014;34(5):548-551. doi:10.1097/BPO.0000000000000143\n[2] Ramaesh R, Clement ND, Rennie L, Court-Brown C, Gaston MS. Social deprivation as a risk factor for fractures in childhood. Bone Joint J. 2015;97-B(2):240-245. doi:10.1302/0301-620X.97B2.34057\n[3] Vazquez S, Dominguez JF, Jacoby M, et al. Poor socioeconomic status is associated with delayed femoral fracture fixation in adolescent patients. Injury. 2023;54(12):111128. doi:10.1016/j.injury.2023.111128', -// license: 'No license', -// publisher: { -// id: 'https://osf.io/registries/osf', -// name: 'OSF Registries', -// }, -// registrationTemplate: 'Generalized Systematic Review Registration', -// doi: 'https://doi.org/10.17605/OSF.IO/CPM64', -// }, -// { -// id: 'https://osf.io/8tk45', -// resourceType: ResourceType.Registration, -// dateCreated: new Date('2025-03-06'), -// dateModified: new Date('2025-03-06'), -// creators: [ -// { -// id: 'https://osf.io/45kpt', -// name: 'Laura A. King', -// }, -// { -// id: 'https://osf.io/cuqrk', -// name: 'Erica A. Holberg', -// }, -// ], -// title: 'Consequentialist Criteria: Money x Effort (Between)', -// description: -// "This is the first of two studies aimed at testing whether the effect of amount of money raised for a good cause is present in within person comparisons, but null when utilizing a between person design. In previous studies, we have found that success in achieving an agent's morally good aim and high cost to agent for doing the right thing are salient to moral evaluators when the explicit contrast to failure or low cost is made (within person), but not particularly salient to moral evaluation when the explicit contrast is not drawn (between person). In this study, we are interested to determine the relative presence and strength of amount of money raised (a consequentialist criterion for moral evaluation) and effort put forth (a non-consequentialist moral criterion centered on the agent's will) when this is not explicitly contrasted because using a between-person design. In a pilot study, we found no effect of money upon moral goodness. There was an effect of money upon effort for the low condition ($50). We want to see how results are altered if we add an explicit factor for effort. Does amount of money have null effect upon perceived moral goodness? Does effort have a larger effect upon perceived moral goodness than amount of money raised? \n\nParticipants will read scenarios reflecting a 3 (money: extremely high vs. high vs. low) x 3 (effort: high vs. no mention vs. low) design. In all 9 scenarios, participants will rate the moral goodness of a person who raises money for and then runs in a 5K for a good cause, where the conditions vary as described above. They also will answer how likely it is that the target would undertake a similar volunteer commitment in the future.", -// publisher: { -// id: 'https://osf.io/registries/osf', -// name: 'OSF Registries', -// }, -// registrationTemplate: 'OSF Preregistration', -// license: 'No license', -// doi: 'https://doi.org/10.17605/OSF.IO/8TK45', -// }, -// { -// id: 'https://osf.io/wa6yf', -// resourceType: ResourceType.File, -// dateCreated: new Date('2025-01-14'), -// dateModified: new Date('2025-01-14'), -// creators: [ -// { -// id: 'https://osf.io/6nkxv', -// name: 'Kari-Anne B. Næss', -// }, -// { -// id: 'https://osf.io/b2g9q', -// name: 'Frida Johanne Holmen ', -// }, -// { -// id: 'https://osf.io/j6hd5', -// name: 'Thormod Idsøe', -// }, -// ], -// from: { -// id: 'https://osf.io/tbzv6', -// name: 'A Cross-sectional Study of Inference-making Comparing First- and Second Language Learners in Early Childhood Education and Care', -// }, -// fileName: 'Model_FH_prereg_140125.pdf', -// license: 'No license', -// }, -// { -// id: 'https://osf.io/4hg87', -// resourceType: ResourceType.ProjectComponent, -// dateCreated: new Date('2025-01-04'), -// dateModified: new Date('2025-01-04'), -// creators: [ -// { -// id: 'https://osf.io/2x5kc', -// name: 'Bowen Wang-Kildegaard', -// }, -// ], -// title: 'Dataset and Codes', -// }, -// { -// id: 'https://osf.io/87vyr', -// resourceType: ResourceType.Preprint, -// dateCreated: new Date('2025-02-20'), -// dateModified: new Date('2025-02-20'), -// creators: [ -// { -// id: 'https://osf.io/2x5kc', -// name: 'Eric L. Olson', -// }, -// ], -// title: 'Evaluative Judgment Across Domains', -// description: -// 'Keberadaan suatu organisme pada suatu tempat dipengaruhi oleh faktor lingkungan dan makanan. Ketersediaan makanan dengan kualitas yang cocok dan kuantitas yang ukup bagi suatu organisme akan meningkatkan populasi cepat. Sebaliknya jika keadaan tersebut tidak mendukung maka akan dipastikan bahwa organisme tersebut akan menurun. Sedangkan faktor abiotik meliputi suhu, kelembaban, cahaya, curah hujan, dan angin. Suhu mempengaruhi aktivitas serangga serta perkembangannya. Serangga juga tertarik pada gelombang cahaya tertentu. Serangga ada yang menerima intensitas cahaya yang tinggi dan aktif pada siang hari (diurnal) dan serangga ada yang aktif menerima intensitas cahaya rendah pada malam hari (nokturnal). Metode yang digunakan yaitu metode Light trap. Hail yang didapatkan bahwa komponen lingkungan (biotik dan abiotik) akan mempengaruhi kelimpahan dan keanekaragaman spesies pada suatu tempat sehingga tingginya kelimpahan individu tiap jenis dapat dipakai untuk menilai kualitas suatu habitat. Sehingga tidak semua serangga yang aktif pada siang hari tidak dapat aktif di malam hari karena efek adanya sinar tergantung sepenuhnya pada kondisi temperature dan kelembaban disekitar.', -// provider: { -// id: 'https://osf.io/preprints/osf', -// name: 'Open Science Framework', -// }, -// conflictOfInterestResponse: 'Author asserted no Conflict of Interest', -// license: 'No License', -// doi: 'https://doi.org/10.31227/osf.io/fcs5r', -// }, -// { -// id: 'https://osf.io/87vyr', -// resourceType: ResourceType.Preprint, -// dateCreated: new Date('2025-02-20'), -// dateModified: new Date('2025-02-20'), -// creators: [ -// { -// id: 'https://osf.io/2x5kc', -// name: 'Fitha Kaamiliyaa Hamka', -// }, -// ], -// title: -// 'Identifikasi Serangga Nokturnal di Bukit Samata Kabupaten Gowa, Sulawesi Selatan', -// description: -// 'Keberadaan suatu organisme pada suatu tempat dipengaruhi oleh faktor lingkungan dan makanan. Ketersediaan makanan dengan kualitas yang cocok dan kuantitas yang ukup bagi suatu organisme akan meningkatkan populasi cepat. Sebaliknya jika keadaan tersebut tidak mendukung maka akan dipastikan bahwa organisme tersebut akan menurun. Sedangkan faktor abiotik meliputi suhu, kelembaban, cahaya, curah hujan, dan angin. Suhu mempengaruhi aktivitas serangga serta perkembangannya. Serangga juga tertarik pada gelombang cahaya tertentu. Serangga ada yang menerima intensitas cahaya yang tinggi dan aktif pada siang hari (diurnal) dan serangga ada yang aktif menerima intensitas cahaya rendah pada malam hari (nokturnal). Metode yang digunakan yaitu metode Light trap. Hail yang didapatkan bahwa komponen lingkungan (biotik dan abiotik) akan mempengaruhi kelimpahan dan keanekaragaman spesies pada suatu tempat sehingga tingginya kelimpahan individu tiap jenis dapat dipakai untuk menilai kualitas suatu habitat. Sehingga tidak semua serangga yang aktif pada siang hari tidak dapat aktif di malam hari karena efek adanya sinar tergantung sepenuhnya pada kondisi temperature dan kelembaban disekitar.', -// provider: { -// id: 'https://osf.io/preprints/osf', -// name: 'Open Science Framework', -// }, -// conflictOfInterestResponse: 'Author asserted no Conflict of Interest', -// license: 'No License', -// doi: 'https://doi.org/10.31234/osf.io/7mgkd', -// }, -// { -// id: 'https://osf.io/cegxv', -// resourceType: ResourceType.User, -// title: 'Amelia Jamison', -// publicProjects: 2, -// publicRegistrations: 0, -// publicPreprints: 0, -// }, -// { -// id: 'https://osf.io/cegxv', -// resourceType: ResourceType.User, -// title: 'Cristal Alvarez', -// publicProjects: 2, -// publicRegistrations: 0, -// publicPreprints: 0, -// orcid: 'https://orcid.org/0000-0003-2697-7146', -// }, -// { -// id: 'https://osf.io/cegxv', -// resourceType: ResourceType.User, -// title: 'Rohini Ganjoo', -// publicProjects: 16, -// publicRegistrations: 6, -// publicPreprints: 3, -// orcid: 'https://orcid.org/0000-0003-2697-7146', -// employment: 'University of Turku', -// education: 'University of Turku', -// }, -// ]; diff --git a/src/app/features/search/mappers/search.mapper.ts b/src/app/features/search/mappers/search.mapper.ts index 08318275b..7af9bb17b 100644 --- a/src/app/features/search/mappers/search.mapper.ts +++ b/src/app/features/search/mappers/search.mapper.ts @@ -41,12 +41,10 @@ export function MapResources(rawItem: ResourceItem): Resource { registrationTemplate: rawItem?.conformsTo?.[0]?.title?.[0]?.['@value'], doi: rawItem?.identifier?.[0]?.['@value'], conflictOfInterestResponse: rawItem?.statedConflictOfInterest?.[0]?.['@id'], + hasDataResource: !!rawItem?.hasDataResource, + hasAnalyticCodeResource: !!rawItem?.hasAnalyticCodeResource, + hasMaterialsResource: !!rawItem?.hasMaterialsResource, + hasPapersResource: !!rawItem?.hasPapersResource, + hasSupplementalResource: !!rawItem?.hasSupplementalResource, }; } - -// publicProjects?: number; -// publicRegistrations?: number; -// publicPreprints?: number; -// orcid?: string; -// employment?: string; -// education?: string; diff --git a/src/app/features/search/models/raw-models/index-card-search.model.ts b/src/app/features/search/models/raw-models/index-card-search.model.ts new file mode 100644 index 000000000..fcc18ad48 --- /dev/null +++ b/src/app/features/search/models/raw-models/index-card-search.model.ts @@ -0,0 +1,27 @@ +import { + ApiData, + JsonApiResponse, +} from '@core/services/json-api/json-api.entity'; +import { ResourceItem } from '@osf/features/search/models/raw-models/resource-response.model'; + +export type IndexCardSearch = JsonApiResponse< + { + attributes: { totalResultCount: number }; + relationships: { + searchResultPage: { + links: { + first: { + href: string; + }; + next: { + href: string; + }; + prev: { + href: string; + }; + }; + }; + }; + }, + ApiData<{ resourceMetadata: ResourceItem }, null, null>[] +>; diff --git a/src/app/features/search/models/raw-models/resource-response.model.ts b/src/app/features/search/models/raw-models/resource-response.model.ts index 1b662be1a..c7ef5db34 100644 --- a/src/app/features/search/models/raw-models/resource-response.model.ts +++ b/src/app/features/search/models/raw-models/resource-response.model.ts @@ -4,7 +4,7 @@ export interface ResourceItem { '@id': string; accessService: MetadataField[]; affiliation: MetadataField[]; - creator: Creator[]; + creator: ResourceCreator[]; conformsTo: ConformsTo[]; dateCopyrighted: { '@value': string }[]; dateCreated: { '@value': string }[]; @@ -26,9 +26,14 @@ export interface ResourceItem { isPartOfCollection: IsPartOfCollection[]; rights: MetadataField[]; statedConflictOfInterest: { '@id': string }[]; + hasDataResource: MetadataField[]; + hasAnalyticCodeResource: MetadataField[]; + hasMaterialsResource: MetadataField[]; + hasPapersResource: MetadataField[]; + hasSupplementalResource: MetadataField[]; } -export interface Creator extends MetadataField { +export interface ResourceCreator extends MetadataField { affiliation: MetadataField[]; sameAs: { '@id': string }[]; } @@ -43,7 +48,7 @@ export interface QualifiedAttribution { } export interface isPartOf extends MetadataField { - creator: Creator[]; + creator: ResourceCreator[]; dateCopyright: { '@value': string }[]; dateCreated: { '@value': string }[]; publisher: MetadataField[]; diff --git a/src/app/features/search/models/resource.entity.ts b/src/app/features/search/models/resource.entity.ts index fb8c6012d..b193d3c1e 100644 --- a/src/app/features/search/models/resource.entity.ts +++ b/src/app/features/search/models/resource.entity.ts @@ -22,4 +22,9 @@ export interface Resource { orcid?: string; employment?: string; education?: string; + hasDataResource: boolean; + hasAnalyticCodeResource: boolean; + hasMaterialsResource: boolean; + hasPapersResource: boolean; + hasSupplementalResource: boolean; } diff --git a/src/app/features/search/models/resources-data.entity.ts b/src/app/features/search/models/resources-data.entity.ts new file mode 100644 index 000000000..643db2a59 --- /dev/null +++ b/src/app/features/search/models/resources-data.entity.ts @@ -0,0 +1,9 @@ +import { Resource } from '@osf/features/search/models/resource.entity'; + +export interface ResourcesData { + resources: Resource[]; + count: number; + first: string; + next: string; + previous: string; +} diff --git a/src/app/features/search/search.component.html b/src/app/features/search/search.component.html index 5f7823d4f..21987bc9a 100644 --- a/src/app/features/search/search.component.html +++ b/src/app/features/search/search.component.html @@ -2,8 +2,13 @@ better-research
-
+
- - - - - - - - - - - - - - + > - - - - - + > - - - - - - + +
+ + + @if (currentStep === 1) { +
+

Improved OSF Search

+

+ Enter any term in the search box and filter by specific object types. + More information is available on our help guides. +

+
+

1 of 3

+
+ + Next +
+
+
+ } + + @if (currentStep === 2) { +
+

Refine Your Search

+

+ Narrow the source, discipline, and more. For example, find content + supported by a specific funder or view only datasets. +

+
+

2 of 3

+
+ + Next +
+
+
+ } + + @if (currentStep === 3) { +
+

Add Metadata

+

+ Remember to add metadata and resources to your own work on OSF to make + it more discoverable! Learn more in our help guides. +

+
+

3 of 3

+
+ Done +
+
+
+ } +
diff --git a/src/app/features/search/search.component.scss b/src/app/features/search/search.component.scss index 7d99fd2e6..9d28d34d9 100644 --- a/src/app/features/search/search.component.scss +++ b/src/app/features/search/search.component.scss @@ -12,4 +12,40 @@ z-index: 1; } } + + .resources { + position: relative; + background: white; + padding: 2rem; + } + + .stepper { + position: absolute; + padding: 1.7rem; + width: 32rem; + display: flex; + flex-direction: column; + row-gap: 1.7rem; + background: white; + border: 1px solid var.$grey-2; + border-radius: 12px; + h3 { + font-size: 1.3rem; + } + } + + .first-stepper { + top: 2rem; + left: 1.7rem; + } + + .second-stepper { + top: calc(2rem + 42px); + left: calc(1.5rem + 30%); + } + + .third-stepper { + top: calc(5rem + 42px); + left: calc(0.4rem + 30%); + } } diff --git a/src/app/features/search/search.component.ts b/src/app/features/search/search.component.ts index ce172bf34..a82436ed4 100644 --- a/src/app/features/search/search.component.ts +++ b/src/app/features/search/search.component.ts @@ -3,15 +3,15 @@ import { Component, effect, inject, - OnInit, signal, + untracked, } from '@angular/core'; import { SearchInputComponent } from '@shared/components/search-input/search-input.component'; import { DropdownModule } from 'primeng/dropdown'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'primeng/tabs'; import { NgOptimizedImage } from '@angular/common'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; import { AutoCompleteModule } from 'primeng/autocomplete'; import { AccordionModule } from 'primeng/accordion'; @@ -20,8 +20,16 @@ import { DataViewModule } from 'primeng/dataview'; import { ResourcesComponent } from '@shared/components/resources/resources.component'; import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; import { Store } from '@ngxs/store'; -import { GetResources, SearchSelectors } from '@osf/features/search/store'; +import { + GetResources, + SearchSelectors, + SetResourceTab, + SetSearchText, +} from '@osf/features/search/store'; import { ResourceFiltersSelectors } from '@shared/components/resources/resource-filters/store'; +import { debounceTime } from 'rxjs'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { Button } from 'primeng/button'; @Component({ selector: 'osf-search', @@ -41,38 +49,107 @@ import { ResourceFiltersSelectors } from '@shared/components/resources/resource- TableModule, DataViewModule, ResourcesComponent, + Button, ], templateUrl: './search.component.html', styleUrl: './search.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SearchComponent implements OnInit { +export class SearchComponent { readonly #store = inject(Store); protected searchValue = signal(''); protected readonly isMobile = toSignal(inject(IS_XSMALL)); - protected readonly resources = this.#store.selectSignal( - SearchSelectors.getResources, - ); + protected readonly creatorsFilter = this.#store.selectSignal( ResourceFiltersSelectors.getCreator, ); + protected readonly dateCreatedFilter = this.#store.selectSignal( + ResourceFiltersSelectors.getDateCreated, + ); + protected readonly funderFilter = this.#store.selectSignal( + ResourceFiltersSelectors.getFunder, + ); + protected readonly subjectFilter = this.#store.selectSignal( + ResourceFiltersSelectors.getSubject, + ); + protected readonly licenseFilter = this.#store.selectSignal( + ResourceFiltersSelectors.getLicense, + ); + protected readonly resourceTypeFilter = this.#store.selectSignal( + ResourceFiltersSelectors.getResourceType, + ); + protected readonly institutionFilter = this.#store.selectSignal( + ResourceFiltersSelectors.getInstitution, + ); + protected readonly providerFilter = this.#store.selectSignal( + ResourceFiltersSelectors.getProvider, + ); + protected readonly partOfCollectionFilter = this.#store.selectSignal( + ResourceFiltersSelectors.getPartOfCollection, + ); + protected searchStoreValue = this.#store.selectSignal( + SearchSelectors.getSearchText, + ); + protected resourcesTabStoreValue = this.#store.selectSignal( + SearchSelectors.getResourceTab, + ); + protected sortByStoreValue = this.#store.selectSignal( + SearchSelectors.getSortBy, + ); - protected selectedTab = 0; + protected selectedTab: ResourceTab = ResourceTab.All; protected readonly ResourceTab = ResourceTab; + protected currentStep = 0; constructor() { effect(() => { this.creatorsFilter(); + this.dateCreatedFilter(); + this.funderFilter(); + this.subjectFilter(); + this.licenseFilter(); + this.resourceTypeFilter(); + this.institutionFilter(); + this.providerFilter(); + this.partOfCollectionFilter(); + this.searchStoreValue(); + this.resourcesTabStoreValue(); + this.sortByStoreValue(); this.#store.dispatch(GetResources); }); - } - ngOnInit() { - this.#store.dispatch(GetResources); + // put search value in store and update resources, filters + toObservable(this.searchValue) + .pipe(debounceTime(500)) + .subscribe((searchText) => { + this.#store.dispatch(new SetSearchText(searchText)); + this.#store.dispatch(GetAllOptions); + }); + + // sync search with query parameters if search is empty and parameters are not + effect(() => { + const storeValue = this.searchStoreValue(); + const currentInput = untracked(() => this.searchValue()); + + if (storeValue && currentInput !== storeValue) { + this.searchValue.set(storeValue); + } + }); + + // sync resource tabs with query parameters + effect(() => { + if ( + !this.selectedTab && + this.selectedTab !== this.resourcesTabStoreValue() + ) { + this.selectedTab = this.resourcesTabStoreValue(); + } + }); } - onTabChange(index: number): void { + onTabChange(index: ResourceTab): void { + this.#store.dispatch(new SetResourceTab(index)); this.selectedTab = index; } } diff --git a/src/app/features/search/search.service.ts b/src/app/features/search/search.service.ts index b7c5e9b68..c7e8bb462 100644 --- a/src/app/features/search/search.service.ts +++ b/src/app/features/search/search.service.ts @@ -2,13 +2,9 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@core/services/json-api/json-api.service'; import { map, Observable } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { ResourceItem } from '@osf/features/search/models/raw-models/resource-response.model'; -import { - ApiData, - JsonApiResponse, -} from '@core/services/json-api/json-api.entity'; import { MapResources } from '@osf/features/search/mappers/search.mapper'; -import { Resource } from '@osf/features/search/models/resource.entity'; +import { IndexCardSearch } from '@osf/features/search/models/raw-models/index-card-search.model'; +import { ResourcesData } from '@osf/features/search/models/resources-data.entity'; @Injectable({ providedIn: 'root', @@ -16,41 +12,72 @@ import { Resource } from '@osf/features/search/models/resource.entity'; export class SearchService { #jsonApiService = inject(JsonApiService); - getResources(filters: Record): Observable { + getResources( + filters: Record, + searchText: string, + sortBy: string, + resourceType: string, + ): Observable { const params: Record = { - 'cardSearchFilter[resourceType]': - 'Registration,RegistrationComponent,Project,ProjectComponent,Preprint,Agent,File', + 'cardSearchFilter[resourceType]': resourceType ?? '', 'cardSearchFilter[accessService]': 'https://staging4.osf.io/', - 'cardSearchText[*,creator.name,isContainedBy.creator.name]': '', - 'page[size]': '20', - sort: '-relevance', + 'cardSearchText[*,creator.name,isContainedBy.creator.name]': + searchText ?? '', + 'page[size]': '10', + sort: sortBy, ...filters, }; return this.#jsonApiService - .get< - JsonApiResponse< - null, - ApiData<{ resourceMetadata: ResourceItem }, null>[] - > - >(`${environment.shareDomainUrl}/index-card-search`, params) + .get( + `${environment.shareDomainUrl}/index-card-search`, + params, + ) .pipe( - map((response) => - response.included - .filter((item) => item.type === 'index-card') - .map((item) => MapResources(item.attributes.resourceMetadata)), - ), + map((response) => { + if (response?.included) { + return { + resources: response?.included + .filter((item) => item.type === 'index-card') + .map((item) => MapResources(item.attributes.resourceMetadata)), + count: response.data.attributes.totalResultCount, + first: + response.data?.relationships?.searchResultPage?.links?.first + ?.href, + next: response.data?.relationships?.searchResultPage?.links?.next + ?.href, + previous: + response.data?.relationships?.searchResultPage?.links?.prev + ?.href, + }; + } + + return {} as ResourcesData; + }), ); } - // getValueSearch(): Observable { - // const params = { - // 'cardSearchFilter[resourceType]': 'Registration,RegistrationComponent,Project,ProjectComponent,Preprint,Agent,File', - // 'cardSearchFilter[accessService]': 'https://staging4.osf.io/', - // 'cardSearchText[*,creator.name,isContainedBy.creator.name]': '', - // sort: '-relevance', - // }; - // - // return - // } + getResourcesByLink(link: string): Observable { + return this.#jsonApiService.get(link).pipe( + map((response) => { + if (response?.included) { + return { + resources: response.included + .filter((item) => item.type === 'index-card') + .map((item) => MapResources(item.attributes.resourceMetadata)), + count: response.data.attributes.totalResultCount, + first: + response.data?.relationships?.searchResultPage?.links?.first + ?.href, + next: response.data?.relationships?.searchResultPage?.links?.next + ?.href, + previous: + response.data?.relationships?.searchResultPage?.links?.prev?.href, + }; + } + + return {} as ResourcesData; + }), + ); + } } diff --git a/src/app/features/search/store/search.actions.ts b/src/app/features/search/store/search.actions.ts index 2fc54668c..bf55a3603 100644 --- a/src/app/features/search/store/search.actions.ts +++ b/src/app/features/search/store/search.actions.ts @@ -1,3 +1,33 @@ +import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; + export class GetResources { static readonly type = '[Search] Get Resources'; } + +export class GetResourcesByLink { + static readonly type = '[Search] Get Resources By Link'; + + constructor(public link: string) {} +} + +export class GetResourcesCount { + static readonly type = '[Search] Get Resources Count'; +} + +export class SetSearchText { + static readonly type = '[Search] Set Search Text'; + + constructor(public searchText: string) {} +} + +export class SetSortBy { + static readonly type = '[Search] Set SortBy'; + + constructor(public sortBy: string) {} +} + +export class SetResourceTab { + static readonly type = '[Search] Set Resource Tab'; + + constructor(public resourceTab: ResourceTab) {} +} diff --git a/src/app/features/search/store/search.model.ts b/src/app/features/search/store/search.model.ts index 458dabfa6..f8c128bde 100644 --- a/src/app/features/search/store/search.model.ts +++ b/src/app/features/search/store/search.model.ts @@ -1,5 +1,13 @@ import { Resource } from '@osf/features/search/models/resource.entity'; +import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; export interface SearchStateModel { resources: Resource[]; + resourcesCount: number; + searchText: string; + sortBy: string; + resourceTab: ResourceTab; + first: string; + next: string; + previous: string; } diff --git a/src/app/features/search/store/search.selectors.ts b/src/app/features/search/store/search.selectors.ts index 11bc27530..929ade1b8 100644 --- a/src/app/features/search/store/search.selectors.ts +++ b/src/app/features/search/store/search.selectors.ts @@ -2,10 +2,46 @@ import { SearchState } from '@osf/features/search/store/search.state'; import { Selector } from '@ngxs/store'; import { SearchStateModel } from '@osf/features/search/store/search.model'; import { Resource } from '@osf/features/search/models/resource.entity'; +import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; export class SearchSelectors { @Selector([SearchState]) static getResources(state: SearchStateModel): Resource[] { return state.resources; } + + @Selector([SearchState]) + static getResourcesCount(state: SearchStateModel): number { + return state.resourcesCount; + } + + @Selector([SearchState]) + static getSearchText(state: SearchStateModel): string { + return state.searchText; + } + + @Selector([SearchState]) + static getSortBy(state: SearchStateModel): string { + return state.sortBy; + } + + @Selector([SearchState]) + static getResourceTab(state: SearchStateModel): ResourceTab { + return state.resourceTab; + } + + @Selector([SearchState]) + static getFirst(state: SearchStateModel): string { + return state.first; + } + + @Selector([SearchState]) + static getNext(state: SearchStateModel): string { + return state.next; + } + + @Selector([SearchState]) + static getPrevious(state: SearchStateModel): string { + return state.previous; + } } diff --git a/src/app/features/search/store/search.state.ts b/src/app/features/search/store/search.state.ts index 216c05568..db69e3546 100644 --- a/src/app/features/search/store/search.state.ts +++ b/src/app/features/search/store/search.state.ts @@ -2,15 +2,32 @@ import { inject, Injectable } from '@angular/core'; import { SearchService } from '@osf/features/search/search.service'; import { SearchStateModel } from '@osf/features/search/store/search.model'; import { Action, State, StateContext, Store } from '@ngxs/store'; -import { GetResources } from '@osf/features/search/store/search.actions'; +import { + GetResources, + GetResourcesByLink, + SetResourceTab, + SetSearchText, + SetSortBy, +} from '@osf/features/search/store/search.actions'; import { tap } from 'rxjs'; import { ResourceFiltersSelectors } from '@shared/components/resources/resource-filters/store'; +import { addFiltersParams } from '@shared/components/resources/resource-filters/utils/add-filters-params.helper'; +import { SearchSelectors } from '@osf/features/search/store/search.selectors'; +import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; +import { getResourceTypes } from '@osf/features/search/utils/helpers/get-resource-types.helper'; @Injectable() @State({ name: 'search', defaults: { resources: [], + resourcesCount: 0, + searchText: '', + sortBy: '-relevance', + resourceTab: ResourceTab.All, + first: '', + next: '', + previous: '', }, }) export class SearchState { @@ -19,19 +36,58 @@ export class SearchState { @Action(GetResources) getResources(ctx: StateContext) { - const creatorParams: Record = {}; - - const creatorFilters = this.store.selectSignal( - ResourceFiltersSelectors.getCreator, + const filters = this.store.selectSnapshot( + ResourceFiltersSelectors.getAllFilters, + ); + const filtersParams = addFiltersParams(filters); + const searchText = this.store.selectSnapshot(SearchSelectors.getSearchText); + const sortBy = this.store.selectSnapshot(SearchSelectors.getSortBy); + const resourceTab = this.store.selectSnapshot( + SearchSelectors.getResourceTab, ); - if (creatorFilters()) { - creatorParams['cardSearchFilter[creator][]'] = creatorFilters(); - } + const resourceTypes = getResourceTypes(resourceTab); - return this.searchService.getResources(creatorParams).pipe( - tap((resources) => { - ctx.patchState({ resources: resources }); + return this.searchService + .getResources(filtersParams, searchText, sortBy, resourceTypes) + .pipe( + tap((response) => { + ctx.patchState({ resources: response.resources }); + ctx.patchState({ resourcesCount: response.count }); + ctx.patchState({ first: response.first }); + ctx.patchState({ next: response.next }); + ctx.patchState({ previous: response.previous }); + }), + ); + } + + @Action(GetResourcesByLink) + getResourcesByLink( + ctx: StateContext, + action: GetResourcesByLink, + ) { + return this.searchService.getResourcesByLink(action.link).pipe( + tap((response) => { + ctx.patchState({ resources: response.resources }); + ctx.patchState({ resourcesCount: response.count }); + ctx.patchState({ first: response.first }); + ctx.patchState({ next: response.next }); + ctx.patchState({ previous: response.previous }); }), ); } + + @Action(SetSearchText) + setSearchText(ctx: StateContext, action: SetSearchText) { + ctx.patchState({ searchText: action.searchText }); + } + + @Action(SetSortBy) + setSortBy(ctx: StateContext, action: SetSortBy) { + ctx.patchState({ sortBy: action.sortBy }); + } + + @Action(SetResourceTab) + setResourceTab(ctx: StateContext, action: SetResourceTab) { + ctx.patchState({ resourceTab: action.resourceTab }); + } } diff --git a/src/app/features/search/utils/helpers/get-resource-types.helper.ts b/src/app/features/search/utils/helpers/get-resource-types.helper.ts new file mode 100644 index 000000000..32e048eb0 --- /dev/null +++ b/src/app/features/search/utils/helpers/get-resource-types.helper.ts @@ -0,0 +1,18 @@ +import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; + +export function getResourceTypes(resourceTab: ResourceTab): string { + switch (resourceTab) { + case ResourceTab.Projects: + return 'Project,ProjectComponent'; + case ResourceTab.Registrations: + return 'Registration,RegistrationComponent'; + case ResourceTab.Preprints: + return 'Preprint'; + case ResourceTab.Files: + return 'File'; + case ResourceTab.Users: + return 'Agent'; + default: + return 'Registration,RegistrationComponent,Project,ProjectComponent,Preprint,Agent,File'; + } +} diff --git a/src/app/shared/components/resources/filter-chips/filter-chips.component.html b/src/app/shared/components/resources/filter-chips/filter-chips.component.html new file mode 100644 index 000000000..9e794fe9e --- /dev/null +++ b/src/app/shared/components/resources/filter-chips/filter-chips.component.html @@ -0,0 +1,141 @@ +@if (filters().creator.value) { + @let creator = filters().creator.filterName + ": " + filters().creator.label; + + + + + +} + +@if (filters().dateCreated.value) { + @let dateCreated = + filters().dateCreated.filterName + ": " + filters().dateCreated.label; + + + + + +} + +@if (filters().funder.value) { + @let funder = filters().funder.filterName + ": " + filters().funder.label; + + + + + +} + +@if (filters().subject.value) { + @let subject = filters().subject.filterName + ": " + filters().subject.label; + + + + + +} + +@if (filters().license.value) { + @let license = filters().license.filterName + ": " + filters().license.label; + + + + + +} + +@if (filters().resourceType.value) { + @let resourceType = + filters().resourceType.filterName + ": " + filters().resourceType.label; + + + + + +} + +@if (filters().institution.value) { + @let institution = + filters().institution.filterName + ": " + filters().institution.label; + + + + + +} + +@if (filters().provider.value) { + @let provider = + filters().provider.filterName + ": " + filters().provider.label; + + + + + +} + +@if (filters().partOfCollection.value) { + @let partOfCollection = + filters().partOfCollection.filterName + + ": " + + filters().partOfCollection.label; + + + + + +} diff --git a/src/app/shared/components/resources/filter-chips/filter-chips.component.scss b/src/app/shared/components/resources/filter-chips/filter-chips.component.scss new file mode 100644 index 000000000..442a7a495 --- /dev/null +++ b/src/app/shared/components/resources/filter-chips/filter-chips.component.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + flex-direction: column; + row-gap: 0.4rem; +} diff --git a/src/app/shared/components/resources/filter-chips/filter-chips.component.spec.ts b/src/app/shared/components/resources/filter-chips/filter-chips.component.spec.ts new file mode 100644 index 000000000..2c6f477c2 --- /dev/null +++ b/src/app/shared/components/resources/filter-chips/filter-chips.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterChipsComponent } from './filter-chips.component'; + +describe('FilterChipsComponent', () => { + let component: FilterChipsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FilterChipsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FilterChipsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resources/filter-chips/filter-chips.component.ts b/src/app/shared/components/resources/filter-chips/filter-chips.component.ts new file mode 100644 index 000000000..953c9ac3b --- /dev/null +++ b/src/app/shared/components/resources/filter-chips/filter-chips.component.ts @@ -0,0 +1,68 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { + ResourceFiltersSelectors, + SetCreator, + SetDateCreated, + SetFunder, + SetInstitution, + SetLicense, + SetPartOfCollection, + SetProvider, + SetResourceType, + SetSubject, +} from '@shared/components/resources/resource-filters/store'; +import { Chip } from 'primeng/chip'; +import { PrimeTemplate } from 'primeng/api'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { FilterType } from '@shared/components/resources/resource-filters/models/filter-type.enum'; + +@Component({ + selector: 'osf-filter-chips', + imports: [Chip, PrimeTemplate], + templateUrl: './filter-chips.component.html', + styleUrl: './filter-chips.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FilterChipsComponent { + readonly #store = inject(Store); + + protected filters = this.#store.selectSignal( + ResourceFiltersSelectors.getAllFilters, + ); + + clearFilter(filter: FilterType) { + switch (filter) { + case FilterType.Creator: + this.#store.dispatch(new SetCreator('', '')); + break; + case FilterType.DateCreated: + this.#store.dispatch(new SetDateCreated('')); + break; + case FilterType.Funder: + this.#store.dispatch(new SetFunder('', '')); + break; + case FilterType.Subject: + this.#store.dispatch(new SetSubject('', '')); + break; + case FilterType.License: + this.#store.dispatch(new SetLicense('', '')); + break; + case FilterType.ResourceType: + this.#store.dispatch(new SetResourceType('', '')); + break; + case FilterType.Institution: + this.#store.dispatch(new SetInstitution('', '')); + break; + case FilterType.Provider: + this.#store.dispatch(new SetProvider('', '')); + break; + case FilterType.PartOfCollection: + this.#store.dispatch(new SetPartOfCollection('', '')); + break; + } + this.#store.dispatch(GetAllOptions); + } + + protected readonly FilterType = FilterType; +} diff --git a/src/app/shared/components/resources/resource-card/mappers/user-counts.mapper.ts b/src/app/shared/components/resources/resource-card/mappers/user-counts.mapper.ts new file mode 100644 index 000000000..714fc8475 --- /dev/null +++ b/src/app/shared/components/resources/resource-card/mappers/user-counts.mapper.ts @@ -0,0 +1,16 @@ +import { UserCountsResponse } from '@shared/components/resources/resource-card/models/user-counts-response.entity'; +import { UserRelatedDataCounts } from '@shared/components/resources/resource-card/models/user-related-data-counts.entity'; + +export function MapUserCounts( + response: UserCountsResponse, +): UserRelatedDataCounts { + return { + projects: response.data?.relationships?.nodes?.links?.related?.meta?.count, + registrations: + response.data?.relationships?.registrations?.links?.related?.meta?.count, + preprints: + response.data?.relationships?.preprints?.links?.related?.meta?.count, + employment: response.data?.attributes?.employment?.[0]?.institution, + education: response.data?.attributes?.education?.[0]?.institution, + }; +} diff --git a/src/app/shared/components/resources/resource-card/models/user-counts-response.entity.ts b/src/app/shared/components/resources/resource-card/models/user-counts-response.entity.ts new file mode 100644 index 000000000..c8eff45b4 --- /dev/null +++ b/src/app/shared/components/resources/resource-card/models/user-counts-response.entity.ts @@ -0,0 +1,44 @@ +import { + ApiData, + JsonApiResponse, +} from '@core/services/json-api/json-api.entity'; + +export type UserCountsResponse = JsonApiResponse< + ApiData< + { + employment: { institution: string }[]; + education: { institution: string }[]; + }, + null, + { + registrations: { + links: { + related: { + meta: { + count: number; + }; + }; + }; + }; + preprints: { + links: { + related: { + meta: { + count: number; + }; + }; + }; + }; + nodes: { + links: { + related: { + meta: { + count: number; + }; + }; + }; + }; + } + >, + null +>; diff --git a/src/app/shared/components/resources/resource-card/models/user-related-data-counts.entity.ts b/src/app/shared/components/resources/resource-card/models/user-related-data-counts.entity.ts new file mode 100644 index 000000000..8a77d9954 --- /dev/null +++ b/src/app/shared/components/resources/resource-card/models/user-related-data-counts.entity.ts @@ -0,0 +1,7 @@ +export interface UserRelatedDataCounts { + projects: number; + registrations: number; + preprints: number; + employment?: string; + education?: string; +} diff --git a/src/app/shared/components/resources/resource-card/resource-card.component.html b/src/app/shared/components/resources/resource-card/resource-card.component.html index b6e5d955f..d7d9feca1 100644 --- a/src/app/shared/components/resources/resource-card/resource-card.component.html +++ b/src/app/shared/components/resources/resource-card/resource-card.component.html @@ -1,22 +1,26 @@
- +
- @if (item().resourceType === ResourceType.Agent) { + @if ( + item()?.resourceType && item()?.resourceType === ResourceType.Agent + ) {

User

- } @else { -

{{ ResourceType[item().resourceType] }}

+ } @else if (item()?.resourceType) { +

{{ ResourceType[item()?.resourceType!] }}

}
- @if (item().resourceType === ResourceType.File) { - {{ item().fileName }} - } @else { - {{ item().title }} + @if ( + item()?.resourceType === ResourceType.File && item()?.fileName + ) { + {{ item()?.fileName }} + } @else if (item()?.title && item()?.title) { + {{ item()?.title }} } - @if (item().orcid) { - + @if (item()?.orcid) { + - @if (item().creators?.length) { + @if (item()?.creators?.length) {
@for ( - creator of item().creators!.slice(0, 4); + creator of item()?.creators!.slice(0, 4); track creator.id; let i = $index ) { {{ creator.name }} - @if (i < item().creators!.length - 1 && i < 3) { + @if (i < (item()?.creators)!.length - 1 && i < 3) { , } } - @if (item().creators!.length > 4) { + @if ((item()?.creators)!.length > 4) {

-  and {{ item().creators!.length - 4 }} more +  and {{ (item()?.creators)!.length - 4 }} more

}
} - @if (item().from?.id) { + @if (item()?.from?.id && item()?.from?.name) { } - @if (item().dateCreated && item().dateModified) { + @if (item()?.dateCreated && item()?.dateModified) {

- Date created: {{ item().dateCreated | date: "MMMM d, y" }} | Date - modified: {{ item().dateModified | date: "MMMM d, y" }} + Date created: {{ item()?.dateCreated | date: "MMMM d, y" }} | Date + modified: {{ item()?.dateModified | date: "MMMM d, y" }}

} - @if (item().resourceType === ResourceType.Registration) { + @if (item()?.resourceType === ResourceType.Registration) { @@ -91,40 +115,40 @@
- @if (item().description) { -

Description: {{ item().description }}

+ @if (item()?.description) { +

Description: {{ item()?.description }}

} - @if (item().provider?.id) { + @if (item()?.provider?.id) {

Registration provider: 

- {{ item().provider?.name }} + {{ item()?.provider?.name }}
} - @if (item().license?.id) { + @if (item()?.license?.id) {

License: 

- {{ item().license?.name }} + {{ item()?.license?.name }}
} - @if (item().registrationTemplate) { + @if (item()?.registrationTemplate) {

- Registration Template: {{ item().registrationTemplate }} + Registration Template: {{ item()?.registrationTemplate }}

} - @if (item().provider?.id) { + @if (item()?.provider?.id) {

Provider: 

- {{ item().provider?.name }} + {{ item()?.provider?.name }}
} @if ( - item().conflictOfInterestResponse && - item().conflictOfInterestResponse === "no-conflict-of-interest" + item()?.conflictOfInterestResponse && + item()?.conflictOfInterestResponse === "no-conflict-of-interest" ) {

Conflict of Interest response: Author asserted no Conflict of @@ -132,32 +156,40 @@

} - @if (item().resourceType !== ResourceType.Agent) { + @if (item()?.resourceType !== ResourceType.Agent && item()?.id) {

URL: 

- {{ item().id }} + {{ item()?.id }}
} - @if (item().doi) { + @if (item()?.doi) {

DOI: 

- {{ item().doi }} + {{ item()?.doi }}
} - @if (item().resourceType === ResourceType.Agent) { -

Public projects: {{ item().publicProjects }}

-

Public registrations: {{ item().publicRegistrations }}

-

Public preprints: {{ item().publicPreprints }}

+ @if (item()?.resourceType === ResourceType.Agent) { + @if (loading) { + + + + } @else { +

Public projects: {{ item()?.publicProjects ?? 0 }}

+

+ Public registrations: {{ item()?.publicRegistrations ?? 0 }} +

+

Public preprints: {{ item()?.publicPreprints ?? 0 }}

+ } } - @if (item().employment) { -

Employment: {{ item().employment }}

+ @if (item()?.employment) { +

Employment: {{ item()?.employment }}

} - @if (item().education) { -

Education: {{ item().education }}

+ @if (item()?.education) { +

Education: {{ item()?.education }}

}
diff --git a/src/app/shared/components/resources/resource-card/resource-card.component.ts b/src/app/shared/components/resources/resource-card/resource-card.component.ts index 896cc8ac2..eb10edbc7 100644 --- a/src/app/shared/components/resources/resource-card/resource-card.component.ts +++ b/src/app/shared/components/resources/resource-card/resource-card.component.ts @@ -1,4 +1,9 @@ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + model, +} from '@angular/core'; import { Accordion, AccordionContent, @@ -8,6 +13,9 @@ import { import { DatePipe, NgOptimizedImage } from '@angular/common'; import { ResourceType } from '@osf/features/search/models/resource-type.enum'; import { Resource } from '@osf/features/search/models/resource.entity'; +import { ResourceCardService } from '@shared/components/resources/resource-card/resource-card.service'; +import { finalize } from 'rxjs'; +import { Skeleton } from 'primeng/skeleton'; @Component({ selector: 'osf-resource-card', @@ -18,13 +26,47 @@ import { Resource } from '@osf/features/search/models/resource.entity'; AccordionPanel, DatePipe, NgOptimizedImage, + Skeleton, ], templateUrl: './resource-card.component.html', styleUrl: './resource-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ResourceCardComponent { - item = input.required(); + item = model(undefined); + readonly #resourceCardService = inject(ResourceCardService); + loading = false; + dataIsLoaded = false; protected readonly ResourceType = ResourceType; + + onOpen() { + if (this.item() && !this.dataIsLoaded) { + const userIri = this.item()?.id.split('/').pop(); + if (userIri) { + this.loading = true; + this.#resourceCardService + .getUserRelatedCounts(userIri) + .pipe( + finalize(() => { + this.loading = false; + this.dataIsLoaded = true; + }), + ) + .subscribe((res) => { + this.item.update( + (current) => + ({ + ...current, + publicProjects: res.projects, + publicPreprints: res.preprints, + publicRegistrations: res.registrations, + education: res.education, + employment: res.employment, + }) as Resource, + ); + }); + } + } + } } diff --git a/src/app/shared/components/resources/resource-card/resource-card.service.ts b/src/app/shared/components/resources/resource-card/resource-card.service.ts new file mode 100644 index 000000000..cf6a74583 --- /dev/null +++ b/src/app/shared/components/resources/resource-card/resource-card.service.ts @@ -0,0 +1,24 @@ +import { inject, Injectable } from '@angular/core'; +import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { map, Observable } from 'rxjs'; +import { environment } from '../../../../../environments/environment'; +import { UserCountsResponse } from '@shared/components/resources/resource-card/models/user-counts-response.entity'; +import { MapUserCounts } from '@shared/components/resources/resource-card/mappers/user-counts.mapper'; +import { UserRelatedDataCounts } from '@shared/components/resources/resource-card/models/user-related-data-counts.entity'; + +@Injectable({ + providedIn: 'root', +}) +export class ResourceCardService { + #jsonApiService = inject(JsonApiService); + + getUserRelatedCounts(userIri: string): Observable { + const params: Record = { + related_counts: 'nodes,registrations,preprints', + }; + + return this.#jsonApiService + .get(`${environment.apiUrl}/users/${userIri}`, params) + .pipe(map((response) => MapUserCounts(response))); + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.html index b08b4e9ae..43e89df69 100644 --- a/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.html +++ b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.html @@ -1,16 +1,16 @@ -
-
-

Filter creators by typing their name below

- -
-
+ diff --git a/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.scss index 7b5b24e58..e69de29bb 100644 --- a/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.scss +++ b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.scss @@ -1,12 +0,0 @@ -:host { - .content-body { - display: flex; - flex-direction: column; - row-gap: 1.5rem; - padding-bottom: 1.5rem; - - p { - font-weight: 300; - } - } -} diff --git a/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.ts index e5e3f08ab..98279b7bc 100644 --- a/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.ts +++ b/src/app/shared/components/resources/resource-filters/filters/creators/creators-filter.component.ts @@ -2,70 +2,101 @@ import { ChangeDetectionStrategy, Component, computed, + effect, inject, - OnInit, + OnDestroy, signal, + untracked, } from '@angular/core'; import { Select, SelectChangeEvent } from 'primeng/select'; -import { debounceTime, finalize, tap } from 'rxjs'; -import { Creator } from '@shared/components/resources/resource-filters/models/creator/creator.entity'; -import { ResourceFiltersService } from '@shared/components/resources/resource-filters/resource-filters.service'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { debounceTime, distinctUntilChanged, Subject, takeUntil } from 'rxjs'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Store } from '@ngxs/store'; -import { SetCreator } from '@shared/components/resources/resource-filters/store'; +import { + ResourceFiltersSelectors, + SetCreator, +} from '@shared/components/resources/resource-filters/store'; +import { + GetAllOptions, + GetCreatorsOptions, +} from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; +import { toObservable } from '@angular/core/rxjs-interop'; @Component({ selector: 'osf-creators-filter', - imports: [Select, ReactiveFormsModule], + imports: [Select, ReactiveFormsModule, FormsModule], templateUrl: './creators-filter.component.html', styleUrl: './creators-filter.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CreatorsFilterComponent implements OnInit { +export class CreatorsFilterComponent implements OnDestroy { readonly #store = inject(Store); - readonly #resourceFiltersService = inject(ResourceFiltersService); - protected searchCreatorsResults = signal([]); + protected searchCreatorsResults = this.#store.selectSignal( + ResourceFiltersOptionsSelectors.getCreators, + ); protected creatorsOptions = computed(() => { return this.searchCreatorsResults().map((creator) => ({ label: creator.name, - value: creator.id, + id: creator.id, })); }); protected creatorsLoading = signal(false); + protected creatorState = this.#store.selectSignal( + ResourceFiltersSelectors.getCreator, + ); + readonly #unsubscribe = new Subject(); + protected creatorsInput = signal(null); + protected initialization = true; - readonly creatorsGroup = new FormGroup({ - creator: new FormControl(''), - }); + constructor() { + toObservable(this.creatorsInput) + .pipe( + debounceTime(500), + distinctUntilChanged(), + takeUntil(this.#unsubscribe), + ) + .subscribe((searchText) => { + if (!this.initialization) { + if (searchText) { + this.#store.dispatch(new GetCreatorsOptions(searchText ?? '')); + } - ngOnInit() { - if (this.creatorsGroup) { - this.creatorsGroup - ?.get('creator')! - .valueChanges.pipe( - debounceTime(500), - tap(() => this.creatorsLoading.set(true)), - finalize(() => this.creatorsLoading.set(false)), - ) - .subscribe((searchText) => { - console.log(searchText); - if (searchText && searchText !== '') { - this.#resourceFiltersService - .getCreators(searchText) - .subscribe((creators) => { - this.searchCreatorsResults.set(creators); - }); - } else { - this.searchCreatorsResults.set([]); - this.#store.dispatch(new SetCreator('')); + if (!searchText) { + this.#store.dispatch(new SetCreator('', '')); + this.#store.dispatch(GetAllOptions); } - }); - } + } else { + this.initialization = false; + } + }); + + effect(() => { + const storeValue = this.creatorState().label; + const currentInput = untracked(() => this.creatorsInput()); + + if (!storeValue && currentInput !== null) { + this.creatorsInput.set(null); + } else if (storeValue && currentInput !== storeValue) { + this.creatorsInput.set(storeValue); + } + }); + } + + ngOnDestroy() { + this.#unsubscribe.complete(); } setCreator(event: SelectChangeEvent): void { - if ((event.originalEvent as PointerEvent).pointerId) { - this.#store.dispatch(new SetCreator(event.value)); + if ((event.originalEvent as PointerEvent).pointerId && event.value) { + const creator = this.creatorsOptions().find((p) => + p.label.includes(event.value), + ); + if (creator) { + this.#store.dispatch(new SetCreator(creator.label, creator.id)); + this.#store.dispatch(GetAllOptions); + } } } } diff --git a/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.html new file mode 100644 index 000000000..b6188ece4 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.html @@ -0,0 +1,13 @@ + diff --git a/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.spec.ts b/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.spec.ts new file mode 100644 index 000000000..2966937b3 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DateCreatedFilterComponent } from './date-created-filter.component'; + +describe('DateCreatedFilterComponent', () => { + let component: DateCreatedFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DateCreatedFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DateCreatedFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.ts new file mode 100644 index 000000000..01f933441 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/date-created/date-created-filter.component.ts @@ -0,0 +1,63 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal, + untracked, +} from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { Store } from '@ngxs/store'; +import { + ResourceFiltersSelectors, + SetDateCreated, +} from '@shared/components/resources/resource-filters/store'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; + +@Component({ + selector: 'osf-date-created-filter', + imports: [ReactiveFormsModule, Select, FormsModule], + templateUrl: './date-created-filter.component.html', + styleUrl: './date-created-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DateCreatedFilterComponent { + readonly #store = inject(Store); + + protected availableDates = this.#store.selectSignal( + ResourceFiltersOptionsSelectors.getDatesCreated, + ); + protected dateCreatedState = this.#store.selectSignal( + ResourceFiltersSelectors.getDateCreated, + ); + protected inputDate = signal(null); + protected datesOptions = computed(() => { + return this.availableDates().map((date) => ({ + label: date.value + ' (' + date.count + ')', + value: date.value, + })); + }); + + constructor() { + effect(() => { + const storeValue = this.dateCreatedState().label; + const currentInput = untracked(() => this.inputDate()); + + if (!storeValue && currentInput !== null) { + this.inputDate.set(null); + } else if (storeValue && currentInput !== storeValue) { + this.inputDate.set(storeValue); + } + }); + } + + setDateCreated(event: SelectChangeEvent): void { + if ((event.originalEvent as PointerEvent).pointerId) { + this.#store.dispatch(new SetDateCreated(event.value)); + this.#store.dispatch(GetAllOptions); + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.html new file mode 100644 index 000000000..fd813ff38 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.html @@ -0,0 +1,19 @@ + diff --git a/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.spec.ts b/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.spec.ts new file mode 100644 index 000000000..1cda6a9b4 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FunderFilterComponent } from './funder-filter.component'; + +describe('FunderComponent', () => { + let component: FunderFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FunderFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FunderFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.ts new file mode 100644 index 000000000..4c8e077c1 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/funder/funder-filter.component.ts @@ -0,0 +1,87 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal, + untracked, +} from '@angular/core'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { Store } from '@ngxs/store'; +import { + ResourceFiltersSelectors, + SetFunder, +} from '@shared/components/resources/resource-filters/store'; +import { FormsModule } from '@angular/forms'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; + +@Component({ + selector: 'osf-funder-filter', + imports: [Select, FormsModule], + templateUrl: './funder-filter.component.html', + styleUrl: './funder-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FunderFilterComponent { + readonly #store = inject(Store); + + protected funderState = this.#store.selectSignal( + ResourceFiltersSelectors.getFunder, + ); + protected availableFunders = this.#store.selectSignal( + ResourceFiltersOptionsSelectors.getFunders, + ); + protected inputText = signal(null); + protected fundersOptions = computed(() => { + if (this.inputText() !== null) { + const search = this.inputText()!.toLowerCase(); + return this.availableFunders() + .filter((funder) => funder.label.toLowerCase().includes(search)) + .map((funder) => ({ + labelCount: funder.label + ' (' + funder.count + ')', + label: funder.label, + id: funder.id, + })); + } + + const res = this.availableFunders().map((funder) => ({ + labelCount: funder.label + ' (' + funder.count + ')', + label: funder.label, + id: funder.id, + })); + + return res; + }); + + constructor() { + effect(() => { + const storeValue = this.funderState().label; + const currentInput = untracked(() => this.inputText()); + + if (!storeValue && currentInput !== null) { + this.inputText.set(null); + } else if (storeValue && currentInput !== storeValue) { + this.inputText.set(storeValue); + } + }); + } + + loading = signal(false); + + setFunders(event: SelectChangeEvent): void { + if ((event.originalEvent as PointerEvent).pointerId && event.value) { + const funder = this.fundersOptions()?.find((funder) => + funder.label.includes(event.value), + ); + if (funder) { + this.#store.dispatch(new SetFunder(funder.label, funder.id)); + this.#store.dispatch(GetAllOptions); + } + } else { + this.#store.dispatch(new SetFunder('', '')); + this.#store.dispatch(GetAllOptions); + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.html new file mode 100644 index 000000000..684603584 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.html @@ -0,0 +1,20 @@ + diff --git a/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.scss new file mode 100644 index 000000000..5fd36a5f1 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.scss @@ -0,0 +1,5 @@ +:host ::ng-deep { + .p-scroller-viewport { + flex: none; + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.spec.ts b/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.spec.ts new file mode 100644 index 000000000..a6e5dab90 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InstitutionFilterComponent } from './institution-filter.component'; + +describe('InstitutionFilterComponent', () => { + let component: InstitutionFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InstitutionFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InstitutionFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.ts new file mode 100644 index 000000000..fdd5fe7e4 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component.ts @@ -0,0 +1,91 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal, + untracked, +} from '@angular/core'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { Store } from '@ngxs/store'; +import { + ResourceFiltersSelectors, + SetInstitution, +} from '@shared/components/resources/resource-filters/store'; +import { FormsModule } from '@angular/forms'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; + +@Component({ + selector: 'osf-institution-filter', + imports: [Select, FormsModule], + templateUrl: './institution-filter.component.html', + styleUrl: './institution-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InstitutionFilterComponent { + readonly #store = inject(Store); + + protected institutionState = this.#store.selectSignal( + ResourceFiltersSelectors.getInstitution, + ); + protected availableInstitutions = this.#store.selectSignal( + ResourceFiltersOptionsSelectors.getInstitutions, + ); + protected inputText = signal(null); + protected institutionsOptions = computed(() => { + if (this.inputText() !== null) { + const search = this.inputText()!.toLowerCase(); + return this.availableInstitutions() + .filter((institution) => + institution.label.toLowerCase().includes(search), + ) + .map((institution) => ({ + labelCount: institution.label + ' (' + institution.count + ')', + label: institution.label, + id: institution.id, + })); + } + + const res = this.availableInstitutions().map((institution) => ({ + labelCount: institution.label + ' (' + institution.count + ')', + label: institution.label, + id: institution.id, + })); + + return res; + }); + + constructor() { + effect(() => { + const storeValue = this.institutionState().label; + const currentInput = untracked(() => this.inputText()); + + if (!storeValue && currentInput !== null) { + this.inputText.set(null); + } else if (storeValue && currentInput !== storeValue) { + this.inputText.set(storeValue); + } + }); + } + + loading = signal(false); + + setInstitutions(event: SelectChangeEvent): void { + if ((event.originalEvent as PointerEvent).pointerId && event.value) { + const institution = this.institutionsOptions()?.find((institution) => + institution.label.includes(event.value), + ); + if (institution) { + this.#store.dispatch( + new SetInstitution(institution.label, institution.id), + ); + this.#store.dispatch(GetAllOptions); + } + } else { + this.#store.dispatch(new SetInstitution('', '')); + this.#store.dispatch(GetAllOptions); + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.html new file mode 100644 index 000000000..2fbbb2b18 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.html @@ -0,0 +1,20 @@ + diff --git a/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.spec.ts b/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.spec.ts new file mode 100644 index 000000000..655bef1ef --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LicenseFilterComponent } from './license-filter.component'; + +describe('LicenseFilterComponent', () => { + let component: LicenseFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LicenseFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LicenseFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.ts new file mode 100644 index 000000000..7f191ecea --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/license-filter/license-filter.component.ts @@ -0,0 +1,85 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal, + untracked, +} from '@angular/core'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { FormsModule } from '@angular/forms'; +import { Store } from '@ngxs/store'; +import { + ResourceFiltersSelectors, + SetLicense, +} from '@shared/components/resources/resource-filters/store'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; + +@Component({ + selector: 'osf-license-filter', + imports: [Select, FormsModule], + templateUrl: './license-filter.component.html', + styleUrl: './license-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LicenseFilterComponent { + readonly #store = inject(Store); + + protected availableLicenses = this.#store.selectSignal( + ResourceFiltersOptionsSelectors.getLicenses, + ); + protected licenseState = this.#store.selectSignal( + ResourceFiltersSelectors.getLicense, + ); + protected inputText = signal(null); + protected licensesOptions = computed(() => { + if (this.inputText() !== null) { + const search = this.inputText()!.toLowerCase(); + return this.availableLicenses() + .filter((license) => license.label.toLowerCase().includes(search)) + .map((license) => ({ + labelCount: license.label + ' (' + license.count + ')', + label: license.label, + id: license.id, + })); + } + + return this.availableLicenses().map((license) => ({ + labelCount: license.label + ' (' + license.count + ')', + label: license.label, + id: license.id, + })); + }); + + loading = signal(false); + + constructor() { + effect(() => { + const storeValue = this.licenseState().label; + const currentInput = untracked(() => this.inputText()); + + if (!storeValue && currentInput !== null) { + this.inputText.set(null); + } else if (storeValue && currentInput !== storeValue) { + this.inputText.set(storeValue); + } + }); + } + + setLicenses(event: SelectChangeEvent): void { + if ((event.originalEvent as PointerEvent).pointerId && event.value) { + const license = this.licensesOptions().find((license) => + license.label.includes(event.value), + ); + if (license) { + this.#store.dispatch(new SetLicense(license.label, license.id)); + this.#store.dispatch(GetAllOptions); + } + } else { + this.#store.dispatch(new SetLicense('', '')); + this.#store.dispatch(GetAllOptions); + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.html new file mode 100644 index 000000000..b8409b2d8 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.html @@ -0,0 +1,19 @@ + diff --git a/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.spec.ts b/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.spec.ts new file mode 100644 index 000000000..9613b6ae8 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PartOfCollectionFilterComponent } from './part-of-collection-filter.component'; + +describe('PartOfCollectionFilterComponent', () => { + let component: PartOfCollectionFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PartOfCollectionFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PartOfCollectionFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.ts new file mode 100644 index 000000000..b969ca804 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component.ts @@ -0,0 +1,74 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal, + untracked, +} from '@angular/core'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { Store } from '@ngxs/store'; +import { + ResourceFiltersSelectors, + SetPartOfCollection, +} from '@shared/components/resources/resource-filters/store'; +import { FormsModule } from '@angular/forms'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; + +@Component({ + selector: 'osf-part-of-collection-filter', + imports: [Select, FormsModule], + templateUrl: './part-of-collection-filter.component.html', + styleUrl: './part-of-collection-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PartOfCollectionFilterComponent { + readonly #store = inject(Store); + + protected availablePartOfCollections = this.#store.selectSignal( + ResourceFiltersOptionsSelectors.getPartOfCollection, + ); + protected partOfCollectionState = this.#store.selectSignal( + ResourceFiltersSelectors.getPartOfCollection, + ); + protected inputText = signal(null); + protected partOfCollectionsOptions = computed(() => { + return this.availablePartOfCollections().map((partOfCollection) => ({ + labelCount: partOfCollection.label + ' (' + partOfCollection.count + ')', + label: partOfCollection.label, + id: partOfCollection.id, + })); + }); + + loading = signal(false); + + constructor() { + effect(() => { + const storeValue = this.partOfCollectionState().label; + const currentInput = untracked(() => this.inputText()); + + if (!storeValue && currentInput !== null) { + this.inputText.set(null); + } else if (storeValue && currentInput !== storeValue) { + this.inputText.set(storeValue); + } + }); + } + + setPartOfCollections(event: SelectChangeEvent): void { + if ((event.originalEvent as PointerEvent).pointerId && event.value) { + const part = this.partOfCollectionsOptions().find((p) => + p.label.includes(event.value), + ); + if (part) { + this.#store.dispatch(new SetPartOfCollection(part.label, part.id)); + this.#store.dispatch(GetAllOptions); + } + } else { + this.#store.dispatch(new SetPartOfCollection('', '')); + this.#store.dispatch(GetAllOptions); + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.html new file mode 100644 index 000000000..0c2f0e870 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.html @@ -0,0 +1,20 @@ + diff --git a/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.spec.ts b/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.spec.ts new file mode 100644 index 000000000..f52b1f8a4 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProviderFilterComponent } from './provider-filter.component'; + +describe('ProviderFilterComponent', () => { + let component: ProviderFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProviderFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ProviderFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.ts new file mode 100644 index 000000000..80cc220a9 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component.ts @@ -0,0 +1,85 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal, + untracked, +} from '@angular/core'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { Store } from '@ngxs/store'; +import { + ResourceFiltersSelectors, + SetProvider, +} from '@shared/components/resources/resource-filters/store'; +import { FormsModule } from '@angular/forms'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; + +@Component({ + selector: 'osf-provider-filter', + imports: [Select, FormsModule], + templateUrl: './provider-filter.component.html', + styleUrl: './provider-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProviderFilterComponent { + readonly #store = inject(Store); + + protected availableProviders = this.#store.selectSignal( + ResourceFiltersOptionsSelectors.getProviders, + ); + protected providerState = this.#store.selectSignal( + ResourceFiltersSelectors.getProvider, + ); + protected inputText = signal(null); + protected providersOptions = computed(() => { + if (this.inputText() !== null) { + const search = this.inputText()!.toLowerCase(); + return this.availableProviders() + .filter((provider) => provider.label.toLowerCase().includes(search)) + .map((provider) => ({ + labelCount: provider.label + ' (' + provider.count + ')', + label: provider.label, + id: provider.id, + })); + } + + return this.availableProviders().map((provider) => ({ + labelCount: provider.label + ' (' + provider.count + ')', + label: provider.label, + id: provider.id, + })); + }); + + loading = signal(false); + + constructor() { + effect(() => { + const storeValue = this.providerState().label; + const currentInput = untracked(() => this.inputText()); + + if (!storeValue && currentInput !== null) { + this.inputText.set(null); + } else if (storeValue && currentInput !== storeValue) { + this.inputText.set(storeValue); + } + }); + } + + setProviders(event: SelectChangeEvent): void { + if ((event.originalEvent as PointerEvent).pointerId && event.value) { + const provider = this.providersOptions().find((p) => + p.label.includes(event.value), + ); + if (provider) { + this.#store.dispatch(new SetProvider(provider.label, provider.id)); + this.#store.dispatch(GetAllOptions); + } + } else { + this.#store.dispatch(new SetProvider('', '')); + this.#store.dispatch(GetAllOptions); + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.html new file mode 100644 index 000000000..89d44409f --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.html @@ -0,0 +1,20 @@ + diff --git a/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.spec.ts b/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.spec.ts new file mode 100644 index 000000000..ed9fb6d63 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResourceTypeFilterComponent } from './resource-type-filter.component'; + +describe('ResourceTypeFilterComponent', () => { + let component: ResourceTypeFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ResourceTypeFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ResourceTypeFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.ts new file mode 100644 index 000000000..be0039266 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component.ts @@ -0,0 +1,89 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal, + untracked, +} from '@angular/core'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { Store } from '@ngxs/store'; +import { + ResourceFiltersSelectors, + SetResourceType, +} from '@shared/components/resources/resource-filters/store'; +import { FormsModule } from '@angular/forms'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; + +@Component({ + selector: 'osf-resource-type-filter', + imports: [Select, FormsModule], + templateUrl: './resource-type-filter.component.html', + styleUrl: './resource-type-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ResourceTypeFilterComponent { + readonly #store = inject(Store); + + protected availableResourceTypes = this.#store.selectSignal( + ResourceFiltersOptionsSelectors.getResourceTypes, + ); + protected resourceTypeState = this.#store.selectSignal( + ResourceFiltersSelectors.getResourceType, + ); + protected inputText = signal(null); + protected resourceTypesOptions = computed(() => { + if (this.inputText() !== null) { + const search = this.inputText()!.toLowerCase(); + return this.availableResourceTypes() + .filter((resourceType) => + resourceType.label.toLowerCase().includes(search), + ) + .map((resourceType) => ({ + labelCount: resourceType.label + ' (' + resourceType.count + ')', + label: resourceType.label, + id: resourceType.id, + })); + } + + return this.availableResourceTypes().map((resourceType) => ({ + labelCount: resourceType.label + ' (' + resourceType.count + ')', + label: resourceType.label, + id: resourceType.id, + })); + }); + + loading = signal(false); + + constructor() { + effect(() => { + const storeValue = this.resourceTypeState().label; + const currentInput = untracked(() => this.inputText()); + + if (!storeValue && currentInput !== null) { + this.inputText.set(null); + } else if (storeValue && currentInput !== storeValue) { + this.inputText.set(storeValue); + } + }); + } + + setResourceTypes(event: SelectChangeEvent): void { + if ((event.originalEvent as PointerEvent).pointerId && event.value) { + const resourceType = this.resourceTypesOptions().find((p) => + p.label.includes(event.value), + ); + if (resourceType) { + this.#store.dispatch( + new SetResourceType(resourceType.label, resourceType.id), + ); + this.#store.dispatch(GetAllOptions); + } + } else { + this.#store.dispatch(new SetResourceType('', '')); + this.#store.dispatch(GetAllOptions); + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.actions.ts b/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.actions.ts new file mode 100644 index 000000000..5f7687cfe --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.actions.ts @@ -0,0 +1,42 @@ +export class GetCreatorsOptions { + static readonly type = '[Resource Filters Options] Get Creators'; + + constructor(public searchName: string) {} +} + +export class GetDatesCreatedOptions { + static readonly type = '[Resource Filters Options] Get Dates Created'; +} + +export class GetFundersOptions { + static readonly type = '[Resource Filters Options] Get Funders'; +} + +export class GetSubjectsOptions { + static readonly type = '[Resource Filters Options] Get Subjects'; +} + +export class GetLicensesOptions { + static readonly type = '[Resource Filters Options] Get Licenses'; +} + +export class GetResourceTypesOptions { + static readonly type = '[Resource Filters Options] Get Resource Types'; +} + +export class GetInstitutionsOptions { + static readonly type = '[Resource Filters Options] Get Institutions'; +} + +export class GetProvidersOptions { + static readonly type = '[Resource Filters Options] Get Providers'; +} + +export class GetPartOfCollectionOptions { + static readonly type = + '[Resource Filters Options] Get Part Of Collection Options'; +} + +export class GetAllOptions { + static readonly type = '[Resource Filters Options] Get All Options'; +} diff --git a/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.model.ts b/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.model.ts new file mode 100644 index 000000000..10fdea660 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.model.ts @@ -0,0 +1,21 @@ +import { Creator } from '@shared/components/resources/resource-filters/models/creator/creator.entity'; +import { DateCreated } from '@shared/components/resources/resource-filters/models/dateCreated/date-created.entity'; +import { FunderFilter } from '@shared/components/resources/resource-filters/models/funder/funder-filter.entity'; +import { SubjectFilter } from '@shared/components/resources/resource-filters/models/subject/subject-filter.entity'; +import { LicenseFilter } from '@shared/components/resources/resource-filters/models/license/license-filter.entity'; +import { ResourceTypeFilter } from '@shared/components/resources/resource-filters/models/resource-type/resource-type.entity'; +import { ProviderFilter } from '@shared/components/resources/resource-filters/models/provider/provider-filter.entity'; +import { PartOfCollectionFilter } from '@shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-filter.entity'; +import { InstitutionFilter } from '@shared/components/resources/resource-filters/models/institution/institution-filter.entity'; + +export interface ResourceFiltersOptionsStateModel { + creators: Creator[]; + datesCreated: DateCreated[]; + funders: FunderFilter[]; + subjects: SubjectFilter[]; + licenses: LicenseFilter[]; + resourceTypes: ResourceTypeFilter[]; + institutions: InstitutionFilter[]; + providers: ProviderFilter[]; + partOfCollection: PartOfCollectionFilter[]; +} diff --git a/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors.ts b/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors.ts new file mode 100644 index 000000000..54d87a027 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors.ts @@ -0,0 +1,69 @@ +import { Selector } from '@ngxs/store'; +import { ResourceFiltersOptionsStateModel } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.model'; +import { ResourceFiltersOptionsState } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.state'; +import { Creator } from '@shared/components/resources/resource-filters/models/creator/creator.entity'; +import { DateCreated } from '@shared/components/resources/resource-filters/models/dateCreated/date-created.entity'; +import { FunderFilter } from '@shared/components/resources/resource-filters/models/funder/funder-filter.entity'; +import { SubjectFilter } from '@shared/components/resources/resource-filters/models/subject/subject-filter.entity'; +import { LicenseFilter } from '@shared/components/resources/resource-filters/models/license/license-filter.entity'; +import { ResourceTypeFilter } from '@shared/components/resources/resource-filters/models/resource-type/resource-type.entity'; +import { ProviderFilter } from '@shared/components/resources/resource-filters/models/provider/provider-filter.entity'; +import { PartOfCollectionFilter } from '@shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-filter.entity'; +import { InstitutionFilter } from '@shared/components/resources/resource-filters/models/institution/institution-filter.entity'; + +export class ResourceFiltersOptionsSelectors { + @Selector([ResourceFiltersOptionsState]) + static getCreators(state: ResourceFiltersOptionsStateModel): Creator[] { + return state.creators; + } + + @Selector([ResourceFiltersOptionsState]) + static getDatesCreated( + state: ResourceFiltersOptionsStateModel, + ): DateCreated[] { + return state.datesCreated; + } + + @Selector([ResourceFiltersOptionsState]) + static getFunders(state: ResourceFiltersOptionsStateModel): FunderFilter[] { + return state.funders; + } + + @Selector([ResourceFiltersOptionsState]) + static getSubjects(state: ResourceFiltersOptionsStateModel): SubjectFilter[] { + return state.subjects; + } + + @Selector([ResourceFiltersOptionsState]) + static getLicenses(state: ResourceFiltersOptionsStateModel): LicenseFilter[] { + return state.licenses; + } + + @Selector([ResourceFiltersOptionsState]) + static getResourceTypes( + state: ResourceFiltersOptionsStateModel, + ): ResourceTypeFilter[] { + return state.resourceTypes; + } + + @Selector([ResourceFiltersOptionsState]) + static getInstitutions( + state: ResourceFiltersOptionsStateModel, + ): InstitutionFilter[] { + return state.institutions; + } + + @Selector([ResourceFiltersOptionsState]) + static getProviders( + state: ResourceFiltersOptionsStateModel, + ): ProviderFilter[] { + return state.providers; + } + + @Selector([ResourceFiltersOptionsState]) + static getPartOfCollection( + state: ResourceFiltersOptionsStateModel, + ): PartOfCollectionFilter[] { + return state.partOfCollection; + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.state.ts b/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.state.ts new file mode 100644 index 000000000..47ab85921 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/store/resource-filters-options.state.ts @@ -0,0 +1,137 @@ +import { Action, State, StateContext, Store } from '@ngxs/store'; +import { inject, Injectable } from '@angular/core'; +import { tap } from 'rxjs'; +import { ResourceFiltersOptionsStateModel } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.model'; +import { ResourceFiltersService } from '@shared/components/resources/resource-filters/resource-filters.service'; +import { + GetAllOptions, + GetCreatorsOptions, + GetDatesCreatedOptions, + GetFundersOptions, + GetInstitutionsOptions, + GetLicensesOptions, + GetPartOfCollectionOptions, + GetProvidersOptions, + GetResourceTypesOptions, + GetSubjectsOptions, +} from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; + +@State({ + name: 'resourceFiltersOptions', + defaults: { + creators: [], + datesCreated: [], + funders: [], + subjects: [], + licenses: [], + resourceTypes: [], + institutions: [], + providers: [], + partOfCollection: [], + }, +}) +@Injectable() +export class ResourceFiltersOptionsState { + readonly #store = inject(Store); + readonly #resourceFiltersService = inject(ResourceFiltersService); + + @Action(GetCreatorsOptions) + getProjects( + ctx: StateContext, + action: GetCreatorsOptions, + ) { + if (!action.searchName) { + ctx.patchState({ creators: [] }); + return []; + } + + return this.#resourceFiltersService.getCreators(action.searchName).pipe( + tap((creators) => { + ctx.patchState({ creators: creators }); + }), + ); + } + + @Action(GetDatesCreatedOptions) + getDatesCreated(ctx: StateContext) { + return this.#resourceFiltersService.getDates().pipe( + tap((datesCreated) => { + ctx.patchState({ datesCreated: datesCreated }); + }), + ); + } + + @Action(GetFundersOptions) + getFunders(ctx: StateContext) { + return this.#resourceFiltersService.getFunders().pipe( + tap((funders) => { + ctx.patchState({ funders: funders }); + }), + ); + } + + @Action(GetSubjectsOptions) + getSubjects(ctx: StateContext) { + return this.#resourceFiltersService.getSubjects().pipe( + tap((subjects) => { + ctx.patchState({ subjects: subjects }); + }), + ); + } + + @Action(GetLicensesOptions) + getLicenses(ctx: StateContext) { + return this.#resourceFiltersService.getLicenses().pipe( + tap((licenses) => { + ctx.patchState({ licenses: licenses }); + }), + ); + } + + @Action(GetResourceTypesOptions) + getResourceTypes(ctx: StateContext) { + return this.#resourceFiltersService.getResourceTypes().pipe( + tap((resourceTypes) => { + ctx.patchState({ resourceTypes: resourceTypes }); + }), + ); + } + + @Action(GetInstitutionsOptions) + getInstitutions(ctx: StateContext) { + return this.#resourceFiltersService.getInstitutions().pipe( + tap((institutions) => { + ctx.patchState({ institutions: institutions }); + }), + ); + } + + @Action(GetProvidersOptions) + getProviders(ctx: StateContext) { + return this.#resourceFiltersService.getProviders().pipe( + tap((providers) => { + ctx.patchState({ providers: providers }); + }), + ); + } + @Action(GetPartOfCollectionOptions) + getPartOfCollection(ctx: StateContext) { + return this.#resourceFiltersService.getPartOtCollections().pipe( + tap((partOfCollection) => { + ctx.patchState({ partOfCollection: partOfCollection }); + }), + ); + } + + @Action(GetAllOptions) + getAllOptions() { + this.#store.dispatch(GetDatesCreatedOptions); + this.#store.dispatch(GetFundersOptions); + this.#store.dispatch(GetSubjectsOptions); + this.#store.dispatch(GetLicensesOptions); + this.#store.dispatch(GetResourceTypesOptions); + this.#store.dispatch(GetInstitutionsOptions); + this.#store.dispatch(GetProvidersOptions); + this.#store.dispatch(GetPartOfCollectionOptions); + } +} diff --git a/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.html b/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.html new file mode 100644 index 000000000..dd18db123 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.html @@ -0,0 +1,20 @@ + diff --git a/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.scss b/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.spec.ts b/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.spec.ts new file mode 100644 index 000000000..fd18cc1f2 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubjectFilterComponent } from './subject-filter.component'; + +describe('SubjectComponent', () => { + let component: SubjectFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SubjectFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SubjectFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.ts b/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.ts new file mode 100644 index 000000000..388825cd0 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/filters/subject/subject-filter.component.ts @@ -0,0 +1,85 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + signal, + untracked, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { Store } from '@ngxs/store'; +import { + ResourceFiltersSelectors, + SetSubject, +} from '@shared/components/resources/resource-filters/store'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; + +@Component({ + selector: 'osf-subject-filter', + imports: [Select, FormsModule], + templateUrl: './subject-filter.component.html', + styleUrl: './subject-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SubjectFilterComponent { + readonly #store = inject(Store); + + protected availableSubjects = this.#store.selectSignal( + ResourceFiltersOptionsSelectors.getSubjects, + ); + protected subjectState = this.#store.selectSignal( + ResourceFiltersSelectors.getSubject, + ); + protected inputText = signal(null); + protected subjectsOptions = computed(() => { + if (this.inputText() !== null) { + const search = this.inputText()!.toLowerCase(); + return this.availableSubjects() + .filter((subject) => subject.label.toLowerCase().includes(search)) + .map((subject) => ({ + labelCount: subject.label + ' (' + subject.count + ')', + label: subject.label, + id: subject.id, + })); + } + + return this.availableSubjects().map((subject) => ({ + labelCount: subject.label + ' (' + subject.count + ')', + label: subject.label, + id: subject.id, + })); + }); + + loading = signal(false); + + constructor() { + effect(() => { + const storeValue = this.subjectState().label; + const currentInput = untracked(() => this.inputText()); + + if (!storeValue && currentInput !== null) { + this.inputText.set(null); + } else if (storeValue && currentInput !== storeValue) { + this.inputText.set(storeValue); + } + }); + } + + setSubject(event: SelectChangeEvent): void { + if ((event.originalEvent as PointerEvent).pointerId && event.value) { + const subject = this.subjectsOptions().find((p) => + p.label.includes(event.value), + ); + if (subject) { + this.#store.dispatch(new SetSubject(subject.label, subject.id)); + this.#store.dispatch(GetAllOptions); + } + } else { + this.#store.dispatch(new SetSubject('', '')); + this.#store.dispatch(GetAllOptions); + } + } +} diff --git a/src/app/shared/components/resources/resource-filters/mappers/dateCreated/date-created.mapper.ts b/src/app/shared/components/resources/resource-filters/mappers/dateCreated/date-created.mapper.ts new file mode 100644 index 000000000..426ca8270 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/mappers/dateCreated/date-created.mapper.ts @@ -0,0 +1,26 @@ +import { DateCreated } from '@shared/components/resources/resource-filters/models/dateCreated/date-created.entity'; +import { IndexValueSearch } from '@shared/components/resources/resource-filters/models/index-value-search.entity'; +import { IndexCardFilter } from '@shared/components/resources/resource-filters/models/index-card-filter.entity'; + +export function MapDateCreated(items: IndexValueSearch[]): DateCreated[] { + const datesCreated: DateCreated[] = []; + + if (!items) { + return []; + } + + for (const item of items) { + if (item.type === 'search-result') { + const indexCard = items.find( + (p) => p.id === item.relationships.indexCard.data.id, + ); + datesCreated.push({ + value: (indexCard as IndexCardFilter).attributes.resourceMetadata + .displayLabel[0]['@value'], + count: item.attributes.cardSearchResultCount, + }); + } + } + + return datesCreated; +} diff --git a/src/app/shared/components/resources/resource-filters/mappers/funder/funder.mapper.ts b/src/app/shared/components/resources/resource-filters/mappers/funder/funder.mapper.ts new file mode 100644 index 000000000..ffb277305 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/mappers/funder/funder.mapper.ts @@ -0,0 +1,29 @@ +import { FunderIndexValueSearch } from '@shared/components/resources/resource-filters/models/funder/funder-index-value-search.entity'; +import { FunderFilter } from '@shared/components/resources/resource-filters/models/funder/funder-filter.entity'; +import { FunderIndexCardFilter } from '@shared/components/resources/resource-filters/models/funder/funder-index-card-filter.entity'; + +export function MapFunders(items: FunderIndexValueSearch[]): FunderFilter[] { + const funders: FunderFilter[] = []; + + if (!items) { + return []; + } + + for (const item of items) { + if (item.type === 'search-result') { + const indexCard = items.find( + (p) => p.id === item.relationships.indexCard.data.id, + ); + funders.push({ + id: (indexCard as FunderIndexCardFilter).attributes.resourceMetadata?.[ + '@id' + ], + label: (indexCard as FunderIndexCardFilter).attributes.resourceMetadata + ?.name?.[0]?.['@value'], + count: item.attributes.cardSearchResultCount, + }); + } + } + + return funders; +} diff --git a/src/app/shared/components/resources/resource-filters/mappers/institution/institution.mapper.ts b/src/app/shared/components/resources/resource-filters/mappers/institution/institution.mapper.ts new file mode 100644 index 000000000..cfa6dd739 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/mappers/institution/institution.mapper.ts @@ -0,0 +1,30 @@ +import { InstitutionIndexValueSearch } from '@shared/components/resources/resource-filters/models/institution/institution-index-value-search.entity'; +import { InstitutionIndexCardFilter } from '@shared/components/resources/resource-filters/models/institution/institution-index-card-filter.entity'; +import { InstitutionFilter } from '@shared/components/resources/resource-filters/models/institution/institution-filter.entity'; + +export function MapInstitutions( + items: InstitutionIndexValueSearch[], +): InstitutionFilter[] { + const institutions: InstitutionFilter[] = []; + + if (!items) { + return []; + } + + for (const item of items) { + if (item.type === 'search-result') { + const indexCard = items.find( + (p) => p.id === item.relationships.indexCard.data.id, + ); + institutions.push({ + id: (indexCard as InstitutionIndexCardFilter).attributes + .resourceMetadata?.['@id'], + label: (indexCard as InstitutionIndexCardFilter).attributes + .resourceMetadata?.name?.[0]?.['@value'], + count: item.attributes.cardSearchResultCount, + }); + } + } + + return institutions; +} diff --git a/src/app/shared/components/resources/resource-filters/mappers/license/license.mapper.ts b/src/app/shared/components/resources/resource-filters/mappers/license/license.mapper.ts new file mode 100644 index 000000000..e5f8b1ec6 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/mappers/license/license.mapper.ts @@ -0,0 +1,29 @@ +import { LicenseIndexValueSearch } from '@shared/components/resources/resource-filters/models/license/license-index-value-search.entity'; +import { LicenseIndexCardFilter } from '@shared/components/resources/resource-filters/models/license/license-index-card-filter.entity'; +import { LicenseFilter } from '@shared/components/resources/resource-filters/models/license/license-filter.entity'; + +export function MapLicenses(items: LicenseIndexValueSearch[]): LicenseFilter[] { + const licenses: LicenseFilter[] = []; + + if (!items) { + return []; + } + + for (const item of items) { + if (item.type === 'search-result') { + const indexCard = items.find( + (p) => p.id === item.relationships.indexCard.data.id, + ); + licenses.push({ + id: (indexCard as LicenseIndexCardFilter).attributes.resourceMetadata?.[ + '@id' + ], + label: (indexCard as LicenseIndexCardFilter).attributes.resourceMetadata + ?.name?.[0]?.['@value'], + count: item.attributes.cardSearchResultCount, + }); + } + } + + return licenses; +} diff --git a/src/app/shared/components/resources/resource-filters/mappers/part-of-collection/part-of-collection.mapper.ts b/src/app/shared/components/resources/resource-filters/mappers/part-of-collection/part-of-collection.mapper.ts new file mode 100644 index 000000000..e3d967844 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/mappers/part-of-collection/part-of-collection.mapper.ts @@ -0,0 +1,30 @@ +import { PartOfCollectionIndexValueSearch } from '@shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-value-search.entity'; +import { PartOfCollectionFilter } from '@shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-filter.entity'; +import { PartOfCollectionIndexCardFilter } from '@shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-card-filter.entity'; + +export function MapPartOfCollections( + items: PartOfCollectionIndexValueSearch[], +): PartOfCollectionFilter[] { + const partOfCollections: PartOfCollectionFilter[] = []; + + if (!items) { + return []; + } + + for (const item of items) { + if (item.type === 'search-result') { + const indexCard = items.find( + (p) => p.id === item.relationships.indexCard.data.id, + ); + partOfCollections.push({ + id: (indexCard as PartOfCollectionIndexCardFilter).attributes + .resourceMetadata?.['@id'], + label: (indexCard as PartOfCollectionIndexCardFilter).attributes + .resourceMetadata?.title?.[0]?.['@value'], + count: item.attributes.cardSearchResultCount, + }); + } + } + + return partOfCollections; +} diff --git a/src/app/shared/components/resources/resource-filters/mappers/provider/provider.mapper.ts b/src/app/shared/components/resources/resource-filters/mappers/provider/provider.mapper.ts new file mode 100644 index 000000000..981f21e56 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/mappers/provider/provider.mapper.ts @@ -0,0 +1,30 @@ +import { ProviderIndexValueSearch } from '@shared/components/resources/resource-filters/models/provider/provider-index-value-search.entity'; +import { ProviderFilter } from '@shared/components/resources/resource-filters/models/provider/provider-filter.entity'; +import { ProviderIndexCardFilter } from '@shared/components/resources/resource-filters/models/provider/provider-index-card-filter.entity'; + +export function MapProviders( + items: ProviderIndexValueSearch[], +): ProviderFilter[] { + const providers: ProviderFilter[] = []; + + if (!items) { + return []; + } + + for (const item of items) { + if (item.type === 'search-result') { + const indexCard = items.find( + (p) => p.id === item.relationships.indexCard.data.id, + ); + providers.push({ + id: (indexCard as ProviderIndexCardFilter).attributes + .resourceMetadata?.['@id'], + label: (indexCard as ProviderIndexCardFilter).attributes + .resourceMetadata?.name?.[0]?.['@value'], + count: item.attributes.cardSearchResultCount, + }); + } + } + + return providers; +} diff --git a/src/app/shared/components/resources/resource-filters/mappers/resource-type/resource-type.mapper.ts b/src/app/shared/components/resources/resource-filters/mappers/resource-type/resource-type.mapper.ts new file mode 100644 index 000000000..bd76a7cd5 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/mappers/resource-type/resource-type.mapper.ts @@ -0,0 +1,30 @@ +import { ResourceTypeIndexValueSearch } from '@shared/components/resources/resource-filters/models/resource-type/resource-type-index-value-search.entity'; +import { ResourceTypeFilter } from '@shared/components/resources/resource-filters/models/resource-type/resource-type.entity'; +import { ResourceTypeIndexCardFilter } from '@shared/components/resources/resource-filters/models/resource-type/resource-type-index-card-filter.entity'; + +export function MapResourceType( + items: ResourceTypeIndexValueSearch[], +): ResourceTypeFilter[] { + const resourceTypes: ResourceTypeFilter[] = []; + + if (!items) { + return []; + } + + for (const item of items) { + if (item.type === 'search-result') { + const indexCard = items.find( + (p) => p.id === item.relationships.indexCard.data.id, + ); + resourceTypes.push({ + id: (indexCard as ResourceTypeIndexCardFilter).attributes + .resourceMetadata?.['@id'], + label: (indexCard as ResourceTypeIndexCardFilter).attributes + .resourceMetadata?.displayLabel?.[0]?.['@value'], + count: item.attributes.cardSearchResultCount, + }); + } + } + + return resourceTypes; +} diff --git a/src/app/shared/components/resources/resource-filters/mappers/subject/subject.mapper.ts b/src/app/shared/components/resources/resource-filters/mappers/subject/subject.mapper.ts new file mode 100644 index 000000000..e3e4e6730 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/mappers/subject/subject.mapper.ts @@ -0,0 +1,27 @@ +import { IndexValueSearch } from '@shared/components/resources/resource-filters/models/index-value-search.entity'; +import { SubjectFilter } from '@shared/components/resources/resource-filters/models/subject/subject-filter.entity'; +import { IndexCardFilter } from '@shared/components/resources/resource-filters/models/index-card-filter.entity'; + +export function MapSubject(items: IndexValueSearch[]): SubjectFilter[] { + const subjects: SubjectFilter[] = []; + + if (!items) { + return []; + } + + for (const item of items) { + if (item.type === 'search-result') { + const indexCard = items.find( + (p) => p.id === item.relationships.indexCard.data.id, + ); + subjects.push({ + id: (indexCard as IndexCardFilter).attributes.resourceMetadata?.['@id'], + label: (indexCard as IndexCardFilter).attributes.resourceMetadata + ?.displayLabel?.[0]?.['@value'], + count: item.attributes.cardSearchResultCount, + }); + } + } + + return subjects; +} diff --git a/src/app/shared/components/resources/resource-filters/models/dateCreated/date-created.entity.ts b/src/app/shared/components/resources/resource-filters/models/dateCreated/date-created.entity.ts new file mode 100644 index 000000000..8948ebb42 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/dateCreated/date-created.entity.ts @@ -0,0 +1,4 @@ +export interface DateCreated { + value: string; + count: number; +} diff --git a/src/app/shared/components/resources/resource-filters/models/dateCreated/date-index-card.entity.ts b/src/app/shared/components/resources/resource-filters/models/dateCreated/date-index-card.entity.ts new file mode 100644 index 000000000..a522a8ab2 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/dateCreated/date-index-card.entity.ts @@ -0,0 +1,10 @@ +// export interface DateIndexCard { +// attributes: { +// resourceIdentifier: string[]; +// resourceMetadata: { +// displayLabel: { '@value': string }[]; +// } +// }, +// id: string; +// type: 'index-card'; +// } diff --git a/src/app/shared/components/resources/resource-filters/models/filter-labels.ts b/src/app/shared/components/resources/resource-filters/models/filter-labels.ts new file mode 100644 index 000000000..2b620967c --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/filter-labels.ts @@ -0,0 +1,11 @@ +export const FilterLabels = { + creator: 'Creator', + dateCreated: 'Date Created', + funder: 'Funder', + subject: 'Subject', + license: 'License', + resourceType: 'Resource Type', + institution: 'Institution', + provider: 'Provider', + partOfCollection: 'Part of Collection', +}; diff --git a/src/app/shared/components/resources/resource-filters/models/filter-type.enum.ts b/src/app/shared/components/resources/resource-filters/models/filter-type.enum.ts new file mode 100644 index 000000000..6c8c13946 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/filter-type.enum.ts @@ -0,0 +1,11 @@ +export enum FilterType { + Creator, + DateCreated, + Funder, + Subject, + License, + ResourceType, + Institution, + Provider, + PartOfCollection, +} diff --git a/src/app/shared/components/resources/resource-filters/models/funder/funder-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/funder/funder-filter.entity.ts new file mode 100644 index 000000000..35cb97a9f --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/funder/funder-filter.entity.ts @@ -0,0 +1,5 @@ +export interface FunderFilter { + id: string; + label: string; + count: number; +} diff --git a/src/app/shared/components/resources/resource-filters/models/funder/funder-index-card-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/funder/funder-index-card-filter.entity.ts new file mode 100644 index 000000000..6c3052fd2 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/funder/funder-index-card-filter.entity.ts @@ -0,0 +1,11 @@ +export interface FunderIndexCardFilter { + attributes: { + resourceIdentifier: string[]; + resourceMetadata: { + name: { '@value': string }[]; + '@id': string; + }; + }; + id: string; + type: 'index-card'; +} diff --git a/src/app/shared/components/resources/resource-filters/models/funder/funder-index-value-search.entity.ts b/src/app/shared/components/resources/resource-filters/models/funder/funder-index-value-search.entity.ts new file mode 100644 index 000000000..db9e4d4f3 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/funder/funder-index-value-search.entity.ts @@ -0,0 +1,4 @@ +import { FunderIndexCardFilter } from '@shared/components/resources/resource-filters/models/funder/funder-index-card-filter.entity'; +import { SearchResultCount } from '@shared/components/resources/resource-filters/models/search-result-count.entity'; + +export type FunderIndexValueSearch = SearchResultCount | FunderIndexCardFilter; diff --git a/src/app/shared/components/resources/resource-filters/models/index-card-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/index-card-filter.entity.ts new file mode 100644 index 000000000..a40665ab3 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/index-card-filter.entity.ts @@ -0,0 +1,11 @@ +export interface IndexCardFilter { + attributes: { + resourceIdentifier: string[]; + resourceMetadata: { + displayLabel: { '@value': string }[]; + '@id': string; + }; + }; + id: string; + type: 'index-card'; +} diff --git a/src/app/shared/components/resources/resource-filters/models/index-value-search.entity.ts b/src/app/shared/components/resources/resource-filters/models/index-value-search.entity.ts new file mode 100644 index 000000000..2c4f62f0d --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/index-value-search.entity.ts @@ -0,0 +1,4 @@ +import { SearchResultCount } from '@shared/components/resources/resource-filters/models/search-result-count.entity'; +import { IndexCardFilter } from '@shared/components/resources/resource-filters/models/index-card-filter.entity'; + +export type IndexValueSearch = SearchResultCount | IndexCardFilter; diff --git a/src/app/shared/components/resources/resource-filters/models/institution/institution-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/institution/institution-filter.entity.ts new file mode 100644 index 000000000..19b5cb9e9 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/institution/institution-filter.entity.ts @@ -0,0 +1,5 @@ +export interface InstitutionFilter { + id: string; + label: string; + count: number; +} diff --git a/src/app/shared/components/resources/resource-filters/models/institution/institution-index-card-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/institution/institution-index-card-filter.entity.ts new file mode 100644 index 000000000..3cc8a68a3 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/institution/institution-index-card-filter.entity.ts @@ -0,0 +1,11 @@ +export interface InstitutionIndexCardFilter { + attributes: { + resourceIdentifier: string[]; + resourceMetadata: { + name: { '@value': string }[]; + '@id': string; + }; + }; + id: string; + type: 'index-card'; +} diff --git a/src/app/shared/components/resources/resource-filters/models/institution/institution-index-value-search.entity.ts b/src/app/shared/components/resources/resource-filters/models/institution/institution-index-value-search.entity.ts new file mode 100644 index 000000000..bacd74265 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/institution/institution-index-value-search.entity.ts @@ -0,0 +1,6 @@ +import { SearchResultCount } from '@shared/components/resources/resource-filters/models/search-result-count.entity'; +import { InstitutionIndexCardFilter } from '@shared/components/resources/resource-filters/models/institution/institution-index-card-filter.entity'; + +export type InstitutionIndexValueSearch = + | SearchResultCount + | InstitutionIndexCardFilter; diff --git a/src/app/shared/components/resources/resource-filters/models/license/license-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/license/license-filter.entity.ts new file mode 100644 index 000000000..79b4c9205 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/license/license-filter.entity.ts @@ -0,0 +1,5 @@ +export interface LicenseFilter { + id: string; + label: string; + count: number; +} diff --git a/src/app/shared/components/resources/resource-filters/models/license/license-index-card-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/license/license-index-card-filter.entity.ts new file mode 100644 index 000000000..818c9d842 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/license/license-index-card-filter.entity.ts @@ -0,0 +1,11 @@ +export interface LicenseIndexCardFilter { + attributes: { + resourceIdentifier: string[]; + resourceMetadata: { + name: { '@value': string }[]; + '@id': string; + }; + }; + id: string; + type: 'index-card'; +} diff --git a/src/app/shared/components/resources/resource-filters/models/license/license-index-value-search.entity.ts b/src/app/shared/components/resources/resource-filters/models/license/license-index-value-search.entity.ts new file mode 100644 index 000000000..d972ddcac --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/license/license-index-value-search.entity.ts @@ -0,0 +1,6 @@ +import { SearchResultCount } from '@shared/components/resources/resource-filters/models/search-result-count.entity'; +import { LicenseIndexCardFilter } from '@shared/components/resources/resource-filters/models/license/license-index-card-filter.entity'; + +export type LicenseIndexValueSearch = + | SearchResultCount + | LicenseIndexCardFilter; diff --git a/src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-filter.entity.ts new file mode 100644 index 000000000..c37f0d213 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-filter.entity.ts @@ -0,0 +1,5 @@ +export interface PartOfCollectionFilter { + id: string; + label: string; + count: number; +} diff --git a/src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-card-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-card-filter.entity.ts new file mode 100644 index 000000000..f2e98b9bb --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-card-filter.entity.ts @@ -0,0 +1,11 @@ +export interface PartOfCollectionIndexCardFilter { + attributes: { + resourceIdentifier: string[]; + resourceMetadata: { + title: { '@value': string }[]; + '@id': string; + }; + }; + id: string; + type: 'index-card'; +} diff --git a/src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-value-search.entity.ts b/src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-value-search.entity.ts new file mode 100644 index 000000000..cbe227b48 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-value-search.entity.ts @@ -0,0 +1,6 @@ +import { SearchResultCount } from '@shared/components/resources/resource-filters/models/search-result-count.entity'; +import { PartOfCollectionIndexCardFilter } from '@shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-card-filter.entity'; + +export type PartOfCollectionIndexValueSearch = + | SearchResultCount + | PartOfCollectionIndexCardFilter; diff --git a/src/app/shared/components/resources/resource-filters/models/provider/provider-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/provider/provider-filter.entity.ts new file mode 100644 index 000000000..054f75bfa --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/provider/provider-filter.entity.ts @@ -0,0 +1,5 @@ +export interface ProviderFilter { + id: string; + label: string; + count: number; +} diff --git a/src/app/shared/components/resources/resource-filters/models/provider/provider-index-card-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/provider/provider-index-card-filter.entity.ts new file mode 100644 index 000000000..f3e7a4e2b --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/provider/provider-index-card-filter.entity.ts @@ -0,0 +1,11 @@ +export interface ProviderIndexCardFilter { + attributes: { + resourceIdentifier: string[]; + resourceMetadata: { + name: { '@value': string }[]; + '@id': string; + }; + }; + id: string; + type: 'index-card'; +} diff --git a/src/app/shared/components/resources/resource-filters/models/provider/provider-index-value-search.entity.ts b/src/app/shared/components/resources/resource-filters/models/provider/provider-index-value-search.entity.ts new file mode 100644 index 000000000..7e4cfb7a6 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/provider/provider-index-value-search.entity.ts @@ -0,0 +1,6 @@ +import { SearchResultCount } from '@shared/components/resources/resource-filters/models/search-result-count.entity'; +import { ProviderIndexCardFilter } from '@shared/components/resources/resource-filters/models/provider/provider-index-card-filter.entity'; + +export type ProviderIndexValueSearch = + | SearchResultCount + | ProviderIndexCardFilter; diff --git a/src/app/shared/components/resources/resource-filters/models/resource-type/resource-type-index-card-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/resource-type/resource-type-index-card-filter.entity.ts new file mode 100644 index 000000000..c588a750c --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/resource-type/resource-type-index-card-filter.entity.ts @@ -0,0 +1,11 @@ +export interface ResourceTypeIndexCardFilter { + attributes: { + resourceIdentifier: string[]; + resourceMetadata: { + displayLabel: { '@value': string }[]; + '@id': string; + }; + }; + id: string; + type: 'index-card'; +} diff --git a/src/app/shared/components/resources/resource-filters/models/resource-type/resource-type-index-value-search.entity.ts b/src/app/shared/components/resources/resource-filters/models/resource-type/resource-type-index-value-search.entity.ts new file mode 100644 index 000000000..126e81cfd --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/resource-type/resource-type-index-value-search.entity.ts @@ -0,0 +1,6 @@ +import { SearchResultCount } from '@shared/components/resources/resource-filters/models/search-result-count.entity'; +import { ResourceTypeIndexCardFilter } from '@shared/components/resources/resource-filters/models/resource-type/resource-type-index-card-filter.entity'; + +export type ResourceTypeIndexValueSearch = + | SearchResultCount + | ResourceTypeIndexCardFilter; diff --git a/src/app/shared/components/resources/resource-filters/models/resource-type/resource-type.entity.ts b/src/app/shared/components/resources/resource-filters/models/resource-type/resource-type.entity.ts new file mode 100644 index 000000000..856aa767b --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/resource-type/resource-type.entity.ts @@ -0,0 +1,5 @@ +export interface ResourceTypeFilter { + id: string; + label: string; + count: number; +} diff --git a/src/app/shared/components/resources/resource-filters/models/search-result-count.entity.ts b/src/app/shared/components/resources/resource-filters/models/search-result-count.entity.ts new file mode 100644 index 000000000..ffb0e6e1a --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/search-result-count.entity.ts @@ -0,0 +1,15 @@ +export interface SearchResultCount { + attributes: { + cardSearchResultCount: number; + }; + id: string; + type: 'search-result'; + relationships: { + indexCard: { + data: { + id: string; + type: string; + }; + }; + }; +} diff --git a/src/app/shared/components/resources/resource-filters/models/subject/subject-filter-response.entity.ts b/src/app/shared/components/resources/resource-filters/models/subject/subject-filter-response.entity.ts new file mode 100644 index 000000000..541e3671a --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/subject/subject-filter-response.entity.ts @@ -0,0 +1,5 @@ +// export interface SubjectFilterResponse { +// '@id': string; +// displayLabel: { '@value': string }[]; +// resourceType: { '@id': string }[]; +// } diff --git a/src/app/shared/components/resources/resource-filters/models/subject/subject-filter.entity.ts b/src/app/shared/components/resources/resource-filters/models/subject/subject-filter.entity.ts new file mode 100644 index 000000000..d94e1e63b --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/models/subject/subject-filter.entity.ts @@ -0,0 +1,5 @@ +export interface SubjectFilter { + id: string; + label: string; + count: number; +} diff --git a/src/app/shared/components/resources/resource-filters/resource-filters.component.html b/src/app/shared/components/resources/resource-filters/resource-filters.component.html index f9de3010d..2371c14fe 100644 --- a/src/app/shared/components/resources/resource-filters/resource-filters.component.html +++ b/src/app/shared/components/resources/resource-filters/resource-filters.component.html @@ -1,139 +1,82 @@ - - - Creator - - - - +
+ + + Creator + + + + - - Date Created - -
-

- Please select the creation date from the dropdown below -

- - -
-
-
+ @if (datesOptionsCount() > 0) { + + Date Created + + + + + } - - Funder - -
-

- Please select the funder from the dropdown below or start typing for - find it -

- - -
-
-
+ @if (funderOptionsCount() > 0) { + + Funder + + + + + } - - Subject - -
-

- Please select the subject from the dropdown below or start typing for - find it -

- - -
-
-
+ @if (subjectOptionsCount() > 0) { + + Subject + + + + + } - - License - -
-

- Please select the license from the dropdown below or start typing for - find it -

- - -
-
-
+ @if (licenseOptionsCount() > 0) { + + License + + + + + } - - Resource Type - -
-

- Please select the resource type from the dropdown below or start - typing for find it -

- - -
-
-
+ @if (resourceTypeOptionsCount() > 0) { + + Resource Type + + + + + } - - Institution - -
-

- Please select the institution from the dropdown below or start typing - for find it -

- - -
-
-
+ @if (institutionOptionsCount() > 0) { + + Institution + + + + + } - - Provider - -
-

- Please select the provider from the dropdown below or start typing for - find it -

- - -
-
-
+ @if (providerOptionsCount() > 0) { + + Provider + + + + + } - - Part of Collection - -
-

- Please select the part of collection from the dropdown below or start - typing for find it -

- - -
-
-
-
+ @if (partOfCollectionOptionsCount() > 0) { + + Part of Collection + + + + + } + +
diff --git a/src/app/shared/components/resources/resource-filters/resource-filters.component.scss b/src/app/shared/components/resources/resource-filters/resource-filters.component.scss index 96985d459..e73580030 100644 --- a/src/app/shared/components/resources/resource-filters/resource-filters.component.scss +++ b/src/app/shared/components/resources/resource-filters/resource-filters.component.scss @@ -4,6 +4,5 @@ border: 1px solid var.$grey-2; border-radius: 12px; padding: 0 1.7rem 0 1.7rem; - width: 30%; height: fit-content; } diff --git a/src/app/shared/components/resources/resource-filters/resource-filters.component.ts b/src/app/shared/components/resources/resource-filters/resource-filters.component.ts index 3bba3241c..335f592b1 100644 --- a/src/app/shared/components/resources/resource-filters/resource-filters.component.ts +++ b/src/app/shared/components/resources/resource-filters/resource-filters.component.ts @@ -1,13 +1,54 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + OnInit, +} from '@angular/core'; import { Accordion, AccordionContent, AccordionHeader, AccordionPanel, } from 'primeng/accordion'; -import { AutoComplete, AutoCompleteModule } from 'primeng/autocomplete'; +import { AutoCompleteModule } from 'primeng/autocomplete'; import { ReactiveFormsModule } from '@angular/forms'; import { CreatorsFilterComponent } from '@shared/components/resources/resource-filters/filters/creators/creators-filter.component'; +import { DateCreatedFilterComponent } from '@shared/components/resources/resource-filters/filters/date-created/date-created-filter.component'; +import { SubjectFilterComponent } from '@shared/components/resources/resource-filters/filters/subject/subject-filter.component'; +import { FunderFilterComponent } from '@shared/components/resources/resource-filters/filters/funder/funder-filter.component'; +import { LicenseFilterComponent } from '@shared/components/resources/resource-filters/filters/license-filter/license-filter.component'; +import { ResourceTypeFilterComponent } from '@shared/components/resources/resource-filters/filters/resource-type-filter/resource-type-filter.component'; +import { ProviderFilterComponent } from '@shared/components/resources/resource-filters/filters/provider-filter/provider-filter.component'; +import { PartOfCollectionFilterComponent } from '@shared/components/resources/resource-filters/filters/part-of-collection-filter/part-of-collection-filter.component'; +import { Store } from '@ngxs/store'; +import { ResourceFiltersOptionsSelectors } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.selectors'; +import { GetAllOptions } from '@shared/components/resources/resource-filters/filters/store/resource-filters-options.actions'; +import { InstitutionFilterComponent } from '@shared/components/resources/resource-filters/filters/institution-filter/institution-filter.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { + ResourceFilterLabel, + ResourceFiltersSelectors, + SetCreator, + SetDateCreated, + SetFunder, + SetInstitution, + SetLicense, + SetPartOfCollection, + SetProvider, + SetResourceType, + SetSubject, +} from '@shared/components/resources/resource-filters/store'; +import { + SearchSelectors, + SetResourceTab, + SetSearchText, + SetSortBy, +} from '@osf/features/search/store'; +import { take } from 'rxjs'; +import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; +import { FilterLabels } from '@shared/components/resources/resource-filters/models/filter-labels'; @Component({ selector: 'osf-resource-filters', @@ -16,13 +57,318 @@ import { CreatorsFilterComponent } from '@shared/components/resources/resource-f AccordionContent, AccordionHeader, AccordionPanel, - AutoComplete, AutoCompleteModule, ReactiveFormsModule, CreatorsFilterComponent, + DateCreatedFilterComponent, + SubjectFilterComponent, + FunderFilterComponent, + LicenseFilterComponent, + ResourceTypeFilterComponent, + ProviderFilterComponent, + PartOfCollectionFilterComponent, + InstitutionFilterComponent, ], templateUrl: './resource-filters.component.html', styleUrl: './resource-filters.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ResourceFiltersComponent {} +export class ResourceFiltersComponent implements OnInit { + readonly #store = inject(Store); + readonly #router = inject(Router); + readonly #activeRoute = inject(ActivatedRoute); + + readonly datesOptionsCount = computed(() => { + return this.#store + .selectSignal(ResourceFiltersOptionsSelectors.getDatesCreated)() + .reduce((accumulator, date) => accumulator + date.count, 0); + }); + + readonly funderOptionsCount = computed(() => + this.#store + .selectSignal(ResourceFiltersOptionsSelectors.getFunders)() + .reduce((acc, item) => acc + item.count, 0), + ); + + readonly subjectOptionsCount = computed(() => + this.#store + .selectSignal(ResourceFiltersOptionsSelectors.getSubjects)() + .reduce((acc, item) => acc + item.count, 0), + ); + + readonly licenseOptionsCount = computed(() => + this.#store + .selectSignal(ResourceFiltersOptionsSelectors.getLicenses)() + .reduce((acc, item) => acc + item.count, 0), + ); + + readonly resourceTypeOptionsCount = computed(() => + this.#store + .selectSignal(ResourceFiltersOptionsSelectors.getResourceTypes)() + .reduce((acc, item) => acc + item.count, 0), + ); + + readonly institutionOptionsCount = computed(() => + this.#store + .selectSignal(ResourceFiltersOptionsSelectors.getInstitutions)() + .reduce((acc, item) => acc + item.count, 0), + ); + + readonly providerOptionsCount = computed(() => + this.#store + .selectSignal(ResourceFiltersOptionsSelectors.getProviders)() + .reduce((acc, item) => acc + item.count, 0), + ); + + readonly partOfCollectionOptionsCount = computed(() => + this.#store + .selectSignal(ResourceFiltersOptionsSelectors.getPartOfCollection)() + .reduce((acc, item) => acc + item.count, 0), + ); + + creatorSelected = this.#store.selectSignal( + ResourceFiltersSelectors.getCreator, + ); + dateCreatedSelected = this.#store.selectSignal( + ResourceFiltersSelectors.getDateCreated, + ); + funderSelected = this.#store.selectSignal(ResourceFiltersSelectors.getFunder); + subjectSelected = this.#store.selectSignal( + ResourceFiltersSelectors.getSubject, + ); + licenseSelected = this.#store.selectSignal( + ResourceFiltersSelectors.getLicense, + ); + resourceTypeSelected = this.#store.selectSignal( + ResourceFiltersSelectors.getResourceType, + ); + institutionSelected = this.#store.selectSignal( + ResourceFiltersSelectors.getInstitution, + ); + providerSelected = this.#store.selectSignal( + ResourceFiltersSelectors.getProvider, + ); + partOfCollectionSelected = this.#store.selectSignal( + ResourceFiltersSelectors.getPartOfCollection, + ); + sortSelected = this.#store.selectSignal(SearchSelectors.getSortBy); + searchInput = this.#store.selectSignal(SearchSelectors.getSearchText); + resourceTabSelected = this.#store.selectSignal( + SearchSelectors.getResourceTab, + ); + + ngOnInit() { + // set all query parameters from route to store when page is loaded + this.#activeRoute.queryParamMap.pipe(take(1)).subscribe((params) => { + const activeFilters = params.get('activeFilters'); + const filters = activeFilters ? JSON.parse(activeFilters) : []; + const sortBy = params.get('sortBy'); + const search = params.get('search'); + const resourceTab = params.get('resourceTab'); + + const creator = filters.find( + (p: ResourceFilterLabel) => p.filterName === FilterLabels.creator, + ); + const dateCreated = filters.find( + (p: ResourceFilterLabel) => p.filterName === 'DateCreated', + ); + const funder = filters.find( + (p: ResourceFilterLabel) => p.filterName === FilterLabels.funder, + ); + const subject = filters.find( + (p: ResourceFilterLabel) => p.filterName === FilterLabels.subject, + ); + const license = filters.find( + (p: ResourceFilterLabel) => p.filterName === FilterLabels.license, + ); + const resourceType = filters.find( + (p: ResourceFilterLabel) => p.filterName === 'ResourceType', + ); + const institution = filters.find( + (p: ResourceFilterLabel) => p.filterName === FilterLabels.institution, + ); + const provider = filters.find( + (p: ResourceFilterLabel) => p.filterName === FilterLabels.provider, + ); + const partOfCollection = filters.find( + (p: ResourceFilterLabel) => p.filterName === 'PartOfCollection', + ); + + if (creator) { + this.#store.dispatch(new SetCreator(creator.label, creator.value)); + } + if (dateCreated) { + this.#store.dispatch(new SetDateCreated(dateCreated.value)); + } + if (funder) { + this.#store.dispatch(new SetFunder(funder.label, funder.value)); + } + if (subject) { + this.#store.dispatch(new SetSubject(subject.label, subject.value)); + } + if (license) { + this.#store.dispatch(new SetLicense(license.label, license.value)); + } + if (resourceType) { + this.#store.dispatch( + new SetResourceType(resourceType.label, resourceType.value), + ); + } + if (institution) { + this.#store.dispatch( + new SetInstitution(institution.label, institution.value), + ); + } + if (provider) { + this.#store.dispatch(new SetProvider(provider.label, provider.value)); + } + if (partOfCollection) { + this.#store.dispatch( + new SetPartOfCollection( + partOfCollection.label, + partOfCollection.value, + ), + ); + } + + if (sortBy) { + this.#store.dispatch(new SetSortBy(sortBy)); + } + if (search) { + this.#store.dispatch(new SetSearchText(search)); + } + if (resourceTab) { + this.#store.dispatch(new SetResourceTab(+resourceTab)); + } + + this.#store.dispatch(GetAllOptions); + }); + } + + constructor() { + // if new value for some filter was put in store, add it to route + effect(() => this.syncFilterToQuery('Creator', this.creatorSelected())); + effect(() => + this.syncFilterToQuery('DateCreated', this.dateCreatedSelected()), + ); + effect(() => this.syncFilterToQuery('Funder', this.funderSelected())); + effect(() => this.syncFilterToQuery('Subject', this.subjectSelected())); + effect(() => this.syncFilterToQuery('License', this.licenseSelected())); + effect(() => + this.syncFilterToQuery('ResourceType', this.resourceTypeSelected()), + ); + effect(() => + this.syncFilterToQuery('Institution', this.institutionSelected()), + ); + effect(() => this.syncFilterToQuery('Provider', this.providerSelected())); + effect(() => + this.syncFilterToQuery( + 'PartOfCollection', + this.partOfCollectionSelected(), + ), + ); + effect(() => this.syncSortingToQuery(this.sortSelected())); + effect(() => this.syncSearchToQuery(this.searchInput())); + effect(() => this.syncResourceTabToQuery(this.resourceTabSelected())); + } + + syncFilterToQuery(filterName: string, filterValue: ResourceFilterLabel) { + const paramMap = this.#activeRoute.snapshot.queryParamMap; + const currentParams = { ...this.#activeRoute.snapshot.queryParams }; + + // Read existing parameters + const currentFiltersRaw = paramMap.get('activeFilters'); + + let filters: ResourceFilterLabel[] = []; + + try { + filters = currentFiltersRaw + ? (JSON.parse(currentFiltersRaw) as ResourceFilterLabel[]) + : []; + } catch (e) { + console.error('Invalid activeFilters format in query params', e); + } + + const index = filters.findIndex((f) => f.filterName === filterName); + + const hasValue = !!filterValue?.value; + + // Update activeFilters array + if (!hasValue && index !== -1) { + filters.splice(index, 1); + } else if (hasValue && filterValue?.label && filterValue.value) { + const newFilter = { + filterName, + label: filterValue.label, + value: filterValue.value, + }; + + if (index !== -1) { + filters[index] = newFilter; + } else { + filters.push(newFilter); + } + } + + if (filters.length > 0) { + currentParams['activeFilters'] = JSON.stringify(filters); + } else { + delete currentParams['activeFilters']; + } + + // Navigation + this.#router.navigate([], { + relativeTo: this.#activeRoute, + queryParams: currentParams, + replaceUrl: true, + }); + } + + syncSortingToQuery(sortBy: string) { + const currentParams = { ...this.#activeRoute.snapshot.queryParams }; + + if (sortBy && sortBy !== '-relevance') { + currentParams['sortBy'] = sortBy; + } else if (sortBy && sortBy === '-relevance') { + delete currentParams['sortBy']; + } + + this.#router.navigate([], { + relativeTo: this.#activeRoute, + queryParams: currentParams, + replaceUrl: true, + }); + } + + syncSearchToQuery(search: string) { + const currentParams = { ...this.#activeRoute.snapshot.queryParams }; + + if (search) { + currentParams['search'] = search; + } else { + delete currentParams['search']; + } + + this.#router.navigate([], { + relativeTo: this.#activeRoute, + queryParams: currentParams, + replaceUrl: true, + }); + } + + syncResourceTabToQuery(resourceTab: ResourceTab) { + const currentParams = { ...this.#activeRoute.snapshot.queryParams }; + + if (resourceTab) { + currentParams['resourceTab'] = resourceTab; + } else { + delete currentParams['resourceTab']; + } + + this.#router.navigate([], { + relativeTo: this.#activeRoute, + queryParams: currentParams, + replaceUrl: true, + }); + } +} diff --git a/src/app/shared/components/resources/resource-filters/resource-filters.service.ts b/src/app/shared/components/resources/resource-filters/resource-filters.service.ts index f9696688c..6c1c2ea30 100644 --- a/src/app/shared/components/resources/resource-filters/resource-filters.service.ts +++ b/src/app/shared/components/resources/resource-filters/resource-filters.service.ts @@ -9,38 +9,241 @@ import { CreatorItem } from '@shared/components/resources/resource-filters/model import { JsonApiService } from '@core/services/json-api/json-api.service'; import { MapCreators } from '@shared/components/resources/resource-filters/mappers/creators/creators.mappers'; import { Creator } from '@shared/components/resources/resource-filters/models/creator/creator.entity'; +import { IndexValueSearch } from '@shared/components/resources/resource-filters/models/index-value-search.entity'; +import { MapDateCreated } from '@shared/components/resources/resource-filters/mappers/dateCreated/date-created.mapper'; +import { DateCreated } from '@shared/components/resources/resource-filters/models/dateCreated/date-created.entity'; +import { SubjectFilter } from '@shared/components/resources/resource-filters/models/subject/subject-filter.entity'; +import { MapSubject } from '@shared/components/resources/resource-filters/mappers/subject/subject.mapper'; +import { FunderIndexValueSearch } from '@shared/components/resources/resource-filters/models/funder/funder-index-value-search.entity'; +import { FunderFilter } from '@shared/components/resources/resource-filters/models/funder/funder-filter.entity'; +import { MapFunders } from '@shared/components/resources/resource-filters/mappers/funder/funder.mapper'; +import { LicenseFilter } from '@shared/components/resources/resource-filters/models/license/license-filter.entity'; +import { LicenseIndexValueSearch } from '@shared/components/resources/resource-filters/models/license/license-index-value-search.entity'; +import { MapLicenses } from '@shared/components/resources/resource-filters/mappers/license/license.mapper'; +import { ResourceTypeIndexValueSearch } from '@shared/components/resources/resource-filters/models/resource-type/resource-type-index-value-search.entity'; +import { ResourceTypeFilter } from '@shared/components/resources/resource-filters/models/resource-type/resource-type.entity'; +import { MapResourceType } from '@shared/components/resources/resource-filters/mappers/resource-type/resource-type.mapper'; +import { ProviderFilter } from '@shared/components/resources/resource-filters/models/provider/provider-filter.entity'; +import { MapProviders } from '@shared/components/resources/resource-filters/mappers/provider/provider.mapper'; +import { ProviderIndexValueSearch } from '@shared/components/resources/resource-filters/models/provider/provider-index-value-search.entity'; +import { PartOfCollectionFilter } from '@shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-filter.entity'; +import { MapPartOfCollections } from '@shared/components/resources/resource-filters/mappers/part-of-collection/part-of-collection.mapper'; +import { PartOfCollectionIndexValueSearch } from '@shared/components/resources/resource-filters/models/part-of-collection/part-of-collection-index-value-search.entity'; +import { Store } from '@ngxs/store'; +import { addFiltersParams } from '@shared/components/resources/resource-filters/utils/add-filters-params.helper'; +import { ResourceFiltersSelectors } from '@shared/components/resources/resource-filters/store'; +import { SearchSelectors } from '@osf/features/search/store'; +import { getResourceTypes } from '@osf/features/search/utils/helpers/get-resource-types.helper'; +import { InstitutionIndexValueSearch } from '@shared/components/resources/resource-filters/models/institution/institution-index-value-search.entity'; +import { MapInstitutions } from '@shared/components/resources/resource-filters/mappers/institution/institution.mapper'; @Injectable({ providedIn: 'root', }) export class ResourceFiltersService { #jsonApiService = inject(JsonApiService); + #store = inject(Store); + + #getFilterParams(): Record { + return addFiltersParams( + this.#store.selectSignal(ResourceFiltersSelectors.getAllFilters)(), + ); + } + + #getParams(): Record { + const params: Record = {}; + const resourceTab = this.#store.selectSnapshot( + SearchSelectors.getResourceTab, + ); + const resourceTypes = getResourceTypes(resourceTab); + const searchText = this.#store.selectSnapshot( + SearchSelectors.getSearchText, + ); + const sort = this.#store.selectSnapshot(SearchSelectors.getSortBy); + + params['cardSearchFilter[resourceType]'] = resourceTypes; + params['cardSearchFilter[accessService]'] = 'https://staging4.osf.io/'; + params['cardSearchText[*,creator.name,isContainedBy.creator.name]'] = + searchText; + params['page[size]'] = '10'; + params['sort'] = sort; + return params; + } getCreators(valueSearchText: string): Observable { - const params = { - 'cardSearchFilter[resourceType]': - 'Registration,RegistrationComponent,Project,ProjectComponent,Preprint,Agent,File', - 'cardSearchFilter[accessService]': 'https://staging4.osf.io/', - 'cardSearchText[*,creator.name,isContainedBy.creator.name]': '', - 'page[size]': '20', - sort: '-relevance', + const dynamicParams = { valueSearchPropertyPath: 'creator', - valueSearchText: valueSearchText, + valueSearchText, + }; + + const fullParams = { + ...this.#getParams(), + ...this.#getFilterParams(), + ...dynamicParams, }; return this.#jsonApiService .get< JsonApiResponse< null, - ApiData<{ resourceMetadata: CreatorItem }, null>[] + ApiData<{ resourceMetadata: CreatorItem }, null, null>[] > - >(`${environment.shareDomainUrl}/index-value-search`, params) + >(`${environment.shareDomainUrl}/index-value-search`, fullParams) .pipe( - map((response) => - response.included + map((response) => { + const included = (response?.included ?? []) as ApiData< + { resourceMetadata: CreatorItem }, + null, + null + >[]; + return included .filter((item) => item.type === 'index-card') - .map((item) => MapCreators(item.attributes.resourceMetadata)), - ), + .map((item) => MapCreators(item.attributes.resourceMetadata)); + }), ); } + + getDates(): Observable { + const dynamicParams = { + valueSearchPropertyPath: 'dateCreated', + }; + + const fullParams = { + ...this.#getParams(), + ...this.#getFilterParams(), + ...dynamicParams, + }; + + return this.#jsonApiService + .get< + JsonApiResponse + >(`${environment.shareDomainUrl}/index-value-search`, fullParams) + .pipe(map((response) => MapDateCreated(response?.included ?? []))); + } + + getFunders(): Observable { + const dynamicParams = { + valueSearchPropertyPath: 'funder', + }; + + const fullParams = { + ...this.#getParams(), + ...this.#getFilterParams(), + ...dynamicParams, + }; + + return this.#jsonApiService + .get< + JsonApiResponse + >(`${environment.shareDomainUrl}/index-value-search`, fullParams) + .pipe(map((response) => MapFunders(response?.included ?? []))); + } + + getSubjects(): Observable { + const dynamicParams = { + valueSearchPropertyPath: 'subject', + }; + + const fullParams = { + ...this.#getParams(), + ...this.#getFilterParams(), + ...dynamicParams, + }; + + return this.#jsonApiService + .get< + JsonApiResponse + >(`${environment.shareDomainUrl}/index-value-search`, fullParams) + .pipe(map((response) => MapSubject(response?.included ?? []))); + } + + getLicenses(): Observable { + const dynamicParams = { + valueSearchPropertyPath: 'rights', + }; + + const fullParams = { + ...this.#getParams(), + ...this.#getFilterParams(), + ...dynamicParams, + }; + + return this.#jsonApiService + .get< + JsonApiResponse + >(`${environment.shareDomainUrl}/index-value-search`, fullParams) + .pipe(map((response) => MapLicenses(response?.included ?? []))); + } + + getResourceTypes(): Observable { + const dynamicParams = { + valueSearchPropertyPath: 'resourceNature', + }; + + const fullParams = { + ...this.#getParams(), + ...this.#getFilterParams(), + ...dynamicParams, + }; + + return this.#jsonApiService + .get< + JsonApiResponse + >(`${environment.shareDomainUrl}/index-value-search`, fullParams) + .pipe(map((response) => MapResourceType(response?.included ?? []))); + } + + getInstitutions(): Observable { + const dynamicParams = { + valueSearchPropertyPath: 'affiliation', + }; + + const fullParams = { + ...this.#getParams(), + ...this.#getFilterParams(), + ...dynamicParams, + }; + + return this.#jsonApiService + .get< + JsonApiResponse + >(`${environment.shareDomainUrl}/index-value-search`, fullParams) + .pipe(map((response) => MapInstitutions(response?.included ?? []))); + } + + getProviders(): Observable { + const dynamicParams = { + valueSearchPropertyPath: 'publisher', + }; + + const fullParams = { + ...this.#getParams(), + ...this.#getFilterParams(), + ...dynamicParams, + }; + + return this.#jsonApiService + .get< + JsonApiResponse + >(`${environment.shareDomainUrl}/index-value-search`, fullParams) + .pipe(map((response) => MapProviders(response?.included ?? []))); + } + + getPartOtCollections(): Observable { + const dynamicParams = { + valueSearchPropertyPath: 'isPartOfCollection', + }; + + const fullParams = { + ...this.#getParams(), + ...this.#getFilterParams(), + ...dynamicParams, + }; + + return this.#jsonApiService + .get< + JsonApiResponse + >(`${environment.shareDomainUrl}/index-value-search`, fullParams) + .pipe(map((response) => MapPartOfCollections(response?.included ?? []))); + } } diff --git a/src/app/shared/components/resources/resource-filters/store/resource-filters.actions.ts b/src/app/shared/components/resources/resource-filters/store/resource-filters.actions.ts index a4b8ab400..547df8280 100644 --- a/src/app/shared/components/resources/resource-filters/store/resource-filters.actions.ts +++ b/src/app/shared/components/resources/resource-filters/store/resource-filters.actions.ts @@ -1,46 +1,68 @@ -import { ResourceType } from '@osf/features/search/models/resource-type.enum'; - export class SetCreator { static readonly type = '[Resource Filters] Set Creator'; - constructor(public payload: string) {} + constructor( + public name: string, + public id: string, + ) {} } export class SetDateCreated { static readonly type = '[Resource Filters] Set DateCreated'; - constructor(public payload: Date) {} + constructor(public date: string) {} } export class SetFunder { static readonly type = '[Resource Filters] Set Funder'; - constructor(public payload: string) {} + constructor( + public funder: string, + public id: string, + ) {} } export class SetSubject { static readonly type = '[Resource Filters] Set Subject'; - constructor(public payload: string) {} + constructor( + public subject: string, + public id: string, + ) {} } export class SetLicense { static readonly type = '[Resource Filters] Set License'; - constructor(public payload: string) {} + constructor( + public license: string, + public id: string, + ) {} } export class SetResourceType { static readonly type = '[Resource Filters] Set Resource Type'; - constructor(public payload: ResourceType) {} + constructor( + public resourceType: string, + public id: string, + ) {} } export class SetInstitution { static readonly type = '[Resource Filters] Set Institution'; - constructor(public payload: string) {} + constructor( + public institution: string, + public id: string, + ) {} } export class SetProvider { static readonly type = '[Resource Filters] Set Provider'; - constructor(public payload: string) {} + constructor( + public provider: string, + public id: string, + ) {} } export class SetPartOfCollection { static readonly type = '[Resource Filters] Set PartOfCollection'; - constructor(public payload: string) {} + constructor( + public partOfCollection: string, + public id: string, + ) {} } diff --git a/src/app/shared/components/resources/resource-filters/store/resource-filters.model.ts b/src/app/shared/components/resources/resource-filters/store/resource-filters.model.ts index 7da4e749b..a8d2ce815 100644 --- a/src/app/shared/components/resources/resource-filters/store/resource-filters.model.ts +++ b/src/app/shared/components/resources/resource-filters/store/resource-filters.model.ts @@ -1,13 +1,17 @@ -import { ResourceType } from '@osf/features/search/models/resource-type.enum'; - export interface ResourceFiltersStateModel { - creator: string; - dateCreated: Date; - funder: string; - subject: string; - license: string; - resourceType: ResourceType; - institution: string; - provider: string; - partOfCollection: string; + creator: ResourceFilterLabel; + dateCreated: ResourceFilterLabel; + funder: ResourceFilterLabel; + subject: ResourceFilterLabel; + license: ResourceFilterLabel; + resourceType: ResourceFilterLabel; + institution: ResourceFilterLabel; + provider: ResourceFilterLabel; + partOfCollection: ResourceFilterLabel; +} + +export interface ResourceFilterLabel { + filterName: string; + label?: string; + value?: string; } diff --git a/src/app/shared/components/resources/resource-filters/store/resource-filters.selectors.ts b/src/app/shared/components/resources/resource-filters/store/resource-filters.selectors.ts index 64b0f654f..6d6ce979e 100644 --- a/src/app/shared/components/resources/resource-filters/store/resource-filters.selectors.ts +++ b/src/app/shared/components/resources/resource-filters/store/resource-filters.selectors.ts @@ -1,51 +1,74 @@ import { ResourceFiltersState } from '@shared/components/resources/resource-filters/store/resource-filters.state'; import { Selector } from '@ngxs/store'; -import { ResourceFiltersStateModel } from '@shared/components/resources/resource-filters/store/resource-filters.model'; -import { ResourceType } from '@osf/features/search/models/resource-type.enum'; +import { + ResourceFilterLabel, + ResourceFiltersStateModel, +} from '@shared/components/resources/resource-filters/store/resource-filters.model'; export class ResourceFiltersSelectors { @Selector([ResourceFiltersState]) - static getCreator(state: ResourceFiltersStateModel): string { + static getAllFilters( + state: ResourceFiltersStateModel, + ): ResourceFiltersStateModel { + return { + creator: state.creator, + dateCreated: state.dateCreated, + funder: state.funder, + subject: state.subject, + license: state.license, + resourceType: state.resourceType, + institution: state.institution, + provider: state.provider, + partOfCollection: state.partOfCollection, + }; + } + + @Selector([ResourceFiltersState]) + static getCreator(state: ResourceFiltersStateModel): ResourceFilterLabel { return state.creator; } @Selector([ResourceFiltersState]) - static getDateCreated(state: ResourceFiltersStateModel): Date { + static getDateCreated(state: ResourceFiltersStateModel): ResourceFilterLabel { return state.dateCreated; } @Selector([ResourceFiltersState]) - static getFunder(state: ResourceFiltersStateModel): string { + static getFunder(state: ResourceFiltersStateModel): ResourceFilterLabel { return state.funder; } @Selector([ResourceFiltersState]) - static getSubject(state: ResourceFiltersStateModel): string { + static getSubject(state: ResourceFiltersStateModel): ResourceFilterLabel { return state.subject; } @Selector([ResourceFiltersState]) - static getLicense(state: ResourceFiltersStateModel): string { + static getLicense(state: ResourceFiltersStateModel): ResourceFilterLabel { return state.license; } @Selector([ResourceFiltersState]) - static getResourceType(state: ResourceFiltersStateModel): ResourceType { + static getResourceType( + state: ResourceFiltersStateModel, + ): ResourceFilterLabel { return state.resourceType; } @Selector([ResourceFiltersState]) - static getInstitution(state: ResourceFiltersStateModel): string { + static getInstitution(state: ResourceFiltersStateModel): ResourceFilterLabel { return state.institution; } @Selector([ResourceFiltersState]) - static getProvider(state: ResourceFiltersStateModel): string { + static getProvider(state: ResourceFiltersStateModel): ResourceFilterLabel { return state.provider; } @Selector([ResourceFiltersState]) - static getPartOfCollection(state: ResourceFiltersStateModel): string { + static getPartOfCollection( + state: ResourceFiltersStateModel, + ): ResourceFilterLabel { return state.partOfCollection; } } diff --git a/src/app/shared/components/resources/resource-filters/store/resource-filters.state.ts b/src/app/shared/components/resources/resource-filters/store/resource-filters.state.ts index 0880cdf89..9c84ca3ad 100644 --- a/src/app/shared/components/resources/resource-filters/store/resource-filters.state.ts +++ b/src/app/shared/components/resources/resource-filters/store/resource-filters.state.ts @@ -1,5 +1,7 @@ import { ResourceFiltersStateModel } from '@shared/components/resources/resource-filters/store/resource-filters.model'; import { Action, State, StateContext } from '@ngxs/store'; + +import { Injectable } from '@angular/core'; import { SetCreator, SetDateCreated, @@ -11,28 +13,70 @@ import { SetResourceType, SetSubject, } from '@shared/components/resources/resource-filters/store/resource-filters.actions'; -import { ResourceType } from '@osf/features/search/models/resource-type.enum'; -import { Injectable } from '@angular/core'; +import { FilterLabels } from '@shared/components/resources/resource-filters/models/filter-labels'; +// Store for user selected filters values @State({ name: 'resourceFilters', defaults: { - creator: '', - dateCreated: new Date(), - funder: '', - subject: '', - license: '', - resourceType: ResourceType.Null, - institution: '', - provider: '', - partOfCollection: '', + creator: { + filterName: FilterLabels.creator, + label: undefined, + value: undefined, + }, + dateCreated: { + filterName: FilterLabels.dateCreated, + label: undefined, + value: undefined, + }, + funder: { + filterName: FilterLabels.funder, + label: undefined, + value: undefined, + }, + subject: { + filterName: FilterLabels.subject, + label: undefined, + value: undefined, + }, + license: { + filterName: FilterLabels.license, + label: undefined, + value: undefined, + }, + resourceType: { + filterName: FilterLabels.resourceType, + label: undefined, + value: undefined, + }, + institution: { + filterName: FilterLabels.institution, + label: undefined, + value: undefined, + }, + provider: { + filterName: FilterLabels.provider, + label: undefined, + value: undefined, + }, + partOfCollection: { + filterName: FilterLabels.partOfCollection, + label: undefined, + value: undefined, + }, }, }) @Injectable() export class ResourceFiltersState { @Action(SetCreator) setCreator(ctx: StateContext, action: SetCreator) { - ctx.patchState({ creator: action.payload }); + ctx.patchState({ + creator: { + filterName: FilterLabels.creator, + label: action.name, + value: action.id, + }, + }); } @Action(SetDateCreated) @@ -40,22 +84,46 @@ export class ResourceFiltersState { ctx: StateContext, action: SetDateCreated, ) { - ctx.patchState({ dateCreated: action.payload }); + ctx.patchState({ + dateCreated: { + filterName: FilterLabels.dateCreated, + label: action.date, + value: action.date, + }, + }); } @Action(SetFunder) setFunder(ctx: StateContext, action: SetFunder) { - ctx.patchState({ funder: action.payload }); + ctx.patchState({ + funder: { + filterName: FilterLabels.funder, + label: action.funder, + value: action.id, + }, + }); } @Action(SetSubject) setSubject(ctx: StateContext, action: SetSubject) { - ctx.patchState({ subject: action.payload }); + ctx.patchState({ + subject: { + filterName: FilterLabels.subject, + label: action.subject, + value: action.id, + }, + }); } @Action(SetLicense) setLicense(ctx: StateContext, action: SetLicense) { - ctx.patchState({ license: action.payload }); + ctx.patchState({ + license: { + filterName: FilterLabels.license, + label: action.license, + value: action.id, + }, + }); } @Action(SetResourceType) @@ -63,7 +131,13 @@ export class ResourceFiltersState { ctx: StateContext, action: SetResourceType, ) { - ctx.patchState({ resourceType: action.payload }); + ctx.patchState({ + resourceType: { + filterName: FilterLabels.resourceType, + label: action.resourceType, + value: action.id, + }, + }); } @Action(SetInstitution) @@ -71,7 +145,13 @@ export class ResourceFiltersState { ctx: StateContext, action: SetInstitution, ) { - ctx.patchState({ institution: action.payload }); + ctx.patchState({ + institution: { + filterName: FilterLabels.institution, + label: action.institution, + value: action.id, + }, + }); } @Action(SetProvider) @@ -79,7 +159,13 @@ export class ResourceFiltersState { ctx: StateContext, action: SetProvider, ) { - ctx.patchState({ provider: action.payload }); + ctx.patchState({ + provider: { + filterName: FilterLabels.provider, + label: action.provider, + value: action.id, + }, + }); } @Action(SetPartOfCollection) @@ -87,6 +173,12 @@ export class ResourceFiltersState { ctx: StateContext, action: SetPartOfCollection, ) { - ctx.patchState({ partOfCollection: action.payload }); + ctx.patchState({ + partOfCollection: { + filterName: FilterLabels.partOfCollection, + label: action.partOfCollection, + value: action.id, + }, + }); } } diff --git a/src/app/shared/components/resources/resource-filters/utils/add-filters-params.helper.ts b/src/app/shared/components/resources/resource-filters/utils/add-filters-params.helper.ts new file mode 100644 index 000000000..e60cf5bf0 --- /dev/null +++ b/src/app/shared/components/resources/resource-filters/utils/add-filters-params.helper.ts @@ -0,0 +1,38 @@ +import { ResourceFiltersStateModel } from '@shared/components/resources/resource-filters/store'; + +export function addFiltersParams( + filters: ResourceFiltersStateModel, +): Record { + const params: Record = {}; + + if (filters.creator.value) { + params['cardSearchFilter[creator][]'] = filters.creator.value; + } + if (filters.dateCreated.value) { + params['cardSearchFilter[dateCreated][]'] = filters.dateCreated.value; + } + if (filters.subject.value) { + params['cardSearchFilter[subject][]'] = filters.subject.value; + } + if (filters.funder.value) { + params['cardSearchFilter[funder][]'] = filters.funder.value; + } + if (filters.license.value) { + params['cardSearchFilter[rights][]'] = filters.license.value; + } + if (filters.resourceType.value) { + params['cardSearchFilter[resourceNature][]'] = filters.resourceType.value; + } + if (filters.institution.value) { + params['cardSearchFilter[affiliation][]'] = filters.institution.value; + } + if (filters.provider.value) { + params['cardSearchFilter[publisher][]'] = filters.provider.value; + } + if (filters.partOfCollection.value) { + params['cardSearchFilter[isPartOfCollection][]'] = + filters.partOfCollection.value; + } + + return params; +} diff --git a/src/app/shared/components/resources/resources.component.html b/src/app/shared/components/resources/resources.component.html index 1b25c9098..667cbc7aa 100644 --- a/src/app/shared/components/resources/resources.component.html +++ b/src/app/shared/components/resources/resources.component.html @@ -1,29 +1,92 @@
-

10 000+ results

+ @if (searchCount() > 10000) { +

10 000+ results

+ } @else if (searchCount() > 0) { +

{{ searchCount() }} results

+ } @else { +

0 results

+ } +

Sort by:

- + [(ngModel)]="selectedSort" + >
- - +
+ @if (isAnyFilterSelected()) { + + } + +
+ +
- @for (item of items; track item.id) { - + @if (items.length > 0) { + @for (item of items; track item.id) { + + } + +
+ @if (first() && prev()) { + + + + } + + + + + + + + +
}
diff --git a/src/app/shared/components/resources/resources.component.scss b/src/app/shared/components/resources/resources.component.scss index 4cd392977..5a8586f3d 100644 --- a/src/app/shared/components/resources/resources.component.scss +++ b/src/app/shared/components/resources/resources.component.scss @@ -17,6 +17,13 @@ } } + .filters-container { + display: flex; + flex-direction: column; + width: 30%; + row-gap: 0.8rem; + } + .resources-container { width: 70%; @@ -26,5 +33,20 @@ flex-direction: column; row-gap: 0.85rem; } + + .switch-icon { + &:hover { + cursor: pointer; + } + } + + .icon-disabled { + opacity: 0.5; + cursor: none; + } + + .icon-active { + fill: var.$grey-1; + } } } diff --git a/src/app/shared/components/resources/resources.component.ts b/src/app/shared/components/resources/resources.component.ts index 5e6106dd4..1dde54021 100644 --- a/src/app/shared/components/resources/resources.component.ts +++ b/src/app/shared/components/resources/resources.component.ts @@ -2,14 +2,14 @@ import { ChangeDetectionStrategy, Component, computed, + effect, inject, input, + signal, + untracked, } from '@angular/core'; -import { TabOption } from '@shared/entities/tab-option.interface'; -import { Resource } from '@osf/features/search/models/resource.entity'; import { toSignal } from '@angular/core/rxjs-interop'; import { IS_XSMALL } from '@shared/utils/breakpoints.tokens'; -import { ResourceType } from '@osf/features/search/models/resource-type.enum'; import { DropdownModule } from 'primeng/dropdown'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ResourceFiltersComponent } from '@shared/components/resources/resource-filters/resource-filters.component'; @@ -18,9 +18,16 @@ import { AccordionModule } from 'primeng/accordion'; import { TableModule } from 'primeng/table'; import { DataViewModule } from 'primeng/dataview'; import { ResourceCardComponent } from '@shared/components/resources/resource-card/resource-card.component'; -import { ResourceFiltersSelectors } from '@shared/components/resources/resource-filters/store/resource-filters.selectors'; import { Store } from '@ngxs/store'; import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; +import { + GetResourcesByLink, + SearchSelectors, + SetSortBy, +} from '@osf/features/search/store'; +import { FilterChipsComponent } from '@shared/components/resources/filter-chips/filter-chips.component'; +import { ResourceFiltersSelectors } from '@shared/components/resources/resource-filters/store'; +import { Select } from 'primeng/select'; @Component({ selector: 'osf-resources', @@ -34,50 +41,76 @@ import { ResourceTab } from '@osf/features/search/models/resource-tab.enum'; TableModule, DataViewModule, ResourceCardComponent, + FilterChipsComponent, + Select, ], templateUrl: './resources.component.html', styleUrl: './resources.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ResourcesComponent { + readonly #store = inject(Store); selectedTab = input.required(); - searchedResources = input.required(); - protected readonly filteredResources = computed(() => { - return this.searchedResources().filter((resources) => { - switch (this.selectedTab()) { - case ResourceTab.Projects: - return ( - resources.resourceType === ResourceType.Project || - resources.resourceType === ResourceType.ProjectComponent - ); - case ResourceTab.Registrations: - return resources.resourceType === ResourceType.Registration; - case ResourceTab.Preprints: - return resources.resourceType === ResourceType.Preprint; - case ResourceTab.Files: - return resources.resourceType === ResourceType.File; - case ResourceTab.Users: - return resources.resourceType === ResourceType.Agent; - default: - return true; - } - }); + searchCount = this.#store.selectSignal(SearchSelectors.getResourcesCount); + resources = this.#store.selectSignal(SearchSelectors.getResources); + sortBy = this.#store.selectSignal(SearchSelectors.getSortBy); + first = this.#store.selectSignal(SearchSelectors.getFirst); + next = this.#store.selectSignal(SearchSelectors.getNext); + prev = this.#store.selectSignal(SearchSelectors.getPrevious); + + protected filters = this.#store.selectSignal( + ResourceFiltersSelectors.getAllFilters, + ); + protected isAnyFilterSelected = computed(() => { + return ( + this.filters().creator.value || + this.filters().dateCreated.value || + this.filters().funder.value || + this.filters().subject.value || + this.filters().license.value || + this.filters().resourceType.value || + this.filters().institution.value || + this.filters().provider.value || + this.filters().partOfCollection.value + ); }); protected readonly isMobile = toSignal(inject(IS_XSMALL)); - defaultTabValue = 0; - protected selectedSortTab = this.defaultTabValue; - protected readonly sortTabOptions: TabOption[] = [ - { label: 'Relevance', value: 0 }, - { label: 'Date created (newest)', value: 1 }, - { label: 'Date created (oldest)', value: 2 }, - { label: 'Date modified (newest)', value: 3 }, - { label: 'Date modified (oldest)', value: 4 }, + protected selectedSort = signal(''); + + protected readonly sortTabOptions = [ + { label: 'Relevance', value: '-relevance' }, + { label: 'Date created (newest)', value: '-dateCreated' }, + { label: 'Date created (oldest)', value: 'dateCreated' }, + { label: 'Date modified (newest)', value: '-dateModified' }, + { label: 'Date modified (oldest)', value: 'dateModified' }, ]; - readonly #store = inject(Store); - protected readonly creator = this.#store.selectSignal( - ResourceFiltersSelectors.getCreator, - ); + constructor() { + // if new value for sorting in store, update value in dropdown + effect(() => { + const storeValue = this.sortBy(); + const currentInput = untracked(() => this.selectedSort()); + + if (storeValue && currentInput !== storeValue) { + this.selectedSort.set(storeValue); + } + }); + + // if the sorting was changed, set new value to store + effect(() => { + const chosenValue = this.selectedSort(); + const storeValue = untracked(() => this.sortBy()); + + if (chosenValue !== storeValue) { + this.#store.dispatch(new SetSortBy(chosenValue)); + } + }); + } + + // pagination + switchPage(link: string) { + this.#store.dispatch(new GetResourcesByLink(link)); + } } diff --git a/src/assets/icons/source/close.svg b/src/assets/icons/source/close.svg index ecb33baa7..ec6576438 100644 --- a/src/assets/icons/source/close.svg +++ b/src/assets/icons/source/close.svg @@ -1,4 +1,4 @@ - + diff --git a/src/assets/icons/source/code-colored.svg b/src/assets/icons/source/code-colored.svg new file mode 100644 index 000000000..7c93dd72a --- /dev/null +++ b/src/assets/icons/source/code-colored.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/source/data-colored.svg b/src/assets/icons/source/data-colored.svg new file mode 100644 index 000000000..2fcc5ca02 --- /dev/null +++ b/src/assets/icons/source/data-colored.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/source/materials-colored.svg b/src/assets/icons/source/materials-colored.svg new file mode 100644 index 000000000..b54edbd1a --- /dev/null +++ b/src/assets/icons/source/materials-colored.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/source/papers-colored.svg b/src/assets/icons/source/papers-colored.svg new file mode 100644 index 000000000..ba354e388 --- /dev/null +++ b/src/assets/icons/source/papers-colored.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/source/supplements-colored.svg b/src/assets/icons/source/supplements-colored.svg new file mode 100644 index 000000000..94bb47334 --- /dev/null +++ b/src/assets/icons/source/supplements-colored.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/system/chevron-left.svg b/src/assets/icons/system/chevron-left.svg new file mode 100644 index 000000000..533060724 --- /dev/null +++ b/src/assets/icons/system/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/system/chevron-right.svg b/src/assets/icons/system/chevron-right.svg new file mode 100644 index 000000000..39f6be076 --- /dev/null +++ b/src/assets/icons/system/chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/system/first.svg b/src/assets/icons/system/first.svg new file mode 100644 index 000000000..b3a1e45a7 --- /dev/null +++ b/src/assets/icons/system/first.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/styles/_variables.scss b/src/assets/styles/_variables.scss index 354ead6ab..7c96b0206 100644 --- a/src/assets/styles/_variables.scss +++ b/src/assets/styles/_variables.scss @@ -55,6 +55,9 @@ $gradient-1: var(--gradient-1); $gradient-2: var(--gradient-2); $gradient-3: var(--gradient-3); +// Opacity Colors +$white-60: var(--white-60); + :root { // Fonts --openSans: "Open Sans", sans-serif; @@ -105,6 +108,9 @@ $gradient-3: var(--gradient-3); --blue-1-bg: #ebf4f7; --blue-2-bg: #ebf2f8; + // Opacity Colors + --white-60: rgba(256, 256, 256, 0.6); + // Gradients --gradient-1: linear-gradient( diff --git a/src/assets/styles/overrides/accordion.scss b/src/assets/styles/overrides/accordion.scss index e5ffab605..85a3d055d 100644 --- a/src/assets/styles/overrides/accordion.scss +++ b/src/assets/styles/overrides/accordion.scss @@ -59,3 +59,20 @@ } } } + +.filters { + .p-accordion { + .p-accordioncontent-content { + padding-bottom: 1.5rem; + .content-body { + display: flex; + flex-direction: column; + row-gap: 1.5rem; + + p { + font-weight: 300; + } + } + } + } +} diff --git a/src/assets/styles/overrides/chip.scss b/src/assets/styles/overrides/chip.scss new file mode 100644 index 000000000..78484ba9f --- /dev/null +++ b/src/assets/styles/overrides/chip.scss @@ -0,0 +1,29 @@ +@use "assets/styles/variables" as var; + +.p-chip { + height: fit-content; + width: fit-content; + background: var.$bg-blue-3; + border-radius: 4px; + padding: 4px 12px 4px 12px; + + &:hover { + cursor: pointer; + background: var.$bg-blue-2; + color: var.$pr-blue-3; + i { + color: var.$pr-blue-1; + } + } + + .p-chip-label { + font-size: 1rem; + font-weight: 400; + line-height: 1.7rem; + } + + .p-chip-remove-icon { + display: flex; + margin-left: 12px; + } +} diff --git a/src/assets/styles/overrides/dropdown.scss b/src/assets/styles/overrides/dropdown.scss index 90e836aaf..bbd7a0baf 100644 --- a/src/assets/styles/overrides/dropdown.scss +++ b/src/assets/styles/overrides/dropdown.scss @@ -20,22 +20,3 @@ } } } - -.no-border-dropdown { - .p-dropdown { - border: none; - font-size: 1rem; - box-shadow: none; - height: 2rem; - - .p-select-label { - padding: 0; - font-weight: 700; - } - - .p-select-overlay { - right: 0; - left: auto; - } - } -} diff --git a/src/assets/styles/overrides/paginator.scss b/src/assets/styles/overrides/paginator.scss index 728bfd5d1..bb42c0ad5 100644 --- a/src/assets/styles/overrides/paginator.scss +++ b/src/assets/styles/overrides/paginator.scss @@ -79,3 +79,50 @@ .p-paginator-prev:not(.p-disabled):hover { background: white; } + +.p-select-overlay { + background: white; + border: 1px solid var.$grey-2; + color: var.$dark-blue-1; +} + +.p-select-option { + &.p-select-option-selected { + background-color: var.$bg-blue-2; + color: var.$dark-blue-1; + + &:hover { + background: var.$bg-blue-3; + color: var.$dark-blue-1; + } + } + + &:not(.p-select-option-selected):not(.p-disabled).p-focus { + background: var.$bg-blue-3; + color: var.$dark-blue-1; + } +} + +p-select { + background: white; + border: 1px solid var.$grey-2; + + span { + color: var.$dark-blue-1; + } +} + +.resources-container { + .p-paginator { + .p-paginator-pages, + .p-paginator-last, + .p-paginator-prev { + display: none; + } + .p-paginator-first, + .p-paginator-next, + .p-paginator-prev { + opacity: 1 !important; + } + } +} diff --git a/src/assets/styles/overrides/select.scss b/src/assets/styles/overrides/select.scss index 0cc4e63f9..76016df54 100644 --- a/src/assets/styles/overrides/select.scss +++ b/src/assets/styles/overrides/select.scss @@ -8,10 +8,10 @@ outline: none; font-size: 16px; color: var.$dark-blue-1; -} -.p-select-overlay { - min-width: 100%; + .p-placeholder { + color: var.$grey-1; + } } .p-select-label { @@ -61,18 +61,45 @@ white-space: nowrap; } } + + .p-select { + border: none; + font-size: 1rem; + box-shadow: none; + height: 2rem; + + .p-select-label { + padding: 0; + font-weight: 700; + } + + .p-select-overlay { + position: absolute; + right: 0; + left: auto; + } + } } .filter { .p-select { - //.p-select-dropdown { - // display: none; - //} - .p-select-label { font-size: 1.2rem; font-weight: 400; color: var.$dark-blue-1; + padding-right: 0.5rem; + } + + .p-select-clear-icon { + right: 1rem; + } + } +} + +.dropdown-filter { + .p-select { + .p-select-clear-icon { + right: 2.2rem; } } } diff --git a/src/assets/styles/overrides/tabs.scss b/src/assets/styles/overrides/tabs.scss index 14a10d441..155348968 100644 --- a/src/assets/styles/overrides/tabs.scss +++ b/src/assets/styles/overrides/tabs.scss @@ -1,5 +1,15 @@ @use "assets/styles/variables" as var; +.p-tab { + padding: 1rem 1.7rem 1rem 1.7rem; + + &:not(.p-tab-active):hover { + background: var.$white-60; + border-top-left-radius: 12px; + border-top-right-radius: 12px; + } +} + .p-tabpanels { padding: 1.71rem; } @@ -24,3 +34,9 @@ display: none; } } + +.resources-tab-panel { + .p-tabpanels { + padding: 0 !important; + } +} diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss index 417cb8182..0f9874834 100644 --- a/src/assets/styles/styles.scss +++ b/src/assets/styles/styles.scss @@ -24,6 +24,7 @@ @use "./overrides/select"; @use "./overrides/accordion"; @use "./overrides/paginator"; +@use "./overrides/chip"; @use "./overrides/tag"; @layer base, primeng, reset; @@ -136,4 +137,9 @@ border-color: var.$grey-1 transparent transparent; } } + + i, + svg { + outline: none; + } }