diff --git a/config/environment.default.js b/config/environment.default.js
index 3c1144fc6f..d46dc10dee 100644
--- a/config/environment.default.js
+++ b/config/environment.default.js
@@ -59,5 +59,25 @@ module.exports = {
// Log directory
logDirectory: '.',
// NOTE: will log all redux actions and transfers in console
- debug: false
+ debug: false,
+ // Default Language in which the UI will be rendered if the user's browser language is not an active language
+ defaultLanguage: 'en',
+ // Languages. DSpace Angular holds a message catalog for each of the following languages. When set to active, users will be able to switch to the use of this language in the user interface.
+ languages: [{
+ code: 'en',
+ label: 'English',
+ active: true,
+ }, {
+ code: 'de',
+ label: 'Deutsch',
+ active: true,
+ }, {
+ code: 'cs',
+ label: 'Čeština',
+ active: true,
+ }, {
+ code: 'nl',
+ label: 'Nederlands',
+ active: false,
+ }]
};
diff --git a/resources/i18n/en.json b/resources/i18n/en.json
index 1f85b8ddac..5e8450eb80 100644
--- a/resources/i18n/en.json
+++ b/resources/i18n/en.json
@@ -86,6 +86,120 @@
"simple": "Simple item page",
"full": "Full item page"
}
+ },
+ "select": {
+ "table": {
+ "collection": "Collection",
+ "author": "Author",
+ "title": "Title"
+ },
+ "confirm": "Confirm selected"
+ },
+ "edit": {
+ "head": "Edit Item",
+ "tabs": {
+ "status": {
+ "head": "Item Status",
+ "description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.",
+ "labels": {
+ "id": "Item Internal ID",
+ "handle": "Handle",
+ "lastModified": "Last Modified",
+ "itemPage": "Item Page"
+ },
+ "buttons": {
+ "authorizations": {
+ "label": "Edit item's authorization policies",
+ "button": "Authorizations..."
+ },
+ "withdraw": {
+ "label": "Withdraw item from the repository",
+ "button": "Withdraw..."
+ },
+ "reinstate": {
+ "label": "Reinstate item into the repository",
+ "button": "Reinstate..."
+ },
+ "move": {
+ "label": "Move item to another collection",
+ "button": "Move..."
+ },
+ "private": {
+ "label": "Make item private",
+ "button": "Make it private..."
+ },
+ "public": {
+ "label": "Make item public",
+ "button": "Make it public..."
+ },
+ "delete": {
+ "label": "Completely expunge item",
+ "button": "Permanently delete"
+ },
+ "mappedCollections": {
+ "label": "Manage mapped collections",
+ "button": "Mapped collections"
+ }
+ }
+ },
+ "bitstreams": {
+ "head": "Item Bitstreams"
+ },
+ "metadata": {
+ "head": "Item Metadata"
+ },
+ "view": {
+ "head": "View Item"
+ },
+ "curate": {
+ "head": "Curate"
+ }
+ },
+ "modify.overview": {
+ "field": "Field",
+ "value": "Value",
+ "language": "Language"
+ },
+ "withdraw": {
+ "header": "Withdraw item: {{ id }}",
+ "description": "Are you sure this item should be withdrawn from the archive?",
+ "confirm": "Withdraw",
+ "cancel": "Cancel",
+ "success": "The item was withdrawn successfully",
+ "error": "An error occured while withdrawing the item"
+ },
+ "reinstate": {
+ "header": "Reinstate item: {{ id }}",
+ "description": "Are you sure this item should be reinstated to the archive?",
+ "confirm": "Reinstate",
+ "cancel": "Cancel",
+ "success": "The item was reinstated successfully",
+ "error": "An error occured while reinstating the item"
+ },
+ "private": {
+ "header": "Make item private: {{ id }}",
+ "description": "Are you sure this item should be made private in the archive?",
+ "confirm": "Make it Private",
+ "cancel": "Cancel",
+ "success": "The item is now private",
+ "error": "An error occured while making the item private"
+ },
+ "public": {
+ "header": "Make item public: {{ id }}",
+ "description": "Are you sure this item should be made public in the archive?",
+ "confirm": "Make it Public",
+ "cancel": "Cancel",
+ "success": "The item is now public",
+ "error": "An error occured while making the item public"
+ },
+ "delete": {
+ "header": "Delete item: {{ id }}",
+ "description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.",
+ "confirm": "Delete",
+ "cancel": "Cancel",
+ "success": "The item has been deleted",
+ "error": "An error occured while deleting the item"
+ }
}
},
"nav": {
diff --git a/resources/images/dspace-logo.png b/resources/images/dspace-logo.png
index 5bb0b36add..40b59feeaa 100644
Binary files a/resources/images/dspace-logo.png and b/resources/images/dspace-logo.png differ
diff --git a/src/app/+home-page/home-news/home-news.component.html b/src/app/+home-page/home-news/home-news.component.html
index ffc88be574..2287329b37 100644
--- a/src/app/+home-page/home-news/home-news.component.html
+++ b/src/app/+home-page/home-news/home-news.component.html
@@ -1,9 +1,7 @@
-
-
-

-
+
+
Welcome to DSpace
DSpace is an open source software platform that enables organisations to:
diff --git a/src/app/+home-page/home-news/home-news.component.scss b/src/app/+home-page/home-news/home-news.component.scss
index a1bb43af91..3a0cbc7817 100644
--- a/src/app/+home-page/home-news/home-news.component.scss
+++ b/src/app/+home-page/home-news/home-news.component.scss
@@ -6,14 +6,11 @@
margin-bottom: -$content-spacing;
}
-.dspace-logo-container {
- margin: 10px 20px 0px 20px;
- .display-3 {
- word-break: break-word;
- }
+.display-3 {
+ word-break: break-word;
}
-.dspace-logo-container img {
- max-height: 110px;
- max-width: 110px;
+.dspace-logo {
+ height: 110px;
+ width: 110px;
}
diff --git a/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts b/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts
new file mode 100644
index 0000000000..8086a62b8f
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts
@@ -0,0 +1,35 @@
+import {RemoteData} from '../../core/data/remote-data';
+import {hot} from 'jasmine-marbles';
+import {Item} from '../../core/shared/item.model';
+import {findSuccessfulAccordingTo} from './edit-item-operators';
+
+describe('findSuccessfulAccordingTo', () => {
+ let mockItem1;
+ let mockItem2;
+ let predicate;
+
+ beforeEach(() => {
+ mockItem1 = new Item();
+ mockItem1.isWithdrawn = true;
+
+ mockItem2 = new Item();
+ mockItem1.isWithdrawn = false;
+
+ predicate = (rd: RemoteData
- ) => rd.payload.isWithdrawn;
+ });
+ it('should return first successful RemoteData Observable that complies to predicate', () => {
+ const testRD = {
+ a: new RemoteData(false, false, true, null, undefined),
+ b: new RemoteData(false, false, false, null, mockItem1),
+ c: new RemoteData(false, false, true, null, mockItem2),
+ d: new RemoteData(false, false, true, null, mockItem1),
+ e: new RemoteData(false, false, true, null, mockItem2),
+ };
+
+ const source = hot('abcde', testRD);
+ const result = source.pipe(findSuccessfulAccordingTo(predicate));
+
+ result.subscribe((value) => expect(value).toEqual(testRD.d));
+ });
+
+});
diff --git a/src/app/+item-page/edit-item-page/edit-item-operators.ts b/src/app/+item-page/edit-item-page/edit-item-operators.ts
new file mode 100644
index 0000000000..26c593cac6
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-operators.ts
@@ -0,0 +1,13 @@
+import {RemoteData} from '../../core/data/remote-data';
+import {Observable} from 'rxjs';
+import {first} from 'rxjs/operators';
+import {getAllSucceededRemoteData} from '../../core/shared/operators';
+
+/**
+ * Return first Observable of a RemoteData object that complies to the provided predicate
+ * @param predicate
+ */
+export const findSuccessfulAccordingTo = (predicate: (rd: RemoteData) => boolean) =>
+ (source: Observable>): Observable> =>
+ source.pipe(getAllSucceededRemoteData(),
+ first(predicate));
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.html b/src/app/+item-page/edit-item-page/edit-item-page.component.html
new file mode 100644
index 0000000000..001b484c2c
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-page.component.html
@@ -0,0 +1,36 @@
+
+
+
+
{{'item.edit.head' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.ts
new file mode 100644
index 0000000000..b8d3ca7957
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-page.component.ts
@@ -0,0 +1,35 @@
+import {fadeIn, fadeInOut} from '../../shared/animations/fade';
+import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {RemoteData} from '../../core/data/remote-data';
+import {Item} from '../../core/shared/item.model';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+
+@Component({
+ selector: 'ds-edit-item-page',
+ templateUrl: './edit-item-page.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [
+ fadeIn,
+ fadeInOut
+ ]
+})
+/**
+ * Page component for editing an item
+ */
+export class EditItemPageComponent implements OnInit {
+
+ /**
+ * The item to edit
+ */
+ itemRD$: Observable>;
+
+ constructor(private route: ActivatedRoute) {
+ }
+
+ ngOnInit(): void {
+ this.itemRD$ = this.route.data.pipe(map((data) => data.item));
+ }
+
+}
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts
new file mode 100644
index 0000000000..0a7b363d6a
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts
@@ -0,0 +1,40 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {SharedModule} from '../../shared/shared.module';
+import {EditItemPageRoutingModule} from './edit-item-page.routing.module';
+import {EditItemPageComponent} from './edit-item-page.component';
+import {ItemStatusComponent} from './item-status/item-status.component';
+import {ItemOperationComponent} from './item-operation/item-operation.component';
+import {ModifyItemOverviewComponent} from './modify-item-overview/modify-item-overview.component';
+import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component';
+import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component';
+import {AbstractSimpleItemActionComponent} from './simple-item-action/abstract-simple-item-action.component';
+import {ItemPrivateComponent} from './item-private/item-private.component';
+import {ItemPublicComponent} from './item-public/item-public.component';
+import {ItemDeleteComponent} from './item-delete/item-delete.component';
+
+/**
+ * Module that contains all components related to the Edit Item page administrator functionality
+ */
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ EditItemPageRoutingModule
+ ],
+ declarations: [
+ EditItemPageComponent,
+ ItemOperationComponent,
+ AbstractSimpleItemActionComponent,
+ ModifyItemOverviewComponent,
+ ItemWithdrawComponent,
+ ItemReinstateComponent,
+ ItemPrivateComponent,
+ ItemPublicComponent,
+ ItemDeleteComponent,
+ ItemStatusComponent
+ ]
+})
+export class EditItemPageModule {
+
+}
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts
new file mode 100644
index 0000000000..8ef6f43e17
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts
@@ -0,0 +1,72 @@
+import {ItemPageResolver} from '../item-page.resolver';
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {EditItemPageComponent} from './edit-item-page.component';
+import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component';
+import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component';
+import {ItemPrivateComponent} from './item-private/item-private.component';
+import {ItemPublicComponent} from './item-public/item-public.component';
+import {ItemDeleteComponent} from './item-delete/item-delete.component';
+
+const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
+const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
+const ITEM_EDIT_PRIVATE_PATH = 'private';
+const ITEM_EDIT_PUBLIC_PATH = 'public';
+const ITEM_EDIT_DELETE_PATH = 'delete';
+
+/**
+ * Routing module that handles the routing for the Edit Item page administrator functionality
+ */
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ {
+ path: '',
+ component: EditItemPageComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_WITHDRAW_PATH,
+ component: ItemWithdrawComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_REINSTATE_PATH,
+ component: ItemReinstateComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_PRIVATE_PATH,
+ component: ItemPrivateComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_PUBLIC_PATH,
+ component: ItemPublicComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_DELETE_PATH,
+ component: ItemDeleteComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ }])
+ ],
+ providers: [
+ ItemPageResolver,
+ ]
+})
+export class EditItemPageRoutingModule {
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts
new file mode 100644
index 0000000000..50c494128a
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts
@@ -0,0 +1,118 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {ItemDeleteComponent} from './item-delete.component';
+import {getItemEditPath} from '../../item-page-routing.module';
+
+let comp: ItemDeleteComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemDeleteComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
+ delete: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemDeleteComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemDeleteComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'delete\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.delete.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.delete.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.delete.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.delete.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call delete function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+ describe('processRestResponse', () => {
+ it('should navigate to the homepage on successful deletion of the item', () => {
+ comp.processRestResponse(successfulRestResponse);
+ expect(routerStub.navigate).toHaveBeenCalledWith(['']);
+ });
+ });
+ describe('processRestResponse', () => {
+ it('should navigate to the item edit page on failed deletion of the item', () => {
+ comp.processRestResponse(failRestResponse);
+ expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath('fake-id')]);
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts
new file mode 100644
index 0000000000..68c5738f7d
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts
@@ -0,0 +1,43 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {getItemEditPath} from '../../item-page-routing.module';
+
+@Component({
+ selector: 'ds-item-delete',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the item delete page
+ */
+export class ItemDeleteComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'delete';
+
+ /**
+ * Perform the delete action to the item
+ */
+ performAction() {
+ this.itemDataService.delete(this.item.id).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+
+ /**
+ * Process the RestResponse retrieved from the server.
+ * When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
+ * @param response
+ */
+ processRestResponse(response: RestResponse) {
+ if (response.isSuccessful) {
+ this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
+ this.router.navigate(['']);
+ } else {
+ this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
+ this.router.navigate([getItemEditPath(this.item.id)]);
+ }
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html
new file mode 100644
index 0000000000..4623195437
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html
@@ -0,0 +1,15 @@
+
+
+ {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
+
+
+
+
+
+ {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
+
+
\ No newline at end of file
diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts
new file mode 100644
index 0000000000..1901bf5fb4
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts
@@ -0,0 +1,45 @@
+import {ItemOperation} from './itemOperation.model';
+import {async, TestBed} from '@angular/core/testing';
+import {ItemOperationComponent} from './item-operation.component';
+import {TranslateModule} from '@ngx-translate/core';
+import {By} from '@angular/platform-browser';
+
+describe('ItemOperationComponent', () => {
+ let itemOperation: ItemOperation;
+
+ let fixture;
+ let comp;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot()],
+ declarations: [ItemOperationComponent]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ itemOperation = new ItemOperation('key1', 'url1');
+
+ fixture = TestBed.createComponent(ItemOperationComponent);
+ comp = fixture.componentInstance;
+ comp.operation = itemOperation;
+ fixture.detectChanges();
+ });
+
+ it('should render operation row', () => {
+ const span = fixture.debugElement.query(By.css('span')).nativeElement;
+ expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
+ const link = fixture.debugElement.query(By.css('a')).nativeElement;
+ expect(link.href).toContain('url1');
+ expect(link.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
+ });
+ it('should render disabled operation row', () => {
+ itemOperation.setDisabled(true);
+ fixture.detectChanges();
+
+ const span = fixture.debugElement.query(By.css('span')).nativeElement;
+ expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
+ const span2 = fixture.debugElement.query(By.css('span.btn-danger')).nativeElement;
+ expect(span2.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
+ });
+});
diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts
new file mode 100644
index 0000000000..76d056df95
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts
@@ -0,0 +1,15 @@
+import {Component, Input} from '@angular/core';
+import {ItemOperation} from './itemOperation.model';
+
+@Component({
+ selector: 'ds-item-operation',
+ templateUrl: './item-operation.component.html'
+})
+/**
+ * Operation that can be performed on an item
+ */
+export class ItemOperationComponent {
+
+ @Input() operation: ItemOperation;
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts
new file mode 100644
index 0000000000..105889d42d
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Represents an item operation used on the edit item page with a key, an operation URL to which will be navigated
+ * when performing the action and an option to disable the operation.
+ */
+export class ItemOperation {
+
+ operationKey: string;
+ operationUrl: string;
+ disabled: boolean;
+
+ constructor(operationKey: string, operationUrl: string) {
+ this.operationKey = operationKey;
+ this.operationUrl = operationUrl;
+ this.setDisabled(false);
+ }
+
+ /**
+ * Set whether this operation should be disabled
+ * @param disabled
+ */
+ setDisabled(disabled: boolean): void {
+ this.disabled = disabled;
+ }
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts
new file mode 100644
index 0000000000..5b99ced743
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts
@@ -0,0 +1,105 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {ItemPrivateComponent} from './item-private.component';
+
+let comp: ItemPrivateComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemPrivateComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
+ setDiscoverable: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemPrivateComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemPrivateComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'private\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.private.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.private.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.private.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.private.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call setDiscoverable function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem.id, false);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts
new file mode 100644
index 0000000000..f1e7600c18
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts
@@ -0,0 +1,30 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Item} from '../../../core/shared/item.model';
+
+@Component({
+ selector: 'ds-item-private',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the make item private page
+ */
+export class ItemPrivateComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'private';
+ protected predicate = (rd: RemoteData- ) => !rd.payload.isDiscoverable;
+
+ /**
+ * Perform the make private action to the item
+ */
+ performAction() {
+ this.itemDataService.setDiscoverable(this.item.id, false).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts
new file mode 100644
index 0000000000..182d3ffabe
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts
@@ -0,0 +1,105 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {ItemPublicComponent} from './item-public.component';
+
+let comp: ItemPublicComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemPublicComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
+ setDiscoverable: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemPublicComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemPublicComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'public\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.public.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.public.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.public.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.public.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call setDiscoverable function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem.id, true);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts
new file mode 100644
index 0000000000..d65d0f171d
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts
@@ -0,0 +1,30 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Item} from '../../../core/shared/item.model';
+
+@Component({
+ selector: 'ds-item-public',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the make item public page
+ */
+export class ItemPublicComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'public';
+ protected predicate = (rd: RemoteData
- ) => rd.payload.isDiscoverable;
+
+ /**
+ * Perform the make public action to the item
+ */
+ performAction() {
+ this.itemDataService.setDiscoverable(this.item.id, true).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts
new file mode 100644
index 0000000000..dbea7f3e69
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts
@@ -0,0 +1,105 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {ItemReinstateComponent} from './item-reinstate.component';
+
+let comp: ItemReinstateComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemReinstateComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
+ setWithDrawn: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemReinstateComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemReinstateComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'reinstate\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.reinstate.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.reinstate.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.reinstate.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.reinstate.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call setWithdrawn function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(mockItem.id, false);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts
new file mode 100644
index 0000000000..5c710b0a81
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts
@@ -0,0 +1,30 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Item} from '../../../core/shared/item.model';
+
+@Component({
+ selector: 'ds-item-reinstate',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the Item Reinstate page
+ */
+export class ItemReinstateComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'reinstate';
+ protected predicate = (rd: RemoteData
- ) => !rd.payload.isWithdrawn;
+
+ /**
+ * Perform the reinstate action to the item
+ */
+ performAction() {
+ this.itemDataService.setWithDrawn(this.item.id, false).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.html b/src/app/+item-page/edit-item-page/item-status/item-status.component.html
new file mode 100644
index 0000000000..0f7d9a5607
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.html
@@ -0,0 +1,21 @@
+
{{'item.edit.tabs.status.description' | translate}}
+
+
+
+ {{'item.edit.tabs.status.labels.' + statusKey | translate}}:
+
+
+ {{statusData[statusKey]}}
+
+
+
+ {{'item.edit.tabs.status.labels.itemPage' | translate}}:
+
+
+
+
+
+
+
diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts
new file mode 100644
index 0000000000..319d4c47ae
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts
@@ -0,0 +1,68 @@
+import { ItemStatusComponent } from './item-status.component';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { CommonModule } from '@angular/common';
+import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
+import { HostWindowService } from '../../../shared/host-window.service';
+import { RouterTestingModule } from '@angular/router/testing';
+import { Router } from '@angular/router';
+import { RouterStub } from '../../../shared/testing/router-stub';
+import { Item } from '../../../core/shared/item.model';
+import { By } from '@angular/platform-browser';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+
+describe('ItemStatusComponent', () => {
+ let comp: ItemStatusComponent;
+ let fixture: ComponentFixture;
+
+ const mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018'
+ });
+
+ const itemPageUrl = `fake-url/${mockItem.id}`;
+ const routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemStatusComponent],
+ providers: [
+ { provide: Router, useValue: routerStub },
+ { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
+ ], schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ItemStatusComponent);
+ comp = fixture.componentInstance;
+ comp.item = mockItem;
+ fixture.detectChanges();
+ });
+
+ it('should display the item\'s internal id', () => {
+ const statusId: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-id')).nativeElement;
+ expect(statusId.textContent).toContain(mockItem.id);
+ });
+
+ it('should display the item\'s handle', () => {
+ const statusHandle: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-handle')).nativeElement;
+ expect(statusHandle.textContent).toContain(mockItem.handle);
+ });
+
+ it('should display the item\'s last modified date', () => {
+ const statusLastModified: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-lastModified')).nativeElement;
+ expect(statusLastModified.textContent).toContain(mockItem.lastModified);
+ });
+
+ it('should display the item\'s page url', () => {
+ const statusItemPage: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-itemPage')).nativeElement;
+ expect(statusItemPage.textContent).toContain(itemPageUrl);
+ });
+
+});
diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts
new file mode 100644
index 0000000000..2b2c7a2ed4
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts
@@ -0,0 +1,95 @@
+import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
+import {fadeIn, fadeInOut} from '../../../shared/animations/fade';
+import {Item} from '../../../core/shared/item.model';
+import {Router} from '@angular/router';
+import {ItemOperation} from '../item-operation/itemOperation.model';
+
+@Component({
+ selector: 'ds-item-status',
+ templateUrl: './item-status.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [
+ fadeIn,
+ fadeInOut
+ ]
+})
+/**
+ * Component for displaying an item's status
+ */
+export class ItemStatusComponent implements OnInit {
+
+ /**
+ * The item to display the status for
+ */
+ @Input() item: Item;
+
+ /**
+ * The data to show in the status
+ */
+ statusData: any;
+ /**
+ * The keys of the data (to loop over)
+ */
+ statusDataKeys;
+
+ /**
+ * The possible actions that can be performed on the item
+ * key: id value: url to action's component
+ */
+ operations: ItemOperation[];
+ /**
+ * The keys of the actions (to loop over)
+ */
+ actionsKeys;
+
+ constructor(private router: Router) {
+ }
+
+ ngOnInit(): void {
+ this.statusData = Object.assign({
+ id: this.item.id,
+ handle: this.item.handle,
+ lastModified: this.item.lastModified
+ });
+ this.statusDataKeys = Object.keys(this.statusData);
+
+ /*
+ The key is used to build messages
+ i18n example: 'item.edit.tabs.status.buttons..label'
+ The value is supposed to be a href for the button
+ */
+ this.operations = [];
+ if (this.item.isWithdrawn) {
+ this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl() + '/reinstate'));
+ } else {
+ this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl() + '/withdraw'));
+ }
+ if (this.item.isDiscoverable) {
+ this.operations.push(new ItemOperation('private', this.getCurrentUrl() + '/private'));
+ } else {
+ this.operations.push(new ItemOperation('public', this.getCurrentUrl() + '/public'));
+ }
+ this.operations.push(new ItemOperation('delete', this.getCurrentUrl() + '/delete'));
+ }
+
+ /**
+ * Get the url to the simple item page
+ * @returns {string} url
+ */
+ getItemPage(): string {
+ return this.router.url.substr(0, this.router.url.lastIndexOf('/'));
+ }
+
+ /**
+ * Get the current url without query params
+ * @returns {string} url
+ */
+ getCurrentUrl(): string {
+ if (this.router.url.indexOf('?') > -1) {
+ return this.router.url.substr(0, this.router.url.indexOf('?'));
+ } else {
+ return this.router.url;
+ }
+ }
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts
new file mode 100644
index 0000000000..e1de52a506
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts
@@ -0,0 +1,105 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {ItemWithdrawComponent} from './item-withdraw.component';
+import {By} from '@angular/platform-browser';
+
+let comp: ItemWithdrawComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemWithdrawComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
+ setWithDrawn: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot(),],
+ declarations: [ItemWithdrawComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemWithdrawComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'withdraw\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.withdraw.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.withdraw.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.withdraw.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.withdraw.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call setWithdrawn function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(mockItem.id, true);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts
new file mode 100644
index 0000000000..6924124efc
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts
@@ -0,0 +1,30 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Item} from '../../../core/shared/item.model';
+
+@Component({
+ selector: 'ds-item-withdraw',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the Item Withdraw page
+ */
+export class ItemWithdrawComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'withdraw';
+ protected predicate = (rd: RemoteData- ) => rd.payload.isWithdrawn;
+
+ /**
+ * Perform the withdraw action to the item
+ */
+ performAction() {
+ this.itemDataService.setWithDrawn(this.item.id, true).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html
new file mode 100644
index 0000000000..d59d29ddbf
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts
new file mode 100644
index 0000000000..942357dc5a
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts
@@ -0,0 +1,55 @@
+import {Item} from '../../../core/shared/item.model';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ModifyItemOverviewComponent} from './modify-item-overview.component';
+import {By} from '@angular/platform-browser';
+import {TranslateModule} from '@ngx-translate/core';
+
+let comp: ModifyItemOverviewComponent;
+let fixture: ComponentFixture;
+
+const mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ metadata: [
+ {key: 'dc.title', value: 'Mock item title', language: 'en'},
+ {key: 'dc.contributor.author', value: 'Mayer, Ed', language: ''}
+ ]
+});
+
+describe('ModifyItemOverviewComponent', () => {
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot()],
+ declarations: [ModifyItemOverviewComponent],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ModifyItemOverviewComponent);
+ comp = fixture.componentInstance;
+ comp.item = mockItem;
+
+ fixture.detectChanges();
+ });
+ it('should render a table of existing metadata fields in the item', () => {
+
+ const metadataRows = fixture.debugElement.queryAll(By.css('tr.metadata-row'));
+ expect(metadataRows.length).toEqual(2);
+
+ const titleRow = metadataRows[0].queryAll(By.css('td'));
+ expect(titleRow.length).toEqual(3);
+
+ expect(titleRow[0].nativeElement.innerHTML).toContain('dc.title');
+ expect(titleRow[1].nativeElement.innerHTML).toContain('Mock item title');
+ expect(titleRow[2].nativeElement.innerHTML).toContain('en');
+
+ const authorRow = metadataRows[1].queryAll(By.css('td'));
+ expect(authorRow.length).toEqual(3);
+
+ expect(authorRow[0].nativeElement.innerHTML).toContain('dc.contributor.author');
+ expect(authorRow[1].nativeElement.innerHTML).toContain('Mayer, Ed');
+ expect(authorRow[2].nativeElement.innerHTML).toEqual('');
+
+ });
+});
diff --git a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts
new file mode 100644
index 0000000000..d32a98d5e0
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts
@@ -0,0 +1,20 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {Item} from '../../../core/shared/item.model';
+import {Metadatum} from '../../../core/shared/metadatum.model';
+
+@Component({
+ selector: 'ds-modify-item-overview',
+ templateUrl: './modify-item-overview.component.html'
+})
+/**
+ * Component responsible for rendering a table containing the metadatavalues from the to be edited item
+ */
+export class ModifyItemOverviewComponent implements OnInit {
+
+ @Input() item: Item;
+ metadata: Metadatum[];
+
+ ngOnInit(): void {
+ this.metadata = this.item.metadata;
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html
new file mode 100644
index 0000000000..fef76231c6
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html
@@ -0,0 +1,16 @@
+
+
+
+
{{headerMessage | translate: {id: item.handle} }}
+
{{descriptionMessage | translate}}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts
new file mode 100644
index 0000000000..2da671517c
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts
@@ -0,0 +1,142 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {CommonModule} from '@angular/common';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {RemoteData} from '../../../core/data/remote-data';
+import {AbstractSimpleItemActionComponent} from './abstract-simple-item-action.component';
+import {By} from '@angular/platform-browser';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {of as observableOf} from 'rxjs';
+import {getItemEditPath} from '../../item-page-routing.module';
+
+/**
+ * Test component that implements the AbstractSimpleItemActionComponent used to test the
+ * AbstractSimpleItemActionComponent component
+ */
+@Component({
+ selector: 'ds-simple-action',
+ templateUrl: './abstract-simple-item-action.component.html'
+})
+export class MySimpleItemActionComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'myEditAction';
+ protected predicate = (rd: RemoteData- ) => rd.payload.isWithdrawn;
+
+ performAction() {
+ // do nothing
+ }
+
+}
+
+let comp: MySimpleItemActionComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('AbstractSimpleItemActionComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj({
+ findById: observableOf(new RemoteData(false, false, true, undefined, mockItem))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [MySimpleItemActionComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(MySimpleItemActionComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the provided messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.myEditAction.header');
+
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.myEditAction.description');
+
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.myEditAction.confirm');
+
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.myEditAction.cancel');
+ });
+
+ it('should perform action when the button is clicked', () => {
+ spyOn(comp, 'performAction');
+ const performButton = fixture.debugElement.query(By.css('.perform-action'));
+ performButton.triggerEventHandler('click', null);
+
+ expect(comp.performAction).toHaveBeenCalled();
+ });
+
+ it('should process a RestResponse to navigate and display success notification', () => {
+ spyOn(notificationsServiceStub, 'success');
+ comp.processRestResponse(successfulRestResponse);
+
+ expect(notificationsServiceStub.success).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath(mockItem.id)]);
+ });
+
+ it('should process a RestResponse to navigate and display success notification', () => {
+ spyOn(notificationsServiceStub, 'error');
+ comp.processRestResponse(failRestResponse);
+
+ expect(notificationsServiceStub.error).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath(mockItem.id)]);
+ });
+
+});
diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts
new file mode 100644
index 0000000000..743b52921f
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts
@@ -0,0 +1,84 @@
+import {Component, OnInit, Predicate} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {TranslateService} from '@ngx-translate/core';
+import {Item} from '../../../core/shared/item.model';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Observable} from 'rxjs';
+import {getSucceededRemoteData} from '../../../core/shared/operators';
+import {first, map} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {findSuccessfulAccordingTo} from '../edit-item-operators';
+import {getItemEditPath} from '../../item-page-routing.module';
+
+/**
+ * Component to render and handle simple item edit actions such as withdrawal and reinstatement.
+ * This component is not meant to be used itself but to be extended.
+ */
+@Component({
+ selector: 'ds-simple-action',
+ templateUrl: './abstract-simple-item-action.component.html'
+})
+export class AbstractSimpleItemActionComponent implements OnInit {
+
+ itemRD$: Observable>;
+ item: Item;
+
+ protected messageKey: string;
+ confirmMessage: string;
+ cancelMessage: string;
+ headerMessage: string;
+ descriptionMessage: string;
+
+ protected predicate: Predicate>;
+
+ constructor(protected route: ActivatedRoute,
+ protected router: Router,
+ protected notificationsService: NotificationsService,
+ protected itemDataService: ItemDataService,
+ protected translateService: TranslateService) {
+ }
+
+ ngOnInit(): void {
+ this.itemRD$ = this.route.data.pipe(
+ map((data) => data.item),
+ getSucceededRemoteData()
+ )as Observable>;
+
+ this.itemRD$.pipe(first()).subscribe((rd) => {
+ this.item = rd.payload;
+ }
+ );
+
+ this.confirmMessage = 'item.edit.' + this.messageKey + '.confirm';
+ this.cancelMessage = 'item.edit.' + this.messageKey + '.cancel';
+ this.headerMessage = 'item.edit.' + this.messageKey + '.header';
+ this.descriptionMessage = 'item.edit.' + this.messageKey + '.description';
+ }
+
+ /**
+ * Perform the operation linked to this action
+ */
+ performAction() {
+ // Overwrite in subclasses
+ };
+
+ /**
+ * Process the response obtained during the performAction method and navigate back to the edit page
+ * @param response from the action in the performAction method
+ */
+ processRestResponse(response: RestResponse) {
+ if (response.isSuccessful) {
+ this.itemDataService.findById(this.item.id).pipe(
+ findSuccessfulAccordingTo(this.predicate)).subscribe(() => {
+ this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
+ this.router.navigate([getItemEditPath(this.item.id)]);
+ });
+ } else {
+ this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
+ this.router.navigate([getItemEditPath(this.item.id)]);
+ }
+ }
+
+}
diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts
index 96158b867e..8c1f317bb7 100644
--- a/src/app/+item-page/item-page-routing.module.ts
+++ b/src/app/+item-page/item-page-routing.module.ts
@@ -4,6 +4,18 @@ import { RouterModule } from '@angular/router';
import { ItemPageComponent } from './simple/item-page.component';
import { FullItemPageComponent } from './full/full-item-page.component';
import { ItemPageResolver } from './item-page.resolver';
+import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
+import {URLCombiner} from '../core/url-combiner/url-combiner';
+import {getItemModulePath} from '../app-routing.module';
+
+export function getItemPageRoute(itemId: string) {
+ return new URLCombiner(getItemModulePath(), itemId).toString();
+}
+export function getItemEditPath(id: string) {
+ return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
+}
+
+const ITEM_EDIT_PATH = ':id/edit';
@NgModule({
imports: [
@@ -22,6 +34,11 @@ import { ItemPageResolver } from './item-page.resolver';
resolve: {
item: ItemPageResolver
}
+ },
+ {
+ path: ITEM_EDIT_PATH,
+ loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
+ canActivate: [AuthenticatedGuard]
}
])
],
diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts
index bd801923e3..d383189a9c 100644
--- a/src/app/+item-page/item-page.module.ts
+++ b/src/app/+item-page/item-page.module.ts
@@ -18,11 +18,13 @@ import { FileSectionComponent } from './simple/field-components/file-section/fil
import { CollectionsComponent } from './field-components/collections/collections.component';
import { FullItemPageComponent } from './full/full-item-page.component';
import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component';
+import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
+ EditItemPageModule,
ItemPageRoutingModule
],
declarations: [
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 7de83651ff..e7ea10598d 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -3,6 +3,10 @@ import { RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
+const ITEM_MODULE_PATH = 'items';
+export function getItemModulePath() {
+ return `/${ITEM_MODULE_PATH}`;
+}
@NgModule({
imports: [
RouterModule.forRoot([
@@ -10,7 +14,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
{ path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
- { path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
+ { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 30a8f01251..10c6643fbb 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -60,10 +60,18 @@ export class AppComponent implements OnInit, AfterViewInit {
private menuService: MenuService,
private windowService: HostWindowService
) {
- // this language will be used as a fallback when a translation isn't found in the current language
- translate.setDefaultLang('en');
- // the lang to use, if the lang isn't available, it will use the current loader to get them
- translate.use('en');
+ // Load all the languages that are defined as active from the config file
+ translate.addLangs(config.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
+
+ // Load the default language from the config file
+ translate.setDefaultLang(config.defaultLanguage);
+
+ // Attempt to get the browser language from the user
+ if (translate.getLangs().includes(translate.getBrowserLang())) {
+ translate.use(translate.getBrowserLang());
+ } else {
+ translate.use(config.defaultLanguage);
+ }
metadata.listenForRouteChange();
diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts
index 0cac6f901d..93ff3b051b 100644
--- a/src/app/core/data/item-data.service.spec.ts
+++ b/src/app/core/data/item-data.service.spec.ts
@@ -7,21 +7,35 @@ import { CoreState } from '../core.reducers';
import { ItemDataService } from './item-data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { FindAllOptions, RestRequest } from './request.models';
import { ObjectCacheService } from '../cache/object-cache.service';
-import { FindAllOptions } from './request.models';
+import { Observable } from 'rxjs';
+import { RestResponse } from '../cache/response.models';
import { NotificationsService } from '../../shared/notifications/notifications.service';
-import { HttpClient } from '@angular/common/http';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
+import { HttpClient } from '@angular/common/http';
describe('ItemDataService', () => {
let scheduler: TestScheduler;
let service: ItemDataService;
let bs: BrowseService;
- const requestService = {} as RequestService;
+ const requestService = {
+ generateRequestId(): string {
+ return scopeID;
+ },
+ configure(request: RestRequest) {
+ // Do nothing
+ }
+ } as RequestService;
const rdbService = {} as RemoteDataBuildService;
- const objectCache = {} as ObjectCacheService;
+
const store = {} as Store;
- const halEndpointService = {} as HALEndpointService;
+ const objectCache = {} as ObjectCacheService;
+ const halEndpointService = {
+ getEndpoint(linkPath: string): Observable {
+ return cold('a', {a: itemEndpoint});
+ }
+ } as HALEndpointService;
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
const options = Object.assign(new FindAllOptions(), {
@@ -41,6 +55,8 @@ describe('ItemDataService', () => {
const http = {} as HttpClient;
const comparator = {} as any;
const dataBuildService = {} as NormalizedObjectBuildService;
+ const itemEndpoint = 'https://rest.api/core/items';
+ const ScopedItemEndpoint = `https://rest.api/core/items/${scopeID}`;
function initMockBrowseService(isSuccessful: boolean) {
const obs = isSuccessful ?
@@ -94,4 +110,70 @@ describe('ItemDataService', () => {
});
});
});
+
+ describe('getItemWithdrawEndpoint', () => {
+ beforeEach(() => {
+ scheduler = getTestScheduler();
+ service = initTestService();
+
+ });
+
+ it('should return the endpoint to withdraw and reinstate items', () => {
+ const result = service.getItemWithdrawEndpoint(scopeID);
+ const expected = cold('a', {a: ScopedItemEndpoint});
+
+ expect(result).toBeObservable(expected);
+ });
+
+ it('should setWithDrawn', () => {
+ const expected = new RestResponse(true, '200');
+ const result = service.setWithDrawn(scopeID, true);
+ result.subscribe((v) => expect(v).toEqual(expected));
+
+ });
+ });
+
+ describe('getItemDiscoverableEndpoint', () => {
+ beforeEach(() => {
+ scheduler = getTestScheduler();
+ service = initTestService();
+
+ });
+
+ it('should return the endpoint to make an item private or public', () => {
+ const result = service.getItemDiscoverableEndpoint(scopeID);
+ const expected = cold('a', {a: ScopedItemEndpoint});
+
+ expect(result).toBeObservable(expected);
+ });
+
+ it('should setDiscoverable', () => {
+ const expected = new RestResponse(true, '200');
+ const result = service.setDiscoverable(scopeID, false);
+ result.subscribe((v) => expect(v).toEqual(expected));
+
+ });
+ });
+
+ describe('getItemDeleteEndpoint', () => {
+ beforeEach(() => {
+ scheduler = getTestScheduler();
+ service = initTestService();
+ });
+
+ it('should return the endpoint to make an item private or public', () => {
+ const result = service.getItemDeleteEndpoint(scopeID);
+ const expected = cold('a', {a: ScopedItemEndpoint});
+
+ expect(result).toBeObservable(expected);
+ });
+
+ it('should delete the item', () => {
+ const expected = new RestResponse(true, '200');
+ const result = service.delete(scopeID);
+ result.subscribe((v) => expect(v).toEqual(expected));
+
+ });
+ });
+
});
diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts
index 32cb00c1a9..c67b49f70d 100644
--- a/src/app/core/data/item-data.service.ts
+++ b/src/app/core/data/item-data.service.ts
@@ -14,12 +14,14 @@ import { URLCombiner } from '../url-combiner/url-combiner';
import { DataService } from './data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
-import { FindAllOptions } from './request.models';
+import { DeleteRequest, FindAllOptions, PatchRequest, RestRequest } from './request.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
+import { configureRequest, getRequestFromRequestHref } from '../shared/operators';
+import { RequestEntry } from './request.reducer';
@Injectable()
export class ItemDataService extends DataService {
@@ -56,4 +58,93 @@ export class ItemDataService extends DataService {
distinctUntilChanged(),);
}
+ /**
+ * Get the endpoint for item withdrawal and reinstatement
+ * @param itemId
+ */
+ public getItemWithdrawEndpoint(itemId: string): Observable {
+ return this.halService.getEndpoint(this.linkPath).pipe(
+ map((endpoint: string) => this.getFindByIDHref(endpoint, itemId))
+ );
+ }
+
+ /**
+ * Get the endpoint to make item private and public
+ * @param itemId
+ */
+ public getItemDiscoverableEndpoint(itemId: string): Observable {
+ return this.halService.getEndpoint(this.linkPath).pipe(
+ map((endpoint: string) => this.getFindByIDHref(endpoint, itemId))
+ );
+ }
+
+ /**
+ * Get the endpoint to delete the item
+ * @param itemId
+ */
+ public getItemDeleteEndpoint(itemId: string): Observable {
+ return this.halService.getEndpoint(this.linkPath).pipe(
+ map((endpoint: string) => this.getFindByIDHref(endpoint, itemId))
+ );
+ }
+
+ /**
+ * Set the isWithdrawn state of an item to a specified state
+ * @param itemId
+ * @param withdrawn
+ */
+ public setWithDrawn(itemId: string, withdrawn: boolean) {
+ const patchOperation = [{
+ op: 'replace', path: '/withdrawn', value: withdrawn
+ }];
+ return this.getItemWithdrawEndpoint(itemId).pipe(
+ distinctUntilChanged(),
+ map((endpointURL: string) =>
+ new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
+ ),
+ configureRequest(this.requestService),
+ map((request: RestRequest) => request.href),
+ getRequestFromRequestHref(this.requestService),
+ map((requestEntry: RequestEntry) => requestEntry.response)
+ );
+ }
+
+ /**
+ * Set the isDiscoverable state of an item to a specified state
+ * @param itemId
+ * @param discoverable
+ */
+ public setDiscoverable(itemId: string, discoverable: boolean) {
+ const patchOperation = [{
+ op: 'replace', path: '/discoverable', value: discoverable
+ }];
+ return this.getItemDiscoverableEndpoint(itemId).pipe(
+ distinctUntilChanged(),
+ map((endpointURL: string) =>
+ new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
+ ),
+ configureRequest(this.requestService),
+ map((request: RestRequest) => request.href),
+ getRequestFromRequestHref(this.requestService),
+ map((requestEntry: RequestEntry) => requestEntry.response)
+ );
+ }
+
+ /**
+ * Delete the item
+ * @param itemId
+ */
+ public delete(itemId: string) {
+ return this.getItemDeleteEndpoint(itemId).pipe(
+ distinctUntilChanged(),
+ map((endpointURL: string) =>
+ new DeleteRequest(this.requestService.generateRequestId(), endpointURL)
+ ),
+ configureRequest(this.requestService),
+ map((request: RestRequest) => request.href),
+ getRequestFromRequestHref(this.requestService),
+ map((requestEntry: RequestEntry) => requestEntry.response)
+ );
+ }
+
}
diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts
index 6aeec230c4..1e92f718ae 100644
--- a/src/app/core/shared/operators.spec.ts
+++ b/src/app/core/shared/operators.spec.ts
@@ -7,9 +7,15 @@ import { RequestService } from '../data/request.service';
import {
configureRequest,
filterSuccessfulResponses,
- getRemoteDataPayload, getRequestFromRequestHref, getRequestFromRequestUUID,
- getResourceLinksFromResponse, getResponseFromEntry,
+ getAllSucceededRemoteData,
+ getRemoteDataPayload,
+ getRequestFromRequestHref,
+ getRequestFromRequestUUID,
+ getResourceLinksFromResponse,
+ getResponseFromEntry,
+ getSucceededRemoteData
} from './operators';
+import { RemoteData } from '../data/remote-data';
describe('Core Module - RxJS Operators', () => {
let scheduler: TestScheduler;
@@ -48,7 +54,7 @@ describe('Core Module - RxJS Operators', () => {
const result = source.pipe(getRequestFromRequestHref(requestService));
const expected = cold('a', { a: new RequestEntry() });
- expect(result).toBeObservable(expected)
+ expect(result).toBeObservable(expected);
});
it('should use the requestService to fetch the request by its self link', () => {
@@ -68,7 +74,7 @@ describe('Core Module - RxJS Operators', () => {
const result = source.pipe(getRequestFromRequestHref(requestService));
const expected = cold('-');
- expect(result).toBeObservable(expected)
+ expect(result).toBeObservable(expected);
});
});
@@ -81,7 +87,7 @@ describe('Core Module - RxJS Operators', () => {
const result = source.pipe(getRequestFromRequestUUID(requestService));
const expected = cold('a', { a: new RequestEntry() });
- expect(result).toBeObservable(expected)
+ expect(result).toBeObservable(expected);
});
it('should use the requestService to fetch the request by its request uuid', () => {
@@ -101,7 +107,7 @@ describe('Core Module - RxJS Operators', () => {
const result = source.pipe(getRequestFromRequestUUID(requestService));
const expected = cold('-');
- expect(result).toBeObservable(expected)
+ expect(result).toBeObservable(expected);
});
});
@@ -111,7 +117,7 @@ describe('Core Module - RxJS Operators', () => {
const result = source.pipe(filterSuccessfulResponses());
const expected = cold('a--d-', testResponses);
- expect(result).toBeObservable(expected)
+ expect(result).toBeObservable(expected);
});
});
@@ -124,7 +130,7 @@ describe('Core Module - RxJS Operators', () => {
d: testRCEs.d.response.resourceSelfLinks
});
- expect(result).toBeObservable(expected)
+ expect(result).toBeObservable(expected);
});
});
@@ -136,7 +142,7 @@ describe('Core Module - RxJS Operators', () => {
scheduler.schedule(() => source.pipe(configureRequest(requestService)).subscribe());
scheduler.flush();
- expect(requestService.configure).toHaveBeenCalledWith(testRequest)
+ expect(requestService.configure).toHaveBeenCalledWith(testRequest);
});
});
@@ -149,7 +155,25 @@ describe('Core Module - RxJS Operators', () => {
a: testRD.a.payload,
});
- expect(result).toBeObservable(expected)
+ expect(result).toBeObservable(expected);
+ });
+ });
+
+ describe('getSucceededRemoteData', () => {
+ it('should return the first() hasSucceeded RemoteData Observable', () => {
+ const testRD = {
+ a: new RemoteData(false, false, true, null, undefined),
+ b: new RemoteData(false, false, false, null, 'b'),
+ c: new RemoteData(false, false, undefined, null, 'c'),
+ d: new RemoteData(false, false, true, null, 'd'),
+ e: new RemoteData(false, false, true, null, 'e'),
+ };
+ const source = hot('abcde', testRD);
+ const result = source.pipe(getSucceededRemoteData());
+
+ result.subscribe((value) => expect(value)
+ .toEqual(new RemoteData(false, false, true, null, 'd')));
+
});
});
@@ -168,4 +192,23 @@ describe('Core Module - RxJS Operators', () => {
expect(result).toBeObservable(expected)
});
});
+
+ describe('getAllSucceededRemoteData', () => {
+ it('should return all hasSucceeded RemoteData Observables', () => {
+ const testRD = {
+ a: new RemoteData(false, false, true, null, undefined),
+ b: new RemoteData(false, false, false, null, 'b'),
+ c: new RemoteData(false, false, undefined, null, 'c'),
+ d: new RemoteData(false, false, true, null, 'd'),
+ e: new RemoteData(false, false, true, null, 'e'),
+ };
+ const source = hot('abcde', testRD);
+ const result = source.pipe(getAllSucceededRemoteData());
+ const expected = cold('---de', testRD);
+
+ expect(result).toBeObservable(expected);
+
+ });
+
+ });
});
diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts
index 64f4ffa570..3b92c71433 100644
--- a/src/app/core/shared/operators.ts
+++ b/src/app/core/shared/operators.ts
@@ -66,6 +66,10 @@ export const getFinishedRemoteData = () =>
(source: Observable>): Observable> =>
source.pipe(find((rd: RemoteData) => !rd.isLoading));
+export const getAllSucceededRemoteData = () =>
+ (source: Observable>): Observable> =>
+ source.pipe(filter((rd: RemoteData) => rd.hasSucceeded));
+
export const toDSpaceObjectListRD = () =>
(source: Observable>>>): Observable>> =>
source.pipe(
diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html
index 461f809357..402eb7a44d 100644
--- a/src/app/header/header.component.html
+++ b/src/app/header/header.component.html
@@ -6,7 +6,7 @@