diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 3e727e2bdf..b4063b0550 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -21,8 +21,5 @@ _This checklist provides a reminder of what we are going to look for when review
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
-- [ ] My PR passes all specs/tests and includes new/updated specs for any bug fixes, improvements or new features. A few reminders about what constitutes good tests:
- * Include tests for different user types (if behavior differs), including: (1) Anonymous user, (2) Logged in user (non-admin), and (3) Administrator.
- * Include tests for error scenarios, e.g. when errors/warnings should appear (or buttons should be disabled).
- * For bug fixes, include a test that reproduces the bug and proves it is fixed. For clarity, it may be useful to provide the test in a separate commit from the bug fix.
+- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
diff --git a/.travis.yml b/.travis.yml
index 54b3c4752a..13a159bfd0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -51,10 +51,10 @@ before_script:
script:
# build app and run all tests
- - ng lint
- - travis_wait yarn run build:prod
- - yarn test:headless
- - yarn run e2e:ci
+ - ng lint || travis_terminate 1;
+ - travis_wait yarn run build:prod || travis_terminate 1;
+ - yarn test:headless || travis_terminate 1;
+ - yarn run e2e:ci || travis_terminate 1;
after_script:
# Shutdown docker after everything runs
diff --git a/angular.json b/angular.json
index 9c55d648b3..d8d8a2dc2e 100644
--- a/angular.json
+++ b/angular.json
@@ -18,7 +18,7 @@
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
- "path": "./webpack/webpack.common.ts",
+ "path": "./webpack/webpack.browser.ts",
"mergeStrategies": {
"loaders": "prepend"
}
@@ -30,7 +30,8 @@
"tsConfig": "tsconfig.app.json",
"aot": false,
"assets": [
- "src/assets"
+ "src/assets",
+ "src/robots.txt"
],
"styles": [
"src/styles.scss"
@@ -84,7 +85,7 @@
"builder": "@angular-builders/custom-webpack:karma",
"options": {
"customWebpackConfig": {
- "path": "./webpack/webpack.common.ts",
+ "path": "./webpack/webpack.test.ts",
"mergeStrategies": {
"loaders": "prepend"
}
diff --git a/package.json b/package.json
index c1e5b05010..52afb7c4c0 100644
--- a/package.json
+++ b/package.json
@@ -146,6 +146,7 @@
"dotenv": "^8.2.0",
"fork-ts-checker-webpack-plugin": "^0.4.10",
"html-webpack-plugin": "^3.2.0",
+ "http-proxy-middleware": "^1.0.5",
"jasmine-core": "^3.3.0",
"jasmine-marbles": "0.3.1",
"jasmine-spec-reporter": "~4.2.1",
diff --git a/scripts/set-env.ts b/scripts/set-env.ts
index 5eee22a4be..5570b77218 100644
--- a/scripts/set-env.ts
+++ b/scripts/set-env.ts
@@ -54,6 +54,13 @@ import(environmentFilePath)
function generateEnvironmentFile(file: GlobalConfig): void {
file.production = production;
buildBaseUrls(file);
+
+ // TODO remove workaround in beta 5
+ if (file.rest.nameSpace.match("(.*)/api/?$") !== null) {
+ const newValue = getNameSpace(file.rest.nameSpace);
+ console.log(colors.white.bgMagenta.bold(`The rest.nameSpace property in your environment file or in your DSPACE_REST_NAMESPACE environment variable ends with '/api'.\nThis is deprecated. As '/api' isn't configurable on the rest side, it shouldn't be repeated in every environment file.\nPlease change the rest nameSpace to '${newValue}'`));
+ }
+
const contents = `export const environment = ` + JSON.stringify(file);
writeFile(targetPath, contents, (err) => {
if (err) {
@@ -112,5 +119,16 @@ function getPort(port: number): string {
}
function getNameSpace(nameSpace: string): string {
- return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : '';
+ // TODO remove workaround in beta 5
+ const apiMatches = nameSpace.match("(.*)/api/?$");
+ if (apiMatches != null) {
+ let newValue = '/'
+ if (hasValue(apiMatches[1])) {
+ newValue = apiMatches[1];
+ }
+ return newValue;
+ }
+ else {
+ return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : '';
+ }
}
diff --git a/server.ts b/server.ts
index a5d47d8bd7..478dd063f6 100644
--- a/server.ts
+++ b/server.ts
@@ -33,6 +33,7 @@ import { enableProdMode, NgModuleFactory, Type } from '@angular/core';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { environment } from './src/environments/environment';
+import { createProxyMiddleware } from 'http-proxy-middleware';
/*
* Set path for the browser application's dist folder
@@ -106,6 +107,11 @@ app.set('view engine', 'html');
*/
app.set('views', DIST_FOLDER);
+/**
+ * Proxy the sitemaps
+ */
+app.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true }));
+
/*
* Adds a cache control header to the response
* The cache control value can be configured in the environments file and defaults to max-age=60
diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.html b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.html
new file mode 100644
index 0000000000..42a04b0de6
--- /dev/null
+++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.html
@@ -0,0 +1,15 @@
+
+
+
{{'admin.metadata-import.page.help' | translate}}
+
+
+
+
+
{{'admin.metadata-import.page.button.return' | translate}}
+
{{'admin.metadata-import.page.button.proceed' | translate}}
+
diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts
new file mode 100644
index 0000000000..9c4efb6796
--- /dev/null
+++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts
@@ -0,0 +1,151 @@
+import { Location } from '@angular/common';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { of as observableOf } from 'rxjs/internal/observable/of';
+import { AuthService } from '../../core/auth/auth.service';
+import {
+ METADATA_IMPORT_SCRIPT_NAME,
+ ScriptDataService
+} from '../../core/data/processes/script-data.service';
+import { EPerson } from '../../core/eperson/models/eperson.model';
+import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
+import { NotificationsService } from '../../shared/notifications/notifications.service';
+import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
+import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
+import { FileValidator } from '../../shared/utils/require-file.validator';
+import { MetadataImportPageComponent } from './metadata-import-page.component';
+
+describe('MetadataImportPageComponent', () => {
+ let comp: MetadataImportPageComponent;
+ let fixture: ComponentFixture;
+
+ let user;
+
+ let notificationService: NotificationsServiceStub;
+ let scriptService: any;
+ let router;
+ let authService;
+ let locationStub;
+
+ function init() {
+ notificationService = new NotificationsServiceStub();
+ scriptService = jasmine.createSpyObj('scriptService',
+ {
+ invoke: observableOf({
+ response:
+ {
+ isSuccessful: true,
+ resourceSelfLinks: ['https://localhost:8080/api/core/processes/45']
+ }
+ })
+ }
+ );
+ user = Object.assign(new EPerson(), {
+ id: 'userId',
+ email: 'user@test.com'
+ });
+ authService = jasmine.createSpyObj('authService', {
+ getAuthenticatedUserFromStore: observableOf(user)
+ });
+ router = jasmine.createSpyObj('router', {
+ navigateByUrl: jasmine.createSpy('navigateByUrl')
+ });
+ locationStub = jasmine.createSpyObj('location', {
+ back: jasmine.createSpy('back')
+ });
+ }
+
+ beforeEach(async(() => {
+ init();
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ TranslateModule.forRoot(),
+ RouterTestingModule.withRoutes([])
+ ],
+ declarations: [MetadataImportPageComponent, FileValueAccessorDirective, FileValidator],
+ providers: [
+ { provide: NotificationsService, useValue: notificationService },
+ { provide: ScriptDataService, useValue: scriptService },
+ { provide: Router, useValue: router },
+ { provide: AuthService, useValue: authService },
+ { provide: Location, useValue: locationStub },
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MetadataImportPageComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(comp).toBeTruthy();
+ });
+
+ describe('if back button is pressed', () => {
+ beforeEach(fakeAsync(() => {
+ const proceed = fixture.debugElement.query(By.css('#backButton')).nativeElement;
+ proceed.click();
+ fixture.detectChanges();
+ }));
+ it('should do location.back', () => {
+ expect(locationStub.back).toHaveBeenCalled();
+ });
+ });
+
+ describe('if file is set', () => {
+ let fileMock: File;
+
+ beforeEach(() => {
+ fileMock = new File([''], 'filename.txt', { type: 'text/plain' });
+ comp.setFile(fileMock);
+ });
+
+ describe('if proceed button is pressed', () => {
+ beforeEach(fakeAsync(() => {
+ const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
+ proceed.click();
+ fixture.detectChanges();
+ }));
+ it('metadata-import script is invoked with its -e currentUserEmail, -f fileName and the mockFile', () => {
+ const parameterValues: ProcessParameter[] = [
+ Object.assign(new ProcessParameter(), { name: '-e', value: user.email }),
+ Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
+ ];
+ expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
+ });
+ it('success notification is shown', () => {
+ expect(notificationService.success).toHaveBeenCalled();
+ });
+ it('redirected to process page', () => {
+ expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
+ });
+ });
+
+ describe('if proceed is pressed; but script invoke fails', () => {
+ beforeEach(fakeAsync(() => {
+ jasmine.getEnv().allowRespy(true);
+ spyOn(scriptService, 'invoke').and.returnValue(observableOf({
+ response:
+ {
+ isSuccessful: false,
+ }
+ }));
+ const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
+ proceed.click();
+ fixture.detectChanges();
+ }));
+ it('error notification is shown', () => {
+ expect(notificationService.error).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts
new file mode 100644
index 0000000000..3db6ad1c7c
--- /dev/null
+++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts
@@ -0,0 +1,106 @@
+import { Location } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { TranslateService } from '@ngx-translate/core';
+import { Observable } from 'rxjs/internal/Observable';
+import { map, switchMap, take } from 'rxjs/operators';
+import { AuthService } from '../../core/auth/auth.service';
+import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
+import { RequestEntry } from '../../core/data/request.reducer';
+import { EPerson } from '../../core/eperson/models/eperson.model';
+import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
+import { isNotEmpty } from '../../shared/empty.util';
+import { NotificationsService } from '../../shared/notifications/notifications.service';
+
+@Component({
+ selector: 'ds-metadata-import-page',
+ templateUrl: './metadata-import-page.component.html'
+})
+
+/**
+ * Component that represents a metadata import page for administrators
+ */
+export class MetadataImportPageComponent implements OnInit {
+
+ /**
+ * The current value of the file
+ */
+ fileObject: File;
+
+ /**
+ * The authenticated user's email
+ */
+ private currentUserEmail$: Observable;
+
+ public constructor(protected authService: AuthService,
+ private location: Location,
+ protected translate: TranslateService,
+ protected notificationsService: NotificationsService,
+ private scriptDataService: ScriptDataService,
+ private router: Router) {
+ }
+
+ /**
+ * Set file
+ * @param file
+ */
+ setFile(file) {
+ this.fileObject = file;
+ }
+
+ /**
+ * Method provided by Angular. Invoked after the constructor.
+ */
+ ngOnInit() {
+ this.currentUserEmail$ = this.authService.getAuthenticatedUserFromStore().pipe(
+ map((user: EPerson) => user.email)
+ );
+ }
+
+ /**
+ * When return button is pressed go to previous location
+ */
+ public onReturn() {
+ this.location.back();
+ }
+
+ /**
+ * Starts import-metadata script with -e currentUserEmail -f fileName (and the selected file)
+ */
+ public importMetadata() {
+ if (this.fileObject == null) {
+ this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
+ } else {
+ this.currentUserEmail$.pipe(
+ switchMap((email: string) => {
+ if (isNotEmpty(email)) {
+ const parameterValues: ProcessParameter[] = [
+ Object.assign(new ProcessParameter(), { name: '-e', value: email }),
+ Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
+ ];
+ return this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject])
+ .pipe(
+ take(1),
+ map((requestEntry: RequestEntry) => {
+ if (requestEntry.response.isSuccessful) {
+ const title = this.translate.get('process.new.notification.success.title');
+ const content = this.translate.get('process.new.notification.success.content');
+ this.notificationsService.success(title, content);
+ const response: any = requestEntry.response;
+ if (isNotEmpty(response.resourceSelfLinks)) {
+ const processNumber = response.resourceSelfLinks[0].split('/').pop();
+ this.router.navigateByUrl('/processes/' + processNumber);
+ }
+ } else {
+ const title = this.translate.get('process.new.notification.error.title');
+ const content = this.translate.get('process.new.notification.error.content');
+ this.notificationsService.error(title, content);
+ }
+ }));
+ }
+ }),
+ take(1)
+ ).subscribe();
+ }
+ }
+}
diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts
index 43b3a4ab34..84b418772a 100644
--- a/src/app/+admin/admin-routing.module.ts
+++ b/src/app/+admin/admin-routing.module.ts
@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { getAdminModulePath } from '../app-routing.module';
+import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
@@ -48,6 +49,12 @@ export function getAccessControlModulePath() {
component: AdminCurationTasksComponent,
data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }
},
+ {
+ path: 'metadata-import',
+ resolve: { breadcrumb: I18nBreadcrumbResolver },
+ component: MetadataImportPageComponent,
+ data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
+ },
])
],
providers: [
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
index e72a17aac1..eb06df3630 100644
--- a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
+++ b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
@@ -5,7 +5,7 @@
-
\ No newline at end of file
+
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.html b/src/app/+admin/admin-sidebar/admin-sidebar.component.html
index 02a25a8227..357ed058d1 100644
--- a/src/app/+admin/admin-sidebar/admin-sidebar.component.html
+++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.html
@@ -25,7 +25,7 @@
+ *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;">
@@ -49,4 +49,4 @@
-
\ No newline at end of file
+
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
index 40539d3e13..9cdcccba28 100644
--- a/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
+++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
@@ -2,6 +2,7 @@ import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
+import { ScriptDataService } from '../../core/data/processes/script-data.service';
import { AdminSidebarComponent } from './admin-sidebar.component';
import { MenuService } from '../../shared/menu/menu.service';
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
@@ -21,11 +22,13 @@ describe('AdminSidebarComponent', () => {
let fixture: ComponentFixture;
const menuService = new MenuServiceStub();
let authorizationService: AuthorizationDataService;
+ let scriptService;
beforeEach(async(() => {
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
+ scriptService = jasmine.createSpyObj('scriptService', { scriptWithNameExistsAndCanExecute: observableOf(true) });
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
declarations: [AdminSidebarComponent],
@@ -36,9 +39,11 @@ describe('AdminSidebarComponent', () => {
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: ActivatedRoute, useValue: {} },
{ provide: AuthorizationDataService, useValue: authorizationService },
+ { provide: ScriptDataService, useValue: scriptService },
{
provide: NgbModal, useValue: {
- open: () => {/*comment*/}
+ open: () => {/*comment*/
+ }
}
}
],
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts
index eb86de5f3c..3bfbf2de5b 100644
--- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts
+++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts
@@ -1,9 +1,14 @@
import { Component, Injector, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
-import { combineLatest as combineLatestObservable } from 'rxjs';
+import { combineLatest as observableCombineLatest } from 'rxjs';
import { Observable } from 'rxjs/internal/Observable';
-import { first, map } from 'rxjs/operators';
+import { first, map, take } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service';
+import {
+ METADATA_EXPORT_SCRIPT_NAME,
+ METADATA_IMPORT_SCRIPT_NAME,
+ ScriptDataService
+} from '../../core/data/processes/script-data.service';
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
@@ -11,6 +16,9 @@ import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/mod
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
+import {
+ ExportMetadataSelectorComponent
+} from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
@@ -64,7 +72,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
private variableService: CSSVariableService,
private authService: AuthService,
private modalService: NgbModal,
- private authorizationService: AuthorizationDataService
+ private authorizationService: AuthorizationDataService,
+ private scriptDataService: ScriptDataService,
) {
super(menuService, injector);
}
@@ -75,6 +84,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
ngOnInit(): void {
this.createMenu();
this.createSiteAdministratorMenuSections();
+ this.createExportMenuSections();
+ this.createImportMenuSections();
super.ngOnInit();
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
this.authService.isAuthenticated()
@@ -88,7 +99,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
this.sidebarOpen = !collapsed;
this.sidebarClosed = collapsed;
});
- this.sidebarExpanded = combineLatestObservable(this.menuCollapsed, this.menuPreviewCollapsed)
+ this.sidebarExpanded = observableCombineLatest(this.menuCollapsed, this.menuPreviewCollapsed)
.pipe(
map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed))
);
@@ -225,94 +236,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
} as OnClickMenuItemModel,
},
- /* Import */
+ /* Curation tasks */
{
- id: 'import',
- active: false,
- visible: true,
- model: {
- type: MenuItemType.TEXT,
- text: 'menu.section.import'
- } as TextMenuItemModel,
- icon: 'sign-in-alt',
- index: 2
- },
- {
- id: 'import_metadata',
- parentID: 'import',
+ id: 'curation_tasks',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
- text: 'menu.section.import_metadata',
- link: ''
- } as LinkMenuItemModel,
- },
- {
- id: 'import_batch',
- parentID: 'import',
- active: false,
- visible: true,
- model: {
- type: MenuItemType.LINK,
- text: 'menu.section.import_batch',
- link: ''
- } as LinkMenuItemModel,
- },
- /* Export */
- {
- id: 'export',
- active: false,
- visible: true,
- model: {
- type: MenuItemType.TEXT,
- text: 'menu.section.export'
- } as TextMenuItemModel,
- icon: 'sign-out-alt',
- index: 3
- },
- {
- id: 'export_community',
- parentID: 'export',
- active: false,
- visible: true,
- model: {
- type: MenuItemType.LINK,
- text: 'menu.section.export_community',
- link: ''
- } as LinkMenuItemModel,
- },
- {
- id: 'export_collection',
- parentID: 'export',
- active: false,
- visible: true,
- model: {
- type: MenuItemType.LINK,
- text: 'menu.section.export_collection',
- link: ''
- } as LinkMenuItemModel,
- },
- {
- id: 'export_item',
- parentID: 'export',
- active: false,
- visible: true,
- model: {
- type: MenuItemType.LINK,
- text: 'menu.section.export_item',
- link: ''
- } as LinkMenuItemModel,
- }, {
- id: 'export_metadata',
- parentID: 'export',
- active: false,
- visible: true,
- model: {
- type: MenuItemType.LINK,
- text: 'menu.section.export_metadata',
+ text: 'menu.section.curation_task',
link: ''
} as LinkMenuItemModel,
+ icon: 'filter',
+ index: 7
},
/* Statistics */
@@ -362,6 +297,146 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
})));
}
+ /**
+ * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
+ * the export scripts exist and the current user is allowed to execute them
+ */
+ createExportMenuSections() {
+ const menuList = [
+ /* Export */
+ {
+ id: 'export',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.TEXT,
+ text: 'menu.section.export'
+ } as TextMenuItemModel,
+ icon: 'sign-out-alt',
+ index: 3,
+ shouldPersistOnRouteChange: true
+ },
+ {
+ id: 'export_community',
+ parentID: 'export',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.export_community',
+ link: ''
+ } as LinkMenuItemModel,
+ shouldPersistOnRouteChange: true
+ },
+ {
+ id: 'export_collection',
+ parentID: 'export',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.export_collection',
+ link: ''
+ } as LinkMenuItemModel,
+ shouldPersistOnRouteChange: true
+ },
+ {
+ id: 'export_item',
+ parentID: 'export',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.export_item',
+ link: ''
+ } as LinkMenuItemModel,
+ shouldPersistOnRouteChange: true
+ },
+ ];
+ menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
+
+ observableCombineLatest(
+ this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
+ // this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
+ ).pipe(
+ // TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed; otherwise even in production mode, the metadata export button is only available after a refresh (and not in dev mode)
+ // filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
+ take(1)
+ ).subscribe(() => {
+ this.menuService.addSection(this.menuID, {
+ id: 'export_metadata',
+ parentID: 'export',
+ active: true,
+ visible: true,
+ model: {
+ type: MenuItemType.ONCLICK,
+ text: 'menu.section.export_metadata',
+ function: () => {
+ this.modalService.open(ExportMetadataSelectorComponent);
+ }
+ } as OnClickMenuItemModel,
+ shouldPersistOnRouteChange: true
+ });
+ });
+ }
+
+ /**
+ * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
+ * the import scripts exist and the current user is allowed to execute them
+ */
+ createImportMenuSections() {
+ const menuList = [
+ /* Import */
+ {
+ id: 'import',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.TEXT,
+ text: 'menu.section.import'
+ } as TextMenuItemModel,
+ icon: 'sign-in-alt',
+ index: 2
+ },
+ {
+ id: 'import_batch',
+ parentID: 'import',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.import_batch',
+ link: ''
+ } as LinkMenuItemModel,
+ }
+ ];
+ menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
+ shouldPersistOnRouteChange: true
+ })));
+
+ observableCombineLatest(
+ this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
+ // this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
+ ).pipe(
+ // TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed
+ // filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
+ take(1)
+ ).subscribe(() => {
+ this.menuService.addSection(this.menuID, {
+ id: 'import_metadata',
+ parentID: 'import',
+ active: true,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.import_metadata',
+ link: '/admin/metadata-import'
+ } as LinkMenuItemModel,
+ shouldPersistOnRouteChange: true
+ });
+ });
+ }
+
/**
* Create menu sections dependent on whether or not the current user is a site administrator
*/
diff --git a/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html
index 808683910e..d9869ddf1f 100644
--- a/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html
+++ b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html
@@ -12,16 +12,16 @@
(click)="toggleSection($event)">
-
\ No newline at end of file
+
diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts
index 85749afe03..c350272c3b 100644
--- a/src/app/+admin/admin.module.ts
+++ b/src/app/+admin/admin.module.ts
@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { AdminAccessControlModule } from './admin-access-control/admin-access-control.module';
+import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
import { AdminRoutingModule } from './admin-routing.module';
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
@@ -40,7 +41,9 @@ import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curati
WorkflowItemSearchResultAdminWorkflowListElementComponent,
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
- WorkflowItemAdminWorkflowActionsComponent
+ WorkflowItemAdminWorkflowActionsComponent,
+
+ MetadataImportPageComponent
],
entryComponents: [
@@ -54,7 +57,9 @@ import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curati
WorkflowItemSearchResultAdminWorkflowListElementComponent,
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
- WorkflowItemAdminWorkflowActionsComponent
+ WorkflowItemAdminWorkflowActionsComponent,
+
+ MetadataImportPageComponent
]
})
export class AdminModule {
diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html
index 7c1719eb82..c6f9f8e944 100644
--- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html
+++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html
@@ -1,29 +1,87 @@
-
-
-
-
-
-
- {{"item.page.filesection.name" | translate}}
- {{file.name}}
-
- {{"item.page.filesection.size" | translate}}
- {{(file.sizeBytes) | dsFileSize }}
+
+
+
0"
+ [hideGear]="true"
+ [hidePagerWhenSinglePage]="true"
+ [paginationOptions]="originalOptions"
+ [pageInfoState]="originals"
+ [collectionSize]="originals?.totalElements"
+ [disableRouteParameterUpdate]="true"
+ (pageChange)="switchOriginalPage($event)">
- {{"item.page.filesection.format" | translate}}
- {{(file.format | async)?.payload?.description}}
+
+
+
+
+
+
+
+ {{"item.page.filesection.name" | translate}}
+ {{file.name}}
+
+ {{"item.page.filesection.size" | translate}}
+ {{(file.sizeBytes) | dsFileSize }}
- {{"item.page.filesection.description" | translate}}
- {{file.firstMetadataValue("dc.description")}}
-
-
-
-
- {{"item.page.filesection.download" | translate}}
-
-
+
{{"item.page.filesection.format" | translate}}
+
{{(file.format | async)?.payload?.description}}
+
+
+
{{"item.page.filesection.description" | translate}}
+
{{file.firstMetadataValue("dc.description")}}
+
+
+
+
+ {{"item.page.filesection.download" | translate}}
+
+
+
+
+
+
+
+
+
0"
+ [hideGear]="true"
+ [hidePagerWhenSinglePage]="true"
+ [paginationOptions]="licenseOptions"
+ [pageInfoState]="licenses"
+ [collectionSize]="licenses?.totalElements"
+ [disableRouteParameterUpdate]="true"
+ (pageChange)="switchLicensePage($event)">
+
+
+
+
+
+
+
+
+
+ {{"item.page.filesection.name" | translate}}
+ {{file.name}}
+
+ {{"item.page.filesection.size" | translate}}
+ {{(file.sizeBytes) | dsFileSize }}
+
+
+ {{"item.page.filesection.format" | translate}}
+ {{(file.format | async)?.payload?.description}}
+
+
+ {{"item.page.filesection.description" | translate}}
+ {{file.firstMetadataValue("dc.description")}}
+
+
+
+
+ {{"item.page.filesection.download" | translate}}
+
+
+
+
diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts
new file mode 100644
index 0000000000..970420f252
--- /dev/null
+++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts
@@ -0,0 +1,117 @@
+import {FullFileSectionComponent} from './full-file-section.component';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {createSuccessfulRemoteDataObject$} from '../../../../shared/remote-data.utils';
+import {createPaginatedList} from '../../../../shared/testing/utils.test';
+import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
+import {TranslateLoaderMock} from '../../../../shared/mocks/translate-loader.mock';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {VarDirective} from '../../../../shared/utils/var.directive';
+import {FileSizePipe} from '../../../../shared/utils/file-size-pipe';
+import {MetadataFieldWrapperComponent} from '../../../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
+import {BitstreamDataService} from '../../../../core/data/bitstream-data.service';
+import {NO_ERRORS_SCHEMA} from '@angular/core';
+import {Bitstream} from '../../../../core/shared/bitstream.model';
+import {of as observableOf} from 'rxjs';
+import {MockBitstreamFormat1} from '../../../../shared/mocks/item.mock';
+import {By} from '@angular/platform-browser';
+
+describe('FullFileSectionComponent', () => {
+ let comp: FullFileSectionComponent;
+ let fixture: ComponentFixture
;
+
+ const mockBitstream: Bitstream = Object.assign(new Bitstream(),
+ {
+ sizeBytes: 10201,
+ content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
+ format: observableOf(MockBitstreamFormat1),
+ bundleName: 'ORIGINAL',
+ _links: {
+ self: {
+ href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713'
+ },
+ content: {
+ href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content'
+ }
+ },
+ id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
+ uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
+ type: 'bitstream',
+ metadata: {
+ 'dc.title': [
+ {
+ language: null,
+ value: 'test_word.docx'
+ }
+ ]
+ }
+ });
+
+ const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
+ findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream, mockBitstream, mockBitstream]))
+ });
+
+ beforeEach(async(() => {
+
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useClass: TranslateLoaderMock
+ }
+ }), BrowserAnimationsModule],
+ declarations: [FullFileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent],
+ providers: [
+ {provide: BitstreamDataService, useValue: bitstreamDataService}
+ ],
+
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(FullFileSectionComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ }));
+
+ describe('when the full file section gets loaded with bitstreams available', () => {
+ it ('should contain a list with bitstreams', () => {
+ const fileSection = fixture.debugElement.queryAll(By.css('.file-section'));
+ expect(fileSection.length).toEqual(6);
+ });
+
+ describe('when we press the pageChange button for original bundle', () => {
+ beforeEach(() => {
+ comp.switchOriginalPage(2);
+ fixture.detectChanges();
+ });
+
+ it ('should give the value to the currentpage', () => {
+ expect(comp.originalOptions.currentPage).toBe(2);
+ })
+ it ('should call the next function on the originalCurrentPage', (done) => {
+ comp.originalCurrentPage$.subscribe((event) => {
+ expect(event).toEqual(2);
+ done();
+ })
+ })
+ })
+
+ describe('when we press the pageChange button for license bundle', () => {
+ beforeEach(() => {
+ comp.switchLicensePage(2);
+ fixture.detectChanges();
+ });
+
+ it ('should give the value to the currentpage', () => {
+ expect(comp.licenseOptions.currentPage).toBe(2);
+ })
+ it ('should call the next function on the licenseCurrentPage', (done) => {
+ comp.licenseCurrentPage$.subscribe((event) => {
+ expect(event).toEqual(2);
+ done();
+ })
+ })
+ })
+ })
+})
diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts
index f18fccd7e9..fdbe662ed9 100644
--- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts
+++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts
@@ -1,13 +1,15 @@
-import { Component, Injector, Input, OnInit } from '@angular/core';
-import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
-import { map, startWith } from 'rxjs/operators';
+import { Component, Input, OnInit } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model';
-import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
import { followLink } from '../../../../shared/utils/follow-link-config.model';
import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component';
+import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
+import { PaginatedList } from '../../../../core/data/paginated-list';
+import { RemoteData } from '../../../../core/data/remote-data';
+import { switchMap } from 'rxjs/operators';
/**
* This component renders the file section of the item
@@ -25,7 +27,23 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
label: string;
- bitstreams$: Observable;
+ originals$: Observable>>;
+ licenses$: Observable>>;
+
+ pageSize = 5;
+ originalOptions = Object.assign(new PaginationComponentOptions(),{
+ id: 'original-bitstreams-options',
+ currentPage: 1,
+ pageSize: this.pageSize
+ });
+ originalCurrentPage$ = new BehaviorSubject(1);
+
+ licenseOptions = Object.assign(new PaginationComponentOptions(),{
+ id: 'license-bitstreams-options',
+ currentPage: 1,
+ pageSize: this.pageSize
+ });
+ licenseCurrentPage$ = new BehaviorSubject(1);
constructor(
bitstreamDataService: BitstreamDataService
@@ -34,40 +52,45 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
}
ngOnInit(): void {
- super.ngOnInit();
+ this.initialize();
}
initialize(): void {
- // TODO pagination
- const originals$ = this.bitstreamDataService.findAllByItemAndBundleName(
- this.item,
- 'ORIGINAL',
- { elementsPerPage: Number.MAX_SAFE_INTEGER },
- followLink( 'format')
- ).pipe(
- getFirstSucceededRemoteListPayload(),
- startWith([])
+ this.originals$ = this.originalCurrentPage$.pipe(
+ switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName(
+ this.item,
+ 'ORIGINAL',
+ { elementsPerPage: this.pageSize, currentPage: pageNumber },
+ followLink( 'format')
+ ))
);
- const licenses$ = this.bitstreamDataService.findAllByItemAndBundleName(
- this.item,
- 'LICENSE',
- { elementsPerPage: Number.MAX_SAFE_INTEGER },
- followLink( 'format')
- ).pipe(
- getFirstSucceededRemoteListPayload(),
- startWith([])
- );
- this.bitstreams$ = observableCombineLatest(originals$, licenses$).pipe(
- map(([o, l]) => [...o, ...l]),
- map((files: Bitstream[]) =>
- files.map(
- (original) => {
- original.thumbnail = this.bitstreamDataService.getMatchingThumbnail(this.item, original);
- return original;
- }
- )
- )
+
+ this.licenses$ = this.licenseCurrentPage$.pipe(
+ switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName(
+ this.item,
+ 'LICENSE',
+ { elementsPerPage: this.pageSize, currentPage: pageNumber },
+ followLink( 'format')
+ ))
);
+
}
+ /**
+ * Update the current page for the original bundle bitstreams
+ * @param page
+ */
+ switchOriginalPage(page: number) {
+ this.originalOptions.currentPage = page;
+ this.originalCurrentPage$.next(page);
+ }
+
+ /**
+ * Update the current page for the license bundle bitstreams
+ * @param page
+ */
+ switchLicensePage(page: number) {
+ this.licenseOptions.currentPage = page;
+ this.licenseCurrentPage$.next(page);
+ }
}
diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.html b/src/app/+item-page/simple/field-components/file-section/file-section.component.html
index 17e4a795e7..1fdee6dc4d 100644
--- a/src/app/+item-page/simple/field-components/file-section/file-section.component.html
+++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.html
@@ -6,6 +6,13 @@
({{(file?.sizeBytes) | dsFileSize }})
+
+
+
diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.spec.ts b/src/app/+item-page/simple/field-components/file-section/file-section.component.spec.ts
new file mode 100644
index 0000000000..1b7fa75ce5
--- /dev/null
+++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.spec.ts
@@ -0,0 +1,169 @@
+import {FileSectionComponent} from './file-section.component';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
+import {TranslateLoaderMock} from '../../../../shared/mocks/translate-loader.mock';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {VarDirective} from '../../../../shared/utils/var.directive';
+import {NO_ERRORS_SCHEMA} from '@angular/core';
+import {BitstreamDataService} from '../../../../core/data/bitstream-data.service';
+import {createSuccessfulRemoteDataObject$} from '../../../../shared/remote-data.utils';
+import {By} from '@angular/platform-browser';
+import {Bitstream} from '../../../../core/shared/bitstream.model';
+import {of as observableOf} from 'rxjs';
+import {MockBitstreamFormat1} from '../../../../shared/mocks/item.mock';
+import {FileSizePipe} from '../../../../shared/utils/file-size-pipe';
+import {PageInfo} from '../../../../core/shared/page-info.model';
+import {MetadataFieldWrapperComponent} from '../../../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
+import {createPaginatedList} from '../../../../shared/testing/utils.test';
+
+describe('FileSectionComponent', () => {
+ let comp: FileSectionComponent;
+ let fixture: ComponentFixture;
+
+ const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
+ findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([]))
+ });
+
+ const mockBitstream: Bitstream = Object.assign(new Bitstream(),
+ {
+ sizeBytes: 10201,
+ content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
+ format: observableOf(MockBitstreamFormat1),
+ bundleName: 'ORIGINAL',
+ _links: {
+ self: {
+ href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713'
+ },
+ content: {
+ href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content'
+ }
+ },
+ id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
+ uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
+ type: 'bitstream',
+ metadata: {
+ 'dc.title': [
+ {
+ language: null,
+ value: 'test_word.docx'
+ }
+ ]
+ }
+ });
+
+ beforeEach(async(() => {
+
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useClass: TranslateLoaderMock
+ }
+ }), BrowserAnimationsModule],
+ declarations: [FileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent],
+ providers: [
+ {provide: BitstreamDataService, useValue: bitstreamDataService}
+ ],
+
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(async(() => {
+ fixture = TestBed.createComponent(FileSectionComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ }));
+
+ describe('when the bitstreams are loading', () => {
+ beforeEach(() => {
+ comp.bitstreams$.next([mockBitstream]);
+ comp.isLoading = true;
+ fixture.detectChanges();
+ });
+
+ it('should display a loading component', () => {
+ const loading = fixture.debugElement.query(By.css('ds-loading'));
+ expect(loading.nativeElement).toBeDefined();
+ });
+ });
+
+ describe('when the "Show more" button is clicked', () => {
+
+ beforeEach(() => {
+ comp.bitstreams$.next([mockBitstream]);
+ comp.currentPage = 1;
+ comp.isLastPage = false;
+ fixture.detectChanges();
+ });
+
+ it('should call the service to retrieve more bitstreams', () => {
+ const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more'));
+ viewMore.triggerEventHandler('click', null);
+ expect(bitstreamDataService.findAllByItemAndBundleName).toHaveBeenCalled()
+ })
+
+ it('one bitstream should be on the page', () => {
+ const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more'));
+ viewMore.triggerEventHandler('click', null);
+ const fileDownloadLink = fixture.debugElement.queryAll(By.css('ds-file-download-link'));
+ expect(fileDownloadLink.length).toEqual(1);
+ })
+
+ describe('when it is then clicked again', () => {
+ beforeEach(() => {
+ bitstreamDataService.findAllByItemAndBundleName.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream])));
+ const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more'));
+ viewMore.triggerEventHandler('click', null);
+ fixture.detectChanges();
+
+ })
+ it('should contain another bitstream', () => {
+ const fileDownloadLink = fixture.debugElement.queryAll(By.css('ds-file-download-link'));
+ expect(fileDownloadLink.length).toEqual(2);
+ })
+ })
+ });
+
+ describe('when its the last page of bitstreams', () => {
+ beforeEach(() => {
+ comp.bitstreams$.next([mockBitstream]);
+ comp.isLastPage = true;
+ comp.currentPage = 2;
+ fixture.detectChanges();
+ });
+
+ it('should not contain a view more link', () => {
+ const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more'));
+ expect(viewMore).toBeNull();
+ })
+
+ it('should contain a view less link', () => {
+ const viewLess = fixture.debugElement.query(By.css('.bitstream-collapse'));
+ expect(viewLess).toBeDefined();
+ })
+
+ it('clicking on the view less link should reset the pages and call getNextPage()', () => {
+ const pageInfo = Object.assign(new PageInfo(), {
+ elementsPerPage: 3,
+ totalElements: 5,
+ totalPages: 2,
+ currentPage: 1,
+ _links: {
+ self: {href: 'https://rest.api/core/bitstreams/'},
+ next: {href: 'https://rest.api/core/bitstreams?page=2'}
+ }
+ });
+ const PaginatedList = Object.assign(createPaginatedList([mockBitstream]), {
+ pageInfo: pageInfo
+ });
+ bitstreamDataService.findAllByItemAndBundleName.and.returnValue(createSuccessfulRemoteDataObject$(PaginatedList));
+ const viewLess = fixture.debugElement.query(By.css('.bitstream-collapse'));
+ viewLess.triggerEventHandler('click', null);
+ expect(bitstreamDataService.findAllByItemAndBundleName).toHaveBeenCalled();
+ expect(comp.currentPage).toBe(1);
+ expect(comp.isLastPage).toBeFalse();
+ })
+
+ })
+})
diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts
index 2e09c1cd49..25b214e200 100644
--- a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts
+++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts
@@ -1,10 +1,13 @@
import { Component, Input, OnInit } from '@angular/core';
-import { Observable } from 'rxjs';
+import { BehaviorSubject } from 'rxjs';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model';
-import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
+import { filter, takeWhile } from 'rxjs/operators';
+import { RemoteData } from '../../../../core/data/remote-data';
+import { hasNoValue, hasValue } from '../../../../shared/empty.util';
+import { PaginatedList } from '../../../../core/data/paginated-list';
/**
* This component renders the file section of the item
@@ -22,7 +25,15 @@ export class FileSectionComponent implements OnInit {
separator = ' ';
- bitstreams$: Observable;
+ bitstreams$: BehaviorSubject;
+
+ currentPage: number;
+
+ isLoading: boolean;
+
+ isLastPage: boolean;
+
+ pageSize = 5;
constructor(
protected bitstreamDataService: BitstreamDataService
@@ -30,13 +41,31 @@ export class FileSectionComponent implements OnInit {
}
ngOnInit(): void {
- this.initialize();
+ this.getNextPage();
}
- initialize(): void {
- this.bitstreams$ = this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL').pipe(
- getFirstSucceededRemoteListPayload()
- );
+ /**
+ * This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
+ * It'll retrieve the currentPage from the class variables and it'll add the next page of bitstreams with the
+ * already existing one.
+ * If the currentPage variable is undefined, we'll set it to 1 and retrieve the first page of Bitstreams
+ */
+ getNextPage(): void {
+ this.isLoading = true;
+ if (this.currentPage === undefined) {
+ this.currentPage = 1;
+ this.bitstreams$ = new BehaviorSubject([]);
+ } else {
+ this.currentPage++;
+ }
+ this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL', { currentPage: this.currentPage, elementsPerPage: this.pageSize }).pipe(
+ filter((bitstreamsRD: RemoteData>) => hasValue(bitstreamsRD)),
+ takeWhile((bitstreamsRD: RemoteData>) => hasNoValue(bitstreamsRD.payload) && hasNoValue(bitstreamsRD.error), true)
+ ).subscribe((bitstreamsRD: RemoteData>) => {
+ const current: Bitstream[] = this.bitstreams$.getValue();
+ this.bitstreams$.next([...current, ...bitstreamsRD.payload.page]);
+ this.isLoading = false;
+ this.isLastPage = this.currentPage === bitstreamsRD.payload.totalPages;
+ });
}
-
}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 35ca4db131..10f81a9adc 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -21,7 +21,6 @@ import { HostWindowState } from './shared/search/host-window.reducer';
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
import { isAuthenticated } from './core/auth/selectors';
import { AuthService } from './core/auth/auth.service';
-import variables from '../styles/_exposed_variables.scss';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service';
import { MenuID } from './shared/menu/initial-menus-state';
diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts
index 7f61bec9f3..a25c8ffa5b 100644
--- a/src/app/core/auth/auth.service.ts
+++ b/src/app/core/auth/auth.service.ts
@@ -206,7 +206,7 @@ export class AuthService {
return this.store.pipe(
select(getAuthenticatedUserId),
hasValueOperator(),
- switchMap((id: string) => this.epersonService.findById(id)),
+ switchMap((id: string) => this.epersonService.findById(id) ),
getAllSucceededRemoteDataPayload()
)
}
diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts
index ae3b0e4fd1..83cecca502 100644
--- a/src/app/core/cache/builders/remote-data-build.service.ts
+++ b/src/app/core/cache/builders/remote-data-build.service.ts
@@ -139,8 +139,8 @@ export class RemoteDataBuildService {
const pageInfo$ = requestEntry$.pipe(
filterSuccessfulResponses(),
map((response: DSOSuccessResponse) => {
- if (hasValue((response as DSOSuccessResponse).pageInfo)) {
- return (response as DSOSuccessResponse).pageInfo;
+ if (hasValue(response.pageInfo)) {
+ return Object.assign(new PageInfo(), response.pageInfo);
}
})
);
diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts
index 6600444ea0..cecfeabf18 100644
--- a/src/app/core/data/processes/script-data.service.ts
+++ b/src/app/core/data/processes/script-data.service.ts
@@ -12,12 +12,17 @@ import { Script } from '../../../process-page/scripts/script.model';
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
import { find, map, switchMap } from 'rxjs/operators';
import { URLCombiner } from '../../url-combiner/url-combiner';
+import { RemoteData } from '../remote-data';
import { MultipartPostRequest, RestRequest } from '../request.models';
import { RequestService } from '../request.service';
import { Observable } from 'rxjs';
import { RequestEntry } from '../request.reducer';
import { dataService } from '../../cache/builders/build-decorators';
import { SCRIPT } from '../../../process-page/scripts/script.resource-type';
+import { hasValue } from '../../../shared/empty.util';
+
+export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import';
+export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export';
@Injectable()
@dataService(SCRIPT)
@@ -58,4 +63,16 @@ export class ScriptDataService extends DataService