Merge branch 'master' into community-and-collection-forms

Conflicts:
	src/app/core/data/item-data.service.spec.ts
	src/app/core/data/item-data.service.ts
	src/app/core/shared/operators.spec.ts
	src/app/core/shared/operators.ts
	src/app/shared/shared.module.ts
This commit is contained in:
lotte
2019-01-22 08:34:51 +01:00
51 changed files with 2212 additions and 36 deletions

View File

@@ -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,
}]
};

View File

@@ -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": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -1,9 +1,7 @@
<div class="jumbotron jumbotron-fluid">
<div class="container">
<div class="d-flex">
<div class="dspace-logo-container">
<img src="assets/images/dspace-logo.png" />
</div>
<div class="d-flex flex-wrap">
<img class="mr-4 dspace-logo" src="assets/images/dspace-logo.svg" alt="" />
<div>
<h1 class="display-3">Welcome to DSpace</h1>
<p class="lead">DSpace is an open source software platform that enables organisations to:</p>

View File

@@ -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;
}

View File

@@ -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<Item>) => 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));
});
});

View File

@@ -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 = <T>(predicate: (rd: RemoteData<T>) => boolean) =>
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe(getAllSucceededRemoteData(),
first(predicate));

View File

@@ -0,0 +1,36 @@
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="border-bottom">{{'item.edit.head' | translate}}</h2>
<div class="pt-2">
<ngb-tabset>
<ngb-tab title="{{'item.edit.tabs.status.head' | translate}}">
<ng-template ngbTabContent>
<ds-item-status [item]="(itemRD$ | async)?.payload"></ds-item-status>
</ng-template>
</ngb-tab>
<ngb-tab title="{{'item.edit.tabs.bitstreams.head' | translate}}">
<ng-template ngbTabContent>
</ng-template>
</ngb-tab>
<ngb-tab title="{{'item.edit.tabs.metadata.head' | translate}}">
<ng-template ngbTabContent>
</ng-template>
</ngb-tab>
<ngb-tab title="{{'item.edit.tabs.view.head' | translate}}">
<ng-template ngbTabContent>
</ng-template>
</ngb-tab>
<ngb-tab title="{{'item.edit.tabs.curate.head' | translate}}">
<ng-template ngbTabContent>
</ng-template>
</ngb-tab>
</ngb-tabset>
</div>
</div>
</div>
</div>

View File

@@ -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<RemoteData<Item>>;
constructor(private route: ActivatedRoute) {
}
ngOnInit(): void {
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
}
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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<ItemDeleteComponent>;
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')]);
});
});
})
;

View File

@@ -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)]);
}
}
}

View File

@@ -0,0 +1,15 @@
<div class="col-3 float-left d-flex h-100 action-label">
<span class="justify-content-center align-self-center">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
</span>
</div>
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
<a class="btn btn-outline-secondary" href="{{operation.operationUrl}}">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
</a>
</div>
<div *ngIf="operation.disabled" class="col-9 float-left action-button">
<span class="btn btn-danger">
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
</span>
</div>

View File

@@ -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');
});
});

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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<ItemPrivateComponent>;
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();
});
});
})
;

View File

@@ -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<Item>) => !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);
}
);
}
}

View File

@@ -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<ItemPublicComponent>;
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();
});
});
})
;

View File

@@ -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<Item>) => 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);
}
);
}
}

View File

@@ -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<ItemReinstateComponent>;
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();
});
});
})
;

View File

@@ -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<Item>) => !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);
}
);
}
}

View File

@@ -0,0 +1,21 @@
<p class="mt-2">{{'item.edit.tabs.status.description' | translate}}</p>
<div class="row">
<div *ngFor="let statusKey of statusDataKeys" class="w-100">
<div class="col-3 float-left status-label">
{{'item.edit.tabs.status.labels.' + statusKey | translate}}:
</div>
<div class="col-9 float-left status-data" id="status-{{statusKey}}">
{{statusData[statusKey]}}
</div>
</div>
<div class="col-3 float-left status-label">
{{'item.edit.tabs.status.labels.itemPage' | translate}}:
</div>
<div class="col-9 float-left status-data" id="status-itemPage">
<a href="{{getItemPage()}}">{{getItemPage()}}</a>
</div>
<div *ngFor="let operation of operations" class="w-100 pt-3">
<ds-item-operation [operation]="operation"></ds-item-operation>
</div>
</div>

