diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index e248685ac3..f6cde90253 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -164,8 +164,12 @@ import { SequenceService } from './shared/sequence.service';
import { CoreState } from './core-state.model';
import { GroupDataService } from './eperson/group-data.service';
import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model';
+<<<<<<< HEAD
import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model';
import { AccessStatusDataService } from './data/access-status-data.service';
+=======
+import { LinkHeadService } from './services/link-head.service';
+>>>>>>> 354768d98 (w2p-85140 ds-rss component now adds a button to all search pages and community and collection, link-head service adds the rss to the head element)
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -205,6 +209,7 @@ const PROVIDERS = [
SectionFormOperationsService,
FormService,
EPersonDataService,
+ LinkHeadService,
HALEndpointService,
HostWindowService,
ItemDataService,
diff --git a/src/app/core/services/link-head.service.ts b/src/app/core/services/link-head.service.ts
new file mode 100644
index 0000000000..29ab62ff13
--- /dev/null
+++ b/src/app/core/services/link-head.service.ts
@@ -0,0 +1,84 @@
+import { Injectable, Optional, RendererFactory2, ViewEncapsulation, Inject } from '@angular/core';
+import { DOCUMENT } from '@angular/common';
+
+@Injectable()
+export class LinkHeadService {
+ constructor(
+ private rendererFactory: RendererFactory2,
+ @Inject(DOCUMENT) private document
+ ) {
+
+ }
+ addTag(tag: LinkDefinition, forceCreation?: boolean) {
+
+ try {
+ const renderer = this.rendererFactory.createRenderer(this.document, {
+ id: '-1',
+ encapsulation: ViewEncapsulation.None,
+ styles: [],
+ data: {}
+ });
+
+ const link = renderer.createElement('link');
+
+ const head = this.document.head;
+ const selector = this._parseSelector(tag);
+
+ if (head === null) {
+ throw new Error('
not found within DOCUMENT.');
+ }
+
+ Object.keys(tag).forEach((prop: string) => {
+ return renderer.setAttribute(link, prop, tag[prop]);
+ });
+
+ renderer.appendChild(head, link);
+
+ } catch (e) {
+ console.error('Error within linkService : ', e);
+ }
+ }
+
+ removeTag(attrSelector: string) {
+ if (attrSelector) {
+ try {
+ const renderer = this.rendererFactory.createRenderer(this.document, {
+ id: '-1',
+ encapsulation: ViewEncapsulation.None,
+ styles: [],
+ data: {}
+ });
+ const head = this.document.head;
+ if (head === null) {
+ throw new Error(' not found within DOCUMENT.');
+ }
+ const linkTags = this.document.querySelectorAll('link[' + attrSelector + ']');
+ for (const link of linkTags) {
+ renderer.removeChild(head, link);
+ }
+ } catch (e) {
+ console.log('Error while removing tag ' + e.message);
+ }
+ }
+ }
+
+ private _parseSelector(tag: LinkDefinition): string {
+ const attr: string = tag.rel ? 'rel' : 'hreflang';
+ return `${attr}="${tag[attr]}"`;
+ }
+}
+
+export declare type LinkDefinition = {
+ charset?: string;
+ crossorigin?: string;
+ href?: string;
+ hreflang?: string;
+ media?: string;
+ rel?: string;
+ rev?: string;
+ sizes?: string;
+ target?: string;
+ type?: string;
+} & {
+ [prop: string]: string;
+ };
diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html
index f5f329c6fd..f0a8866049 100644
--- a/src/app/shared/pagination/pagination.component.html
+++ b/src/app/shared/pagination/pagination.component.html
@@ -15,6 +15,7 @@
+
diff --git a/src/app/shared/rss-feed/rss.component.html b/src/app/shared/rss-feed/rss.component.html
new file mode 100644
index 0000000000..8868539b5c
--- /dev/null
+++ b/src/app/shared/rss-feed/rss.component.html
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/app/shared/rss-feed/rss.component.scss b/src/app/shared/rss-feed/rss.component.scss
new file mode 100644
index 0000000000..929bb453ac
--- /dev/null
+++ b/src/app/shared/rss-feed/rss.component.scss
@@ -0,0 +1,12 @@
+:host {
+ .dropdown-toggle::after {
+ display: none;
+ }
+ .dropdown-item {
+ padding-left: 20px;
+ }
+}
+
+.margin-right {
+ margin-right: .5em;
+}
\ 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
new file mode 100644
index 0000000000..bbfd5442b3
--- /dev/null
+++ b/src/app/shared/rss-feed/rss.component.spec.ts
@@ -0,0 +1,109 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+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';
+import { PaginationService } from '../../core/pagination/pagination.service';
+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 { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
+import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
+import { PaginatedSearchOptions } from '../search/paginated-search-options.model';
+import { PaginationServiceStub } from '../testing/pagination-service.stub';
+import { createPaginatedList } from '../testing/utils.test';
+import { RSSComponent } from './rss.component';
+import { of as observableOf } from 'rxjs';
+
+
+
+describe('RssComponent', () => {
+ let comp: RSSComponent;
+ let options: SortOptions;
+ let fixture: ComponentFixture;
+ let uuid: string;
+ let query: string;
+ let groupDataService: GroupDataService;
+ let linkHeadService: LinkHeadService;
+ let configurationDataService: ConfigurationDataService;
+ let paginationService;
+
+ beforeEach(waitForAsync(() => {
+ const mockCollection: Collection = Object.assign(new Collection(), {
+ id: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4',
+ name: 'test-collection',
+ _links: {
+ mappedItems: {
+ href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4/mappedItems'
+ },
+ self: {
+ href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4'
+ }
+ }
+ });
+ configurationDataService = jasmine.createSpyObj('configurationDataService', {
+ findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
+ name: 'test',
+ values: [
+ 'org.dspace.ctask.general.ProfileFormats = test'
+ ]
+ }))
+ });
+ linkHeadService = jasmine.createSpyObj('linkHeadService', {
+ addTag: ''
+ });
+ const mockCollectionRD: RemoteData = createSuccessfulRemoteDataObject(mockCollection);
+ const mockSearchOptions = observableOf(new PaginatedSearchOptions({
+ pagination: Object.assign(new PaginationComponentOptions(), {
+ id: 'search-page-configuration',
+ pageSize: 10,
+ currentPage: 1
+ }),
+ sort: new SortOptions('dc.title', SortDirection.ASC),
+ scope: mockCollection.id
+ }));
+ groupDataService = jasmine.createSpyObj('groupsDataService', {
+ findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
+ getGroupRegistryRouterLink: ''
+ });
+ paginationService = new PaginationServiceStub();
+ const searchConfigService = {
+ paginatedSearchOptions: mockSearchOptions
+ };
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: GroupDataService, useValue: groupDataService },
+ { provide: LinkHeadService, useValue: linkHeadService },
+ { provide: ConfigurationDataService, useValue: configurationDataService },
+ { provide: SearchConfigurationService, useValue: searchConfigService},
+ { provide: PaginationService, useValue: paginationService }
+ ],
+ declarations: [RSSComponent]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ options = new SortOptions('dc.title', SortDirection.DESC);
+ uuid = '2cfcf65e-0a51-4bcb-8592-b8db7b064790';
+ query = 'test';
+ fixture = TestBed.createComponent(RSSComponent);
+ comp = fixture.componentInstance;
+ });
+
+ it('should formulate the correct url given params in url', () => {
+ const route = comp.formulateRoute(uuid, 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, 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, options, null);
+ expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=*');
+ });
+});
+
diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts
new file mode 100644
index 0000000000..036148b368
--- /dev/null
+++ b/src/app/shared/rss-feed/rss.component.ts
@@ -0,0 +1,94 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ OnDestroy,
+ OnInit,
+ ViewEncapsulation
+} from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { GroupDataService } from '../../core/eperson/group-data.service';
+import { LinkHeadService } from '../../core/services/link-head.service';
+import { ConfigurationDataService } from '../../core/data/configuration-data.service';
+import { getFirstCompletedRemoteData } from '../../core/shared/operators';
+import { environment } from '../../../../src/environments/environment';
+import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
+import { SortOptions } from '../../core/cache/models/sort-options.model';
+import { PaginationService } from '../../core/pagination/pagination.service';
+
+
+/**
+ * The default pagination controls component.
+ */
+@Component({
+ exportAs: 'rssComponent',
+ selector: 'ds-rss',
+ styleUrls: ['rss.component.scss'],
+ templateUrl: 'rss.component.html',
+ changeDetection: ChangeDetectionStrategy.Default,
+ encapsulation: ViewEncapsulation.Emulated
+})
+export class RSSComponent implements OnInit, OnDestroy {
+
+ route$: BehaviorSubject;
+
+ isEnabled$: BehaviorSubject = new BehaviorSubject(false);
+
+ uuid: string;
+ configuration$: Observable;
+ sortOption$: Observable;
+
+ constructor(private groupDataService: GroupDataService,
+ private linkHeadService: LinkHeadService,
+ private configurationService: ConfigurationDataService,
+ private searchConfigurationService: SearchConfigurationService,
+ protected paginationService: PaginationService) {
+ }
+ ngOnDestroy(): void {
+ this.linkHeadService.removeTag("rel='alternate'");
+ }
+
+ ngOnInit(): void {
+ this.configuration$ = this.searchConfigurationService.getCurrentConfiguration('default');
+
+ this.configurationService.findByPropertyName('websvc.opensearch.enable').pipe(
+ getFirstCompletedRemoteData(),
+ ).subscribe((result) => {
+ const enabled = Boolean(result.payload.values[0]);
+ this.isEnabled$.next(enabled);
+ });
+
+ this.searchConfigurationService.getCurrentQuery('').subscribe((query) => {
+ this.sortOption$ = this.paginationService.getCurrentSort(this.searchConfigurationService.paginationID, null, true);
+ this.sortOption$.subscribe((sort) => {
+ this.uuid = this.groupDataService.getUUIDFromString(window.location.href);
+
+ const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, sort, query);
+
+ this.linkHeadService.addTag({
+ href: route,
+ type: 'application/atom+xml',
+ rel: 'alternate',
+ title: 'Sitewide Atom feed'
+ });
+ this.route$ = new BehaviorSubject(route);
+ });
+ });
+ }
+
+ formulateRoute(uuid: string, sort: SortOptions, query: string): string {
+ let route = 'search?format=atom';
+ if (uuid) {
+ route += `&scope=${uuid}`;
+ }
+ if (sort.direction && sort.field) {
+ route += `&sort=${sort.field}&sort_direction=${sort.direction}`;
+ }
+ if (query) {
+ route += `&query=${query}`;
+ } else {
+ route += `&query=*`;
+ }
+ route = '/opensearch/' + route;
+ return route;
+ }
+}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 12b6a482dc..c70aff6192 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -173,10 +173,9 @@ import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-p
import { DsSelectComponent } from './ds-select/ds-select.component';
import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component';
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
-import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component';
+import { RSSComponent } from './rss-feed/rss.component';
const MODULES = [
- // Do NOT include UniversalModule, HttpModule, or JsonpModule here
CommonModule,
SortablejsModule,
FileUploadModule,
@@ -239,6 +238,7 @@ const COMPONENTS = [
AbstractListableElementComponent,
ObjectCollectionComponent,
PaginationComponent,
+ RSSComponent,
SearchFormComponent,
PageWithSidebarComponent,
SidebarDropdownComponent,
diff --git a/src/environments/environment.dev.ts b/src/environments/environment.dev.ts
new file mode 100644
index 0000000000..999abd32ee
--- /dev/null
+++ b/src/environments/environment.dev.ts
@@ -0,0 +1,17 @@
+export const environment = {
+ ui: {
+ ssl: false,
+ host: 'localhost',
+ port: 18080,
+ nameSpace: '/'
+ },
+ rest: {
+ ssl: false,
+ host: 'localhost',
+ port: 8080,
+ nameSpace: '/server'
+ },
+ universal: {
+ preboot: false
+ }
+};
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
new file mode 100644
index 0000000000..c31da7b791
--- /dev/null
+++ b/src/environments/environment.prod.ts
@@ -0,0 +1,17 @@
+export const environment = {
+ ui: {
+ ssl: false,
+ host: 'localhost',
+ port: 18080,
+ nameSpace: '/'
+ },
+ rest: {
+ ssl: false,
+ host: 'localhost',
+ port: 8080,
+ nameSpace: '/server'
+ },
+ universal: {
+ preboot: true
+ }
+};