diff --git a/src/app/home-page/recent-item-list/recent-item-list.component.html b/src/app/home-page/recent-item-list/recent-item-list.component.html index 4d77e5027e..7a7ecdacce 100644 --- a/src/app/home-page/recent-item-list/recent-item-list.component.html +++ b/src/app/home-page/recent-item-list/recent-item-list.component.html @@ -3,6 +3,7 @@

{{'home.recent-submissions.head' | translate}}

+ @for (item of itemRD?.payload?.page; track item) {
diff --git a/src/app/home-page/recent-item-list/recent-item-list.component.ts b/src/app/home-page/recent-item-list/recent-item-list.component.ts index b336cf7e9b..1855eed909 100644 --- a/src/app/home-page/recent-item-list/recent-item-list.component.ts +++ b/src/app/home-page/recent-item-list/recent-item-list.component.ts @@ -41,6 +41,7 @@ import { ErrorComponent } from '../../shared/error/error.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ListableObjectComponentLoaderComponent } from '../../shared/object-collection/shared/listable-object/listable-object-component-loader.component'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { RSSComponent } from '../../shared/rss-feed/rss.component'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { followLink, @@ -59,7 +60,7 @@ import { VarDirective } from '../../shared/utils/var.directive'; fadeInOut, ], standalone: true, - imports: [VarDirective, NgClass, ListableObjectComponentLoaderComponent, ErrorComponent, ThemedLoadingComponent, AsyncPipe, TranslateModule], + imports: [VarDirective, NgClass, ListableObjectComponentLoaderComponent, ErrorComponent, ThemedLoadingComponent, AsyncPipe, TranslateModule, RSSComponent], }) export class RecentItemListComponent implements OnInit, OnDestroy { itemRD$: Observable>>; 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..c7849adbff 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 = 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..eae56a217d 100644 --- a/src/app/shared/object-detail/object-detail.component.ts +++ b/src/app/shared/object-detail/object-detail.component.ts @@ -85,6 +85,8 @@ export class ObjectDetailComponent { */ @Input() showThumbnails; + @Input() showRSS = 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..19d3cc9906 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 = 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..65309678bf 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) { + + }
diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 907b427354..7392d8f91e 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 = 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( diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 8d2b49f0e1..8c81f7975e 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'; @@ -31,13 +33,14 @@ import { GroupDataService } from '../../core/eperson/group-data.service'; import { PaginationService } from '../../core/pagination/pagination.service'; import { LinkHeadService } from '../../core/services/link-head.service'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { SearchService } from '../../core/shared/search/search.service'; 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,30 +54,28 @@ 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, private configurationService: ConfigurationDataService, private searchConfigurationService: SearchConfigurationService, + private searchService: SearchService, private router: Router, private route: ActivatedRoute, 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 +83,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 +137,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 +212,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 +225,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 +243,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"