From d13d8860f24dd641e1408904430f65d0e65df7b9 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Thu, 19 Dec 2024 12:03:52 -0500 Subject: [PATCH] RSS feed from search results (Angular) (#3227) * Port rss to 7.6 and upgrades to search functionality * 116466: add missing imports * 116466: fix tests and lint issues * 116466: rss component use activated route data * 116466: lint fixes * 116466: More Lint fixes --------- Co-authored-by: Nathan Buckingham --- src/app/app-routes.ts | 8 ++- src/app/shared/rss-feed/rss.component.html | 10 ++- src/app/shared/rss-feed/rss.component.spec.ts | 53 ++++++++++++--- src/app/shared/rss-feed/rss.component.ts | 64 +++++++++++++++---- 4 files changed, 107 insertions(+), 28 deletions(-) diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 29a78364b53..51101f5a2d8 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -63,7 +63,7 @@ export const APP_ROUTES: Route[] = [ path: 'home', loadChildren: () => import('./home-page/home-page-routes') .then((m) => m.ROUTES), - data: { showBreadcrumbs: false }, + data: { showBreadcrumbs: false, enableRSS: true }, providers: [provideSuggestionNotificationsState()], canActivate: [endUserAgreementCurrentUserGuard], }, @@ -101,12 +101,14 @@ export const APP_ROUTES: Route[] = [ path: COMMUNITY_MODULE_PATH, loadChildren: () => import('./community-page/community-page-routes') .then((m) => m.ROUTES), + data: { enableRSS: true }, canActivate: [endUserAgreementCurrentUserGuard], }, { path: COLLECTION_MODULE_PATH, loadChildren: () => import('./collection-page/collection-page-routes') .then((m) => m.ROUTES), + data: { showBreadcrumbs: false, enableRSS: true }, canActivate: [endUserAgreementCurrentUserGuard], }, { @@ -137,6 +139,7 @@ export const APP_ROUTES: Route[] = [ path: 'mydspace', loadChildren: () => import('./my-dspace-page/my-dspace-page-routes') .then((m) => m.ROUTES), + data: { enableRSS: true }, providers: [provideSuggestionNotificationsState()], canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard], }, @@ -144,6 +147,7 @@ export const APP_ROUTES: Route[] = [ path: 'search', loadChildren: () => import('./search-page/search-page-routes') .then((m) => m.ROUTES), + data: { enableRSS: true }, canActivate: [endUserAgreementCurrentUserGuard], }, { @@ -156,6 +160,7 @@ export const APP_ROUTES: Route[] = [ path: ADMIN_MODULE_PATH, loadChildren: () => import('./admin/admin-routes') .then((m) => m.ROUTES), + data: { enableRSS: true }, canActivate: [siteAdministratorGuard, endUserAgreementCurrentUserGuard], }, { @@ -200,6 +205,7 @@ export const APP_ROUTES: Route[] = [ providers: [provideSubmissionState()], loadChildren: () => import('./workflowitems-edit-page/workflowitems-edit-page-routes') .then((m) => m.ROUTES), + data: { enableRSS: true }, canActivate: [endUserAgreementCurrentUserGuard], }, { diff --git a/src/app/shared/rss-feed/rss.component.html b/src/app/shared/rss-feed/rss.component.html index 91140c50c5a..133880c593a 100644 --- a/src/app/shared/rss-feed/rss.component.html +++ b/src/app/shared/rss-feed/rss.component.html @@ -1,5 +1,9 @@ - +
- + + +
-
+
\ No newline at end of file diff --git a/src/app/shared/rss-feed/rss.component.spec.ts b/src/app/shared/rss-feed/rss.component.spec.ts index 724d0abf4a3..60de4caef6d 100644 --- a/src/app/shared/rss-feed/rss.component.spec.ts +++ b/src/app/shared/rss-feed/rss.component.spec.ts @@ -3,9 +3,17 @@ import { TestBed, waitForAsync, } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; +import { + SortDirection, + SortOptions, +} from '../../core/cache/models/sort-options.model'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { GroupDataService } from '../../core/eperson/group-data.service'; @@ -14,22 +22,24 @@ import { LinkHeadService } from '../../core/services/link-head.service'; import { Collection } from '../../core/shared/collection.model'; import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { MockActivatedRoute } from '../mocks/active-router.mock'; import { RouterMock } from '../mocks/router.mock'; +import { getMockTranslateService } from '../mocks/translate.service.mock'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$, } from '../remote-data.utils'; import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; +import { SearchFilter } from '../search/models/search-filter.model'; import { PaginationServiceStub } from '../testing/pagination-service.stub'; import { SearchConfigurationServiceStub } from '../testing/search-configuration-service.stub'; import { createPaginatedList } from '../testing/utils.test'; import { RSSComponent } from './rss.component'; - - describe('RssComponent', () => { let comp: RSSComponent; + let options: SortOptions; let fixture: ComponentFixture; let uuid: string; let query: string; @@ -69,6 +79,7 @@ describe('RssComponent', () => { pageSize: 10, currentPage: 1, }), + sort: new SortOptions('dc.title', SortDirection.ASC), })); groupDataService = jasmine.createSpyObj('groupsDataService', { findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -80,7 +91,6 @@ describe('RssComponent', () => { paginatedSearchOptions: mockSearchOptions, }; TestBed.configureTestingModule({ - imports: [RSSComponent], providers: [ { provide: GroupDataService, useValue: groupDataService }, { provide: LinkHeadService, useValue: linkHeadService }, @@ -88,11 +98,15 @@ describe('RssComponent', () => { { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, { provide: PaginationService, useValue: paginationService }, { provide: Router, useValue: new RouterMock() }, + { provide: ActivatedRoute, useValue: new MockActivatedRoute }, + { provide: TranslateService, useValue: getMockTranslateService() }, ], + declarations: [], }).compileComponents(); })); beforeEach(() => { + options = new SortOptions('dc.title', SortDirection.DESC); uuid = '2cfcf65e-0a51-4bcb-8592-b8db7b064790'; query = 'test'; fixture = TestBed.createComponent(RSSComponent); @@ -100,18 +114,37 @@ describe('RssComponent', () => { }); it('should formulate the correct url given params in url', () => { - const route = comp.formulateRoute(uuid, 'opensearch/search', query); - expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&query=test'); + const route = comp.formulateRoute(uuid, 'opensearch/search', options, query); + expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&sort=dc.title&sort_direction=DESC&query=test'); }); it('should skip uuid if its null', () => { - const route = comp.formulateRoute(null, 'opensearch/search', query); - expect(route).toBe('/opensearch/search?format=atom&query=test'); + const route = comp.formulateRoute(null, 'opensearch/search', options, query); + expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=test'); }); it('should default to query * if none provided', () => { - const route = comp.formulateRoute(null, 'opensearch/search', null); - expect(route).toBe('/opensearch/search?format=atom&query=*'); + const route = comp.formulateRoute(null, 'opensearch/search', options, null); + expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=*'); + }); + + it('should include filters in opensearch url if provided', () => { + const filters = [ + new SearchFilter('f.test', ['value','another value'], 'contains'), // should be split into two arguments, spaces should be URI-encoded + new SearchFilter('f.range', ['[1987 TO 1988]'], 'equals'), // value should be URI-encoded, ',equals' should not + ]; + const route = comp.formulateRoute(uuid, 'opensearch/search', options, query, filters); + expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&sort=dc.title&sort_direction=DESC&query=test&f.test=value,contains&f.test=another%20value,contains&f.range=%5B1987%20TO%201988%5D,equals'); + }); + + it('should include configuration in opensearch url if provided', () => { + const route = comp.formulateRoute(uuid, 'opensearch/search', options, query, null, 'adminConfiguration'); + expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&sort=dc.title&sort_direction=DESC&query=test&configuration=adminConfiguration'); + }); + + it('should include rpp in opensearch url if provided', () => { + const route = comp.formulateRoute(uuid, 'opensearch/search', options, query, null, null, 50); + expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&sort=dc.title&sort_direction=DESC&query=test&rpp=50'); }); }); diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index e089525a72d..7d0338ba972 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -9,11 +9,16 @@ import { OnInit, ViewEncapsulation, } from '@angular/core'; -import { Router } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { BehaviorSubject, - Observable, Subscription, } from 'rxjs'; import { @@ -21,7 +26,8 @@ import { switchMap, } from 'rxjs/operators'; -import { environment } from '../../../../src/environments/environment'; +import { environment } from '../../../environments/environment'; +import { SortOptions } from '../../core/cache/models/sort-options.model'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { GroupDataService } from '../../core/eperson/group-data.service'; @@ -29,9 +35,12 @@ import { PaginationService } from '../../core/pagination/pagination.service'; import { LinkHeadService } from '../../core/services/link-head.service'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { + hasValue, + isUndefined, +} from '../empty.util'; import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; - - +import { SearchFilter } from '../search/models/search-filter.model'; /** * The Rss feed button component. */ @@ -51,8 +60,9 @@ export class RSSComponent implements OnInit, OnDestroy { isEnabled$: BehaviorSubject = new BehaviorSubject(null); + isActivated$: BehaviorSubject = new BehaviorSubject(false); + uuid: string; - configuration$: Observable; subs: Subscription[] = []; @@ -61,7 +71,9 @@ export class RSSComponent implements OnInit, OnDestroy { private configurationService: ConfigurationDataService, private searchConfigurationService: SearchConfigurationService, private router: Router, - protected paginationService: PaginationService) { + private route: ActivatedRoute, + protected paginationService: PaginationService, + protected translateService: TranslateService) { } /** * Removes the linktag created when the component gets removed from the page. @@ -78,8 +90,11 @@ export class RSSComponent implements OnInit, OnDestroy { * Generates the link tags and the url to opensearch when the component is loaded. */ ngOnInit(): void { - this.configuration$ = this.searchConfigurationService.getCurrentConfiguration('default'); - + if (hasValue(this.route.snapshot.data?.enableRSS)) { + this.isActivated$.next(this.route.snapshot.data.enableRSS); + } else if (isUndefined(this.route.snapshot.data?.enableRSS)) { + this.isActivated$.next(false); + } this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.enable').pipe( getFirstCompletedRemoteData(), ).subscribe((result) => { @@ -106,7 +121,7 @@ export class RSSComponent implements OnInit, OnDestroy { return null; } this.uuid = this.groupDataService.getUUIDFromString(this.router.url); - const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, openSearchUri, searchOptions.query); + const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, openSearchUri, searchOptions.sort, searchOptions.query, searchOptions.filters, searchOptions.configuration, searchOptions.pagination?.pageSize, searchOptions.fixedFilter); this.addLinks(route); this.linkHeadService.addTag({ href: environment.rest.baseUrl + '/' + openSearchUri + '/service', @@ -122,20 +137,40 @@ export class RSSComponent implements OnInit, OnDestroy { * Function created a route given the different params available to opensearch * @param uuid The uuid if a scope is present * @param opensearch openSearch uri + * @param sort The sort options for the opensearch request * @param query The query string that was provided in the search * @returns The combine URL to opensearch */ - formulateRoute(uuid: string, opensearch: string, query: string): string { - let route = '?format=atom'; + formulateRoute(uuid: string, opensearch: string, sort?: SortOptions, query?: string, searchFilters?: SearchFilter[], configuration?: string, pageSize?: number, fixedFilter?: string): string { + let route = 'format=atom'; if (uuid) { route += `&scope=${uuid}`; } + if (sort && sort.direction && sort.field && sort.field !== 'id') { + route += `&sort=${sort.field}&sort_direction=${sort.direction}`; + } if (query) { route += `&query=${query}`; } else { route += `&query=*`; } - route = '/' + opensearch + route; + if (configuration) { + route += `&configuration=${configuration}`; + } + if (pageSize) { + route += `&rpp=${pageSize}`; + } + if (searchFilters) { + for (const filter of searchFilters) { + for (const val of filter.values) { + route += '&' + filter.key + '=' + encodeURIComponent(val) + (filter.operator ? ',' + filter.operator : ''); + } + } + } + if (fixedFilter) { + route += '&' + fixedFilter; + } + route = '/' + opensearch + '?' + route; return route; } @@ -169,4 +204,5 @@ export class RSSComponent implements OnInit, OnDestroy { title: 'Sitewide RSS feed', }); } + }