View File

@@ -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<ItemStatusComponent>;
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);
});
});

View File

@@ -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.<key>.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;
}
}
}

View File

@@ -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<ItemWithdrawComponent>;
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();
});
});
})
;

View File

@@ -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<Item>) => 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);
}
);
}
}

View File

@@ -0,0 +1,16 @@
<table id="metadata" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">{{'item.edit.modify.overview.field'| translate}}</th>
<th scope="col">{{'item.edit.modify.overview.value'| translate}}</th>
<th scope="col">{{'item.edit.modify.overview.language'| translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let metadatum of metadata" class="metadata-row">
<td>{{metadatum.key}}</td>
<td>{{metadatum.value}}</td>
<td>{{metadatum.language}}</td>
</tr>
</tbody>
</table>

View File

@@ -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<ModifyItemOverviewComponent>;
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('');
});
});

View File

@@ -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;
}
}

View File

@@ -0,0 +1,16 @@
<div class="container">
<div class="row">
<div class="col-12">
<h2>{{headerMessage | translate: {id: item.handle} }}</h2>
<p>{{descriptionMessage | translate}}</p>
<ds-modify-item-overview [item]="item"></ds-modify-item-overview>
<button (click)="performAction()" class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
</button>
<button [routerLink]="['/items/', item.id, 'edit']" class="btn btn-outline-secondary cancel">
{{cancelMessage| translate}}
</button>
</div>
</div>
</div>

View File

@@ -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<Item>) => rd.payload.isWithdrawn;
performAction() {
// do nothing
}
}
let comp: MySimpleItemActionComponent;
let fixture: ComponentFixture<MySimpleItemActionComponent>;
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)]);
});
});

View File

@@ -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<RemoteData<Item>>;
item: Item;
protected messageKey: string;
confirmMessage: string;
cancelMessage: string;
headerMessage: string;
descriptionMessage: string;
protected predicate: Predicate<RemoteData<Item>>;
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<RemoteData<Item>>;
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)]);
}
}
}

View File

@@ -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]
}
])
],

View File

@@ -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: [

View File

@@ -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' },

View File

@@ -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();

View File

@@ -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<CoreState>;
const halEndpointService = {} as HALEndpointService;
const objectCache = {} as ObjectCacheService;
const halEndpointService = {
getEndpoint(linkPath: string): Observable<string> {
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));
});
});
});

View File

@@ -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<NormalizedItem, Item> {
@@ -56,4 +58,93 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
distinctUntilChanged(),);
}
/**
* Get the endpoint for item withdrawal and reinstatement
* @param itemId
*/
public getItemWithdrawEndpoint(itemId: string): Observable<string> {
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<string> {
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<string> {
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)
);
}
}

View File

@@ -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);
});
});
});

View File

