diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 26e99f4a2..7014a5980 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -4,7 +4,6 @@ import { CollectionsState } from '@osf/features/collections/store'; import { InstitutionsState } from '@osf/features/institutions/store'; import { MeetingsState } from '@osf/features/meetings/store'; import { MyProjectsState } from '@osf/features/my-projects/store'; -import { PreprintsState } from '@osf/features/preprints/store'; import { AnalyticsState } from '@osf/features/project/analytics/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; import { RegistrationsState } from '@osf/features/project/registrations/store'; @@ -35,5 +34,4 @@ export const STATES = [ WikiState, MeetingsState, RegistrationsState, - PreprintsState, ]; diff --git a/src/app/features/home/home.component.html b/src/app/features/home/home.component.html index f032700b9..e0b636867 100644 --- a/src/app/features/home/home.component.html +++ b/src/app/features/home/home.component.html @@ -45,7 +45,7 @@

{{ 'home.loggedIn.latestResearch.title' | translate }}

{{ 'home.loggedIn.latestResearch.subtitle' | translate }}

- +
@@ -54,6 +54,6 @@

{{ 'home.loggedIn.hosting.title' | translate }}

{{ 'home.loggedIn.hosting.subtitle' | translate }}

- + diff --git a/src/app/features/my-profile/components/index.ts b/src/app/features/my-profile/components/index.ts index ebcca0578..45ced79dc 100644 --- a/src/app/features/my-profile/components/index.ts +++ b/src/app/features/my-profile/components/index.ts @@ -1,6 +1,5 @@ export * from './filters'; export { MyProfileFilterChipsComponent } from './my-profile-filter-chips/my-profile-filter-chips.component'; -export { MyProfileResourceCardComponent } from './my-profile-resource-card/my-profile-resource-card.component'; export { MyProfileResourceFiltersComponent } from './my-profile-resource-filters/my-profile-resource-filters.component'; export { MyProfileResourcesComponent } from './my-profile-resources/my-profile-resources.component'; export { MyProfileSearchComponent } from './my-profile-search/my-profile-search.component'; diff --git a/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.html b/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.html deleted file mode 100644 index 9d0040f19..000000000 --- a/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.html +++ /dev/null @@ -1,182 +0,0 @@ -
- - - -
- @if (item()?.resourceType && item()?.resourceType === ResourceType.Agent) { -

User

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

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

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

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

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

- @if (!isSmall()) { - Date created: {{ item()?.dateCreated | date: 'MMMM d, y' }} | Date modified: - {{ item()?.dateModified | date: 'MMMM d, y' }} - } @else { -

-

Date created: {{ item()?.dateCreated | date: 'MMMM d, y' }}

-

- Date modified: - {{ item()?.dateModified | date: 'MMMM d, y' }} -

-
- } -

- } - - @if (item()?.resourceType === ResourceType.Registration) { - - } -
-
- -
- @if (item()?.description) { -

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

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

Registration provider: 

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

License: 

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

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

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

Provider: 

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

Conflict of Interest response: Author asserted no Conflict of Interest

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

URL: 

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

DOI: 

- {{ item()?.doi }} -
- } - - @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()?.education) { -

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

- } -
-
-
-
-
diff --git a/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.scss b/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.scss deleted file mode 100644 index f65fb1607..000000000 --- a/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.scss +++ /dev/null @@ -1 +0,0 @@ -@use "assets/styles/components/resource-card" as resource-card; diff --git a/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.spec.ts b/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.spec.ts deleted file mode 100644 index 58d77776e..000000000 --- a/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { MyProfileResourceCardComponent } from './my-profile-resource-card.component'; - -describe('MyProfileResourceCardComponent', () => { - let component: MyProfileResourceCardComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [MyProfileResourceCardComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(MyProfileResourceCardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.ts b/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.ts deleted file mode 100644 index 73747da2a..000000000 --- a/src/app/features/my-profile/components/my-profile-resource-card/my-profile-resource-card.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; -import { Skeleton } from 'primeng/skeleton'; - -import { finalize } from 'rxjs'; - -import { DatePipe, NgOptimizedImage } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject, model } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { RouterLink } from '@angular/router'; - -import { ResourceType } from '@osf/shared/enums'; -import { Resource } from '@osf/shared/models'; -import { IS_XSMALL } from '@osf/shared/utils'; - -import { ResourceCardService } from '../../services'; - -import { environment } from 'src/environments/environment'; - -@Component({ - selector: 'osf-my-profile-resource-card', - imports: [ - Accordion, - AccordionContent, - AccordionHeader, - AccordionPanel, - DatePipe, - NgOptimizedImage, - Skeleton, - RouterLink, - ], - templateUrl: './my-profile-resource-card.component.html', - styleUrl: './my-profile-resource-card.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MyProfileResourceCardComponent { - item = model(undefined); - readonly #resourceCardService = inject(ResourceCardService); - loading = false; - dataIsLoaded = false; - isSmall = toSignal(inject(IS_XSMALL)); - - 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 - ); - }); - } - } - } - - protected readonly environment = environment; -} diff --git a/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.model.ts b/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.model.ts index e1b20d42a..441399cea 100644 --- a/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.model.ts +++ b/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.model.ts @@ -1,3 +1,5 @@ +import { ResourceFilterLabel } from '@shared/models'; + export interface MyProfileResourceFiltersStateModel { creator: ResourceFilterLabel; dateCreated: ResourceFilterLabel; @@ -9,9 +11,3 @@ export interface MyProfileResourceFiltersStateModel { provider: ResourceFilterLabel; partOfCollection: ResourceFilterLabel; } - -export interface ResourceFilterLabel { - filterName: string; - label?: string; - value?: string; -} diff --git a/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.selectors.ts b/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.selectors.ts index 470e56136..4d7564ab6 100644 --- a/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.selectors.ts +++ b/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.selectors.ts @@ -1,8 +1,8 @@ import { Selector } from '@ngxs/store'; import { ResourceFiltersStateModel } from '@osf/features/search/components/resource-filters/store'; +import { ResourceFilterLabel } from '@shared/models'; -import { ResourceFilterLabel } from './my-profile-resource-filters.model'; import { MyProfileResourceFiltersState } from './my-profile-resource-filters.state'; export class MyProfileResourceFiltersSelectors { diff --git a/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.state.ts b/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.state.ts index e5766bade..6bed76c34 100644 --- a/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.state.ts +++ b/src/app/features/my-profile/components/my-profile-resource-filters/store/my-profile-resource-filters.state.ts @@ -3,7 +3,8 @@ import { Action, NgxsOnInit, State, StateContext, Store } from '@ngxs/store'; import { inject, Injectable } from '@angular/core'; import { UserSelectors } from '@osf/core/store/user'; -import { FilterLabelsModel, resourceFiltersDefaultsModel } from '@osf/shared/models'; +import { FilterLabelsModel } from '@osf/shared/models'; +import { resourceFiltersDefaults } from '@shared/constants'; import { SetCreator, @@ -21,7 +22,7 @@ import { MyProfileResourceFiltersStateModel } from './my-profile-resource-filter // Store for user selected filters values @State({ name: 'myProfileResourceFilters', - defaults: resourceFiltersDefaultsModel, + defaults: resourceFiltersDefaults, }) @Injectable() export class MyProfileResourceFiltersState implements NgxsOnInit { @@ -41,6 +42,7 @@ export class MyProfileResourceFiltersState implements NgxsOnInit { } }); } + @Action(SetCreator) setCreator(ctx: StateContext, action: SetCreator) { ctx.patchState({ diff --git a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html index 6178a6a94..70aa971f4 100644 --- a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html +++ b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.html @@ -1,7 +1,7 @@
@if (isMobile()) { - + } @if (searchCount() > 10000) {

10 000+ results

@@ -16,7 +16,7 @@

0 results

@if (isWeb()) {

Sort by:

Sort by:
} @else if (isSortingOpen()) {
- @for (option of sortTabOptions; track option.value) { + @for (option of searchSortingOptions; track option.value) {
Sort by: } - +
@if (items.length > 0) { @for (item of items; track item.id) { - + }
@@ -144,6 +144,6 @@

Sort by:

}
- +
} diff --git a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.ts b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.ts index e4fe535f2..1ac8619b9 100644 --- a/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.ts +++ b/src/app/features/my-profile/components/my-profile-resources/my-profile-resources.component.ts @@ -8,14 +8,14 @@ import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, u import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; +import { MyProfileFilterChipsComponent, MyProfileResourceFiltersComponent } from '@osf/features/my-profile/components'; import { ResourceTab } from '@osf/shared/enums'; import { IS_WEB, IS_XSMALL } from '@osf/shared/utils'; +import { ResourceCardComponent } from '@shared/components/resource-card/resource-card.component'; +import { searchSortingOptions } from '@shared/constants'; import { GetResourcesByLink, MyProfileSelectors, SetResourceTab, SetSortBy } from '../../store'; import { MyProfileResourceFiltersOptionsSelectors } from '../filters/store'; -import { MyProfileFilterChipsComponent } from '../my-profile-filter-chips/my-profile-filter-chips.component'; -import { MyProfileResourceCardComponent } from '../my-profile-resource-card/my-profile-resource-card.component'; -import { MyProfileResourceFiltersComponent } from '../my-profile-resource-filters/my-profile-resource-filters.component'; import { MyProfileResourceFiltersSelectors } from '../my-profile-resource-filters/store'; @Component({ @@ -24,10 +24,10 @@ import { MyProfileResourceFiltersSelectors } from '../my-profile-resource-filter DataView, MyProfileFilterChipsComponent, NgOptimizedImage, - MyProfileResourceCardComponent, MyProfileResourceFiltersComponent, Select, FormsModule, + ResourceCardComponent, ], templateUrl: './my-profile-resources.component.html', styleUrl: './my-profile-resources.component.scss', @@ -35,6 +35,7 @@ import { MyProfileResourceFiltersSelectors } from '../my-profile-resource-filter }) export class MyProfileResourcesComponent { readonly #store = inject(Store); + protected readonly searchSortingOptions = searchSortingOptions; selectedTabStore = this.#store.selectSignal(MyProfileSelectors.getResourceTab); searchCount = this.#store.selectSignal(MyProfileSelectors.getResourcesCount); @@ -79,13 +80,6 @@ export class MyProfileResourcesComponent { protected readonly isMobile = toSignal(inject(IS_XSMALL)); 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' }, - ]; protected selectedTab = signal(ResourceTab.All); protected readonly tabsOptions = [ diff --git a/src/app/features/my-profile/services/index.ts b/src/app/features/my-profile/services/index.ts index a5c40bd4c..4eb8401b2 100644 --- a/src/app/features/my-profile/services/index.ts +++ b/src/app/features/my-profile/services/index.ts @@ -1,2 +1 @@ -export { ResourceCardService } from './my-profile-resource-card.service'; export { MyProfileFiltersOptionsService } from './my-profile-resource-filters.service'; diff --git a/src/app/features/my-profile/services/my-profile-resource-card.service.ts b/src/app/features/my-profile/services/my-profile-resource-card.service.ts deleted file mode 100644 index f1dc221a0..000000000 --- a/src/app/features/my-profile/services/my-profile-resource-card.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { map, Observable } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiService } from '@osf/core/services'; -import { MapUserCounts } from '@osf/shared/mappers'; -import { UserCountsResponse, UserRelatedDataCounts } from '@osf/shared/models'; - -import { environment } from 'src/environments/environment'; - -@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/features/my-profile/store/my-profile.state.ts b/src/app/features/my-profile/store/my-profile.state.ts index 50df360e0..3aa8cb977 100644 --- a/src/app/features/my-profile/store/my-profile.state.ts +++ b/src/app/features/my-profile/store/my-profile.state.ts @@ -15,9 +15,9 @@ import { SetSearchText, SetSortBy, } from '@osf/features/my-profile/store'; -import { searchStateDefaults } from '@osf/features/search/utils/data'; import { SearchService } from '@osf/shared/services'; import { addFiltersParams, getResourceTypes } from '@osf/shared/utils'; +import { searchStateDefaults } from '@shared/constants'; import { MyProfileResourceFiltersSelectors } from '../components/my-profile-resource-filters/store'; diff --git a/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.html b/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.html new file mode 100644 index 000000000..43e89df69 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.html @@ -0,0 +1,16 @@ + diff --git a/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.scss b/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.spec.ts b/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.spec.ts new file mode 100644 index 000000000..d9768e298 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.spec.ts @@ -0,0 +1,84 @@ +import { Store } from '@ngxs/store'; + +import { MockProvider } from 'ng-mocks'; + +import { SelectChangeEvent } from 'primeng/select'; + +import { signal } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { + PreprintsResourcesFiltersSelectors, + SetCreator, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { + GetAllOptions, + PreprintsResourcesFiltersOptionsSelectors, +} from '@osf/features/preprints/store/preprints-resources-filters-options'; +import { mockStore } from '@osf/shared/mocks'; +import { Creator } from '@osf/shared/models'; + +import { PreprintsCreatorsFilterComponent } from './preprints-creators-filter.component'; + +describe('CreatorsFilterComponent', () => { + let component: PreprintsCreatorsFilterComponent; + let fixture: ComponentFixture; + + const store = mockStore; + + const mockCreators: Creator[] = [ + { id: '1', name: 'John Doe' }, + { id: '2', name: 'Jane Smith' }, + { id: '3', name: 'Bob Johnson' }, + ]; + + beforeEach(async () => { + store.selectSignal.mockImplementation((selector) => { + if (selector === PreprintsResourcesFiltersOptionsSelectors.getCreators) { + return signal(mockCreators); + } + + if (selector === PreprintsResourcesFiltersSelectors.getCreator) { + return signal({ label: '', value: '' }); + } + + return signal(null); + }); + + await TestBed.configureTestingModule({ + imports: [PreprintsCreatorsFilterComponent], + providers: [MockProvider(Store, store)], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsCreatorsFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize with empty input', () => { + expect(component['creatorsInput']()).toBeNull(); + }); + + it('should show all creators when no search text is entered', () => { + const options = component['creatorsOptions'](); + expect(options.length).toBe(3); + expect(options[0].label).toBe('John Doe'); + expect(options[1].label).toBe('Jane Smith'); + expect(options[2].label).toBe('Bob Johnson'); + }); + + it('should set creator when a valid selection is made', () => { + const event = { + originalEvent: { pointerId: 1 } as unknown as PointerEvent, + value: 'John Doe', + } as SelectChangeEvent; + + component.setCreator(event); + expect(store.dispatch).toHaveBeenCalledWith(new SetCreator('John Doe', '1')); + expect(store.dispatch).toHaveBeenCalledWith(GetAllOptions); + }); +}); diff --git a/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.ts b/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.ts new file mode 100644 index 000000000..2337e2338 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component.ts @@ -0,0 +1,95 @@ +import { Store } from '@ngxs/store'; + +import { Select, SelectChangeEvent } from 'primeng/select'; + +import { debounceTime, distinctUntilChanged, Subject, takeUntil } from 'rxjs'; + +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + OnDestroy, + signal, + untracked, +} from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { + PreprintsResourcesFiltersSelectors, + SetCreator, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { + GetAllOptions, + GetCreatorsOptions, + PreprintsResourcesFiltersOptionsSelectors, +} from '@osf/features/preprints/store/preprints-resources-filters-options'; + +@Component({ + selector: 'osf-preprints-creators-filter', + imports: [Select, ReactiveFormsModule, FormsModule], + templateUrl: './preprints-creators-filter.component.html', + styleUrl: './preprints-creators-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsCreatorsFilterComponent implements OnDestroy { + readonly #store = inject(Store); + + protected searchCreatorsResults = this.#store.selectSignal(PreprintsResourcesFiltersOptionsSelectors.getCreators); + protected creatorsOptions = computed(() => { + return this.searchCreatorsResults().map((creator) => ({ + label: creator.name, + id: creator.id, + })); + }); + protected creatorState = this.#store.selectSignal(PreprintsResourcesFiltersSelectors.getCreator); + readonly #unsubscribe = new Subject(); + protected creatorsInput = signal(null); + protected initialization = true; + + constructor() { + toObservable(this.creatorsInput) + .pipe(debounceTime(500), distinctUntilChanged(), takeUntil(this.#unsubscribe)) + .subscribe((searchText) => { + if (!this.initialization) { + if (searchText) { + this.#store.dispatch(new GetCreatorsOptions(searchText ?? '')); + } + + 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 && 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/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.html b/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.html new file mode 100644 index 000000000..b6188ece4 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.html @@ -0,0 +1,13 @@ + diff --git a/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.scss b/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.spec.ts b/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.spec.ts new file mode 100644 index 000000000..440924c1a --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintsDateCreatedFilterComponent } from './preprints-date-created-filter.component'; + +describe('PreprintsDateCreatedFilterComponent', () => { + let component: PreprintsDateCreatedFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintsDateCreatedFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsDateCreatedFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.ts b/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.ts new file mode 100644 index 000000000..5b7cc5445 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component.ts @@ -0,0 +1,62 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { Select, SelectChangeEvent } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, computed, effect, signal, untracked } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { + PreprintsResourcesFiltersSelectors, + SetDateCreated, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { + GetAllOptions, + PreprintsResourcesFiltersOptionsSelectors, +} from '@osf/features/preprints/store/preprints-resources-filters-options'; + +@Component({ + selector: 'osf-preprints-date-created-filter', + imports: [Select, FormsModule], + templateUrl: './preprints-date-created-filter.component.html', + styleUrl: './preprints-date-created-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsDateCreatedFilterComponent { + private readonly actions = createDispatchMap({ + setDateCreated: SetDateCreated, + getAllOptions: GetAllOptions, + }); + + dateCreatedState = select(PreprintsResourcesFiltersSelectors.getDateCreated); + inputDate = signal(null); + + availableDates = select(PreprintsResourcesFiltersOptionsSelectors.getDatesCreated); + 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) { + return; + } + + this.actions.setDateCreated(event.value); + this.actions.getAllOptions(); + } +} diff --git a/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.html b/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.html new file mode 100644 index 000000000..0f59afc86 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.html @@ -0,0 +1,17 @@ + diff --git a/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.scss b/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.scss new file mode 100644 index 000000000..5fd36a5f1 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.scss @@ -0,0 +1,5 @@ +:host ::ng-deep { + .p-scroller-viewport { + flex: none; + } +} diff --git a/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.spec.ts b/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.spec.ts new file mode 100644 index 000000000..1e4944e9d --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.spec.ts @@ -0,0 +1,91 @@ +import { Store } from '@ngxs/store'; + +import { MockProvider } from 'ng-mocks'; + +import { SelectChangeEvent } from 'primeng/select'; + +import { signal } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { + PreprintsResourcesFiltersSelectors, + SetInstitution, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { + GetAllOptions, + PreprintsResourcesFiltersOptionsSelectors, +} from '@osf/features/preprints/store/preprints-resources-filters-options'; +import { mockStore } from '@osf/shared/mocks'; +import { InstitutionFilter } from '@osf/shared/models'; + +import { PreprintsInstitutionFilterComponent } from './preprints-institution-filter.component'; + +describe('InstitutionFilterComponent', () => { + let component: PreprintsInstitutionFilterComponent; + let fixture: ComponentFixture; + + const store = mockStore; + + const mockInstitutions: InstitutionFilter[] = [ + { id: '1', label: 'Harvard University', count: 15 }, + { id: '2', label: 'MIT', count: 12 }, + { id: '3', label: 'Stanford University', count: 8 }, + ]; + + beforeEach(async () => { + store.selectSignal.mockImplementation((selector) => { + if (selector === PreprintsResourcesFiltersOptionsSelectors.getInstitutions) { + return signal(mockInstitutions); + } + + if (selector === PreprintsResourcesFiltersSelectors.getInstitution) { + return signal({ label: '', value: '' }); + } + + return signal(null); + }); + + await TestBed.configureTestingModule({ + imports: [PreprintsInstitutionFilterComponent], + providers: [MockProvider(Store, store)], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsInstitutionFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize with empty input text', () => { + expect(component['inputText']()).toBeNull(); + }); + + it('should show all institutions when no search text is entered', () => { + const options = component['institutionsOptions'](); + expect(options.length).toBe(3); + expect(options[0].labelCount).toBe('Harvard University (15)'); + expect(options[1].labelCount).toBe('MIT (12)'); + expect(options[2].labelCount).toBe('Stanford University (8)'); + }); + + it('should filter institutions based on search text', () => { + component['inputText'].set('MIT'); + const options = component['institutionsOptions'](); + expect(options.length).toBe(1); + expect(options[0].labelCount).toBe('MIT (12)'); + }); + + it('should clear institution when selection is cleared', () => { + const event = { + originalEvent: new Event('change'), + value: '', + } as SelectChangeEvent; + + component.setInstitutions(event); + expect(store.dispatch).toHaveBeenCalledWith(new SetInstitution('', '')); + expect(store.dispatch).toHaveBeenCalledWith(GetAllOptions); + }); +}); diff --git a/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.ts b/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.ts new file mode 100644 index 000000000..c19b7cf56 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component.ts @@ -0,0 +1,76 @@ +import { Store } from '@ngxs/store'; + +import { Select, SelectChangeEvent } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, untracked } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { + PreprintsResourcesFiltersSelectors, + SetInstitution, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { + GetAllOptions, + PreprintsResourcesFiltersOptionsSelectors, +} from '@osf/features/preprints/store/preprints-resources-filters-options'; + +@Component({ + selector: 'osf-preprints-institution-filter', + imports: [Select, FormsModule], + templateUrl: './preprints-institution-filter.component.html', + styleUrl: './preprints-institution-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsInstitutionFilterComponent { + readonly #store = inject(Store); + + protected institutionState = this.#store.selectSignal(PreprintsResourcesFiltersSelectors.getInstitution); + protected availableInstitutions = this.#store.selectSignal(PreprintsResourcesFiltersOptionsSelectors.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, + })); + } + + return this.availableInstitutions().map((institution) => ({ + labelCount: institution.label + ' (' + institution.count + ')', + label: institution.label, + id: institution.id, + })); + }); + + 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/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.html b/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.html new file mode 100644 index 000000000..ce0d34b4d --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.html @@ -0,0 +1,17 @@ + diff --git a/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.scss b/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.spec.ts b/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.spec.ts new file mode 100644 index 000000000..c2a5fc63b --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.spec.ts @@ -0,0 +1,91 @@ +import { Store } from '@ngxs/store'; + +import { MockProvider } from 'ng-mocks'; + +import { SelectChangeEvent } from 'primeng/select'; + +import { signal } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { + PreprintsResourcesFiltersSelectors, + SetLicense, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { + GetAllOptions, + PreprintsResourcesFiltersOptionsSelectors, +} from '@osf/features/preprints/store/preprints-resources-filters-options'; +import { LicenseFilter } from '@osf/shared/models'; + +import { PreprintsLicenseFilterComponent } from './preprints-license-filter.component'; + +describe('LicenseFilterComponent', () => { + let component: PreprintsLicenseFilterComponent; + let fixture: ComponentFixture; + + const mockStore = { + selectSignal: jest.fn(), + dispatch: jest.fn(), + }; + + const mockLicenses: LicenseFilter[] = [ + { id: '1', label: 'MIT License', count: 10 }, + { id: '2', label: 'Apache License 2.0', count: 5 }, + { id: '3', label: 'GNU GPL v3', count: 3 }, + ]; + + beforeEach(async () => { + mockStore.selectSignal.mockImplementation((selector) => { + if (selector === PreprintsResourcesFiltersOptionsSelectors.getLicenses) { + return signal(mockLicenses); + } + if (selector === PreprintsResourcesFiltersSelectors.getLicense) { + return signal({ label: '', value: '' }); + } + return signal(null); + }); + + await TestBed.configureTestingModule({ + imports: [PreprintsLicenseFilterComponent], + providers: [MockProvider(Store, mockStore)], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsLicenseFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize with empty input text', () => { + expect(component['inputText']()).toBeNull(); + }); + + it('should show all licenses when no search text is entered', () => { + const options = component['licensesOptions'](); + expect(options.length).toBe(3); + expect(options[0].labelCount).toBe('MIT License (10)'); + expect(options[1].labelCount).toBe('Apache License 2.0 (5)'); + expect(options[2].labelCount).toBe('GNU GPL v3 (3)'); + }); + + it('should filter licenses based on search text', () => { + component['inputText'].set('MIT'); + const options = component['licensesOptions'](); + expect(options.length).toBe(1); + expect(options[0].labelCount).toBe('MIT License (10)'); + }); + + it('should clear license when selection is cleared', () => { + const event = { + originalEvent: new Event('change'), + value: '', + } as SelectChangeEvent; + + component.setLicenses(event); + expect(mockStore.dispatch).toHaveBeenCalledWith(new SetLicense('', '')); + expect(mockStore.dispatch).toHaveBeenCalledWith(GetAllOptions); + }); +}); diff --git a/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.ts b/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.ts new file mode 100644 index 000000000..79c3de5ef --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component.ts @@ -0,0 +1,76 @@ +import { Store } from '@ngxs/store'; + +import { Select, SelectChangeEvent } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, untracked } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { + PreprintsResourcesFiltersSelectors, + SetLicense, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { + GetAllOptions, + PreprintsResourcesFiltersOptionsSelectors, +} from '@osf/features/preprints/store/preprints-resources-filters-options'; + +@Component({ + selector: 'osf-preprints-license-filter', + imports: [Select, FormsModule], + templateUrl: './preprints-license-filter.component.html', + styleUrl: './preprints-license-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsLicenseFilterComponent { + readonly #store = inject(Store); + + protected availableLicenses = this.#store.selectSignal(PreprintsResourcesFiltersOptionsSelectors.getLicenses); + protected licenseState = this.#store.selectSignal(PreprintsResourcesFiltersSelectors.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/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.html b/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.html new file mode 100644 index 000000000..a33f1f7af --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.html @@ -0,0 +1,17 @@ + diff --git a/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.scss b/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.spec.ts b/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.spec.ts new file mode 100644 index 000000000..397b79390 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.spec.ts @@ -0,0 +1,54 @@ +import { Store } from '@ngxs/store'; + +import { MockProvider } from 'ng-mocks'; + +import { of } from 'rxjs'; + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintsResourcesFiltersSelectors } from '@osf/features/preprints/store/preprints-resources-filters'; +import { PreprintsResourcesFiltersOptionsSelectors } from '@osf/features/preprints/store/preprints-resources-filters-options'; + +import { PreprintsSubjectFilterComponent } from './preprints-subject-filter.component'; + +describe('SubjectFilterComponent', () => { + let component: PreprintsSubjectFilterComponent; + let fixture: ComponentFixture; + + const mockSubjects = [ + { id: '1', label: 'Physics', count: 10 }, + { id: '2', label: 'Chemistry', count: 15 }, + { id: '3', label: 'Biology', count: 20 }, + ]; + + const mockStore = { + selectSignal: jest.fn().mockImplementation((selector) => { + if (selector === PreprintsResourcesFiltersOptionsSelectors.getSubjects) { + return () => mockSubjects; + } + if (selector === PreprintsResourcesFiltersSelectors.getSubject) { + return () => ({ label: '', id: '' }); + } + return () => null; + }), + dispatch: jest.fn().mockReturnValue(of({})), + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintsSubjectFilterComponent], + providers: [MockProvider(Store, mockStore)], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsSubjectFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create and initialize with subjects', () => { + expect(component).toBeTruthy(); + expect(component['availableSubjects']()).toEqual(mockSubjects); + expect(component['subjectsOptions']().length).toBe(3); + expect(component['subjectsOptions']()[0].labelCount).toBe('Physics (10)'); + }); +}); diff --git a/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.ts b/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.ts new file mode 100644 index 000000000..3eaed3498 --- /dev/null +++ b/src/app/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component.ts @@ -0,0 +1,76 @@ +import { Store } from '@ngxs/store'; + +import { Select, SelectChangeEvent } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, untracked } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { + PreprintsResourcesFiltersSelectors, + SetSubject, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { + GetAllOptions, + PreprintsResourcesFiltersOptionsSelectors, +} from '@osf/features/preprints/store/preprints-resources-filters-options'; + +@Component({ + selector: 'osf-preprints-subject-filter', + imports: [Select, FormsModule], + templateUrl: './preprints-subject-filter.component.html', + styleUrl: './preprints-subject-filter.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsSubjectFilterComponent { + readonly #store = inject(Store); + + protected availableSubjects = this.#store.selectSignal(PreprintsResourcesFiltersOptionsSelectors.getSubjects); + protected subjectState = this.#store.selectSignal(PreprintsResourcesFiltersSelectors.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/features/preprints/components/index.ts b/src/app/features/preprints/components/index.ts index 455c300b4..0aa788de3 100644 --- a/src/app/features/preprints/components/index.ts +++ b/src/app/features/preprints/components/index.ts @@ -1,4 +1,12 @@ export { BrowseBySubjectsComponent } from './browse-by-subjects/browse-by-subjects.component'; export { PreprintServicesComponent } from './preprint-services/preprint-services.component'; export { AdvisoryBoardComponent } from '@osf/features/preprints/components/advisory-board/advisory-board.component'; +export { PreprintsCreatorsFilterComponent } from '@osf/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component'; +export { PreprintsDateCreatedFilterComponent } from '@osf/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component'; +export { PreprintsInstitutionFilterComponent } from '@osf/features/preprints/components/filters/preprints-institution-filter/preprints-institution-filter.component'; +export { PreprintsLicenseFilterComponent } from '@osf/features/preprints/components/filters/preprints-license-filter/preprints-license-filter.component'; +export { PreprintsSubjectFilterComponent } from '@osf/features/preprints/components/filters/preprints-subject/preprints-subject-filter.component'; +export { PreprintsFilterChipsComponent } from '@osf/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component'; export { PreprintsHelpDialogComponent } from '@osf/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component'; +export { PreprintsResourcesComponent } from '@osf/features/preprints/components/preprints-resources/preprints-resources.component'; +export { PreprintsResourcesFiltersComponent } from '@osf/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component'; diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html index a80aad27c..117f77e01 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html @@ -56,9 +56,9 @@

{{ preprintProvider()!.name }}

diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts index d59d62340..034e63039 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts @@ -7,12 +7,11 @@ import { Skeleton } from 'primeng/skeleton'; import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { Router, RouterLink } from '@angular/router'; +import { RouterLink } from '@angular/router'; import { PreprintsHelpDialogComponent } from '@osf/features/preprints/components'; import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { SearchInputComponent } from '@shared/components'; -import { ResourceTab } from '@shared/enums'; import { DecodeHtmlPipe } from '@shared/pipes'; @Component({ @@ -26,23 +25,19 @@ import { DecodeHtmlPipe } from '@shared/pipes'; export class PreprintProviderHeroComponent { protected translateService = inject(TranslateService); protected dialogService = inject(DialogService); - private readonly router = inject(Router); + + searchControl = input(new FormControl()); preprintProvider = input.required(); isPreprintProviderLoading = input.required(); addPreprintClicked = output(); - - protected searchControl = new FormControl(''); + triggerSearch = output(); addPreprint() { this.addPreprintClicked.emit(); } - redirectToDiscoverPageWithValue() { - const searchValue = this.searchControl.value; - - this.router.navigate(['/discover'], { - queryParams: { search: searchValue, resourceTab: ResourceTab.Preprints }, - }); + onTriggerSearch(value: string) { + this.triggerSearch.emit(value); } openHelpDialog() { diff --git a/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.html b/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.html new file mode 100644 index 000000000..1626a7853 --- /dev/null +++ b/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.html @@ -0,0 +1,74 @@ +@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().subject.value) { + @let subject = filters().subject.filterName + ': ' + filters().subject.label; + + + + + +} + +@if (filters().license.value) { + @let license = filters().license.filterName + ': ' + filters().license.label; + + + + + +} + +@if (filters().institution.value) { + @let institution = filters().institution.filterName + ': ' + filters().institution.label; + + + + + +} diff --git a/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.scss b/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.scss new file mode 100644 index 000000000..9ff3d3c87 --- /dev/null +++ b/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.scss @@ -0,0 +1,16 @@ +@use "assets/styles/mixins" as mix; +@use "assets/styles/variables" as var; + +:host { + display: flex; + flex-direction: column; + gap: mix.rem(6px); + + @media (max-width: var.$breakpoint-xl) { + flex-direction: row; + } + + @media (max-width: var.$breakpoint-sm) { + flex-direction: column; + } +} diff --git a/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.spec.ts b/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.spec.ts new file mode 100644 index 000000000..aaa162f12 --- /dev/null +++ b/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintsFilterChipsComponent } from './preprints-filter-chips.component'; + +describe('PreprintsFilterChipsComponent', () => { + let component: PreprintsFilterChipsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintsFilterChipsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsFilterChipsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.ts b/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.ts new file mode 100644 index 000000000..f9461b526 --- /dev/null +++ b/src/app/features/preprints/components/preprints-filter-chips/preprints-filter-chips.component.ts @@ -0,0 +1,59 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { PrimeTemplate } from 'primeng/api'; +import { Chip } from 'primeng/chip'; + +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +import { + PreprintsResourcesFiltersSelectors, + SetCreator, + SetDateCreated, + SetInstitution, + SetLicense, + SetSubject, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { GetAllOptions } from '@osf/features/preprints/store/preprints-resources-filters-options'; +import { FilterType } from '@osf/shared/enums'; + +@Component({ + selector: 'osf-preprints-filter-chips', + imports: [Chip, PrimeTemplate], + templateUrl: './preprints-filter-chips.component.html', + styleUrl: './preprints-filter-chips.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsFilterChipsComponent { + protected readonly FilterType = FilterType; + private readonly actions = createDispatchMap({ + setCreator: SetCreator, + setDateCreated: SetDateCreated, + setSubject: SetSubject, + setInstitution: SetInstitution, + setLicense: SetLicense, + getAllOptions: GetAllOptions, + }); + + filters = select(PreprintsResourcesFiltersSelectors.getAllFilters); + + clearFilter(filter: FilterType) { + switch (filter) { + case FilterType.Creator: + this.actions.setCreator('', ''); + break; + case FilterType.DateCreated: + this.actions.setDateCreated(''); + break; + case FilterType.Subject: + this.actions.setSubject('', ''); + break; + case FilterType.Institution: + this.actions.setInstitution('', ''); + break; + case FilterType.License: + this.actions.setLicense('', ''); + break; + } + this.actions.getAllOptions(); + } +} diff --git a/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.html b/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.html new file mode 100644 index 000000000..ecffb0e26 --- /dev/null +++ b/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.html @@ -0,0 +1,48 @@ +@if (anyOptionsCount()) { +
+ + + Creator + + + + + + @if (datesOptionsCount() > 0) { + + Date Created + + + + + } + + @if (subjectOptionsCount() > 0) { + + Subject + + + + + } + + @if (licenseOptionsCount() > 0) { + + License + + + + + } + + @if (institutionOptionsCount() > 0) { + + Institution + + + + + } + +
+} diff --git a/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.scss b/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.scss new file mode 100644 index 000000000..1dd7a98c8 --- /dev/null +++ b/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.scss @@ -0,0 +1,16 @@ +@use "assets/styles/variables" as var; +@use "assets/styles/mixins" as mix; + +:host { + width: 30%; + + .filters { + border: 1px solid var.$grey-2; + border-radius: mix.rem(12px); + padding: 0 mix.rem(24px) 0 mix.rem(24px); + display: flex; + flex-direction: column; + row-gap: mix.rem(12px); + height: fit-content; + } +} diff --git a/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.spec.ts b/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.spec.ts new file mode 100644 index 000000000..1f8c6791e --- /dev/null +++ b/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintsResourcesFiltersComponent } from './preprints-resources-filters.component'; + +describe('PreprintsResourcesFiltersComponent', () => { + let component: PreprintsResourcesFiltersComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintsResourcesFiltersComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsResourcesFiltersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.ts b/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.ts new file mode 100644 index 000000000..8b9f0ebe4 --- /dev/null +++ b/src/app/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component.ts @@ -0,0 +1,78 @@ +import { select } from '@ngxs/store'; + +import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; + +import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; + +import { + PreprintsCreatorsFilterComponent, + PreprintsDateCreatedFilterComponent, + PreprintsInstitutionFilterComponent, + PreprintsLicenseFilterComponent, + PreprintsSubjectFilterComponent, +} from '@osf/features/preprints/components'; +import { PreprintsResourcesFiltersOptionsSelectors } from '@osf/features/preprints/store/preprints-resources-filters-options'; + +@Component({ + selector: 'osf-preprints-resources-filters', + imports: [ + Accordion, + AccordionPanel, + AccordionHeader, + AccordionContent, + PreprintsDateCreatedFilterComponent, + PreprintsCreatorsFilterComponent, + PreprintsSubjectFilterComponent, + PreprintsInstitutionFilterComponent, + PreprintsLicenseFilterComponent, + ], + templateUrl: './preprints-resources-filters.component.html', + styleUrl: './preprints-resources-filters.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsResourcesFiltersComponent { + datesCreated = select(PreprintsResourcesFiltersOptionsSelectors.getDatesCreated); + datesOptionsCount = computed(() => { + if (!this.datesCreated()) { + return 0; + } + + return this.datesCreated().reduce((acc, date) => acc + date.count, 0); + }); + + subjectOptions = select(PreprintsResourcesFiltersOptionsSelectors.getSubjects); + subjectOptionsCount = computed(() => { + if (!this.subjectOptions()) { + return 0; + } + + return this.subjectOptions().reduce((acc, item) => acc + item.count, 0); + }); + + institutionOptions = select(PreprintsResourcesFiltersOptionsSelectors.getInstitutions); + institutionOptionsCount = computed(() => { + if (!this.institutionOptions()) { + return 0; + } + + return this.institutionOptions().reduce((acc, item) => acc + item.count, 0); + }); + + licenseOptions = select(PreprintsResourcesFiltersOptionsSelectors.getLicenses); + licenseOptionsCount = computed(() => { + if (!this.licenseOptions()) { + return 0; + } + + return this.licenseOptions().reduce((acc, item) => acc + item.count, 0); + }); + + anyOptionsCount = computed(() => { + return ( + this.datesOptionsCount() > 0 || + this.subjectOptionsCount() > 0 || + this.licenseOptionsCount() > 0 || + this.institutionOptionsCount() > 0 + ); + }); +} diff --git a/src/app/features/preprints/components/preprints-resources/preprints-resources.component.html b/src/app/features/preprints/components/preprints-resources/preprints-resources.component.html new file mode 100644 index 000000000..98a51588a --- /dev/null +++ b/src/app/features/preprints/components/preprints-resources/preprints-resources.component.html @@ -0,0 +1,147 @@ +
+
+ @if (resourcesCount() > 10000) { +

10 000+ results

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

{{ resourcesCount() }} results

+ } @else { +

0 results

+ } + +
+ @if (isWeb()) { +

Sort by:

+ + } @else { + @if (isAnyFilterOptions()) { + filter by + } + sort by + } +
+
+ + @if (isFiltersOpen()) { +
+ +
+ } @else if (isSortingOpen()) { +
+ @for (option of searchSortingOptions; track option.value) { +
+ {{ option.label }} +
+ } +
+ } @else { + @if (isAnyFilterSelected()) { +
+ +
+ } + +
+ @if (isWeb() && isAnyFilterOptions()) { + + } + + + +
+ @if (items.length > 0) { + @for (item of items; track item.id) { + + } + +
+ @if (first() && prev()) { + + + + } + + + + + + + + +
+ } +
+
+
+
+ } +
diff --git a/src/app/features/preprints/components/preprints-resources/preprints-resources.component.scss b/src/app/features/preprints/components/preprints-resources/preprints-resources.component.scss new file mode 100644 index 000000000..56362826c --- /dev/null +++ b/src/app/features/preprints/components/preprints-resources/preprints-resources.component.scss @@ -0,0 +1,43 @@ +@use "assets/styles/variables" as var; +@use "assets/styles/mixins" as mix; + +h4 { + color: var.$pr-blue-1; +} + +.sorting-container { + display: flex; + align-items: center; + gap: mix.rem(6px); + + h4 { + color: var.$dark-blue-1; + font-weight: 400; + text-wrap: nowrap; + } +} + +.sort-card { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: mix.rem(44px); + border: 1px solid var.$grey-2; + border-radius: mix.rem(12px); + padding: 0 mix.rem(24px) 0 mix.rem(24px); + cursor: pointer; +} + +.card-selected { + background: var.$bg-blue-2; +} + +.icon-disabled { + opacity: 0.5; + cursor: none; +} + +.icon-active { + fill: var.$grey-1; +} diff --git a/src/app/features/preprints/components/preprints-resources/preprints-resources.component.spec.ts b/src/app/features/preprints/components/preprints-resources/preprints-resources.component.spec.ts new file mode 100644 index 000000000..c4f65e4b8 --- /dev/null +++ b/src/app/features/preprints/components/preprints-resources/preprints-resources.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintsResourcesComponent } from './preprints-resources.component'; + +describe('PreprintsResourcesComponent', () => { + let component: PreprintsResourcesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintsResourcesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintsResourcesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/preprints-resources/preprints-resources.component.ts b/src/app/features/preprints/components/preprints-resources/preprints-resources.component.ts new file mode 100644 index 000000000..1d3b7d4c6 --- /dev/null +++ b/src/app/features/preprints/components/preprints-resources/preprints-resources.component.ts @@ -0,0 +1,79 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { DataView } from 'primeng/dataview'; +import { Select } from 'primeng/select'; + +import { NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, Component, HostBinding, inject, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; + +import { Primitive } from '@core/helpers'; +import { PreprintsFilterChipsComponent, PreprintsResourcesFiltersComponent } from '@osf/features/preprints/components'; +import { + GetResourcesByLink, + PreprintsDiscoverSelectors, + SetSortBy, +} from '@osf/features/preprints/store/preprints-discover'; +import { PreprintsResourcesFiltersSelectors } from '@osf/features/preprints/store/preprints-resources-filters'; +import { PreprintsResourcesFiltersOptionsSelectors } from '@osf/features/preprints/store/preprints-resources-filters-options'; +import { ResourceCardComponent } from '@shared/components'; +import { searchSortingOptions } from '@shared/constants'; +import { IS_WEB, IS_XSMALL } from '@shared/utils'; + +@Component({ + selector: 'osf-preprints-resources', + imports: [ + Select, + FormsModule, + NgOptimizedImage, + PreprintsResourcesFiltersComponent, + PreprintsFilterChipsComponent, + DataView, + ResourceCardComponent, + ], + templateUrl: './preprints-resources.component.html', + styleUrl: './preprints-resources.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintsResourcesComponent { + @HostBinding('class') classes = 'flex-1 flex flex-column w-full h-full'; + + private readonly actions = createDispatchMap({ setSortBy: SetSortBy, getResourcesByLink: GetResourcesByLink }); + searchSortingOptions = searchSortingOptions; + + isWeb = toSignal(inject(IS_WEB)); + isMobile = toSignal(inject(IS_XSMALL)); + + resources = select(PreprintsDiscoverSelectors.getResources); + resourcesCount = select(PreprintsDiscoverSelectors.getResourcesCount); + + sortBy = select(PreprintsDiscoverSelectors.getSortBy); + first = select(PreprintsDiscoverSelectors.getFirst); + next = select(PreprintsDiscoverSelectors.getNext); + prev = select(PreprintsDiscoverSelectors.getPrevious); + + isSortingOpen = signal(false); + isFiltersOpen = signal(false); + + isAnyFilterSelected = select(PreprintsResourcesFiltersSelectors.getAllFilters); + isAnyFilterOptions = select(PreprintsResourcesFiltersOptionsSelectors.isAnyFilterOptions); + + switchPage(link: string) { + this.actions.getResourcesByLink(link); + } + + switchMobileFiltersSectionVisibility() { + this.isFiltersOpen.set(!this.isFiltersOpen()); + this.isSortingOpen.set(false); + } + + switchMobileSortingSectionVisibility() { + this.isSortingOpen.set(!this.isSortingOpen()); + this.isFiltersOpen.set(false); + } + + sortOptionSelected(value: Primitive) { + this.actions.setSortBy(value as string); + } +} diff --git a/src/app/features/preprints/constants/preprints.routes.ts b/src/app/features/preprints/constants/preprints.routes.ts index e5a7bd24e..bdca37437 100644 --- a/src/app/features/preprints/constants/preprints.routes.ts +++ b/src/app/features/preprints/constants/preprints.routes.ts @@ -1,11 +1,25 @@ +import { provideStates } from '@ngxs/store'; + import { Routes } from '@angular/router'; import { PreprintsComponent } from '@osf/features/preprints/preprints.component'; +import { PreprintsState } from '@osf/features/preprints/store/preprints'; +import { PreprintsDiscoverState } from '@osf/features/preprints/store/preprints-discover'; +import { PreprintsResourcesFiltersState } from '@osf/features/preprints/store/preprints-resources-filters'; +import { PreprintsResourcesFiltersOptionsState } from '@osf/features/preprints/store/preprints-resources-filters-options'; export const preprintsRoutes: Routes = [ { path: '', component: PreprintsComponent, + providers: [ + provideStates([ + PreprintsState, + PreprintsDiscoverState, + PreprintsResourcesFiltersState, + PreprintsResourcesFiltersOptionsState, + ]), + ], children: [ { path: '', @@ -26,6 +40,13 @@ export const preprintsRoutes: Routes = [ (c) => c.PreprintProviderOverviewComponent ), }, + { + path: 'overview/:providerId/discover', + loadComponent: () => + import('@osf/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component').then( + (c) => c.PreprintProviderDiscoverComponent + ), + }, ], }, ]; diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index 732e7ff8a..5370489cb 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -28,6 +28,7 @@ export class PreprintsMapper { primaryColor: brandRaw.attributes.primary_color, secondaryColor: brandRaw.attributes.secondary_color, }, + iri: response.links.iri, }; } diff --git a/src/app/features/preprints/models/preprints.models.ts b/src/app/features/preprints/models/preprints.models.ts index 9c4743a96..3c6fba4a6 100644 --- a/src/app/features/preprints/models/preprints.models.ts +++ b/src/app/features/preprints/models/preprints.models.ts @@ -23,6 +23,7 @@ export interface PreprintProviderDetails { allowSubmissions: boolean; brand: Brand; lastFetched?: number; + iri: string; } export interface PreprintProviderToAdvertise { @@ -60,6 +61,9 @@ export interface PreprintProviderDetailsGetResponse { data: BrandGetResponse; }; }; + links: { + iri: string; + }; } export interface BrandGetResponse { diff --git a/src/app/features/preprints/pages/index.ts b/src/app/features/preprints/pages/index.ts index b96b4bc7a..e79f5f462 100644 --- a/src/app/features/preprints/pages/index.ts +++ b/src/app/features/preprints/pages/index.ts @@ -1,2 +1,3 @@ export { PreprintsLandingComponent } from '@osf/features/preprints/pages/landing/preprints-landing.component'; +export { PreprintProviderDiscoverComponent } from '@osf/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component'; export { PreprintProviderOverviewComponent } from '@osf/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component'; diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.ts b/src/app/features/preprints/pages/landing/preprints-landing.component.ts index 3175b3088..4eaa63df4 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.ts +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.ts @@ -20,7 +20,7 @@ import { GetPreprintProviderById, GetPreprintProvidersToAdvertise, PreprintsSelectors, -} from '@osf/features/preprints/store'; +} from '@osf/features/preprints/store/preprints'; import { SearchInputComponent } from '@shared/components'; import { ResourceTab } from '@shared/enums'; diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.html b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.html new file mode 100644 index 000000000..2b00e414b --- /dev/null +++ b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.html @@ -0,0 +1,6 @@ + + diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.scss b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts new file mode 100644 index 000000000..7b018d31b --- /dev/null +++ b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintProviderDiscoverComponent } from './preprint-provider-discover.component'; + +describe('PreprintProviderDiscoverComponent', () => { + let component: PreprintProviderDiscoverComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintProviderDiscoverComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintProviderDiscoverComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts new file mode 100644 index 000000000..41689ad64 --- /dev/null +++ b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts @@ -0,0 +1,290 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { debounceTime, map, of, skip, take } from 'rxjs'; + +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + effect, + HostBinding, + inject, + OnDestroy, + OnInit, + untracked, +} from '@angular/core'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { PreprintsResourcesComponent } from '@osf/features/preprints/components'; +import { PreprintProviderHeroComponent } from '@osf/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component'; +import { BrandService } from '@osf/features/preprints/services'; +import { GetPreprintProviderById, PreprintsSelectors } from '@osf/features/preprints/store/preprints'; +import { + GetResources, + ResetState, + SetProviderIri, + SetSearchText, + SetSortBy, +} from '@osf/features/preprints/store/preprints-discover'; +import { PreprintsDiscoverSelectors } from '@osf/features/preprints/store/preprints-discover/preprints-discover.selectors'; +import { + PreprintsResourcesFiltersSelectors, + ResetFiltersState, + SetCreator, + SetDateCreated, + SetInstitution, + SetLicense, + SetProvider, + SetSubject, +} from '@osf/features/preprints/store/preprints-resources-filters'; +import { GetAllOptions } from '@osf/features/preprints/store/preprints-resources-filters-options'; +import { FilterLabelsModel, ResourceFilterLabel } from '@shared/models'; +import { HeaderStyleHelper } from '@shared/utils'; + +@Component({ + selector: 'osf-preprint-provider-discover', + imports: [PreprintProviderHeroComponent, PreprintsResourcesComponent], + templateUrl: './preprint-provider-discover.component.html', + styleUrl: './preprint-provider-discover.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintProviderDiscoverComponent implements OnInit, OnDestroy { + @HostBinding('class') classes = 'flex-1 flex flex-column w-full h-full'; + private readonly activatedRoute = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + private initAfterIniReceived = false; + private providerId = toSignal( + this.activatedRoute.params.pipe(map((params) => params['providerId'])) ?? of(undefined) + ); + + private actions = createDispatchMap({ + getPreprintProviderById: GetPreprintProviderById, + setCreator: SetCreator, + setDateCreated: SetDateCreated, + setSubject: SetSubject, + setInstitution: SetInstitution, + setLicense: SetLicense, + setProvider: SetProvider, + setSearchText: SetSearchText, + setSortBy: SetSortBy, + getAllOptions: GetAllOptions, + getResources: GetResources, + resetFiltersState: ResetFiltersState, + resetDiscoverState: ResetState, + setProviderIri: SetProviderIri, + }); + + searchControl = new FormControl(''); + + preprintProvider = select(PreprintsSelectors.getPreprintProviderDetails(this.providerId())); + isPreprintProviderLoading = select(PreprintsSelectors.isPreprintProviderDetailsLoading); + + creatorSelected = select(PreprintsResourcesFiltersSelectors.getCreator); + dateCreatedSelected = select(PreprintsResourcesFiltersSelectors.getDateCreated); + subjectSelected = select(PreprintsResourcesFiltersSelectors.getSubject); + licenseSelected = select(PreprintsResourcesFiltersSelectors.getLicense); + providerSelected = select(PreprintsResourcesFiltersSelectors.getProvider); + institutionSelected = select(PreprintsResourcesFiltersSelectors.getInstitution); + sortSelected = select(PreprintsDiscoverSelectors.getSortBy); + searchValue = select(PreprintsDiscoverSelectors.getSearchText); + + constructor() { + effect(() => { + const provider = this.preprintProvider(); + + if (provider) { + this.actions.setProviderIri(provider.iri); + + if (!this.initAfterIniReceived) { + this.initAfterIniReceived = true; + this.actions.getResources(); + this.actions.getAllOptions(); + } + + BrandService.applyBranding(provider.brand); + HeaderStyleHelper.applyHeaderStyles( + provider.brand.primaryColor, + provider.brand.secondaryColor, + provider.brand.heroBackgroundImageUrl + ); + } + }); + + // 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('Subject', this.subjectSelected())); + effect(() => this.syncFilterToQuery('License', this.licenseSelected())); + effect(() => this.syncFilterToQuery('Provider', this.providerSelected())); + effect(() => this.syncFilterToQuery('Institution', this.institutionSelected())); + effect(() => this.syncSortingToQuery(this.sortSelected())); + effect(() => this.syncSearchToQuery(this.searchValue())); + + // if new value for some filter was put in store, fetch resources + effect(() => { + this.creatorSelected(); + this.dateCreatedSelected(); + this.subjectSelected(); + this.licenseSelected(); + this.providerSelected(); + this.sortSelected(); + this.searchValue(); + this.actions.getResources(); + }); + + this.configureSearchControl(); + } + + ngOnInit() { + this.actions.getPreprintProviderById(this.providerId()); + + // set all query parameters from route to store when page is loaded + this.activatedRoute.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 creator = filters.find((p: ResourceFilterLabel) => p.filterName === FilterLabelsModel.creator); + const dateCreated = filters.find((p: ResourceFilterLabel) => p.filterName === 'DateCreated'); + const subject = filters.find((p: ResourceFilterLabel) => p.filterName === FilterLabelsModel.subject); + const license = filters.find((p: ResourceFilterLabel) => p.filterName === FilterLabelsModel.license); + const provider = filters.find((p: ResourceFilterLabel) => p.filterName === FilterLabelsModel.provider); + const institution = filters.find((p: ResourceFilterLabel) => p.filterName === FilterLabelsModel.institution); + + if (creator) { + this.actions.setCreator(creator.label, creator.value); + } + if (dateCreated) { + this.actions.setDateCreated(dateCreated.value); + } + if (subject) { + this.actions.setSubject(subject.label, subject.value); + } + if (institution) { + this.actions.setInstitution(institution.label, institution.value); + } + if (license) { + this.actions.setLicense(license.label, license.value); + } + if (provider) { + this.actions.setProvider(provider.label, provider.value); + } + if (sortBy) { + this.actions.setSortBy(sortBy); + } + if (search) { + this.actions.setSearchText(search); + } + + this.actions.getAllOptions(); + }); + } + + ngOnDestroy() { + HeaderStyleHelper.resetToDefaults(); + BrandService.resetBranding(); + this.actions.resetFiltersState(); + this.actions.resetDiscoverState(); + } + + syncFilterToQuery(filterName: string, filterValue: ResourceFilterLabel) { + const paramMap = this.activatedRoute.snapshot.queryParamMap; + const currentParams = { ...this.activatedRoute.snapshot.queryParams }; + + 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; + + if (!hasValue && index !== -1) { + filters.splice(index, 1); + } else if (hasValue && filterValue?.label) { + 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']; + } + + this.router.navigate([], { + relativeTo: this.activatedRoute, + queryParams: currentParams, + replaceUrl: true, + }); + } + + syncSortingToQuery(sortBy: string) { + const currentParams = { ...this.activatedRoute.snapshot.queryParams }; + + if (sortBy && sortBy !== '-relevance') { + currentParams['sortBy'] = sortBy; + } else if (sortBy && sortBy === '-relevance') { + delete currentParams['sortBy']; + } + + this.router.navigate([], { + relativeTo: this.activatedRoute, + queryParams: currentParams, + replaceUrl: true, + }); + } + + syncSearchToQuery(search: string) { + const currentParams = { ...this.activatedRoute.snapshot.queryParams }; + + if (search) { + currentParams['search'] = search; + } else { + delete currentParams['search']; + } + + this.router.navigate([], { + relativeTo: this.activatedRoute, + queryParams: currentParams, + replaceUrl: true, + }); + } + + private configureSearchControl() { + this.searchControl.valueChanges + .pipe(skip(1), debounceTime(500), takeUntilDestroyed(this.destroyRef)) + .subscribe((searchText) => { + this.actions.setSearchText(searchText ?? ''); + this.actions.getAllOptions(); + }); + + effect(() => { + const storeValue = this.searchValue(); + const currentInput = untracked(() => this.searchControl.value); + + if (storeValue && currentInput !== storeValue) { + this.searchControl.setValue(storeValue); + } + }); + } +} diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.html b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.html index c0f7884d4..e4757725c 100644 --- a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.html +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.html @@ -1,6 +1,7 @@ params['providerId'])) ?? of(undefined)); private actions = createDispatchMap({ @@ -67,4 +68,11 @@ export class PreprintProviderOverviewComponent implements OnInit, OnDestroy { HeaderStyleHelper.resetToDefaults(); BrandService.resetBranding(); } + + redirectToDiscoverPageWithValue(searchValue: string) { + this.router.navigate(['discover'], { + relativeTo: this.route, + queryParams: { search: searchValue }, + }); + } } diff --git a/src/app/features/preprints/services/index.ts b/src/app/features/preprints/services/index.ts index fd2d1dc28..d93e065fa 100644 --- a/src/app/features/preprints/services/index.ts +++ b/src/app/features/preprints/services/index.ts @@ -1,2 +1,3 @@ export { BrandService } from './brand.service'; export { PreprintsService } from './preprints.service'; +export { PreprintsFiltersOptionsService } from './preprints-resource-filters.service'; diff --git a/src/app/features/preprints/services/preprints-resource-filters.service.ts b/src/app/features/preprints/services/preprints-resource-filters.service.ts new file mode 100644 index 000000000..5596551b6 --- /dev/null +++ b/src/app/features/preprints/services/preprints-resource-filters.service.ts @@ -0,0 +1,72 @@ +import { select, Store } from '@ngxs/store'; + +import { Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { PreprintsDiscoverSelectors } from '@osf/features/preprints/store/preprints-discover'; +import { PreprintsResourcesFiltersSelectors } from '@osf/features/preprints/store/preprints-resources-filters'; +import { ResourceFiltersStateModel } from '@osf/features/search/components/resource-filters/store'; +import { + Creator, + DateCreated, + LicenseFilter, + ProviderFilter, + ResourceTypeFilter, + SubjectFilter, +} from '@osf/shared/models'; +import { FiltersOptionsService } from '@osf/shared/services'; +import { addFiltersParams, getResourceTypes } from '@osf/shared/utils'; +import { ResourceTab } from '@shared/enums'; + +@Injectable({ + providedIn: 'root', +}) +export class PreprintsFiltersOptionsService { + store = inject(Store); + filtersOptions = inject(FiltersOptionsService); + + private getFilterParams(): Record { + return addFiltersParams(select(PreprintsResourcesFiltersSelectors.getAllFilters)() as ResourceFiltersStateModel); + } + + private getParams(): Record { + const params: Record = {}; + const resourceTab = ResourceTab.Preprints; + const resourceTypes = getResourceTypes(resourceTab); + const searchText = this.store.selectSnapshot(PreprintsDiscoverSelectors.getSearchText); + const sort = this.store.selectSnapshot(PreprintsDiscoverSelectors.getSortBy); + + params['cardSearchFilter[resourceType]'] = resourceTypes; + params['cardSearchFilter[accessService]'] = 'https://staging4.osf.io/'; + params['cardSearchText[*,creator.name,isContainedBy.creator.name]'] = searchText; + params['cardSearchFilter[publisher][]'] = this.store.selectSnapshot(PreprintsDiscoverSelectors.getIri); + params['page[size]'] = '10'; + params['sort'] = sort; + return params; + } + + getCreators(valueSearchText: string): Observable { + return this.filtersOptions.getCreators(valueSearchText, this.getParams(), this.getFilterParams()); + } + + getDates(): Observable { + return this.filtersOptions.getDates(this.getParams(), this.getFilterParams()); + } + + getSubjects(): Observable { + return this.filtersOptions.getSubjects(this.getParams(), this.getFilterParams()); + } + + getInstitutions(): Observable { + return this.filtersOptions.getInstitutions(this.getParams(), this.getFilterParams()); + } + + getLicenses(): Observable { + return this.filtersOptions.getLicenses(this.getParams(), this.getFilterParams()); + } + + getProviders(): Observable { + return this.filtersOptions.getProviders(this.getParams(), this.getFilterParams()); + } +} diff --git a/src/app/features/preprints/store/preprints-discover/index.ts b/src/app/features/preprints/store/preprints-discover/index.ts new file mode 100644 index 000000000..6e0281f9d --- /dev/null +++ b/src/app/features/preprints/store/preprints-discover/index.ts @@ -0,0 +1,4 @@ +export * from './preprints-discover.actions'; +export * from './preprints-discover.model'; +export * from './preprints-discover.selectors'; +export * from './preprints-discover.state'; diff --git a/src/app/features/preprints/store/preprints-discover/preprints-discover.actions.ts b/src/app/features/preprints/store/preprints-discover/preprints-discover.actions.ts new file mode 100644 index 000000000..b488d206e --- /dev/null +++ b/src/app/features/preprints/store/preprints-discover/preprints-discover.actions.ts @@ -0,0 +1,31 @@ +export class GetResources { + static readonly type = '[Preprints Discover] Get Resources'; +} + +export class GetResourcesByLink { + static readonly type = '[Preprints Discover] Get Resources By Link'; + + constructor(public link: string) {} +} + +export class SetSearchText { + static readonly type = '[Preprints Discover] Set Search Text'; + + constructor(public searchText: string) {} +} + +export class SetSortBy { + static readonly type = '[Preprints Discover] Set SortBy'; + + constructor(public sortBy: string) {} +} + +export class SetProviderIri { + static readonly type = '[Preprints Discover] Set Provider Iri'; + + constructor(public providerIri: string) {} +} + +export class ResetState { + static readonly type = '[Preprints Discover] Reset State'; +} diff --git a/src/app/features/preprints/store/preprints-discover/preprints-discover.model.ts b/src/app/features/preprints/store/preprints-discover/preprints-discover.model.ts new file mode 100644 index 000000000..174ac3465 --- /dev/null +++ b/src/app/features/preprints/store/preprints-discover/preprints-discover.model.ts @@ -0,0 +1,12 @@ +import { AsyncStateModel, Resource } from '@shared/models'; + +export interface PreprintsDiscoverStateModel { + resources: AsyncStateModel; + providerIri: string; + resourcesCount: number; + searchText: string; + sortBy: string; + first: string; + next: string; + previous: string; +} diff --git a/src/app/features/preprints/store/preprints-discover/preprints-discover.selectors.ts b/src/app/features/preprints/store/preprints-discover/preprints-discover.selectors.ts new file mode 100644 index 000000000..e7a5a2a76 --- /dev/null +++ b/src/app/features/preprints/store/preprints-discover/preprints-discover.selectors.ts @@ -0,0 +1,48 @@ +import { Selector } from '@ngxs/store'; + +import { Resource } from '@shared/models'; + +import { PreprintsDiscoverStateModel } from './preprints-discover.model'; +import { PreprintsDiscoverState } from './preprints-discover.state'; + +export class PreprintsDiscoverSelectors { + @Selector([PreprintsDiscoverState]) + static getResources(state: PreprintsDiscoverStateModel): Resource[] { + return state.resources.data; + } + + @Selector([PreprintsDiscoverState]) + static getResourcesCount(state: PreprintsDiscoverStateModel): number { + return state.resourcesCount; + } + + @Selector([PreprintsDiscoverState]) + static getSearchText(state: PreprintsDiscoverStateModel): string { + return state.searchText; + } + + @Selector([PreprintsDiscoverState]) + static getSortBy(state: PreprintsDiscoverStateModel): string { + return state.sortBy; + } + + @Selector([PreprintsDiscoverState]) + static getIri(state: PreprintsDiscoverStateModel): string { + return state.providerIri; + } + + @Selector([PreprintsDiscoverState]) + static getFirst(state: PreprintsDiscoverStateModel): string { + return state.first; + } + + @Selector([PreprintsDiscoverState]) + static getNext(state: PreprintsDiscoverStateModel): string { + return state.next; + } + + @Selector([PreprintsDiscoverState]) + static getPrevious(state: PreprintsDiscoverStateModel): string { + return state.previous; + } +} diff --git a/src/app/features/preprints/store/preprints-discover/preprints-discover.state.ts b/src/app/features/preprints/store/preprints-discover/preprints-discover.state.ts new file mode 100644 index 000000000..65167f668 --- /dev/null +++ b/src/app/features/preprints/store/preprints-discover/preprints-discover.state.ts @@ -0,0 +1,149 @@ +import { Action, NgxsOnInit, State, StateContext, Store } from '@ngxs/store'; + +import { BehaviorSubject, EMPTY, switchMap, tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { + GetResources, + GetResourcesByLink, + ResetState, + SetProviderIri, + SetSearchText, + SetSortBy, +} from '@osf/features/preprints/store/preprints-discover/preprints-discover.actions'; +import { PreprintsDiscoverStateModel } from '@osf/features/preprints/store/preprints-discover/preprints-discover.model'; +import { PreprintsDiscoverSelectors } from '@osf/features/preprints/store/preprints-discover/preprints-discover.selectors'; +import { PreprintsResourcesFiltersSelectors } from '@osf/features/preprints/store/preprints-resources-filters'; +import { ResourceFiltersStateModel } from '@osf/features/search/components/resource-filters/store'; +import { GetResourcesRequestTypeEnum, ResourceTab } from '@shared/enums'; +import { SearchService } from '@shared/services'; +import { addFiltersParams, getResourceTypes } from '@shared/utils'; + +@State({ + name: 'preprintsDiscover', + defaults: { + resources: { + data: [], + isLoading: false, + error: null, + }, + providerIri: '', + resourcesCount: 0, + searchText: '', + sortBy: '-relevance', + first: '', + next: '', + previous: '', + }, +}) +@Injectable() +export class PreprintsDiscoverState implements NgxsOnInit { + searchService = inject(SearchService); + store = inject(Store); + loadRequests = new BehaviorSubject<{ type: GetResourcesRequestTypeEnum; link?: string } | null>(null); + + ngxsOnInit(ctx: StateContext): void { + this.loadRequests + .pipe( + switchMap((query) => { + if (!query) return EMPTY; + const state = ctx.getState(); + ctx.patchState({ resources: { ...state.resources, isLoading: true } }); + if (query.type === GetResourcesRequestTypeEnum.GetResources) { + const filters = this.store.selectSnapshot(PreprintsResourcesFiltersSelectors.getAllFilters); + const filtersParams = addFiltersParams(filters as ResourceFiltersStateModel); + const searchText = this.store.selectSnapshot(PreprintsDiscoverSelectors.getSearchText); + const sortBy = this.store.selectSnapshot(PreprintsDiscoverSelectors.getSortBy); + const resourceTab = ResourceTab.Preprints; + const resourceTypes = getResourceTypes(resourceTab); + filtersParams['cardSearchFilter[publisher][]'] = this.store.selectSnapshot( + PreprintsDiscoverSelectors.getIri + ); + + return this.searchService.getResources(filtersParams, searchText, sortBy, resourceTypes).pipe( + tap((response) => { + ctx.patchState({ resources: { data: response.resources, isLoading: false, error: null } }); + ctx.patchState({ resourcesCount: response.count }); + ctx.patchState({ first: response.first }); + ctx.patchState({ next: response.next }); + ctx.patchState({ previous: response.previous }); + }) + ); + } else if (query.type === GetResourcesRequestTypeEnum.GetResourcesByLink) { + if (query.link) { + return this.searchService.getResourcesByLink(query.link!).pipe( + tap((response) => { + ctx.patchState({ + resources: { + data: response.resources, + isLoading: false, + error: null, + }, + }); + ctx.patchState({ resourcesCount: response.count }); + ctx.patchState({ first: response.first }); + ctx.patchState({ next: response.next }); + ctx.patchState({ previous: response.previous }); + }) + ); + } + return EMPTY; + } + return EMPTY; + }) + ) + .subscribe(); + } + + @Action(GetResources) + getResources() { + if (!this.store.selectSnapshot(PreprintsDiscoverSelectors.getIri)) { + return; + } + this.loadRequests.next({ + type: GetResourcesRequestTypeEnum.GetResources, + }); + } + + @Action(GetResourcesByLink) + getResourcesByLink(ctx: StateContext, action: GetResourcesByLink) { + this.loadRequests.next({ + type: GetResourcesRequestTypeEnum.GetResourcesByLink, + link: action.link, + }); + } + + @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(SetProviderIri) + setProviderIri(ctx: StateContext, action: SetProviderIri) { + ctx.patchState({ providerIri: action.providerIri }); + } + + @Action(ResetState) + resetState(ctx: StateContext) { + ctx.patchState({ + resources: { + data: [], + isLoading: false, + error: null, + }, + providerIri: '', + resourcesCount: 0, + searchText: '', + sortBy: '-relevance', + first: '', + next: '', + previous: '', + }); + } +} diff --git a/src/app/features/preprints/store/preprints-resources-filters-options/index.ts b/src/app/features/preprints/store/preprints-resources-filters-options/index.ts new file mode 100644 index 000000000..c8dc317d6 --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters-options/index.ts @@ -0,0 +1,4 @@ +export * from './preprints-resources-filters-options.actions'; +export * from './preprints-resources-filters-options.model'; +export * from './preprints-resources-filters-options.selectors'; +export * from './preprints-resources-filters-options.state'; diff --git a/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.actions.ts b/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.actions.ts new file mode 100644 index 000000000..6546ddf65 --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.actions.ts @@ -0,0 +1,29 @@ +export class GetCreatorsOptions { + static readonly type = '[Preprints Resource Filters Options] Get Creators'; + + constructor(public searchName: string) {} +} + +export class GetDatesCreatedOptions { + static readonly type = '[Preprints Resource Filters Options] Get Dates Created'; +} + +export class GetSubjectsOptions { + static readonly type = '[Preprints Resource Filters Options] Get Subjects'; +} + +export class GetInstitutionsOptions { + static readonly type = '[Preprints Resource Filters Options] Get Institutions'; +} + +export class GetLicensesOptions { + static readonly type = '[Preprints Resource Filters Options] Get Licenses'; +} + +export class GetProvidersOptions { + static readonly type = '[Preprints Resource Filters Options] Get Providers'; +} + +export class GetAllOptions { + static readonly type = '[Preprints Resource Filters Options] Get All Options'; +} diff --git a/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.model.ts b/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.model.ts new file mode 100644 index 000000000..50c58382c --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.model.ts @@ -0,0 +1,17 @@ +import { + Creator, + DateCreated, + InstitutionFilter, + LicenseFilter, + ProviderFilter, + SubjectFilter, +} from '@osf/shared/models'; + +export interface PreprintsResourceFiltersOptionsStateModel { + creators: Creator[]; + datesCreated: DateCreated[]; + subjects: SubjectFilter[]; + licenses: LicenseFilter[]; + providers: ProviderFilter[]; + institutions: InstitutionFilter[]; +} diff --git a/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.selectors.ts b/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.selectors.ts new file mode 100644 index 000000000..ebc3936fa --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.selectors.ts @@ -0,0 +1,62 @@ +import { Selector } from '@ngxs/store'; + +import { + Creator, + DateCreated, + InstitutionFilter, + LicenseFilter, + ProviderFilter, + SubjectFilter, +} from '@osf/shared/models'; + +import { PreprintsResourceFiltersOptionsStateModel } from './preprints-resources-filters-options.model'; +import { PreprintsResourcesFiltersOptionsState } from './preprints-resources-filters-options.state'; + +export class PreprintsResourcesFiltersOptionsSelectors { + @Selector([PreprintsResourcesFiltersOptionsState]) + static isAnyFilterOptions(state: PreprintsResourceFiltersOptionsStateModel): boolean { + return ( + state.datesCreated.length > 0 || + state.subjects.length > 0 || + state.licenses.length > 0 || + state.providers.length > 0 + ); + } + + @Selector([PreprintsResourcesFiltersOptionsState]) + static getCreators(state: PreprintsResourceFiltersOptionsStateModel): Creator[] { + return state.creators; + } + + @Selector([PreprintsResourcesFiltersOptionsState]) + static getDatesCreated(state: PreprintsResourceFiltersOptionsStateModel): DateCreated[] { + return state.datesCreated; + } + + @Selector([PreprintsResourcesFiltersOptionsState]) + static getSubjects(state: PreprintsResourceFiltersOptionsStateModel): SubjectFilter[] { + return state.subjects; + } + + @Selector([PreprintsResourcesFiltersOptionsState]) + static getInstitutions(state: PreprintsResourceFiltersOptionsStateModel): InstitutionFilter[] { + return state.institutions; + } + + @Selector([PreprintsResourcesFiltersOptionsState]) + static getLicenses(state: PreprintsResourceFiltersOptionsStateModel): LicenseFilter[] { + return state.licenses; + } + + @Selector([PreprintsResourcesFiltersOptionsState]) + static getProviders(state: PreprintsResourceFiltersOptionsStateModel): ProviderFilter[] { + return state.providers; + } + + @Selector([PreprintsResourcesFiltersOptionsState]) + static getAllOptions(state: PreprintsResourceFiltersOptionsStateModel): PreprintsResourceFiltersOptionsStateModel { + return { + ...state, + }; + } +} diff --git a/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.state.ts b/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.state.ts new file mode 100644 index 000000000..ed9272d16 --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters-options/preprints-resources-filters-options.state.ts @@ -0,0 +1,107 @@ +import { Action, State, StateContext, Store } from '@ngxs/store'; + +import { tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { PreprintsFiltersOptionsService } from '@osf/features/preprints/services'; +import { PreprintsDiscoverSelectors } from '@osf/features/preprints/store/preprints-discover'; + +import { + GetAllOptions, + GetCreatorsOptions, + GetDatesCreatedOptions, + GetInstitutionsOptions, + GetLicensesOptions, + GetProvidersOptions, + GetSubjectsOptions, +} from './preprints-resources-filters-options.actions'; +import { PreprintsResourceFiltersOptionsStateModel } from './preprints-resources-filters-options.model'; + +@State({ + name: 'preprintsResourceFiltersOptions', + defaults: { + creators: [], + datesCreated: [], + subjects: [], + licenses: [], + providers: [], + institutions: [], + }, +}) +@Injectable() +export class PreprintsResourcesFiltersOptionsState { + readonly store = inject(Store); + readonly resourceFiltersService = inject(PreprintsFiltersOptionsService); + + @Action(GetCreatorsOptions) + getCreatorsOptions(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(GetSubjectsOptions) + getSubjects(ctx: StateContext) { + return this.resourceFiltersService.getSubjects().pipe( + tap((subjects) => { + ctx.patchState({ subjects: subjects }); + }) + ); + } + + @Action(GetInstitutionsOptions) + getInstitutions(ctx: StateContext) { + return this.resourceFiltersService.getInstitutions().pipe( + tap((institutions) => { + ctx.patchState({ institutions: institutions }); + }) + ); + } + + @Action(GetLicensesOptions) + getLicenses(ctx: StateContext) { + return this.resourceFiltersService.getLicenses().pipe( + tap((licenses) => { + ctx.patchState({ licenses: licenses }); + }) + ); + } + + @Action(GetProvidersOptions) + getProviders(ctx: StateContext) { + return this.resourceFiltersService.getProviders().pipe( + tap((providers) => { + ctx.patchState({ providers: providers }); + }) + ); + } + + @Action(GetAllOptions) + getAllOptions() { + if (!this.store.selectSnapshot(PreprintsDiscoverSelectors.getIri)) { + return; + } + this.store.dispatch(GetDatesCreatedOptions); + this.store.dispatch(GetSubjectsOptions); + this.store.dispatch(GetLicensesOptions); + this.store.dispatch(GetProvidersOptions); + this.store.dispatch(GetInstitutionsOptions); + } +} diff --git a/src/app/features/preprints/store/preprints-resources-filters/index.ts b/src/app/features/preprints/store/preprints-resources-filters/index.ts new file mode 100644 index 000000000..c8e42ec6e --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters/index.ts @@ -0,0 +1,4 @@ +export * from './preprints-resources-filters.actions'; +export * from './preprints-resources-filters.model'; +export * from './preprints-resources-filters.selectors'; +export * from './preprints-resources-filters.state'; diff --git a/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.actions.ts b/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.actions.ts new file mode 100644 index 000000000..3eacd6ad2 --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.actions.ts @@ -0,0 +1,54 @@ +export class SetCreator { + static readonly type = '[Preprints Resource Filters] Set Creator'; + + constructor( + public name: string, + public id: string + ) {} +} + +export class SetDateCreated { + static readonly type = '[Preprints Resource Filters] Set DateCreated'; + + constructor(public date: string) {} +} + +export class SetSubject { + static readonly type = '[Preprints Resource Filters] Set Subject'; + + constructor( + public subject: string, + public id: string + ) {} +} + +export class SetInstitution { + static readonly type = '[Preprints Resource Filters] Set Institution'; + + constructor( + public institution: string, + public id: string + ) {} +} + +export class SetLicense { + static readonly type = '[Preprints Resource Filters] Set License'; + + constructor( + public license: string, + public id: string + ) {} +} + +export class SetProvider { + static readonly type = '[Preprints Resource Filters] Set Provider'; + + constructor( + public provider: string, + public id: string + ) {} +} + +export class ResetFiltersState { + static readonly type = '[Preprints Resource Filters] Reset State'; +} diff --git a/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.model.ts b/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.model.ts new file mode 100644 index 000000000..69bbcb511 --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.model.ts @@ -0,0 +1,10 @@ +import { ResourceFilterLabel } from '@shared/models'; + +export interface PreprintsResourcesFiltersStateModel { + creator: ResourceFilterLabel; + dateCreated: ResourceFilterLabel; + subject: ResourceFilterLabel; + license: ResourceFilterLabel; + provider: ResourceFilterLabel; + institution: ResourceFilterLabel; +} diff --git a/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.selectors.ts b/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.selectors.ts new file mode 100644 index 000000000..45b073362 --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.selectors.ts @@ -0,0 +1,50 @@ +import { Selector } from '@ngxs/store'; + +import { ResourceFilterLabel } from '@shared/models'; + +import { PreprintsResourcesFiltersStateModel } from './preprints-resources-filters.model'; +import { PreprintsResourcesFiltersState } from './preprints-resources-filters.state'; + +export class PreprintsResourcesFiltersSelectors { + @Selector([PreprintsResourcesFiltersState]) + static getAllFilters(state: PreprintsResourcesFiltersStateModel): PreprintsResourcesFiltersStateModel { + return { + ...state, + }; + } + + @Selector([PreprintsResourcesFiltersState]) + static isAnyFilterSelected(state: PreprintsResourcesFiltersStateModel): boolean { + return Boolean(state.dateCreated.value || state.subject.value || state.license.value || state.provider.value); + } + + @Selector([PreprintsResourcesFiltersState]) + static getCreator(state: PreprintsResourcesFiltersStateModel): ResourceFilterLabel { + return state.creator; + } + + @Selector([PreprintsResourcesFiltersState]) + static getDateCreated(state: PreprintsResourcesFiltersStateModel): ResourceFilterLabel { + return state.dateCreated; + } + + @Selector([PreprintsResourcesFiltersState]) + static getSubject(state: PreprintsResourcesFiltersStateModel): ResourceFilterLabel { + return state.subject; + } + + @Selector([PreprintsResourcesFiltersState]) + static getInstitution(state: PreprintsResourcesFiltersStateModel): ResourceFilterLabel { + return state.institution; + } + + @Selector([PreprintsResourcesFiltersState]) + static getLicense(state: PreprintsResourcesFiltersStateModel): ResourceFilterLabel { + return state.license; + } + + @Selector([PreprintsResourcesFiltersState]) + static getProvider(state: PreprintsResourcesFiltersStateModel): ResourceFilterLabel { + return state.provider; + } +} diff --git a/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.state.ts b/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.state.ts new file mode 100644 index 000000000..6ea3927fe --- /dev/null +++ b/src/app/features/preprints/store/preprints-resources-filters/preprints-resources-filters.state.ts @@ -0,0 +1,95 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { Injectable } from '@angular/core'; + +import { FilterLabelsModel } from '@osf/shared/models'; +import { resourceFiltersDefaults } from '@shared/constants'; + +import { + ResetFiltersState, + SetCreator, + SetDateCreated, + SetInstitution, + SetLicense, + SetProvider, + SetSubject, +} from './preprints-resources-filters.actions'; +import { PreprintsResourcesFiltersStateModel } from './preprints-resources-filters.model'; + +@State({ + name: 'preprintsResourceFilters', + defaults: { ...resourceFiltersDefaults }, +}) +@Injectable() +export class PreprintsResourcesFiltersState { + @Action(SetCreator) + setCreator(ctx: StateContext, action: SetCreator) { + ctx.patchState({ + creator: { + filterName: FilterLabelsModel.creator, + label: action.name, + value: action.id, + }, + }); + } + + @Action(SetDateCreated) + setDateCreated(ctx: StateContext, action: SetDateCreated) { + ctx.patchState({ + dateCreated: { + filterName: FilterLabelsModel.dateCreated, + label: action.date, + value: action.date, + }, + }); + } + + @Action(SetSubject) + setSubject(ctx: StateContext, action: SetSubject) { + ctx.patchState({ + subject: { + filterName: FilterLabelsModel.subject, + label: action.subject, + value: action.id, + }, + }); + } + + @Action(SetInstitution) + setInstitution(ctx: StateContext, action: SetInstitution) { + ctx.patchState({ + institution: { + filterName: FilterLabelsModel.institution, + label: action.institution, + value: action.id, + }, + }); + } + + @Action(SetLicense) + setLicense(ctx: StateContext, action: SetLicense) { + ctx.patchState({ + license: { + filterName: FilterLabelsModel.license, + label: action.license, + value: action.id, + }, + }); + } + + @Action(SetProvider) + setProvider(ctx: StateContext, action: SetProvider) { + ctx.patchState({ + provider: { + filterName: FilterLabelsModel.provider, + label: action.provider, + value: action.id, + }, + }); + } + + @Action(ResetFiltersState) + resetState(ctx: StateContext) { + ctx.patchState({ ...resourceFiltersDefaults }); + } +} diff --git a/src/app/features/preprints/store/index.ts b/src/app/features/preprints/store/preprints/index.ts similarity index 100% rename from src/app/features/preprints/store/index.ts rename to src/app/features/preprints/store/preprints/index.ts diff --git a/src/app/features/preprints/store/preprints.actions.ts b/src/app/features/preprints/store/preprints/preprints.actions.ts similarity index 100% rename from src/app/features/preprints/store/preprints.actions.ts rename to src/app/features/preprints/store/preprints/preprints.actions.ts diff --git a/src/app/features/preprints/store/preprints.model.ts b/src/app/features/preprints/store/preprints/preprints.model.ts similarity index 100% rename from src/app/features/preprints/store/preprints.model.ts rename to src/app/features/preprints/store/preprints/preprints.model.ts diff --git a/src/app/features/preprints/store/preprints.selectors.ts b/src/app/features/preprints/store/preprints/preprints.selectors.ts similarity index 97% rename from src/app/features/preprints/store/preprints.selectors.ts rename to src/app/features/preprints/store/preprints/preprints.selectors.ts index 4e3732f72..29f3e15a0 100644 --- a/src/app/features/preprints/store/preprints.selectors.ts +++ b/src/app/features/preprints/store/preprints/preprints.selectors.ts @@ -1,6 +1,6 @@ import { Selector } from '@ngxs/store'; -import { PreprintsState, PreprintsStateModel } from '@osf/features/preprints/store'; +import { PreprintsState, PreprintsStateModel } from '@osf/features/preprints/store/preprints'; export class PreprintsSelectors { @Selector([PreprintsState]) diff --git a/src/app/features/preprints/store/preprints.state.ts b/src/app/features/preprints/store/preprints/preprints.state.ts similarity index 96% rename from src/app/features/preprints/store/preprints.state.ts rename to src/app/features/preprints/store/preprints/preprints.state.ts index 261f65a61..45cc5dba1 100644 --- a/src/app/features/preprints/store/preprints.state.ts +++ b/src/app/features/preprints/store/preprints/preprints.state.ts @@ -11,8 +11,9 @@ import { GetHighlightedSubjectsByProviderId, GetPreprintProviderById, GetPreprintProvidersToAdvertise, -} from '@osf/features/preprints/store/preprints.actions'; -import { PreprintsStateModel } from '@osf/features/preprints/store/preprints.model'; +} from '@osf/features/preprints/store/preprints/preprints.actions'; + +import { PreprintsStateModel } from './'; @State({ name: 'preprints', diff --git a/src/app/features/search/components/index.ts b/src/app/features/search/components/index.ts index b80a5b0d5..fa4051313 100644 --- a/src/app/features/search/components/index.ts +++ b/src/app/features/search/components/index.ts @@ -1,6 +1,5 @@ export { FilterChipsComponent } from './filter-chips/filter-chips.component'; export * from './filters'; -export { ResourceCardComponent } from './resource-card/resource-card.component'; export { ResourceFiltersComponent } from './resource-filters/resource-filters.component'; export { ResourcesComponent } from './resources/resources.component'; export { ResourcesWrapperComponent } from './resources-wrapper/resources-wrapper.component'; diff --git a/src/app/features/search/components/resource-card/resource-card.component.html b/src/app/features/search/components/resource-card/resource-card.component.html deleted file mode 100644 index 74bfd69c9..000000000 --- a/src/app/features/search/components/resource-card/resource-card.component.html +++ /dev/null @@ -1,181 +0,0 @@ -
- - - -
- @if (item()?.resourceType && item()?.resourceType === ResourceType.Agent) { -

User

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

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

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

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

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

- @if (!isSmall()) { - Date created: {{ item()?.dateCreated | date: 'MMMM d, y' }} | Date modified: - {{ item()?.dateModified | date: 'MMMM d, y' }} - } @else { -

-

Date created: {{ item()?.dateCreated | date: 'MMMM d, y' }}

-

- Date modified: - {{ item()?.dateModified | date: 'MMMM d, y' }} -

-
- } -

- } - - @if (item()?.resourceType === ResourceType.Registration) { - - } -
-
- -
- @if (item()?.description) { -

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

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

Registration provider: 

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

License: 

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

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

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

Provider: 

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

Conflict of Interest response: Author asserted no Conflict of Interest

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

URL: 

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

DOI: 

- {{ item()?.doi }} -
- } - - @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()?.education) { -

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

- } -
-
-
-
-
diff --git a/src/app/features/search/components/resource-card/resource-card.component.scss b/src/app/features/search/components/resource-card/resource-card.component.scss deleted file mode 100644 index f65fb1607..000000000 --- a/src/app/features/search/components/resource-card/resource-card.component.scss +++ /dev/null @@ -1 +0,0 @@ -@use "assets/styles/components/resource-card" as resource-card; diff --git a/src/app/features/search/components/resource-card/resource-card.component.ts b/src/app/features/search/components/resource-card/resource-card.component.ts deleted file mode 100644 index e0773cac0..000000000 --- a/src/app/features/search/components/resource-card/resource-card.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; -import { Skeleton } from 'primeng/skeleton'; - -import { finalize } from 'rxjs'; - -import { DatePipe, NgOptimizedImage } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject, model } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; - -import { ResourceType } from '@osf/shared/enums'; -import { Resource } from '@osf/shared/models'; -import { IS_XSMALL } from '@osf/shared/utils'; - -import { ResourceCardService } from '../../services'; - -@Component({ - selector: 'osf-resource-card', - imports: [Accordion, AccordionContent, AccordionHeader, AccordionPanel, DatePipe, NgOptimizedImage, Skeleton], - templateUrl: './resource-card.component.html', - styleUrl: './resource-card.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ResourceCardComponent { - item = model(undefined); - readonly #resourceCardService = inject(ResourceCardService); - loading = false; - dataIsLoaded = false; - isSmall = toSignal(inject(IS_XSMALL)); - - 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/features/search/components/resource-filters/store/resource-filters.model.ts b/src/app/features/search/components/resource-filters/store/resource-filters.model.ts index a8d2ce815..c58b9fba6 100644 --- a/src/app/features/search/components/resource-filters/store/resource-filters.model.ts +++ b/src/app/features/search/components/resource-filters/store/resource-filters.model.ts @@ -1,3 +1,5 @@ +import { ResourceFilterLabel } from '@osf/shared/models'; + export interface ResourceFiltersStateModel { creator: ResourceFilterLabel; dateCreated: ResourceFilterLabel; @@ -9,9 +11,3 @@ export interface ResourceFiltersStateModel { provider: ResourceFilterLabel; partOfCollection: ResourceFilterLabel; } - -export interface ResourceFilterLabel { - filterName: string; - label?: string; - value?: string; -} diff --git a/src/app/features/search/components/resource-filters/store/resource-filters.selectors.ts b/src/app/features/search/components/resource-filters/store/resource-filters.selectors.ts index eaed18ed3..2055b759d 100644 --- a/src/app/features/search/components/resource-filters/store/resource-filters.selectors.ts +++ b/src/app/features/search/components/resource-filters/store/resource-filters.selectors.ts @@ -1,6 +1,8 @@ import { Selector } from '@ngxs/store'; -import { ResourceFilterLabel, ResourceFiltersStateModel } from './resource-filters.model'; +import { ResourceFilterLabel } from '@shared/models'; + +import { ResourceFiltersStateModel } from './resource-filters.model'; import { ResourceFiltersState } from './resource-filters.state'; export class ResourceFiltersSelectors { diff --git a/src/app/features/search/components/resource-filters/store/resource-filters.state.ts b/src/app/features/search/components/resource-filters/store/resource-filters.state.ts index 5ab0ceb7f..8d541b3bf 100644 --- a/src/app/features/search/components/resource-filters/store/resource-filters.state.ts +++ b/src/app/features/search/components/resource-filters/store/resource-filters.state.ts @@ -2,7 +2,8 @@ import { Action, State, StateContext } from '@ngxs/store'; import { Injectable } from '@angular/core'; -import { FilterLabelsModel, resourceFiltersDefaultsModel } from '@osf/shared/models'; +import { FilterLabelsModel } from '@osf/shared/models'; +import { resourceFiltersDefaults } from '@shared/constants'; import { ResetFiltersState, @@ -21,7 +22,7 @@ import { ResourceFiltersStateModel } from './resource-filters.model'; // Store for user selected filters values @State({ name: 'resourceFilters', - defaults: resourceFiltersDefaultsModel, + defaults: resourceFiltersDefaults, }) @Injectable() export class ResourceFiltersState { @@ -126,6 +127,6 @@ export class ResourceFiltersState { @Action(ResetFiltersState) resetState(ctx: StateContext) { - ctx.patchState(resourceFiltersDefaultsModel); + ctx.patchState(resourceFiltersDefaults); } } diff --git a/src/app/features/search/components/resources-wrapper/resources-wrapper.component.ts b/src/app/features/search/components/resources-wrapper/resources-wrapper.component.ts index ddfe54c2b..4939bf518 100644 --- a/src/app/features/search/components/resources-wrapper/resources-wrapper.component.ts +++ b/src/app/features/search/components/resources-wrapper/resources-wrapper.component.ts @@ -5,13 +5,13 @@ import { take } from 'rxjs'; import { ChangeDetectionStrategy, Component, effect, inject, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { ResourcesComponent } from '@osf/features/search/components'; import { ResourceTab } from '@osf/shared/enums'; -import { FilterLabelsModel } from '@osf/shared/models'; +import { FilterLabelsModel, ResourceFilterLabel } from '@osf/shared/models'; import { SearchSelectors, SetResourceTab, SetSearchText, SetSortBy } from '../../store'; import { GetAllOptions } from '../filters/store'; import { - ResourceFilterLabel, ResourceFiltersSelectors, SetCreator, SetDateCreated, @@ -23,7 +23,6 @@ import { SetResourceType, SetSubject, } from '../resource-filters/store'; -import { ResourcesComponent } from '../resources/resources.component'; @Component({ selector: 'osf-resources-wrapper', diff --git a/src/app/features/search/components/resources/resources.component.html b/src/app/features/search/components/resources/resources.component.html index 6bd13ef2f..9a28361e3 100644 --- a/src/app/features/search/components/resources/resources.component.html +++ b/src/app/features/search/components/resources/resources.component.html @@ -1,7 +1,7 @@
@if (isMobile()) { - + } @if (searchCount() > 10000) {

10 000+ results

@@ -16,7 +16,7 @@

0 results

@if (isWeb()) {

Sort by:

Sort by:
} @else if (isSortingOpen()) {
- @for (option of sortTabOptions; track option.value) { + @for (option of searchSortingOptions; track option.value) {
Sort by: } - +
@if (items.length > 0) { @for (item of items; track item.id) { - + }
@@ -145,6 +145,6 @@

Sort by:

}
- +
} diff --git a/src/app/features/search/components/resources/resources.component.spec.ts b/src/app/features/search/components/resources/resources.component.spec.ts index 4283f6ec1..4692ddf54 100644 --- a/src/app/features/search/components/resources/resources.component.spec.ts +++ b/src/app/features/search/components/resources/resources.component.spec.ts @@ -8,11 +8,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ResourceTab } from '@osf/shared/enums'; import { IS_WEB, IS_XSMALL } from '@osf/shared/utils'; +import { ResourceCardComponent } from '@shared/components/resource-card/resource-card.component'; import { GetResourcesByLink, SearchSelectors } from '../../store'; import { FilterChipsComponent } from '../filter-chips/filter-chips.component'; import { ResourceFiltersOptionsSelectors } from '../filters/store'; -import { ResourceCardComponent } from '../resource-card/resource-card.component'; import { ResourceFiltersComponent } from '../resource-filters/resource-filters.component'; import { ResourceFiltersSelectors } from '../resource-filters/store'; diff --git a/src/app/features/search/components/resources/resources.component.ts b/src/app/features/search/components/resources/resources.component.ts index 105addc98..a5f310593 100644 --- a/src/app/features/search/components/resources/resources.component.ts +++ b/src/app/features/search/components/resources/resources.component.ts @@ -11,14 +11,14 @@ import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, u import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FilterChipsComponent, ResourceFiltersComponent } from '@osf/features/search/components'; import { ResourceTab } from '@osf/shared/enums'; import { IS_WEB, IS_XSMALL } from '@osf/shared/utils'; +import { ResourceCardComponent } from '@shared/components'; +import { searchSortingOptions } from '@shared/constants'; import { GetResourcesByLink, SearchSelectors, SetResourceTab, SetSortBy } from '../../store'; -import { FilterChipsComponent } from '../filter-chips/filter-chips.component'; import { ResourceFiltersOptionsSelectors } from '../filters/store'; -import { ResourceCardComponent } from '../resource-card/resource-card.component'; -import { ResourceFiltersComponent } from '../resource-filters/resource-filters.component'; import { ResourceFiltersSelectors } from '../resource-filters/store'; @Component({ @@ -31,10 +31,10 @@ import { ResourceFiltersSelectors } from '../resource-filters/store'; AccordionModule, TableModule, DataViewModule, - ResourceCardComponent, FilterChipsComponent, Select, NgOptimizedImage, + ResourceCardComponent, ], templateUrl: './resources.component.html', styleUrl: './resources.component.scss', @@ -42,6 +42,7 @@ import { ResourceFiltersSelectors } from '../resource-filters/store'; }) export class ResourcesComponent { readonly #store = inject(Store); + protected readonly searchSortingOptions = searchSortingOptions; selectedTabStore = this.#store.selectSignal(SearchSelectors.getResourceTab); searchCount = this.#store.selectSignal(SearchSelectors.getResourcesCount); @@ -90,13 +91,6 @@ export class ResourcesComponent { protected readonly isMobile = toSignal(inject(IS_XSMALL)); 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' }, - ]; protected selectedTab = signal(ResourceTab.All); protected readonly tabsOptions = [ diff --git a/src/app/features/search/services/index.ts b/src/app/features/search/services/index.ts index 70f3be8ac..29ca64498 100644 --- a/src/app/features/search/services/index.ts +++ b/src/app/features/search/services/index.ts @@ -1,2 +1 @@ -export { ResourceCardService } from './resource-card.service'; export { ResourceFiltersService } from './resource-filters.service'; diff --git a/src/app/features/search/store/search.state.ts b/src/app/features/search/store/search.state.ts index af5695c58..5fa319f19 100644 --- a/src/app/features/search/store/search.state.ts +++ b/src/app/features/search/store/search.state.ts @@ -4,12 +4,12 @@ import { BehaviorSubject, EMPTY, switchMap, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { GetResourcesRequestTypeEnum } from '@osf/features/search/enums/get-resources-request-type.enum'; import { SearchService } from '@osf/shared/services'; import { addFiltersParams, getResourceTypes } from '@osf/shared/utils'; +import { searchStateDefaults } from '@shared/constants'; +import { GetResourcesRequestTypeEnum } from '@shared/enums'; import { ResourceFiltersSelectors } from '../components/resource-filters/store'; -import { searchStateDefaults } from '../utils/data'; import { GetResources, diff --git a/src/app/features/search/utils/index.ts b/src/app/features/search/utils/index.ts deleted file mode 100644 index 370767922..000000000 --- a/src/app/features/search/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './data'; diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index 9b3862021..d37535e65 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -11,6 +11,7 @@ export { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.compo export { MyProjectsTableComponent } from './my-projects-table/my-projects-table.component'; export { PasswordInputHintComponent } from './password-input-hint/password-input-hint.component'; export { PieChartComponent } from './pie-chart/pie-chart.component'; +export { ResourceCardComponent } from './resource-card/resource-card.component'; export { SearchInputComponent } from './search-input/search-input.component'; export { SubHeaderComponent } from './sub-header/sub-header.component'; export { TextInputComponent } from './text-input/text-input.component'; diff --git a/src/app/shared/components/resource-card/resource-card.component.html b/src/app/shared/components/resource-card/resource-card.component.html new file mode 100644 index 000000000..c8c33900a --- /dev/null +++ b/src/app/shared/components/resource-card/resource-card.component.html @@ -0,0 +1,181 @@ +
+ + + +
+ @if (item().resourceType && item().resourceType === ResourceType.Agent) { +

User

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

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

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

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

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

+ @if (!isSmall()) { + Date created: {{ item().dateCreated | date: 'MMMM d, y' }} | Date modified: + {{ item().dateModified | date: 'MMMM d, y' }} + } @else { +

+

Date created: {{ item().dateCreated | date: 'MMMM d, y' }}

+

+ Date modified: + {{ item().dateModified | date: 'MMMM d, y' }} +

+
+ } +

+ } + + @if (item().resourceType === ResourceType.Registration) { + + } +
+
+ +
+ @if (item().description) { +

Description: {{ item().description }}

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

Registration provider: 

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

License: 

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

Registration Template: {{ item().registrationTemplate }}

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

Provider: 

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

Conflict of Interest response: Author asserted no Conflict of Interest

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

URL:

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

DOI: 

+ {{ item().doi }} +
+ } + + @if (item().resourceType === ResourceType.Agent) { + @if (isLoading) { + + + + } @else { +

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

+

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

+

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

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

Employment: {{ item().employment }}

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

Education: {{ item().education }}

+ } +
+
+
+
+
diff --git a/src/assets/styles/components/resource-card.scss b/src/app/shared/components/resource-card/resource-card.component.scss similarity index 99% rename from src/assets/styles/components/resource-card.scss rename to src/app/shared/components/resource-card/resource-card.component.scss index e0a92b726..572744b3b 100644 --- a/src/assets/styles/components/resource-card.scss +++ b/src/app/shared/components/resource-card/resource-card.component.scss @@ -14,6 +14,7 @@ line-height: 1.7rem; color: var.$dark-blue-1; padding-bottom: 4px; + &:hover { text-decoration: underline; } diff --git a/src/app/features/search/components/resource-card/resource-card.component.spec.ts b/src/app/shared/components/resource-card/resource-card.component.spec.ts similarity index 85% rename from src/app/features/search/components/resource-card/resource-card.component.spec.ts rename to src/app/shared/components/resource-card/resource-card.component.spec.ts index 7aad6b7cc..1a1175a10 100644 --- a/src/app/features/search/components/resource-card/resource-card.component.spec.ts +++ b/src/app/shared/components/resource-card/resource-card.component.spec.ts @@ -4,11 +4,9 @@ import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { IS_XSMALL } from '@osf/shared/utils'; - -import { ResourceCardService } from '../../services'; - -import { ResourceCardComponent } from './resource-card.component'; +import { ResourceCardComponent } from '@shared/components'; +import { ResourceCardService } from '@shared/services'; +import { IS_XSMALL } from '@shared/utils'; describe('MyProfileResourceCardComponent', () => { let component: ResourceCardComponent; diff --git a/src/app/shared/components/resource-card/resource-card.component.ts b/src/app/shared/components/resource-card/resource-card.component.ts new file mode 100644 index 000000000..c3ee18a79 --- /dev/null +++ b/src/app/shared/components/resource-card/resource-card.component.ts @@ -0,0 +1,62 @@ +import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; +import { Skeleton } from 'primeng/skeleton'; + +import { finalize } from 'rxjs'; + +import { DatePipe, NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject, model } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; + +import { ResourceType } from '@shared/enums'; +import { Resource } from '@shared/models'; +import { ResourceCardService } from '@shared/services'; +import { IS_XSMALL } from '@shared/utils'; + +@Component({ + selector: 'osf-resource-card', + imports: [Accordion, AccordionContent, AccordionHeader, AccordionPanel, DatePipe, NgOptimizedImage, Skeleton], + templateUrl: './resource-card.component.html', + styleUrl: './resource-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ResourceCardComponent { + private readonly resourceCardService = inject(ResourceCardService); + ResourceType = ResourceType; + isSmall = toSignal(inject(IS_XSMALL)); + item = model.required(); + + isLoading = false; + dataIsLoaded = false; + + onOpen() { + if (!this.item() || this.dataIsLoaded || this.item().resourceType !== ResourceType.Agent) { + return; + } + + const userIri = this.item()?.id.split('/').pop(); + if (userIri) { + this.isLoading = true; + this.resourceCardService + .getUserRelatedCounts(userIri) + .pipe( + finalize(() => { + this.isLoading = 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/constants/index.ts b/src/app/shared/constants/index.ts index 9c28141dd..f98445908 100644 --- a/src/app/shared/constants/index.ts +++ b/src/app/shared/constants/index.ts @@ -1,4 +1,7 @@ export * from './input-limits.const'; export * from './input-validation-messages.const'; export * from './remove-nullable.const'; +export * from './resource-filters-defaults'; export * from './scientists.const'; +export * from './search-sort-options.const'; +export * from './search-state-defaults.const'; diff --git a/src/app/shared/models/resource-filters-defaults.model.ts b/src/app/shared/constants/resource-filters-defaults.ts similarity index 90% rename from src/app/shared/models/resource-filters-defaults.model.ts rename to src/app/shared/constants/resource-filters-defaults.ts index 1522f3b31..c01ac7b5b 100644 --- a/src/app/shared/models/resource-filters-defaults.model.ts +++ b/src/app/shared/constants/resource-filters-defaults.ts @@ -1,6 +1,6 @@ -import { FilterLabelsModel } from './filter-labels.model'; +import { FilterLabelsModel } from '@shared/models'; -export const resourceFiltersDefaultsModel = { +export const resourceFiltersDefaults = { creator: { filterName: FilterLabelsModel.creator, label: undefined, diff --git a/src/app/shared/constants/search-sort-options.const.ts b/src/app/shared/constants/search-sort-options.const.ts new file mode 100644 index 000000000..6c26646be --- /dev/null +++ b/src/app/shared/constants/search-sort-options.const.ts @@ -0,0 +1,7 @@ +export const searchSortingOptions = [ + { 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' }, +]; diff --git a/src/app/features/search/utils/data.ts b/src/app/shared/constants/search-state-defaults.const.ts similarity index 80% rename from src/app/features/search/utils/data.ts rename to src/app/shared/constants/search-state-defaults.const.ts index 0e89de1a1..19b9ddbc7 100644 --- a/src/app/features/search/utils/data.ts +++ b/src/app/shared/constants/search-state-defaults.const.ts @@ -1,4 +1,4 @@ -import { ResourceTab } from '@osf/shared/enums/resource-tab.enum'; +import { ResourceTab } from '@shared/enums'; export const searchStateDefaults = { resources: { diff --git a/src/app/features/search/enums/get-resources-request-type.enum.ts b/src/app/shared/enums/get-resources-request-type.enum.ts similarity index 100% rename from src/app/features/search/enums/get-resources-request-type.enum.ts rename to src/app/shared/enums/get-resources-request-type.enum.ts diff --git a/src/app/shared/enums/index.ts b/src/app/shared/enums/index.ts index 5b89b32f6..db1b959a3 100644 --- a/src/app/shared/enums/index.ts +++ b/src/app/shared/enums/index.ts @@ -2,6 +2,7 @@ export * from './breakpoint-queries.enum'; export * from './create-component-form-controls.enum'; export * from './create-project-form-controls.enum'; export * from './filter-type.enum'; +export * from './get-resources-request-type.enum'; export * from './resource-tab.enum'; export * from './resource-type.enum'; export * from './share-indexing.enum'; diff --git a/src/app/shared/models/filters/index.ts b/src/app/shared/models/filters/index.ts index fe8e5ad79..375df8e0a 100644 --- a/src/app/shared/models/filters/index.ts +++ b/src/app/shared/models/filters/index.ts @@ -7,6 +7,7 @@ export * from './institution'; export * from './license'; export * from './part-of-collection'; export * from './provider'; +export * from './resource-filter-label'; export * from './resource-type'; export * from './search-filters.model'; export * from './search-result-count.model'; diff --git a/src/app/shared/models/filters/resource-filter-label.ts b/src/app/shared/models/filters/resource-filter-label.ts new file mode 100644 index 000000000..8d7d6693a --- /dev/null +++ b/src/app/shared/models/filters/resource-filter-label.ts @@ -0,0 +1,5 @@ +export interface ResourceFilterLabel { + filterName: string; + label?: string; + value?: string; +} diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 6c9819889..74def860f 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -10,7 +10,6 @@ export * from './node-response.model'; export * from './paginated-data.model'; export * from './query-params.model'; export * from './resource-card'; -export * from './resource-filters-defaults.model'; export * from './select-option.model'; export * from './social-icon.model'; export * from './store'; diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index e3aa084cc..63411591b 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -1,4 +1,5 @@ export { FiltersOptionsService } from './filters-options.service'; export { LoaderService } from './loader.service'; +export { ResourceCardService } from './resource-card.service'; export { SearchService } from './search.service'; export { ToastService } from './toast.service'; diff --git a/src/app/features/search/services/resource-card.service.ts b/src/app/shared/services/resource-card.service.ts similarity index 66% rename from src/app/features/search/services/resource-card.service.ts rename to src/app/shared/services/resource-card.service.ts index f1dc221a0..aeaf1eb49 100644 --- a/src/app/features/search/services/resource-card.service.ts +++ b/src/app/shared/services/resource-card.service.ts @@ -2,9 +2,9 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { JsonApiService } from '@osf/core/services'; -import { MapUserCounts } from '@osf/shared/mappers'; -import { UserCountsResponse, UserRelatedDataCounts } from '@osf/shared/models'; +import { JsonApiService } from '@core/services'; +import { MapUserCounts } from '@shared/mappers'; +import { UserCountsResponse, UserRelatedDataCounts } from '@shared/models'; import { environment } from 'src/environments/environment'; @@ -12,15 +12,15 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class ResourceCardService { - #jsonApiService = inject(JsonApiService); + private jsonApiService = inject(JsonApiService); getUserRelatedCounts(userIri: string): Observable { const params: Record = { related_counts: 'nodes,registrations,preprints', }; - return this.#jsonApiService - .get(`${environment.apiUrl}/users/${userIri}`, params) + return this.jsonApiService + .get(`${environment.apiUrl}/users/${userIri}/`, params) .pipe(map((response) => MapUserCounts(response))); } }