diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index dc9b427634..9b1a2f28c9 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -109,14 +109,12 @@ 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: { enableRSS: true }, canActivate: [endUserAgreementCurrentUserGuard], }, { diff --git a/src/app/collection-page/collection-page-routes.ts b/src/app/collection-page/collection-page-routes.ts index d03c57779f..ce583b2cd5 100644 --- a/src/app/collection-page/collection-page-routes.ts +++ b/src/app/collection-page/collection-page-routes.ts @@ -100,6 +100,7 @@ export const ROUTES: Route[] = [ data: { breadcrumbKey: 'collection.search', menuRoute: MenuRoute.COLLECTION_PAGE, + enableRSS: true, }, }, { diff --git a/src/app/community-page/community-page-routes.ts b/src/app/community-page/community-page-routes.ts index 35eb7780c8..979525ee1e 100644 --- a/src/app/community-page/community-page-routes.ts +++ b/src/app/community-page/community-page-routes.ts @@ -90,6 +90,7 @@ export const ROUTES: Route[] = [ data: { breadcrumbKey: 'community.search', menuRoute: MenuRoute.COMMUNITY_PAGE, + enableRSS: true, }, }, { diff --git a/src/app/home-page/top-level-community-list/top-level-community-list.component.html b/src/app/home-page/top-level-community-list/top-level-community-list.component.html index 00446d658a..8b0ee4b853 100644 --- a/src/app/home-page/top-level-community-list/top-level-community-list.component.html +++ b/src/app/home-page/top-level-community-list/top-level-community-list.component.html @@ -8,6 +8,7 @@ diff --git a/src/app/home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/home-page/top-level-community-list/top-level-community-list.component.ts index 4af7bcc483..7f3e384df8 100644 --- a/src/app/home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/home-page/top-level-community-list/top-level-community-list.component.ts @@ -65,9 +65,10 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy { pageId = 'tl'; /** - * The sorting configuration + * The sorting configuration for the community list itself, and the optional RSS feed button */ sortConfig: SortOptions; + rssSortConfig: SortOptions; /** * The subscription to the observable for the current page. @@ -84,6 +85,7 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy { this.config.pageSize = appConfig.homePage.topLevelCommunityList.pageSize; this.config.currentPage = 1; this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.rssSortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC); } ngOnInit() { diff --git a/src/app/shared/object-collection/object-collection.component.html b/src/app/shared/object-collection/object-collection.component.html index 10378ba3f5..1d9be701d2 100644 --- a/src/app/shared/object-collection/object-collection.component.html +++ b/src/app/shared/object-collection/object-collection.component.html @@ -13,6 +13,7 @@ [linkType]="linkType" [context]="context" [hidePaginationDetail]="hidePaginationDetail" + [showRSS]="showRSS" [showPaginator]="showPaginator" [showThumbnails]="showThumbnails" (paginationChange)="onPaginationChange($event)" @@ -58,6 +59,7 @@ [sortConfig]="sortConfig" [objects]="objects" [hideGear]="hideGear" + [showRSS]="showRSS" [linkType]="linkType" [context]="context" [hidePaginationDetail]="hidePaginationDetail" diff --git a/src/app/shared/object-collection/object-collection.component.ts b/src/app/shared/object-collection/object-collection.component.ts index 204685930a..d44c0153b2 100644 --- a/src/app/shared/object-collection/object-collection.component.ts +++ b/src/app/shared/object-collection/object-collection.component.ts @@ -71,17 +71,22 @@ export class ObjectCollectionComponent implements OnInit { @Input() sortConfig: SortOptions; /** - * Whether or not the list elements have a border or not + * Whether the list elements have a border or not */ @Input() hasBorder = false; /** - * Whether or not to hide the gear to change the sort and pagination configuration + * Whether to hide the gear to change the sort and pagination configuration */ @Input() hideGear = false; @Input() selectable = false; @Input() selectionConfig: {repeatable: boolean, listId: string}; + /** + * Whether to show an RSS syndication button for the current search options + */ + @Input() showRSS: SortOptions | boolean = false; + /** * Emit custom event for listable object custom actions. */ diff --git a/src/app/shared/object-detail/object-detail.component.html b/src/app/shared/object-detail/object-detail.component.html index ad9f0bce0e..2696730cf6 100644 --- a/src/app/shared/object-detail/object-detail.component.html +++ b/src/app/shared/object-detail/object-detail.component.html @@ -4,6 +4,7 @@ [sortOptions]="sortConfig" [objects]="objects" [hideGear]="hideGear" + [showRSS]="showRSS" [hidePaginationDetail]="hidePaginationDetail" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage" [showPaginator]="showPaginator" diff --git a/src/app/shared/object-detail/object-detail.component.ts b/src/app/shared/object-detail/object-detail.component.ts index 01e1795fcb..67037aa6bd 100644 --- a/src/app/shared/object-detail/object-detail.component.ts +++ b/src/app/shared/object-detail/object-detail.component.ts @@ -85,6 +85,11 @@ export class ObjectDetailComponent { */ @Input() showThumbnails; + /** + * Whether to show the RSS syndication link. Either false, or valid SortOptions object + */ + @Input() showRSS: SortOptions | boolean = false; + /** * Emit when one of the listed object has changed. */ diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index 3073623c2e..5670199fc5 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -4,6 +4,7 @@ [objects]="objects" [sortOptions]="sortConfig" [hideGear]="hideGear" + [showRSS]="showRSS" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage" [hidePaginationDetail]="hidePaginationDetail" [showPaginator]="showPaginator" diff --git a/src/app/shared/object-list/object-list.component.ts b/src/app/shared/object-list/object-list.component.ts index cf3e0164f6..3c0136fec9 100644 --- a/src/app/shared/object-list/object-list.component.ts +++ b/src/app/shared/object-list/object-list.component.ts @@ -39,7 +39,7 @@ import { SelectableListService } from './selectable-list/selectable-list.service }) export class ObjectListComponent { /** - * The view mode of the this component + * The view mode of this component */ viewMode = ViewMode.ListElement; @@ -70,6 +70,11 @@ export class ObjectListComponent { @Input() selectable = false; @Input() selectionConfig: { repeatable: boolean, listId: string }; + /** + * Whether to show an RSS syndication button for the current search options + */ + @Input() showRSS: SortOptions | boolean = false; + /** * The link type of the listable elements */ diff --git a/src/app/shared/object-list/themed-object-list.component.ts b/src/app/shared/object-list/themed-object-list.component.ts index e6e0527c90..df932e3a0f 100644 --- a/src/app/shared/object-list/themed-object-list.component.ts +++ b/src/app/shared/object-list/themed-object-list.component.ts @@ -59,6 +59,8 @@ export class ThemedObjectListComponent extends ThemedComponent } - + @if (showRSS !== false) { + + } diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 907b427354..a10373af1e 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -168,6 +168,13 @@ export class PaginationComponent implements OnChanges, OnDestroy, OnInit { */ @Input() public retainScrollPosition = false; + /** + * Options for showing or hiding the RSS syndication feed. This is useful for e.g. top-level community lists + * or other lists where an RSS feed doesn't make sense, but uses the same components as recent items or search result + * lists. + */ + @Input() public showRSS: SortOptions | boolean = false; + /** * Current page. */ @@ -266,7 +273,6 @@ export class PaginationComponent implements OnChanges, OnDestroy, OnInit { * Initializes all default variables */ private initializeConfig() { - // Set initial values this.id = this.paginationOptions.id || null; this.pageSizeOptions = this.paginationOptions.pageSizeOptions; this.currentPage$ = this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe( @@ -436,4 +442,19 @@ export class PaginationComponent implements OnChanges, OnDestroy, OnInit { }); } + /** + * Get the sort options to use for the RSS feed. Defaults to the sort options used for this pagination component + * so it matches the search/browse context, but also allows more flexibility if, for example a top-level community + * list is displayed in "title asc" order, but the RSS feed should default to an item list of "date desc" order. + * If the SortOptions are null, incomplete or invalid, the pagination sortOptions will be used instead. + */ + get rssSortOptions() { + if (this.showRSS !== false && this.showRSS instanceof SortOptions + && this.showRSS.direction !== null + && this.showRSS.field !== null) { + return this.showRSS; + } + return this.sortOptions; + } + } diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 8d2b49f0e1..1ed29697bf 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -2,12 +2,16 @@ import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, + Input, + OnChanges, OnDestroy, OnInit, + SimpleChanges, ViewEncapsulation, } from '@angular/core'; import { ActivatedRoute, + NavigationEnd, Router, } from '@angular/router'; import { @@ -16,12 +20,10 @@ import { } from '@ngx-translate/core'; import { BehaviorSubject, + filter, Subscription, } from 'rxjs'; -import { - map, - switchMap, -} from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { environment } from '../../../environments/environment'; import { SortOptions } from '../../core/cache/models/sort-options.model'; @@ -36,8 +38,8 @@ 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,17 +53,16 @@ import { SearchFilter } from '../search/models/search-filter.model'; standalone: true, imports: [AsyncPipe, TranslateModule], }) -export class RSSComponent implements OnInit, OnDestroy { +export class RSSComponent implements OnInit, OnDestroy, OnChanges { route$: BehaviorSubject = new BehaviorSubject(''); - isEnabled$: BehaviorSubject = new BehaviorSubject(null); - isActivated$: BehaviorSubject = new BehaviorSubject(false); + @Input() sortConfig?: SortOptions; uuid: string; - subs: Subscription[] = []; + openSearchUri: string; constructor(private groupDataService: GroupDataService, private linkHeadService: LinkHeadService, @@ -72,9 +73,7 @@ export class RSSComponent implements OnInit, OnDestroy { protected paginationService: PaginationService, protected translateService: TranslateService) { } - /** - * Removes the linktag created when the component gets removed from the page. - */ + ngOnDestroy(): void { this.linkHeadService.removeTag("rel='alternate'"); this.subs.forEach(sub => { @@ -82,24 +81,52 @@ export class RSSComponent implements OnInit, OnDestroy { }); } - - /** - * Generates the link tags and the url to opensearch when the component is loaded. - */ ngOnInit(): void { + // Set initial activation state 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); } + + // Get initial UUID from URL + this.uuid = this.groupDataService.getUUIDFromString(this.router.url); + + // Check if RSS is enabled this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.enable').pipe( getFirstCompletedRemoteData(), ).subscribe((result) => { if (result.hasSucceeded) { const enabled = (result.payload.values[0] === 'true'); this.isEnabled$.next(enabled); + + // If enabled, get the OpenSearch URI + if (enabled) { + this.getOpenSearchUri(); + } } })); + + // Listen for navigation events to update the UUID + this.subs.push(this.router.events.pipe( + filter(event => event instanceof NavigationEnd), + ).subscribe(() => { + this.uuid = this.groupDataService.getUUIDFromString(this.router.url); + this.updateRssLinks(); + })); + } + + ngOnChanges(changes: SimpleChanges): void { + // If sortConfig changes, update the RSS links + if (changes.sortConfig && this.openSearchUri && this.isEnabled$.getValue()) { + this.updateRssLinks(); + } + } + + /** + * Get the OpenSearch URI and update RSS links + */ + private getOpenSearchUri(): void { this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.svccontext').pipe( getFirstCompletedRemoteData(), map((result: RemoteData) => { @@ -108,35 +135,60 @@ export class RSSComponent implements OnInit, OnDestroy { } return null; }), - switchMap((openSearchUri: string) => - this.searchConfigurationService.paginatedSearchOptions.pipe( - map((searchOptions: PaginatedSearchOptions) => ({ openSearchUri, searchOptions })), - ), - ), - ).subscribe(({ openSearchUri, searchOptions }) => { - if (!openSearchUri) { - return null; - } - this.uuid = this.groupDataService.getUUIDFromString(this.router.url); - 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', - type: 'application/atom+xml', - rel: 'search', - title: 'Dspace', - }); - this.route$.next(route); + filter(uri => !!uri), + ).subscribe(uri => { + this.openSearchUri = uri; + this.updateRssLinks(); })); } /** - * 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 + * Update RSS links based on current search configuration and sortConfig input + */ + private updateRssLinks(): void { + if (!this.openSearchUri || !this.isEnabled$.getValue()) { + return; + } + + // Remove existing link tags before adding new ones + this.linkHeadService.removeTag("rel='alternate'"); + + // Get the current search options and apply our sortConfig if provided + const searchOptions = this.searchConfigurationService.paginatedSearchOptions.value; + const modifiedOptions = { ...searchOptions }; + if (hasValue(this.sortConfig)) { + modifiedOptions.sort = this.sortConfig; + } + + // Create the RSS feed URL + const route = environment.rest.baseUrl + this.formulateRoute( + this.uuid, + this.openSearchUri, + modifiedOptions.sort, + modifiedOptions.query, + modifiedOptions.filters, + modifiedOptions.configuration, + modifiedOptions.pagination?.pageSize, + modifiedOptions.fixedFilter, + ); + + // Add the link tags + this.addLinks(route); + + // Add the OpenSearch service link + this.linkHeadService.addTag({ + href: environment.rest.baseUrl + '/' + this.openSearchUri + '/service', + type: 'application/atom+xml', + rel: 'search', + title: 'Dspace', + }); + + // Update the route subject + this.route$.next(route); + } + + /** + * Create a route given the different params available to opensearch */ formulateRoute(uuid: string, opensearch: string, sort?: SortOptions, query?: string, searchFilters?: SearchFilter[], configuration?: string, pageSize?: number, fixedFilter?: string): string { let route = 'format=atom'; @@ -158,9 +210,9 @@ export class RSSComponent implements OnInit, OnDestroy { 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 : ''); + for (const searchFilter of searchFilters) { + for (const val of searchFilter.values) { + route += '&' + searchFilter.key + '=' + encodeURIComponent(val) + (searchFilter.operator ? ',' + searchFilter.operator : ''); } } } @@ -171,20 +223,8 @@ export class RSSComponent implements OnInit, OnDestroy { return route; } - /** - * Check if the router url contains the specified route - * - * @param {string} route - * @returns - * @memberof MyComponent - */ - hasRoute(route: string) { - return this.router.url.includes(route); - } - /** * Creates tags in the header of the page - * @param route The composed url to opensearch */ addLinks(route: string): void { this.linkHeadService.addTag({ @@ -201,5 +241,4 @@ export class RSSComponent implements OnInit, OnDestroy { title: 'Sitewide RSS feed', }); } - } diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index 0026d0ea5b..75e5fdf9b4 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -25,6 +25,7 @@ [sortConfig]="searchConfig.sort" [objects]="searchResults" [hideGear]="true" + [showRSS]="true" [selectable]="selectable" [selectionConfig]="selectionConfig" [linkType]="linkType"