@@ -66,6 +66,10 @@ export const getFinishedRemoteData = () =>
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe(find((rd: RemoteData<T>) => !rd.isLoading));
export const getAllSucceededRemoteData = () =>
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe(filter((rd: RemoteData<T>) => rd.hasSucceeded));
export const toDSpaceObjectListRD = () =>
<T extends DSpaceObject>(source: Observable<RemoteData<PaginatedList<SearchResult<T>>>>): Observable<RemoteData<PaginatedList<T>>> =>
source.pipe(

View File

@@ -6,7 +6,7 @@
<nav class="navbar navbar-light navbar-expand-md float-right px-0">
<a href="#" class="px-1"><i class="fas fa-search fa-lg fa-fw" [title]="'nav.search' | translate"></i></a>
<a href="#" class="px-1"><i class="fas fa-globe-asia fa-lg fa-fw" [title]="'nav.language' | translate"></i></a>
<ds-lang-switch></ds-lang-switch>
<ds-auth-nav-menu></ds-auth-nav-menu>
<div class="pl-2">
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"

View File

@@ -0,0 +1,12 @@
<div ngbDropdown class="navbar-nav" *ngIf="moreThanOneLanguage">
<a href="#" id="dropdownLang" role="button" class="px-1" (click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle>
<i class="fas fa-globe-asia fa-lg fa-fw" [title]="'nav.language' | translate"></i>
</a>
<ul ngbDropdownMenu class="dropdown-menu" aria-labelledby="dropdownLang">
<li class="dropdown-item" #langSelect *ngFor="let lang of translate.getLangs()"
(click)="translate.use(lang)"
[class.active]="lang === translate.currentLang">
{{ langLabel(lang) }}
</li>
</ul>
</div>

View File

@@ -0,0 +1,3 @@
.dropdown-toggle::after {
display:none;
}

View File

@@ -0,0 +1,156 @@
import {LangSwitchComponent} from './lang-switch.component';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import { GLOBAL_CONFIG } from '../../../config';
import {LangConfig} from '../../../config/lang-config.interface';
import {Observable, of} from 'rxjs';
// This test is completely independent from any message catalogs or keys in the codebase
// The translation module is instantiated with these bogus messages that we aren't using anyway.
// Double quotes are mandatory in JSON, so de-activating the tslint rule checking for single quotes here.
/* tslint:disable:quotemark */
// JSON for the language files has double quotes around all literals
/* tslint:disable:object-literal-key-quotes */
class CustomLoader implements TranslateLoader {
getTranslation(lang: string): Observable<any> {
return of({
"footer": {
"copyright": "copyright © 2002-{{ year }}",
"link.dspace": "DSpace software",
"link.duraspace": "DuraSpace"
}
});
}
}
/* tslint:enable:quotemark */
/* tslint:enable:object-literal-key-quotes */
describe('LangSwitchComponent', () => {
describe('with English and Deutsch activated, English as default', () => {
let component: LangSwitchComponent;
let fixture: ComponentFixture<LangSwitchComponent>;
let de: DebugElement;
let langSwitchElement: HTMLElement;
let translate: TranslateService;
let http: HttpTestingController;
beforeEach(async(() => {
const mockConfig = {
languages: [{
code: 'en',
label: 'English',
active: true,
}, {
code: 'de',
label: 'Deutsch',
active: true,
}]
};
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, TranslateModule.forRoot(
{
loader: {provide: TranslateLoader, useClass: CustomLoader}
}
)],
declarations: [LangSwitchComponent],
schemas: [NO_ERRORS_SCHEMA],
providers: [TranslateService, {provide: GLOBAL_CONFIG, useValue: mockConfig}]
}).compileComponents()
.then(() => {
translate = TestBed.get(TranslateService);
translate.addLangs(mockConfig.languages.filter((langConfig:LangConfig) => langConfig.active === true).map((a) => a.code));
translate.setDefaultLang('en');
translate.use('en');
http = TestBed.get(HttpTestingController);
fixture = TestBed.createComponent(LangSwitchComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
langSwitchElement = de.nativeElement;
});
}));
it('should create', () => {
expect(component).toBeDefined();
});
it('should identify English as the label for the current active language in the component', async(() => {
fixture.detectChanges();
expect(component.currentLangLabel()).toEqual('English');
}));
it('should be initialized with more than one language active', async(() => {
fixture.detectChanges();
expect(component.moreThanOneLanguage).toBeTruthy();
}));
it('should define the main A HREF in the UI', (() => {
expect(langSwitchElement.querySelector('a')).toBeDefined();
}));
});
describe('with English as the only active and also default language', () => {
let component: LangSwitchComponent;
let fixture: ComponentFixture<LangSwitchComponent>;
let de: DebugElement;
let langSwitchElement: HTMLElement;
let translate: TranslateService;
let http: HttpTestingController;
beforeEach(async(() => {
const mockConfig = {
languages: [{
code: 'en',
label: 'English',
active: true,
}, {
code: 'de',
label: 'Deutsch',
active: false
}]
};
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, TranslateModule.forRoot(
{
loader: {provide: TranslateLoader, useClass: CustomLoader}
}
)],
declarations: [LangSwitchComponent],
schemas: [NO_ERRORS_SCHEMA],
providers: [TranslateService, {provide: GLOBAL_CONFIG, useValue: mockConfig}]
}).compileComponents();
translate = TestBed.get(TranslateService);
translate.addLangs(mockConfig.languages.filter((MyLangConfig) => MyLangConfig.active === true).map((a) => a.code));
translate.setDefaultLang('en');
translate.use('en');
http = TestBed.get(HttpTestingController);
}));
beforeEach(() => {
fixture = TestBed.createComponent(LangSwitchComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
langSwitchElement = de.nativeElement;
});
it('should create', () => {
expect(component).toBeDefined();
});
it('should not define the main header for the language switch, as it should be invisible', (() => {
expect(langSwitchElement.querySelector('a')).toBeNull();
}));
});
});

