mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'community-and-collection-forms' into w2p-58522_delete-collections-and-communities
Conflicts: src/app/core/data/data.service.ts src/app/shared/shared.module.ts
This commit is contained in:
@@ -59,5 +59,25 @@ module.exports = {
|
|||||||
// Log directory
|
// Log directory
|
||||||
logDirectory: '.',
|
logDirectory: '.',
|
||||||
// NOTE: will log all redux actions and transfers in console
|
// 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,
|
||||||
|
}]
|
||||||
};
|
};
|
||||||
|
@@ -108,6 +108,120 @@
|
|||||||
"simple": "Simple item page",
|
"simple": "Simple item page",
|
||||||
"full": "Full 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": {
|
"nav": {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8.4 KiB |
@@ -1,4 +1,4 @@
|
|||||||
import { mergeMap, filter, map, first, tap } from 'rxjs/operators';
|
import { mergeMap, filter, map } from 'rxjs/operators';
|
||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
<div class="jumbotron jumbotron-fluid">
|
<div class="jumbotron jumbotron-fluid">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex">
|
<div class="d-flex flex-wrap">
|
||||||
<div class="dspace-logo-container">
|
<img class="mr-4 dspace-logo" src="assets/images/dspace-logo.svg" alt="" />
|
||||||
<img src="assets/images/dspace-logo.png" />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 class="display-3">Welcome to DSpace</h1>
|
<h1 class="display-3">Welcome to DSpace</h1>
|
||||||
<p class="lead">DSpace is an open source software platform that enables organisations to:</p>
|
<p class="lead">DSpace is an open source software platform that enables organisations to:</p>
|
||||||
|
@@ -6,14 +6,11 @@
|
|||||||
margin-bottom: -$content-spacing;
|
margin-bottom: -$content-spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dspace-logo-container {
|
.display-3 {
|
||||||
margin: 10px 20px 0px 20px;
|
word-break: break-word;
|
||||||
.display-3 {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dspace-logo-container img {
|
.dspace-logo {
|
||||||
max-height: 110px;
|
height: 110px;
|
||||||
max-width: 110px;
|
width: 110px;
|
||||||
}
|
}
|
||||||
|
@@ -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));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
13
src/app/+item-page/edit-item-page/edit-item-operators.ts
Normal file
13
src/app/+item-page/edit-item-page/edit-item-operators.ts
Normal 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));
|
@@ -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>
|
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
src/app/+item-page/edit-item-page/edit-item-page.module.ts
Normal file
40
src/app/+item-page/edit-item-page/edit-item-page.module.ts
Normal 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 {
|
||||||
|
|
||||||
|
}
|
@@ -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 {
|
||||||
|
|
||||||
|
}
|
@@ -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 { 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';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
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')]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,43 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
@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)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
@@ -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;
|
||||||
|
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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 {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';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
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';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -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 { 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';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
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';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -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 { 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';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
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';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
@@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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 { 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';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
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';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
@@ -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('');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
@@ -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 { of as observableOf } from 'rxjs';
|
||||||
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -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 {findSuccessfulAccordingTo} from '../edit-item-operators';
|
||||||
|
import {getItemEditPath} from '../../item-page-routing.module';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -4,6 +4,18 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { ItemPageComponent } from './simple/item-page.component';
|
import { ItemPageComponent } from './simple/item-page.component';
|
||||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||||
import { ItemPageResolver } from './item-page.resolver';
|
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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -22,6 +34,11 @@ import { ItemPageResolver } from './item-page.resolver';
|
|||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver
|
item: ItemPageResolver
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_PATH,
|
||||||
|
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@@ -18,11 +18,13 @@ import { FileSectionComponent } from './simple/field-components/file-section/fil
|
|||||||
import { CollectionsComponent } from './field-components/collections/collections.component';
|
import { CollectionsComponent } from './field-components/collections/collections.component';
|
||||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||||
import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component';
|
import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component';
|
||||||
|
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
EditItemPageModule,
|
||||||
ItemPageRoutingModule
|
ItemPageRoutingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@@ -6,7 +6,7 @@ import {
|
|||||||
Subject,
|
Subject,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { switchMap, distinctUntilChanged, first, map, take } from 'rxjs/operators';
|
import { switchMap, distinctUntilChanged, map, take } from 'rxjs/operators';
|
||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { first, take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { SearchFilterService } from './search-filter.service';
|
import { SearchFilterService } from './search-filter.service';
|
||||||
|
@@ -3,6 +3,10 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||||
|
|
||||||
|
const ITEM_MODULE_PATH = 'items';
|
||||||
|
export function getItemModulePath() {
|
||||||
|
return `/${ITEM_MODULE_PATH}`;
|
||||||
|
}
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
@@ -10,7 +14,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
|||||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
||||||
{ path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
{ path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||||
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
{ 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: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
||||||
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
|
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { filter, first, map, take } from 'rxjs/operators';
|
import { filter, map, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -60,10 +60,18 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private windowService: HostWindowService
|
private windowService: HostWindowService
|
||||||
) {
|
) {
|
||||||
// this language will be used as a fallback when a translation isn't found in the current language
|
// Load all the languages that are defined as active from the config file
|
||||||
translate.setDefaultLang('en');
|
translate.addLangs(config.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
||||||
// the lang to use, if the lang isn't available, it will use the current loader to get them
|
|
||||||
translate.use('en');
|
// 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();
|
metadata.listenForRouteChange();
|
||||||
|
|
||||||
|
@@ -25,8 +25,6 @@ export class AuthRequestService {
|
|||||||
protected fetchRequest(request: RestRequest): Observable<any> {
|
protected fetchRequest(request: RestRequest): Observable<any> {
|
||||||
return this.requestService.getByUUID(request.uuid).pipe(
|
return this.requestService.getByUUID(request.uuid).pipe(
|
||||||
getResponseFromEntry(),
|
getResponseFromEntry(),
|
||||||
// TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
|
|
||||||
// tap(() => this.responseCache.remove(request.href)),
|
|
||||||
mergeMap((response) => {
|
mergeMap((response) => {
|
||||||
if (response.isSuccessful && isNotEmpty(response)) {
|
if (response.isSuccessful && isNotEmpty(response)) {
|
||||||
return observableOf((response as AuthStatusResponse).response);
|
return observableOf((response as AuthStatusResponse).response);
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { of as observableOf, Observable } from 'rxjs';
|
import { of as observableOf, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { filter, debounceTime, switchMap, take, tap, catchError, map, first } from 'rxjs/operators';
|
import { filter, debounceTime, switchMap, take, tap, catchError, map } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
// import @ngrx
|
// import @ngrx
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { first, map, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
@@ -3,24 +3,38 @@ import { NormalizedObject } from '../models/normalized-object.model';
|
|||||||
import { CacheableObject } from '../object-cache.reducer';
|
import { CacheableObject } from '../object-cache.reducer';
|
||||||
import { getRelationships } from './build-decorators';
|
import { getRelationships } from './build-decorators';
|
||||||
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
|
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
|
||||||
import { map, take } from 'rxjs/operators';
|
|
||||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { PaginatedList } from '../../data/paginated-list';
|
|
||||||
|
|
||||||
export function isRestDataObject(halObj: any) {
|
/**
|
||||||
|
* Return true if halObj has a value for `_links.self`
|
||||||
|
*
|
||||||
|
* @param {any} halObj The object to test
|
||||||
|
*/
|
||||||
|
export function isRestDataObject(halObj: any): boolean {
|
||||||
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRestPaginatedList(halObj: any) {
|
/**
|
||||||
|
* Return true if halObj has a value for `page` and `_embedded`
|
||||||
|
*
|
||||||
|
* @param {any} halObj The object to test
|
||||||
|
*/
|
||||||
|
export function isRestPaginatedList(halObj: any): boolean {
|
||||||
return hasValue(halObj.page) && hasValue(halObj._embedded);
|
return hasValue(halObj.page) && hasValue(halObj._embedded);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPaginatedList(halObj: any) {
|
/**
|
||||||
return hasValue(halObj.page) && hasValue(halObj.pageInfo);
|
* A service to turn domain models in to their normalized
|
||||||
}
|
* counterparts.
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataBuildService {
|
export class NormalizedObjectBuildService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the normalized model that corresponds to the given domain model
|
||||||
|
*
|
||||||
|
* @param {TDomain} domainModel a domain model
|
||||||
|
*/
|
||||||
normalize<TDomain extends CacheableObject, TNormalized extends NormalizedObject>(domainModel: TDomain): TNormalized {
|
normalize<TDomain extends CacheableObject, TNormalized extends NormalizedObject>(domainModel: TDomain): TNormalized {
|
||||||
const normalizedConstructor = NormalizedObjectFactory.getConstructor(domainModel.type);
|
const normalizedConstructor = NormalizedObjectFactory.getConstructor(domainModel.type);
|
||||||
const relationships = getRelationships(normalizedConstructor) || [];
|
const relationships = getRelationships(normalizedConstructor) || [];
|
10
src/app/core/cache/object-cache.reducer.ts
vendored
10
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -17,8 +17,18 @@ export enum DirtyType {
|
|||||||
Deleted = 'Deleted'
|
Deleted = 'Deleted'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to represent a JsonPatch
|
||||||
|
*/
|
||||||
export interface Patch {
|
export interface Patch {
|
||||||
|
/**
|
||||||
|
* The identifier for this Patch
|
||||||
|
*/
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the list of operations this Patch is composed of
|
||||||
|
*/
|
||||||
operations: Operation[];
|
operations: Operation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
src/app/core/cache/object-cache.service.ts
vendored
2
src/app/core/cache/object-cache.service.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { distinctUntilChanged, filter, first, map, mergeMap, take, } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import { IndexName } from '../index/index.reducer';
|
import { IndexName } from '../index/index.reducer';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { delay, exhaustMap, first, map, switchMap, take, tap } from 'rxjs/operators';
|
import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
import {
|
import {
|
||||||
|
@@ -65,8 +65,8 @@ import { BrowseItemsResponseParsingService } from './data/browse-items-response-
|
|||||||
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
||||||
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { DataBuildService } from './cache/builders/data-build.service';
|
import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service';
|
||||||
import { DSOUpdateComparator } from './data/dso-update-comparator';
|
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -103,7 +103,7 @@ const PROVIDERS = [
|
|||||||
ObjectCacheService,
|
ObjectCacheService,
|
||||||
PaginationComponentOptions,
|
PaginationComponentOptions,
|
||||||
RegistryService,
|
RegistryService,
|
||||||
DataBuildService,
|
NormalizedObjectBuildService,
|
||||||
RemoteDataBuildService,
|
RemoteDataBuildService,
|
||||||
RequestService,
|
RequestService,
|
||||||
EndpointMapResponseParsingService,
|
EndpointMapResponseParsingService,
|
||||||
@@ -131,7 +131,7 @@ const PROVIDERS = [
|
|||||||
UploaderService,
|
UploaderService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
DSpaceObjectDataService,
|
DSpaceObjectDataService,
|
||||||
DSOUpdateComparator,
|
DSOChangeAnalyzer,
|
||||||
CSSVariableService,
|
CSSVariableService,
|
||||||
MenuService,
|
MenuService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
|
@@ -8,7 +8,7 @@ import { GenericConstructor } from '../shared/generic-constructor';
|
|||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { ResourceType } from '../shared/resource-type';
|
import { ResourceType } from '../shared/resource-type';
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
import { isRestDataObject, isRestPaginatedList } from '../cache/builders/data-build.service';
|
import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service';
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
export abstract class BaseResponseParsingService {
|
export abstract class BaseResponseParsingService {
|
||||||
|
20
src/app/core/data/change-analyzer.ts
Normal file
20
src/app/core/data/change-analyzer.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
|
import { Operation } from 'fast-json-patch/lib/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to determine what differs between two
|
||||||
|
* NormalizedObjects
|
||||||
|
*/
|
||||||
|
export interface ChangeAnalyzer<TNormalized extends NormalizedObject> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two objects and return their differences as a
|
||||||
|
* JsonPatch Operation Array
|
||||||
|
*
|
||||||
|
* @param {NormalizedObject} object1
|
||||||
|
* The first object to compare
|
||||||
|
* @param {NormalizedObject} object2
|
||||||
|
* The second object to compare
|
||||||
|
*/
|
||||||
|
diff(object1: TNormalized, object2: TNormalized): Operation[];
|
||||||
|
}
|
@@ -11,8 +11,8 @@ import { RequestService } from './request.service';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOUpdateComparator } from './dso-update-comparator';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
|
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
|
||||||
@@ -21,14 +21,14 @@ export class CollectionDataService extends ComColDataService<NormalizedCollectio
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected dataBuildService: DataBuildService,
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
protected cds: CommunityDataService,
|
protected cds: CommunityDataService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator: DSOUpdateComparator
|
protected comparator: DSOChangeAnalyzer
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@@ -16,8 +16,8 @@ import { RequestEntry } from './request.reducer';
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOUpdateComparator } from './dso-update-comparator';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
|
||||||
const LINK_NAME = 'test';
|
const LINK_NAME = 'test';
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class TestService extends ComColDataService<NormalizedTestObject, any> {
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected dataBuildService: DataBuildService,
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
protected EnvConfig: GlobalConfig,
|
protected EnvConfig: GlobalConfig,
|
||||||
protected cds: CommunityDataService,
|
protected cds: CommunityDataService,
|
||||||
@@ -38,7 +38,7 @@ class TestService extends ComColDataService<NormalizedTestObject, any> {
|
|||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator: DSOUpdateComparator,
|
protected comparator: DSOChangeAnalyzer,
|
||||||
protected linkPath: string
|
protected linkPath: string
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@@ -61,7 +61,7 @@ describe('ComColDataService', () => {
|
|||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const http = {} as HttpClient;
|
const http = {} as HttpClient;
|
||||||
const comparator = {} as any;
|
const comparator = {} as any;
|
||||||
const dataBuildService = {} as DataBuildService;
|
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||||
|
|
||||||
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
||||||
const options = Object.assign(new FindAllOptions(), {
|
const options = Object.assign(new FindAllOptions(), {
|
||||||
|
@@ -17,8 +17,8 @@ import { Observable } from 'rxjs';
|
|||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOUpdateComparator } from './dso-update-comparator';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
|
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
|
||||||
@@ -29,13 +29,13 @@ export class CommunityDataService extends ComColDataService<NormalizedCommunity,
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected dataBuildService: DataBuildService,
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator: DSOUpdateComparator
|
protected comparator: DSOChangeAnalyzer
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@@ -11,9 +11,9 @@ import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { Operation } from '../../../../node_modules/fast-json-patch';
|
import { Operation } from '../../../../node_modules/fast-json-patch';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { UpdateComparator } from './update-comparator';
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { compare } from 'fast-json-patch';
|
import { compare } from 'fast-json-patch';
|
||||||
|
|
||||||
@@ -27,14 +27,14 @@ class TestService extends DataService<NormalizedTestObject, any> {
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected dataBuildService: DataBuildService,
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
protected linkPath: string,
|
protected linkPath: string,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator: UpdateComparator<NormalizedTestObject>
|
protected comparator: ChangeAnalyzer<NormalizedTestObject>
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -44,8 +44,8 @@ class TestService extends DataService<NormalizedTestObject, any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DummyComparator implements UpdateComparator<NormalizedTestObject> {
|
class DummyChangeAnalyzer implements ChangeAnalyzer<NormalizedTestObject> {
|
||||||
compare(object1: NormalizedTestObject, object2: NormalizedTestObject): Operation[] {
|
diff(object1: NormalizedTestObject, object2: NormalizedTestObject): Operation[] {
|
||||||
return compare((object1 as any).metadata, (object2 as any).metadata);
|
return compare((object1 as any).metadata, (object2 as any).metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +58,10 @@ describe('DataService', () => {
|
|||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const http = {} as HttpClient;
|
const http = {} as HttpClient;
|
||||||
const comparator = new DummyComparator() as any;
|
const comparator = new DummyChangeAnalyzer() as any;
|
||||||
const dataBuildService = {
|
const dataBuildService = {
|
||||||
normalize: (object) => object
|
normalize: (object) => object
|
||||||
} as DataBuildService;
|
} as NormalizedObjectBuildService;
|
||||||
const objectCache = {
|
const objectCache = {
|
||||||
addPatch: () => {
|
addPatch: () => {
|
||||||
/* empty */
|
/* empty */
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
delay,
|
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
filter,
|
filter,
|
||||||
find,
|
find,
|
||||||
switchMap,
|
first,
|
||||||
map,
|
map,
|
||||||
take,
|
mergeMap,
|
||||||
tap, first, mergeMap
|
switchMap,
|
||||||
|
take
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -18,44 +18,41 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import {
|
import {
|
||||||
CreateRequest, DeleteByIDRequest,
|
CreateRequest,
|
||||||
|
DeleteByIDRequest,
|
||||||
FindAllOptions,
|
FindAllOptions,
|
||||||
FindAllRequest,
|
FindAllRequest,
|
||||||
FindByIDRequest,
|
FindByIDRequest,
|
||||||
GetRequest, RestRequest
|
GetRequest
|
||||||
} from './request.models';
|
} from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
import { compare, Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import {
|
import { configureRequest, getResponseFromEntry } from '../shared/operators';
|
||||||
configureRequest,
|
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||||
filterSuccessfulResponses, getFinishedRemoteData, getResourceLinksFromResponse,
|
|
||||||
getResponseFromEntry
|
|
||||||
} from '../shared/operators';
|
|
||||||
import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models';
|
|
||||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
|
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
|
||||||
import { UpdateComparator } from './update-comparator';
|
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
|
|
||||||
export abstract class DataService<TNormalized extends NormalizedObject, TDomain extends CacheableObject> {
|
export abstract class DataService<TNormalized extends NormalizedObject, TDomain extends CacheableObject> {
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
protected abstract rdbService: RemoteDataBuildService;
|
protected abstract rdbService: RemoteDataBuildService;
|
||||||
protected abstract dataBuildService: DataBuildService;
|
protected abstract dataBuildService: NormalizedObjectBuildService;
|
||||||
protected abstract store: Store<CoreState>;
|
protected abstract store: Store<CoreState>;
|
||||||
protected abstract linkPath: string;
|
protected abstract linkPath: string;
|
||||||
protected abstract halService: HALEndpointService;
|
protected abstract halService: HALEndpointService;
|
||||||
protected abstract objectCache: ObjectCacheService;
|
protected abstract objectCache: ObjectCacheService;
|
||||||
protected abstract notificationsService: NotificationsService;
|
protected abstract notificationsService: NotificationsService;
|
||||||
protected abstract http: HttpClient;
|
protected abstract http: HttpClient;
|
||||||
protected abstract comparator: UpdateComparator<TNormalized>;
|
protected abstract comparator: ChangeAnalyzer<TNormalized>;
|
||||||
|
|
||||||
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
|
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
|
||||||
|
|
||||||
@@ -139,7 +136,7 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain
|
|||||||
const oldVersion$ = this.objectCache.getBySelfLink(object.self);
|
const oldVersion$ = this.objectCache.getBySelfLink(object.self);
|
||||||
return oldVersion$.pipe(first(), mergeMap((oldVersion: TNormalized) => {
|
return oldVersion$.pipe(first(), mergeMap((oldVersion: TNormalized) => {
|
||||||
const newVersion = this.dataBuildService.normalize<TDomain, TNormalized>(object);
|
const newVersion = this.dataBuildService.normalize<TDomain, TNormalized>(object);
|
||||||
const operations = this.comparator.compare(oldVersion, newVersion);
|
const operations = this.comparator.diff(oldVersion, newVersion);
|
||||||
if (isNotEmpty(operations)) {
|
if (isNotEmpty(operations)) {
|
||||||
this.objectCache.addPatch(object.self, operations);
|
this.objectCache.addPatch(object.self, operations);
|
||||||
}
|
}
|
||||||
@@ -149,6 +146,15 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DSpaceObject on the server, and store the response
|
||||||
|
* in the object cache
|
||||||
|
*
|
||||||
|
* @param {DSpaceObject} dso
|
||||||
|
* The object to create
|
||||||
|
* @param {string} parentUUID
|
||||||
|
* The UUID of the parent to create the new object under
|
||||||
|
*/
|
||||||
create(dso: TDomain, parentUUID: string): Observable<RemoteData<TDomain>> {
|
create(dso: TDomain, parentUUID: string): Observable<RemoteData<TDomain>> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
|
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
26
src/app/core/data/dso-change-analyzer.service.ts
Normal file
26
src/app/core/data/dso-change-analyzer.service.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Operation } from 'fast-json-patch/lib/core';
|
||||||
|
import { compare } from 'fast-json-patch';
|
||||||
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
|
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to determine what differs between two
|
||||||
|
* DSpaceObjects
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class DSOChangeAnalyzer implements ChangeAnalyzer<NormalizedDSpaceObject> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare the metadata of two DSpaceObjects and return the differences as
|
||||||
|
* a JsonPatch Operation Array
|
||||||
|
*
|
||||||
|
* @param {NormalizedDSpaceObject} object1
|
||||||
|
* The first object to compare
|
||||||
|
* @param {NormalizedDSpaceObject} object2
|
||||||
|
* The second object to compare
|
||||||
|
*/
|
||||||
|
diff(object1: NormalizedDSpaceObject, object2: NormalizedDSpaceObject): Operation[] {
|
||||||
|
return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path }));
|
||||||
|
}
|
||||||
|
}
|
@@ -1,12 +0,0 @@
|
|||||||
import { Operation } from 'fast-json-patch/lib/core';
|
|
||||||
import { compare } from 'fast-json-patch';
|
|
||||||
import { UpdateComparator } from './update-comparator';
|
|
||||||
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DSOUpdateComparator implements UpdateComparator<NormalizedDSpaceObject> {
|
|
||||||
compare(object1: NormalizedDSpaceObject, object2: NormalizedDSpaceObject): Operation[] {
|
|
||||||
return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path }));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -9,7 +9,7 @@ import { DSpaceObjectDataService } from './dspace-object-data.service';
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
|
||||||
describe('DSpaceObjectDataService', () => {
|
describe('DSpaceObjectDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -46,7 +46,7 @@ describe('DSpaceObjectDataService', () => {
|
|||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const http = {} as HttpClient;
|
const http = {} as HttpClient;
|
||||||
const comparator = {} as any;
|
const comparator = {} as any;
|
||||||
const dataBuildService = {} as DataBuildService;
|
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||||
|
|
||||||
service = new DSpaceObjectDataService(
|
service = new DSpaceObjectDataService(
|
||||||
requestService,
|
requestService,
|
||||||
|
@@ -13,8 +13,8 @@ import { FindAllOptions } from './request.models';
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOUpdateComparator } from './dso-update-comparator';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject> {
|
class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject> {
|
||||||
@@ -23,13 +23,13 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected dataBuildService: DataBuildService,
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator: DSOUpdateComparator) {
|
protected comparator: DSOChangeAnalyzer) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,12 +50,12 @@ export class DSpaceObjectDataService {
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected dataBuildService: DataBuildService,
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator: DSOUpdateComparator) {
|
protected comparator: DSOChangeAnalyzer) {
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator);
|
this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,21 +7,42 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { ItemDataService } from './item-data.service';
|
import { ItemDataService } from './item-data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { FindAllOptions, RestRequest } from './request.models';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
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 { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
describe('ItemDataService', () => {
|
describe('ItemDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: ItemDataService;
|
let service: ItemDataService;
|
||||||
let bs: BrowseService;
|
let bs: BrowseService;
|
||||||
const requestService = {} as RequestService;
|
const requestService = {
|
||||||
|
generateRequestId(): string {
|
||||||
|
return scopeID;
|
||||||
|
},
|
||||||
|
configure(request: RestRequest) {
|
||||||
|
// Do nothing
|
||||||
|
},
|
||||||
|
getByHref(requestHref: string) {
|
||||||
|
const responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.response = new RestResponse(true, '200');
|
||||||
|
return observableOf(responseCacheEntry);
|
||||||
|
}
|
||||||
|
} as RequestService;
|
||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
const objectCache = {} as ObjectCacheService;
|
|
||||||
const store = {} as Store<CoreState>;
|
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 scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
||||||
const options = Object.assign(new FindAllOptions(), {
|
const options = Object.assign(new FindAllOptions(), {
|
||||||
@@ -40,7 +61,9 @@ describe('ItemDataService', () => {
|
|||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const http = {} as HttpClient;
|
const http = {} as HttpClient;
|
||||||
const comparator = {} as any;
|
const comparator = {} as any;
|
||||||
const dataBuildService = {} as DataBuildService;
|
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||||
|
const itemEndpoint = 'https://rest.api/core/items';
|
||||||
|
const ScopedItemEndpoint = `https://rest.api/core/items/${scopeID}`;
|
||||||
|
|
||||||
function initMockBrowseService(isSuccessful: boolean) {
|
function initMockBrowseService(isSuccessful: boolean) {
|
||||||
const obs = isSuccessful ?
|
const obs = isSuccessful ?
|
||||||
@@ -94,4 +117,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));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -14,12 +14,14 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.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 { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOUpdateComparator } from './dso-update-comparator';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
import { configureRequest, getRequestFromRequestHref } from '../shared/operators';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||||
@@ -28,14 +30,14 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected dataBuildService: DataBuildService,
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
private bs: BrowseService,
|
private bs: BrowseService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator: DSOUpdateComparator) {
|
protected comparator: DSOChangeAnalyzer) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,4 +58,93 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
|||||||
distinctUntilChanged(),);
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -86,7 +86,17 @@ function completeRequest(state: RequestState, action: RequestCompleteAction): Re
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction) {
|
/**
|
||||||
|
* Reset the timeAdded property of all responses
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* a RequestCompleteAction
|
||||||
|
* @return RequestState
|
||||||
|
* the new state, with the timeAdded property reset
|
||||||
|
*/
|
||||||
|
function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction): RequestState {
|
||||||
const newState = Object.create(null);
|
const newState = Object.create(null);
|
||||||
Object.keys(state).forEach((key) => {
|
Object.keys(state).forEach((key) => {
|
||||||
newState[key] = Object.assign({}, state[key],
|
newState[key] = Object.assign({}, state[key],
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
|
||||||
import { Operation } from 'fast-json-patch/lib/core';
|
|
||||||
|
|
||||||
export interface UpdateComparator<TNormalized extends NormalizedObject> {
|
|
||||||
compare(object1: TNormalized, object2: TNormalized): Operation[];
|
|
||||||
}
|
|
@@ -79,6 +79,14 @@ export class DSpaceRESTv2Service {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a FormData object from a DSpaceObject
|
||||||
|
*
|
||||||
|
* @param {DSpaceObject} dso
|
||||||
|
* the DSpaceObject
|
||||||
|
* @return {FormData}
|
||||||
|
* the result
|
||||||
|
*/
|
||||||
buildFormData(dso: DSpaceObject): FormData {
|
buildFormData(dso: DSpaceObject): FormData {
|
||||||
const form: FormData = new FormData();
|
const form: FormData = new FormData();
|
||||||
form.append('name', dso.name);
|
form.append('name', dso.name);
|
||||||
|
@@ -35,8 +35,8 @@ import { AuthService } from '../auth/auth.service';
|
|||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { EmptyError } from 'rxjs/internal-compatibility';
|
import { EmptyError } from 'rxjs/internal-compatibility';
|
||||||
import { DataBuildService } from '../cache/builders/data-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOUpdateComparator } from '../data/dso-update-comparator';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@Component({
|
@Component({
|
||||||
@@ -119,8 +119,8 @@ describe('MetadataService', () => {
|
|||||||
{ provide: AuthService, useValue: {} },
|
{ provide: AuthService, useValue: {} },
|
||||||
{ provide: NotificationsService, useValue: {} },
|
{ provide: NotificationsService, useValue: {} },
|
||||||
{ provide: HttpClient, useValue: {} },
|
{ provide: HttpClient, useValue: {} },
|
||||||
{ provide: DataBuildService, useValue: {} },
|
{ provide: NormalizedObjectBuildService, useValue: {} },
|
||||||
{ provide: DSOUpdateComparator, useValue: {} },
|
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||||
Meta,
|
Meta,
|
||||||
Title,
|
Title,
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
|
@@ -7,9 +7,15 @@ import { RequestService } from '../data/request.service';
|
|||||||
import {
|
import {
|
||||||
configureRequest,
|
configureRequest,
|
||||||
filterSuccessfulResponses,
|
filterSuccessfulResponses,
|
||||||
getRemoteDataPayload, getRequestFromRequestHref, getRequestFromRequestUUID,
|
getAllSucceededRemoteData,
|
||||||
getResourceLinksFromResponse, getResponseFromEntry,
|
getRemoteDataPayload,
|
||||||
|
getRequestFromRequestHref,
|
||||||
|
getRequestFromRequestUUID,
|
||||||
|
getResourceLinksFromResponse,
|
||||||
|
getResponseFromEntry,
|
||||||
|
getSucceededRemoteData
|
||||||
} from './operators';
|
} from './operators';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
|
||||||
describe('Core Module - RxJS Operators', () => {
|
describe('Core Module - RxJS Operators', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -48,7 +54,7 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
const result = source.pipe(getRequestFromRequestHref(requestService));
|
const result = source.pipe(getRequestFromRequestHref(requestService));
|
||||||
const expected = cold('a', { a: new RequestEntry() });
|
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', () => {
|
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 result = source.pipe(getRequestFromRequestHref(requestService));
|
||||||
const expected = cold('-');
|
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 result = source.pipe(getRequestFromRequestUUID(requestService));
|
||||||
const expected = cold('a', { a: new RequestEntry() });
|
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', () => {
|
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 result = source.pipe(getRequestFromRequestUUID(requestService));
|
||||||
const expected = cold('-');
|
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 result = source.pipe(filterSuccessfulResponses());
|
||||||
const expected = cold('a--d-', testResponses);
|
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
|
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.schedule(() => source.pipe(configureRequest(requestService)).subscribe());
|
||||||
scheduler.flush();
|
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,
|
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)
|
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);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, find, first, flatMap, map, tap } from 'rxjs/operators';
|
import { filter, find, flatMap, map, tap } from 'rxjs/operators';
|
||||||
import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
|
import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
@@ -66,6 +66,10 @@ export const getFinishedRemoteData = () =>
|
|||||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
source.pipe(find((rd: RemoteData<T>) => !rd.isLoading));
|
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 = () =>
|
export const toDSpaceObjectListRD = () =>
|
||||||
<T extends DSpaceObject>(source: Observable<RemoteData<PaginatedList<SearchResult<T>>>>): Observable<RemoteData<PaginatedList<T>>> =>
|
<T extends DSpaceObject>(source: Observable<RemoteData<PaginatedList<SearchResult<T>>>>): Observable<RemoteData<PaginatedList<T>>> =>
|
||||||
source.pipe(
|
source.pipe(
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<nav class="navbar navbar-light navbar-expand-md float-right px-0">
|
<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-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>
|
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||||
<div class="pl-2">
|
<div class="pl-2">
|
||||||
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
||||||
|
@@ -11,6 +11,9 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
|||||||
import { isNotEmpty } from '../../empty.util';
|
import { isNotEmpty } from '../../empty.util';
|
||||||
import { ResourceType } from '../../../core/shared/resource-type';
|
import { ResourceType } from '../../../core/shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A form for creating and editing Communities or Collections
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-comcol-form',
|
selector: 'ds-comcol-form',
|
||||||
styleUrls: ['./comcol-form.component.scss'],
|
styleUrls: ['./comcol-form.component.scss'],
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { filter, distinctUntilChanged, map, first } from 'rxjs/operators';
|
import { filter, distinctUntilChanged, map } from 'rxjs/operators';
|
||||||
import { HostWindowState } from './host-window.reducer';
|
import { HostWindowState } from './host-window.reducer';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
|
12
src/app/shared/lang-switch/lang-switch.component.html
Normal file
12
src/app/shared/lang-switch/lang-switch.component.html
Normal 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>
|
3
src/app/shared/lang-switch/lang-switch.component.scss
Normal file
3
src/app/shared/lang-switch/lang-switch.component.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.dropdown-toggle::after {
|
||||||
|
display:none;
|
||||||
|
}
|
156
src/app/shared/lang-switch/lang-switch.component.spec.ts
Normal file
156
src/app/shared/lang-switch/lang-switch.component.spec.ts
Normal 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();
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
49
src/app/shared/lang-switch/lang-switch.component.ts
Normal file
49
src/app/shared/lang-switch/lang-switch.component.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -90,6 +90,7 @@ import { ComColFormComponent } from './comcol-forms/comcol-form/comcol-form.comp
|
|||||||
import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/create-comcol-page.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 { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||||
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||||
|
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -148,6 +149,7 @@ const COMPONENTS = [
|
|||||||
DsDatePickerComponent,
|
DsDatePickerComponent,
|
||||||
ErrorComponent,
|
ErrorComponent,
|
||||||
FormComponent,
|
FormComponent,
|
||||||
|
LangSwitchComponent,
|
||||||
LoadingComponent,
|
LoadingComponent,
|
||||||
LogInComponent,
|
LogInComponent,
|
||||||
LogOutComponent,
|
LogOutComponent,
|
||||||
|
@@ -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 { 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 { RouterLinkDirectiveStub } from './router-link-directive-stub';
|
||||||
import { NgComponentOutletDirectiveStub } from './ng-component-outlet-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
|
* See https://github.com/angular/angular/issues/13590
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule
|
||||||
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
QueryParamsDirectiveStub,
|
QueryParamsDirectiveStub,
|
||||||
|
MySimpleItemActionComponent,
|
||||||
RouterLinkDirectiveStub,
|
RouterLinkDirectiveStub,
|
||||||
NgComponentOutletDirectiveStub
|
NgComponentOutletDirectiveStub
|
||||||
|
], schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class TestModule {}
|
export class TestModule {}
|
||||||
|
@@ -31,6 +31,14 @@ export const createTestComponent = <T>(html: string, type: { new(...args: any[])
|
|||||||
return fixture as ComponentFixture<T>;
|
return fixture as ComponentFixture<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows you to spy on a read only property
|
||||||
|
*
|
||||||
|
* @param obj
|
||||||
|
* The object to spy on
|
||||||
|
* @param prop
|
||||||
|
* The property to spy on
|
||||||
|
*/
|
||||||
export function spyOnOperator(obj: any, prop: string): any {
|
export function spyOnOperator(obj: any, prop: string): any {
|
||||||
const oldProp = obj[prop];
|
const oldProp = obj[prop];
|
||||||
Object.defineProperty(obj, prop, {
|
Object.defineProperty(obj, prop, {
|
||||||
|
@@ -1,11 +1,30 @@
|
|||||||
import { RestRequestMethod } from '../app/core/data/rest-request-method';
|
import { RestRequestMethod } from '../app/core/data/rest-request-method';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds between automatic syncs to the
|
||||||
|
* server for requests using a certain HTTP Method
|
||||||
|
*/
|
||||||
type TimePerMethod = {
|
type TimePerMethod = {
|
||||||
[method in RestRequestMethod]: number;
|
[method in RestRequestMethod]: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The config that determines how the automatic syncing
|
||||||
|
* of changed data to the server works
|
||||||
|
*/
|
||||||
export interface AutoSyncConfig {
|
export interface AutoSyncConfig {
|
||||||
|
/**
|
||||||
|
* The number of seconds between automatic syncs to the server
|
||||||
|
*/
|
||||||
defaultTime: number;
|
defaultTime: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Method specific overrides of defaultTime
|
||||||
|
*/
|
||||||
timePerMethod: TimePerMethod;
|
timePerMethod: TimePerMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The max number of requests in the buffer before a sync to the server
|
||||||
|
*/
|
||||||
maxBufferSize: number;
|
maxBufferSize: number;
|
||||||
};
|
};
|
||||||
|
@@ -4,6 +4,7 @@ import { CacheConfig } from './cache-config.interface';
|
|||||||
import { UniversalConfig } from './universal-config.interface';
|
import { UniversalConfig } from './universal-config.interface';
|
||||||
import { INotificationBoardOptions } from './notifications-config.interfaces';
|
import { INotificationBoardOptions } from './notifications-config.interfaces';
|
||||||
import { FormConfig } from './form-config.interfaces';
|
import { FormConfig } from './form-config.interfaces';
|
||||||
|
import {LangConfig} from './lang-config.interface';
|
||||||
|
|
||||||
export interface GlobalConfig extends Config {
|
export interface GlobalConfig extends Config {
|
||||||
ui: ServerConfig;
|
ui: ServerConfig;
|
||||||
@@ -16,4 +17,6 @@ export interface GlobalConfig extends Config {
|
|||||||
gaTrackingId: string;
|
gaTrackingId: string;
|
||||||
logDirectory: string;
|
logDirectory: string;
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
|
defaultLanguage: string;
|
||||||
|
languages: LangConfig[];
|
||||||
}
|
}
|
||||||
|
12
src/config/lang-config.interface.ts
Normal file
12
src/config/lang-config.interface.ts
Normal 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;
|
||||||
|
}
|
Reference in New Issue
Block a user