View File

@@ -0,0 +1,49 @@
import {Component, Inject, OnInit} from '@angular/core';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import {TranslateService} from '@ngx-translate/core';
import {LangConfig} from '../../../config/lang-config.interface';
@Component({
selector: 'ds-lang-switch',
styleUrls: ['lang-switch.component.scss'],
templateUrl: 'lang-switch.component.html',
})
/**
* Component representing a switch for changing the interface language throughout the application
* If only one language is active, the component will disappear as there are no languages to switch to.
*/
export class LangSwitchComponent implements OnInit {
// All of the languages that are active, meaning that a user can switch between them.
activeLangs: LangConfig[];
// A language switch only makes sense if there is more than one active language to switch between.
moreThanOneLanguage: boolean;
constructor(
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
public translate: TranslateService
) {
}
ngOnInit(): void {
this.activeLangs = this.config.languages.filter((MyLangConfig) => MyLangConfig.active === true);
this.moreThanOneLanguage = (this.activeLangs.length > 1);
}
/**
* Returns the label for the current language
*/
currentLangLabel(): string {
return this.activeLangs.find((MyLangConfig) => MyLangConfig.code === this.translate.currentLang).label;
}
/**
* Returns the label for a specific language code
*/
langLabel(langcode: string): string {
return this.activeLangs.find((MyLangConfig) => MyLangConfig.code === langcode).label;
}
}

View File

@@ -89,6 +89,7 @@ import { MenuModule } from './menu/menu.module';
import { ComColFormComponent } from './comcol-forms/comcol-form/comcol-form.component';
import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/create-comcol-page.component';
import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component';
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -146,6 +147,7 @@ const COMPONENTS = [
DsDatePickerComponent,
ErrorComponent,
FormComponent,
LangSwitchComponent,
LoadingComponent,
LogInComponent,
LogOutComponent,

View File

@@ -1,5 +1,8 @@
import { NgModule } from '@angular/core';
import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core';
import { QueryParamsDirectiveStub } from './query-params-directive-stub';
import { MySimpleItemActionComponent } from '../../+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec';
import {CommonModule} from '@angular/common';
import {SharedModule} from '../shared.module';
import { RouterLinkDirectiveStub } from './router-link-directive-stub';
import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive-stub';
@@ -10,10 +13,17 @@ import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive-
* See https://github.com/angular/angular/issues/13590
*/
@NgModule({
imports: [
CommonModule,
SharedModule
],
declarations: [
QueryParamsDirectiveStub,
MySimpleItemActionComponent,
RouterLinkDirectiveStub,
NgComponentOutletDirectiveStub
], schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
})
export class TestModule {}

View File

@@ -4,6 +4,7 @@ import { CacheConfig } from './cache-config.interface';
import { UniversalConfig } from './universal-config.interface';
import { INotificationBoardOptions } from './notifications-config.interfaces';
import { FormConfig } from './form-config.interfaces';
import {LangConfig} from './lang-config.interface';
export interface GlobalConfig extends Config {
ui: ServerConfig;
@@ -16,4 +17,6 @@ export interface GlobalConfig extends Config {
gaTrackingId: string;
logDirectory: string;
debug: boolean;
defaultLanguage: string;
languages: LangConfig[];
}

View File

@@ -0,0 +1,12 @@
import { Config } from './config.interface';
/**
* An interface to represent a language in the configuration. A LangConfig has a code which should be the official
* language code for the language (e.g. fr), a label which should be the name of the language in that language
* (e.g. Français), and a boolean to determine whether or not it should be listed in the language select.
*/
export interface LangConfig extends Config {
code: string;
label: string;
active: boolean;
}