mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-09 02:54:13 +00:00
Merge branch 'delete-communities-and-collections' into w2p-59334_edit-item-metadata-branch
Conflicts: src/app/+community-page/community-page.component.ts src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts src/app/+item-page/edit-item-page/item-private/item-private.component.ts src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts src/app/+item-page/edit-item-page/item-public/item-public.component.ts src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts src/app/core/auth/server-auth.service.ts src/app/core/cache/response.models.ts src/app/core/data/item-data.service.spec.ts src/app/core/data/item-data.service.ts src/app/core/data/request.models.ts src/app/core/index/index.effects.ts src/app/core/metadata/metadata.service.spec.ts src/app/core/shared/operators.spec.ts src/app/header/header.component.spec.ts src/app/shared/form/form.component.ts
This commit is contained in:
@@ -20,7 +20,6 @@ module.exports = {
|
|||||||
// NOTE: how long should objects be cached for by default
|
// NOTE: how long should objects be cached for by default
|
||||||
msToLive: {
|
msToLive: {
|
||||||
default: 15 * 60 * 1000, // 15 minutes
|
default: 15 * 60 * 1000, // 15 minutes
|
||||||
exportToZip: 5 * 1000 // 5 seconds
|
|
||||||
},
|
},
|
||||||
// msToLive: 1000, // 15 minutes
|
// msToLive: 1000, // 15 minutes
|
||||||
control: 'max-age=60', // revalidate browser
|
control: 'max-age=60', // revalidate browser
|
||||||
|
@@ -13,6 +13,38 @@
|
|||||||
"head": "Recent Submissions"
|
"head": "Recent Submissions"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"title": "Name",
|
||||||
|
"description": "Introductory text (HTML)",
|
||||||
|
"abstract": "Short Description",
|
||||||
|
"rights": "Copyright text (HTML)",
|
||||||
|
"tableofcontents": "News (HTML)",
|
||||||
|
"license": "License",
|
||||||
|
"provenance": "Provenance",
|
||||||
|
"errors": {
|
||||||
|
"title": {
|
||||||
|
"required": "Please enter a collection name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"head": "Edit Collection",
|
||||||
|
"delete": "Delete this collection"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"head": "Create a Collection",
|
||||||
|
"sub-head": "Create a Collection for Community {{ parent }}"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"head": "Delete Collection",
|
||||||
|
"text": "Are you sure you want to delete collection \"{{ dso }}\"",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"notification": {
|
||||||
|
"success": "Successfully deleted collection",
|
||||||
|
"fail": "Collection could not be deleted"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"community": {
|
"community": {
|
||||||
@@ -25,6 +57,36 @@
|
|||||||
},
|
},
|
||||||
"sub-community-list": {
|
"sub-community-list": {
|
||||||
"head": "Communities of this Community"
|
"head": "Communities of this Community"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"title": "Name",
|
||||||
|
"description": "Introductory text (HTML)",
|
||||||
|
"abstract": "Short Description",
|
||||||
|
"rights": "Copyright text (HTML)",
|
||||||
|
"tableofcontents": "News (HTML)",
|
||||||
|
"errors": {
|
||||||
|
"title": {
|
||||||
|
"required": "Please enter a community name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"head": "Edit Community",
|
||||||
|
"delete": "Delete this community"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"head": "Create a Community",
|
||||||
|
"sub-head": "Create a Sub-Community for Community {{ parent }}"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"head": "Delete Community",
|
||||||
|
"text": "Are you sure you want to delete community \"{{ dso }}\"",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"notification": {
|
||||||
|
"success": "Successfully deleted community",
|
||||||
|
"fail": "Community could not be deleted"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import {
|
||||||
|
DynamicInputModel,
|
||||||
|
DynamicTextAreaModel
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||||
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form used for creating and editing collections
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-form',
|
||||||
|
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||||
|
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||||
|
})
|
||||||
|
export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||||
|
/**
|
||||||
|
* @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited
|
||||||
|
*/
|
||||||
|
@Input() dso: Collection = new Collection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {ResourceType.Collection} This is a collection-type form
|
||||||
|
*/
|
||||||
|
protected type = ResourceType.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dynamic form fields used for creating/editing a collection
|
||||||
|
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[] = [
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'title',
|
||||||
|
name: 'dc.title',
|
||||||
|
required: true,
|
||||||
|
validators: {
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
errorMessages: {
|
||||||
|
required: 'Please enter a name for this title'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'description',
|
||||||
|
name: 'dc.description',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'abstract',
|
||||||
|
name: 'dc.description.abstract',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'rights',
|
||||||
|
name: 'dc.rights',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'tableofcontents',
|
||||||
|
name: 'dc.description.tableofcontents',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'license',
|
||||||
|
name: 'dc.rights.license',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'provenance',
|
||||||
|
name: 'dc.description.provenance',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
@@ -3,10 +3,38 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { CollectionPageComponent } from './collection-page.component';
|
import { CollectionPageComponent } from './collection-page.component';
|
||||||
import { CollectionPageResolver } from './collection-page.resolver';
|
import { CollectionPageResolver } from './collection-page.resolver';
|
||||||
|
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||||
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
|
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||||
|
import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard';
|
||||||
|
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
component: CreateCollectionPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/edit',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: EditCollectionPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
resolve: {
|
||||||
|
dso: CollectionPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/delete',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: DeleteCollectionPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
resolve: {
|
||||||
|
dso: CollectionPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: CollectionPageComponent,
|
component: CollectionPageComponent,
|
||||||
@@ -19,6 +47,7 @@ import { CollectionPageResolver } from './collection-page.resolver';
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CollectionPageResolver,
|
CollectionPageResolver,
|
||||||
|
CreateCollectionPageGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageRoutingModule {
|
export class CollectionPageRoutingModule {
|
||||||
|
@@ -55,7 +55,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collectionRD$ = this.route.data.pipe(
|
this.collectionRD$ = this.route.data.pipe(
|
||||||
map((data) => data.collection)
|
map((data) => data.collection),
|
||||||
|
tap((data) => this.collectionId = data.payload.id)
|
||||||
);
|
);
|
||||||
this.logoRD$ = this.collectionRD$.pipe(
|
this.logoRD$ = this.collectionRD$.pipe(
|
||||||
map((rd: RemoteData<Collection>) => rd.payload),
|
map((rd: RemoteData<Collection>) => rd.payload),
|
||||||
|
@@ -5,7 +5,11 @@ import { SharedModule } from '../shared/shared.module';
|
|||||||
|
|
||||||
import { CollectionPageComponent } from './collection-page.component';
|
import { CollectionPageComponent } from './collection-page.component';
|
||||||
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||||
|
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||||
|
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
||||||
import { SearchPageModule } from '../+search-page/search-page.module';
|
import { SearchPageModule } from '../+search-page/search-page.module';
|
||||||
|
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||||
|
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -16,6 +20,10 @@ import { SearchPageModule } from '../+search-page/search-page.module';
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CollectionPageComponent,
|
CollectionPageComponent,
|
||||||
|
CreateCollectionPageComponent,
|
||||||
|
EditCollectionPageComponent,
|
||||||
|
DeleteCollectionPageComponent,
|
||||||
|
CollectionFormComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageModule {
|
export class CollectionPageModule {
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-collection-form (submitForm)="onSubmit($event)"></ds-collection-form>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { CreateCollectionPageComponent } from './create-collection-page.component';
|
||||||
|
|
||||||
|
describe('CreateCollectionPageComponent', () => {
|
||||||
|
let comp: CreateCollectionPageComponent;
|
||||||
|
let fixture: ComponentFixture<CreateCollectionPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [CreateCollectionPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CollectionDataService, useValue: {} },
|
||||||
|
{
|
||||||
|
provide: CommunityDataService,
|
||||||
|
useValue: { findById: () => observableOf({ payload: { name: 'test' } }) }
|
||||||
|
},
|
||||||
|
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||||
|
{ provide: Router, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CreateCollectionPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||||
|
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can create a new Collection
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-create-collection',
|
||||||
|
styleUrls: ['./create-collection-page.component.scss'],
|
||||||
|
templateUrl: './create-collection-page.component.html'
|
||||||
|
})
|
||||||
|
export class CreateCollectionPageComponent extends CreateComColPageComponent<Collection, NormalizedCollection> {
|
||||||
|
protected frontendURL = '/collections/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected communityDataService: CommunityDataService,
|
||||||
|
protected collectionDataService: CollectionDataService,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected router: Router
|
||||||
|
) {
|
||||||
|
super(collectionDataService, communityDataService, routeService, router);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,67 @@
|
|||||||
|
import { CreateCollectionPageGuard } from './create-collection-page.guard';
|
||||||
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
|
||||||
|
describe('CreateCollectionPageGuard', () => {
|
||||||
|
describe('canActivate', () => {
|
||||||
|
let guard: CreateCollectionPageGuard;
|
||||||
|
let router;
|
||||||
|
let communityDataServiceStub: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
communityDataServiceStub = {
|
||||||
|
findById: (id: string) => {
|
||||||
|
if (id === 'valid-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, new Community()));
|
||||||
|
} else if (id === 'invalid-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, undefined));
|
||||||
|
} else if (id === 'error-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, false, null, new Community()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
router = new MockRouter();
|
||||||
|
|
||||||
|
guard = new CreateCollectionPageGuard(router, communityDataServiceStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when the parent ID resolves to a community', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when no parent ID has been provided', () => {
|
||||||
|
guard.canActivate({ queryParams: { } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the parent ID does not resolve to a community', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the parent ID resolves to an error response', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { map, tap } from 'rxjs/operators';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent creation of a collection without a parent community provided
|
||||||
|
* @class CreateCollectionPageGuard
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CreateCollectionPageGuard implements CanActivate {
|
||||||
|
public constructor(private router: Router, private communityService: CommunityDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community
|
||||||
|
* Reroutes to a 404 page when the page cannot be activated
|
||||||
|
* @method canActivate
|
||||||
|
*/
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
const parentID = route.queryParams.parent;
|
||||||
|
if (hasNoValue(parentID)) {
|
||||||
|
this.router.navigate(['/404']);
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
|
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||||
|
.pipe(
|
||||||
|
getFinishedRemoteData(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return parent.pipe(
|
||||||
|
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||||
|
tap((isValid: boolean) => {
|
||||||
|
if (!isValid) {
|
||||||
|
this.router.navigate(['/404']);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||||
|
}}</h2>
|
||||||
|
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||||
|
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||||
|
{{'community.delete.confirm' |
|
||||||
|
translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { DeleteCollectionPageComponent } from './delete-collection-page.component';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
|
||||||
|
describe('DeleteCollectionPageComponent', () => {
|
||||||
|
let comp: DeleteCollectionPageComponent;
|
||||||
|
let fixture: ComponentFixture<DeleteCollectionPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [DeleteCollectionPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CollectionDataService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DeleteCollectionPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||||
|
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can delete an existing Collection
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-delete-collection',
|
||||||
|
styleUrls: ['./delete-collection-page.component.scss'],
|
||||||
|
templateUrl: './delete-collection-page.component.html'
|
||||||
|
})
|
||||||
|
export class DeleteCollectionPageComponent extends DeleteComColPageComponent<Collection, NormalizedCollection> {
|
||||||
|
protected frontendURL = '/collections/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected dsoDataService: CollectionDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected notifications: NotificationsService,
|
||||||
|
protected translate: TranslateService
|
||||||
|
) {
|
||||||
|
super(dsoDataService, router, route, notifications, translate);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="header" class="border-bottom pb-2">{{ 'collection.edit.head' | translate }}</h2>
|
||||||
|
<ds-collection-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-collection-form>
|
||||||
|
<a class="btn btn-danger"
|
||||||
|
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete'
|
||||||
|
| translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { EditCollectionPageComponent } from './edit-collection-page.component';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
describe('EditCollectionPageComponent', () => {
|
||||||
|
let comp: EditCollectionPageComponent;
|
||||||
|
let fixture: ComponentFixture<EditCollectionPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [EditCollectionPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CollectionDataService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditCollectionPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||||
|
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can edit an existing Collection
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-collection',
|
||||||
|
styleUrls: ['./edit-collection-page.component.scss'],
|
||||||
|
templateUrl: './edit-collection-page.component.html'
|
||||||
|
})
|
||||||
|
export class EditCollectionPageComponent extends EditComColPageComponent<Collection, NormalizedCollection> {
|
||||||
|
protected frontendURL = '/collections/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected collectionDataService: CollectionDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
super(collectionDataService, router, route);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
|
||||||
|
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
|
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form used for creating and editing communities
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-community-form',
|
||||||
|
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||||
|
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||||
|
})
|
||||||
|
export class CommunityFormComponent extends ComColFormComponent<Community> {
|
||||||
|
/**
|
||||||
|
* @type {Community} A new community when a community is being created, an existing Input community when a community is being edited
|
||||||
|
*/
|
||||||
|
@Input() dso: Community = new Community();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {ResourceType.Community} This is a community-type form
|
||||||
|
*/
|
||||||
|
protected type = ResourceType.Community;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dynamic form fields used for creating/editing a community
|
||||||
|
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[] = [
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'title',
|
||||||
|
name: 'dc.title',
|
||||||
|
required: true,
|
||||||
|
validators: {
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
errorMessages: {
|
||||||
|
required: 'Please enter a name for this title'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'description',
|
||||||
|
name: 'dc.description',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'abstract',
|
||||||
|
name: 'dc.description.abstract',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'rights',
|
||||||
|
name: 'dc.rights',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'tableofcontents',
|
||||||
|
name: 'dc.description.tableofcontents',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
@@ -3,10 +3,38 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { CommunityPageComponent } from './community-page.component';
|
import { CommunityPageComponent } from './community-page.component';
|
||||||
import { CommunityPageResolver } from './community-page.resolver';
|
import { CommunityPageResolver } from './community-page.resolver';
|
||||||
|
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||||
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
|
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||||
|
import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard';
|
||||||
|
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
component: CreateCommunityPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/edit',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: EditCommunityPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
resolve: {
|
||||||
|
dso: CommunityPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/delete',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: DeleteCommunityPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
resolve: {
|
||||||
|
dso: CommunityPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: CommunityPageComponent,
|
component: CommunityPageComponent,
|
||||||
@@ -19,6 +47,7 @@ import { CommunityPageResolver } from './community-page.resolver';
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CommunityPageResolver,
|
CommunityPageResolver,
|
||||||
|
CreateCommunityPageGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CommunityPageRoutingModule {
|
export class CommunityPageRoutingModule {
|
||||||
|
@@ -28,6 +28,7 @@
|
|||||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
|
@@ -7,6 +7,10 @@ import { CommunityPageComponent } from './community-page.component';
|
|||||||
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
||||||
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||||
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
||||||
|
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||||
|
import { CommunityFormComponent } from './community-form/community-form.component';
|
||||||
|
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||||
|
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -18,8 +22,13 @@ import {CommunityPageSubCommunityListComponent} from './sub-community-list/commu
|
|||||||
CommunityPageComponent,
|
CommunityPageComponent,
|
||||||
CommunityPageSubCollectionListComponent,
|
CommunityPageSubCollectionListComponent,
|
||||||
CommunityPageSubCommunityListComponent,
|
CommunityPageSubCommunityListComponent,
|
||||||
|
CreateCommunityPageComponent,
|
||||||
|
EditCommunityPageComponent,
|
||||||
|
DeleteCommunityPageComponent,
|
||||||
|
CommunityFormComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class CommunityPageModule {
|
export class CommunityPageModule {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<ng-container *ngVar="(parentRD$ | async)?.payload as parent">
|
||||||
|
<h2 *ngIf="!parent" id="header" class="border-bottom pb-2">{{ 'community.create.head' | translate }}</h2>
|
||||||
|
<h2 *ngIf="parent" id="sub-header" class="border-bottom pb-2">{{ 'community.create.sub-head' | translate:{ parent: parent.name } }}</h2>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-community-form (submitForm)="onSubmit($event)"></ds-community-form>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { CreateCommunityPageComponent } from './create-community-page.component';
|
||||||
|
|
||||||
|
describe('CreateCommunityPageComponent', () => {
|
||||||
|
let comp: CreateCommunityPageComponent;
|
||||||
|
let fixture: ComponentFixture<CreateCommunityPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [CreateCommunityPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
|
||||||
|
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||||
|
{ provide: Router, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CreateCommunityPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||||
|
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can create a new Community
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-create-community',
|
||||||
|
styleUrls: ['./create-community-page.component.scss'],
|
||||||
|
templateUrl: './create-community-page.component.html'
|
||||||
|
})
|
||||||
|
export class CreateCommunityPageComponent extends CreateComColPageComponent<Community, NormalizedCommunity> {
|
||||||
|
protected frontendURL = '/communities/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected communityDataService: CommunityDataService,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected router: Router
|
||||||
|
) {
|
||||||
|
super(communityDataService, communityDataService, routeService, router);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,67 @@
|
|||||||
|
import { CreateCommunityPageGuard } from './create-community-page.guard';
|
||||||
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
|
||||||
|
describe('CreateCommunityPageGuard', () => {
|
||||||
|
describe('canActivate', () => {
|
||||||
|
let guard: CreateCommunityPageGuard;
|
||||||
|
let router;
|
||||||
|
let communityDataServiceStub: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
communityDataServiceStub = {
|
||||||
|
findById: (id: string) => {
|
||||||
|
if (id === 'valid-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, new Community()));
|
||||||
|
} else if (id === 'invalid-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, undefined));
|
||||||
|
} else if (id === 'error-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, false, null, new Community()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
router = new MockRouter();
|
||||||
|
|
||||||
|
guard = new CreateCommunityPageGuard(router, communityDataServiceStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when the parent ID resolves to a community', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when no parent ID has been provided', () => {
|
||||||
|
guard.canActivate({ queryParams: { } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the parent ID does not resolve to a community', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the parent ID resolves to an error response', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { map, tap } from 'rxjs/operators';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent creation of a community with an invalid parent community provided
|
||||||
|
* @class CreateCommunityPageGuard
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CreateCommunityPageGuard implements CanActivate {
|
||||||
|
public constructor(private router: Router, private communityService: CommunityDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community
|
||||||
|
* Reroutes to a 404 page when the page cannot be activated
|
||||||
|
* @method canActivate
|
||||||
|
*/
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
const parentID = route.queryParams.parent;
|
||||||
|
if (hasNoValue(parentID)) {
|
||||||
|
return observableOf(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||||
|
.pipe(
|
||||||
|
getFinishedRemoteData(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return parent.pipe(
|
||||||
|
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||||
|
tap((isValid: boolean) => {
|
||||||
|
if (!isValid) {
|
||||||
|
this.router.navigate(['/404']);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||||
|
}}</h2>
|
||||||
|
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||||
|
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||||
|
{{'community.delete.confirm' |
|
||||||
|
translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { DeleteCommunityPageComponent } from './delete-community-page.component';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
|
||||||
|
describe('DeleteCommunityPageComponent', () => {
|
||||||
|
let comp: DeleteCommunityPageComponent;
|
||||||
|
let fixture: ComponentFixture<DeleteCommunityPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [DeleteCommunityPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CommunityDataService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DeleteCommunityPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||||
|
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can delete an existing Community
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-delete-community',
|
||||||
|
styleUrls: ['./delete-community-page.component.scss'],
|
||||||
|
templateUrl: './delete-community-page.component.html'
|
||||||
|
})
|
||||||
|
export class DeleteCommunityPageComponent extends DeleteComColPageComponent<Community, NormalizedCommunity> {
|
||||||
|
protected frontendURL = '/communities/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected dsoDataService: CommunityDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected notifications: NotificationsService,
|
||||||
|
protected translate: TranslateService
|
||||||
|
) {
|
||||||
|
super(dsoDataService, router, route, notifications, translate);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="header" class="border-bottom pb-2">{{ 'community.edit.head' | translate }}</h2>
|
||||||
|
<ds-community-form (submitForm)="onSubmit($event)"
|
||||||
|
[dso]="(dsoRD$ | async)?.payload"></ds-community-form>
|
||||||
|
<a class="btn btn-danger"
|
||||||
|
[routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete'
|
||||||
|
| translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { EditCommunityPageComponent } from './edit-community-page.component';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
|
||||||
|
describe('EditCommunityPageComponent', () => {
|
||||||
|
let comp: EditCommunityPageComponent;
|
||||||
|
let fixture: ComponentFixture<EditCommunityPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [EditCommunityPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CommunityDataService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditCommunityPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||||
|
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can edit an existing Community
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-community',
|
||||||
|
styleUrls: ['./edit-community-page.component.scss'],
|
||||||
|
templateUrl: './edit-community-page.component.html'
|
||||||
|
})
|
||||||
|
export class EditCommunityPageComponent extends EditComColPageComponent<Community, NormalizedCommunity> {
|
||||||
|
protected frontendURL = '/communities/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected communityDataService: CommunityDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
super(communityDataService, router, route);
|
||||||
|
}
|
||||||
|
}
|
@@ -17,7 +17,6 @@ describe('SubCommunityList Component', () => {
|
|||||||
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
||||||
|
|
||||||
const subcommunities = [Object.assign(new Community(), {
|
const subcommunities = [Object.assign(new Community(), {
|
||||||
name: 'SubCommunity 1',
|
|
||||||
id: '123456789-1',
|
id: '123456789-1',
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
@@ -27,7 +26,6 @@ describe('SubCommunityList Component', () => {
|
|||||||
}]
|
}]
|
||||||
}),
|
}),
|
||||||
Object.assign(new Community(), {
|
Object.assign(new Community(), {
|
||||||
name: 'SubCommunity 2',
|
|
||||||
id: '123456789-2',
|
id: '123456789-2',
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
|
@@ -1,21 +1,21 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {CommonModule} from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {By} from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {ItemDeleteComponent} from './item-delete.component';
|
import { ItemDeleteComponent } from './item-delete.component';
|
||||||
import {getItemEditPath} from '../../item-page-routing.module';
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
let comp: ItemDeleteComponent;
|
let comp: ItemDeleteComponent;
|
||||||
@@ -27,8 +27,6 @@ let routerStub;
|
|||||||
let mockItemDataService: ItemDataService;
|
let mockItemDataService: ItemDataService;
|
||||||
let routeStub;
|
let routeStub;
|
||||||
let notificationsServiceStub;
|
let notificationsServiceStub;
|
||||||
let successfulRestResponse;
|
|
||||||
let failRestResponse;
|
|
||||||
|
|
||||||
describe('ItemDeleteComponent', () => {
|
describe('ItemDeleteComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@@ -46,14 +44,12 @@ describe('ItemDeleteComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
||||||
delete: observableOf(new RestResponse(true, '200'))
|
delete: observableOf(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: new RemoteData(false, false, true, null, {
|
item: new RemoteData(false, false, true, null, mockItem)
|
||||||
id: 'fake-id'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,10 +59,10 @@ describe('ItemDeleteComponent', () => {
|
|||||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
declarations: [ItemDeleteComponent],
|
declarations: [ItemDeleteComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: ActivatedRoute, useValue: routeStub},
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{provide: Router, useValue: routerStub},
|
{ provide: Router, useValue: routerStub },
|
||||||
{provide: ItemDataService, useValue: mockItemDataService},
|
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||||
{provide: NotificationsService, useValue: notificationsServiceStub},
|
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||||
], schemas: [
|
], schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
]
|
]
|
||||||
@@ -74,9 +70,6 @@ describe('ItemDeleteComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
successfulRestResponse = new RestResponse(true, '200');
|
|
||||||
failRestResponse = new RestResponse(false, '500');
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ItemDeleteComponent);
|
fixture = TestBed.createComponent(ItemDeleteComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -95,22 +88,21 @@ describe('ItemDeleteComponent', () => {
|
|||||||
|
|
||||||
describe('performAction', () => {
|
describe('performAction', () => {
|
||||||
it('should call delete function from the ItemDataService', () => {
|
it('should call delete function from the ItemDataService', () => {
|
||||||
spyOn(comp, 'processRestResponse');
|
spyOn(comp, 'notify');
|
||||||
comp.performAction();
|
comp.performAction();
|
||||||
|
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem);
|
||||||
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id);
|
expect(comp.notify).toHaveBeenCalled();
|
||||||
expect(comp.processRestResponse).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('processRestResponse', () => {
|
describe('notify', () => {
|
||||||
it('should navigate to the homepage on successful deletion of the item', () => {
|
it('should navigate to the homepage on successful deletion of the item', () => {
|
||||||
comp.processRestResponse(successfulRestResponse);
|
comp.notify(true);
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith(['']);
|
expect(routerStub.navigate).toHaveBeenCalledWith(['']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('processRestResponse', () => {
|
describe('notify', () => {
|
||||||
it('should navigate to the item edit page on failed deletion of the item', () => {
|
it('should navigate to the item edit page on failed deletion of the item', () => {
|
||||||
comp.processRestResponse(failRestResponse);
|
comp.notify(false);
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath('fake-id')]);
|
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath('fake-id')]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {getItemEditPath} from '../../item-page-routing.module';
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -19,20 +19,19 @@ export class ItemDeleteComponent extends AbstractSimpleItemActionComponent {
|
|||||||
* Perform the delete action to the item
|
* Perform the delete action to the item
|
||||||
*/
|
*/
|
||||||
performAction() {
|
performAction() {
|
||||||
this.itemDataService.delete(this.item.id).pipe(first()).subscribe(
|
this.itemDataService.delete(this.item).pipe(first()).subscribe(
|
||||||
(response: RestResponse) => {
|
(succeeded: boolean) => {
|
||||||
this.processRestResponse(response);
|
this.notify(succeeded);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
|
||||||
* @param response
|
* @param response
|
||||||
*/
|
*/
|
||||||
processRestResponse(response: RestResponse) {
|
notify(succeeded: boolean) {
|
||||||
if (response.isSuccessful) {
|
if (succeeded) {
|
||||||
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
|
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
|
||||||
this.router.navigate(['']);
|
this.router.navigate(['']);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@@ -1,20 +1,20 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {CommonModule} from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {By} from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {ItemPublicComponent} from './item-public.component';
|
import { ItemPublicComponent } from './item-public.component';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
let comp: ItemPublicComponent;
|
let comp: ItemPublicComponent;
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@@ -1,20 +1,20 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {CommonModule} from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {By} from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {ItemReinstateComponent} from './item-reinstate.component';
|
import { ItemReinstateComponent } from './item-reinstate.component';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
let comp: ItemReinstateComponent;
|
let comp: ItemReinstateComponent;
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@@ -1,20 +1,20 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {CommonModule} from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {ItemWithdrawComponent} from './item-withdraw.component';
|
import { ItemWithdrawComponent } from './item-withdraw.component';
|
||||||
import {By} from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
let comp: ItemWithdrawComponent;
|
let comp: ItemWithdrawComponent;
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@@ -1,21 +1,21 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {CommonModule} from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {AbstractSimpleItemActionComponent} from './abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from './abstract-simple-item-action.component';
|
||||||
import {By} from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {getItemEditPath} from '../../item-page-routing.module';
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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';
|
||||||
|
@@ -111,7 +111,6 @@ export const objects = [
|
|||||||
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||||
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||||
type: ResourceType.Community,
|
type: ResourceType.Community,
|
||||||
name: 'OR2017 - Demonstration',
|
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
key: 'dc.description',
|
key: 'dc.description',
|
||||||
@@ -161,7 +160,6 @@ export const objects = [
|
|||||||
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
type: ResourceType.Community,
|
type: ResourceType.Community,
|
||||||
name: 'Sample Community',
|
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
key: 'dc.description',
|
key: 'dc.description',
|
||||||
|
@@ -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,
|
||||||
|
@@ -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';
|
||||||
|
50
src/app/core/cache/builders/normalized-object-build.service.ts
vendored
Normal file
50
src/app/core/cache/builders/normalized-object-build.service.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { NormalizedObject } from '../models/normalized-object.model';
|
||||||
|
import { CacheableObject } from '../object-cache.reducer';
|
||||||
|
import { getRelationships } from './build-decorators';
|
||||||
|
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
|
||||||
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service to turn domain models in to their normalized
|
||||||
|
* counterparts.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
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 {
|
||||||
|
const normalizedConstructor = NormalizedObjectFactory.getConstructor(domainModel.type);
|
||||||
|
const relationships = getRelationships(normalizedConstructor) || [];
|
||||||
|
|
||||||
|
const normalizedModel = Object.assign({}, domainModel) as any;
|
||||||
|
relationships.forEach((key: string) => {
|
||||||
|
if (hasValue(domainModel[key])) {
|
||||||
|
domainModel[key] = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return normalizedModel;
|
||||||
|
}
|
||||||
|
}
|
@@ -45,7 +45,6 @@ export class RemoteDataBuildService {
|
|||||||
href$.pipe(getRequestFromRequestHref(this.requestService)),
|
href$.pipe(getRequestFromRequestHref(this.requestService)),
|
||||||
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)),
|
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// always use self link if that is cached, only if it isn't, get it via the response.
|
// always use self link if that is cached, only if it isn't, get it via the response.
|
||||||
const payload$ =
|
const payload$ =
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { autoserialize, inheritSerialization } from 'cerialize';
|
import { autoserialize, deserialize, inheritSerialization, serialize } from 'cerialize';
|
||||||
|
|
||||||
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
||||||
import { Community } from '../../shared/community.model';
|
import { Community } from '../../shared/community.model';
|
||||||
@@ -21,32 +21,32 @@ export class NormalizedCommunity extends NormalizedDSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Bitstream that represents the logo of this Community
|
* The Bitstream that represents the logo of this Community
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Bitstream, false)
|
@relationship(ResourceType.Bitstream, false)
|
||||||
logo: string;
|
logo: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Communities that are direct parents of this Community
|
* An array of Communities that are direct parents of this Community
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Community, true)
|
@relationship(ResourceType.Community, true)
|
||||||
parents: string[];
|
parents: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Community that owns this Community
|
* The Community that owns this Community
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Community, false)
|
@relationship(ResourceType.Community, false)
|
||||||
owner: string;
|
owner: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of Collections that are owned by this Community
|
* List of Collections that are owned by this Community
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Collection, true)
|
@relationship(ResourceType.Collection, true)
|
||||||
collections: string[];
|
collections: string[];
|
||||||
|
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Community, true)
|
@relationship(ResourceType.Community, true)
|
||||||
subcommunities: string[];
|
subcommunities: string[];
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { autoserialize, autoserializeAs } from 'cerialize';
|
import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize';
|
||||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
|
||||||
import { Metadatum } from '../../shared/metadatum.model';
|
import { Metadatum } from '../../shared/metadatum.model';
|
||||||
@@ -45,12 +45,6 @@ export class NormalizedDSpaceObject extends NormalizedObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
type: ResourceType;
|
type: ResourceType;
|
||||||
|
|
||||||
/**
|
|
||||||
* The name for this DSpaceObject
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array containing all metadata of this DSpaceObject
|
* An array containing all metadata of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@@ -60,13 +54,13 @@ export class NormalizedDSpaceObject extends NormalizedObject {
|
|||||||
/**
|
/**
|
||||||
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
parents: string[];
|
parents: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The DSpaceObject that owns this DSpaceObject
|
* The DSpaceObject that owns this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
owner: string;
|
owner: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +69,7 @@ export class NormalizedDSpaceObject extends NormalizedObject {
|
|||||||
* Repeated here to make the serialization work,
|
* Repeated here to make the serialization work,
|
||||||
* inheritSerialization doesn't seem to work for more than one level
|
* inheritSerialization doesn't seem to work for more than one level
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
_links: {
|
_links: {
|
||||||
[name: string]: string
|
[name: string]: string
|
||||||
}
|
}
|
||||||
|
12
src/app/core/cache/object-cache.reducer.ts
vendored
12
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -17,12 +17,22 @@ 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[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**conca
|
/**
|
||||||
* An interface to represent objects that can be cached
|
* An interface to represent objects that can be cached
|
||||||
*
|
*
|
||||||
* A cacheable object should have a self link
|
* A cacheable object should have a self link
|
||||||
|
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,6 +65,8 @@ import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
|||||||
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
|
import { MetadataschemaParsingService } from './data/metadataschema-parsing.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 { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service';
|
||||||
|
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -101,6 +103,7 @@ const PROVIDERS = [
|
|||||||
ObjectCacheService,
|
ObjectCacheService,
|
||||||
PaginationComponentOptions,
|
PaginationComponentOptions,
|
||||||
RegistryService,
|
RegistryService,
|
||||||
|
NormalizedObjectBuildService,
|
||||||
RemoteDataBuildService,
|
RemoteDataBuildService,
|
||||||
RequestService,
|
RequestService,
|
||||||
EndpointMapResponseParsingService,
|
EndpointMapResponseParsingService,
|
||||||
@@ -128,6 +131,7 @@ const PROVIDERS = [
|
|||||||
UploaderService,
|
UploaderService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
DSpaceObjectDataService,
|
DSpaceObjectDataService,
|
||||||
|
DSOChangeAnalyzer,
|
||||||
CSSVariableService,
|
CSSVariableService,
|
||||||
MenuService,
|
MenuService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
|
@@ -8,15 +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/normalized-object-build.service';
|
||||||
function isObjectLevel(halObj: any) {
|
|
||||||
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPaginatedResponse(halObj: any) {
|
|
||||||
return hasValue(halObj.page) && hasValue(halObj._embedded);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
export abstract class BaseResponseParsingService {
|
export abstract class BaseResponseParsingService {
|
||||||
@@ -29,11 +21,11 @@ export abstract class BaseResponseParsingService {
|
|||||||
if (isNotEmpty(data)) {
|
if (isNotEmpty(data)) {
|
||||||
if (hasNoValue(data) || (typeof data !== 'object')) {
|
if (hasNoValue(data) || (typeof data !== 'object')) {
|
||||||
return data;
|
return data;
|
||||||
} else if (isPaginatedResponse(data)) {
|
} else if (isRestPaginatedList(data)) {
|
||||||
return this.processPaginatedList(data, requestUUID);
|
return this.processPaginatedList(data, requestUUID);
|
||||||
} else if (Array.isArray(data)) {
|
} else if (Array.isArray(data)) {
|
||||||
return this.processArray(data, requestUUID);
|
return this.processArray(data, requestUUID);
|
||||||
} else if (isObjectLevel(data)) {
|
} else if (isRestDataObject(data)) {
|
||||||
data = this.fixBadEPersonRestResponse(data);
|
data = this.fixBadEPersonRestResponse(data);
|
||||||
const object = this.deserialize(data);
|
const object = this.deserialize(data);
|
||||||
if (isNotEmpty(data._embedded)) {
|
if (isNotEmpty(data._embedded)) {
|
||||||
@@ -43,10 +35,10 @@ export abstract class BaseResponseParsingService {
|
|||||||
.forEach((property) => {
|
.forEach((property) => {
|
||||||
const parsedObj = this.process<ObjectDomain, ObjectType>(data._embedded[property], requestUUID);
|
const parsedObj = this.process<ObjectDomain, ObjectType>(data._embedded[property], requestUUID);
|
||||||
if (isNotEmpty(parsedObj)) {
|
if (isNotEmpty(parsedObj)) {
|
||||||
if (isPaginatedResponse(data._embedded[property])) {
|
if (isRestPaginatedList(data._embedded[property])) {
|
||||||
object[property] = parsedObj;
|
object[property] = parsedObj;
|
||||||
object[property].page = parsedObj.page.map((obj) => obj.self);
|
object[property].page = parsedObj.page.map((obj) => obj.self);
|
||||||
} else if (isObjectLevel(data._embedded[property])) {
|
} else if (isRestDataObject(data._embedded[property])) {
|
||||||
object[property] = parsedObj.self;
|
object[property] = parsedObj.self;
|
||||||
} else if (Array.isArray(parsedObj)) {
|
} else if (Array.isArray(parsedObj)) {
|
||||||
object[property] = parsedObj.map((obj) => obj.self)
|
object[property] = parsedObj.map((obj) => obj.self)
|
||||||
@@ -80,7 +72,7 @@ export abstract class BaseResponseParsingService {
|
|||||||
list = this.flattenSingleKeyObject(list);
|
list = this.flattenSingleKeyObject(list);
|
||||||
}
|
}
|
||||||
const page: ObjectDomain[] = this.processArray(list, requestUUID);
|
const page: ObjectDomain[] = this.processArray(list, requestUUID);
|
||||||
return new PaginatedList<ObjectDomain>(pageInfo, page);
|
return new PaginatedList<ObjectDomain>(pageInfo, page, );
|
||||||
}
|
}
|
||||||
|
|
||||||
protected processArray<ObjectDomain, ObjectType>(data: any, requestUUID: string): ObjectDomain[] {
|
protected processArray<ObjectDomain, ObjectType>(data: any, requestUUID: string): ObjectDomain[] {
|
||||||
|
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[];
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { NormalizedCollection } from '../cache/models/normalized-collection.model';
|
import { NormalizedCollection } from '../cache/models/normalized-collection.model';
|
||||||
@@ -9,6 +9,10 @@ import { ComColDataService } from './comcol-data.service';
|
|||||||
import { CommunityDataService } from './community-data.service';
|
import { CommunityDataService } from './community-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 { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
|
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
|
||||||
@@ -17,11 +21,16 @@ export class CollectionDataService extends ComColDataService<NormalizedCollectio
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
protected cds: CommunityDataService,
|
protected cds: CommunityDataService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected objectCache: ObjectCacheService
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,10 @@ import { NormalizedObject } from '../cache/models/normalized-object.model';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestEntry } from './request.reducer';
|
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 { HttpClient } from '@angular/common/http';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
|
||||||
const LINK_NAME = 'test';
|
const LINK_NAME = 'test';
|
||||||
|
|
||||||
@@ -26,11 +30,15 @@ class TestService extends ComColDataService<NormalizedTestObject, any> {
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
protected EnvConfig: GlobalConfig,
|
protected EnvConfig: GlobalConfig,
|
||||||
protected cds: CommunityDataService,
|
protected cds: CommunityDataService,
|
||||||
protected halService: HALEndpointService,
|
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer,
|
||||||
protected linkPath: string
|
protected linkPath: string
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@@ -45,11 +53,15 @@ describe('ComColDataService', () => {
|
|||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let cds: CommunityDataService;
|
let cds: CommunityDataService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
const halService: any = {};
|
let halService: any = {};
|
||||||
|
|
||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
const store = {} as Store<CoreState>;
|
const store = {} as Store<CoreState>;
|
||||||
const EnvConfig = {} as GlobalConfig;
|
const EnvConfig = {} as GlobalConfig;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
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(), {
|
||||||
@@ -65,11 +77,16 @@ describe('ComColDataService', () => {
|
|||||||
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
|
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
|
||||||
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
|
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
|
||||||
const serviceEndpoint = `https://rest.api/core/${LINK_NAME}`;
|
const serviceEndpoint = `https://rest.api/core/${LINK_NAME}`;
|
||||||
|
const authHeader = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiJhNjA4NmIzNC0zOTE4LTQ1YjctOGRkZC05MzI5YTcwMmEyNmEiLCJzZyI6W10sImV4cCI6MTUzNDk0MDcyNX0.RV5GAtiX6cpwBN77P_v16iG9ipeyiO7faNYSNMzq_sQ';
|
||||||
|
|
||||||
|
const mockHalService = {
|
||||||
|
getEndpoint: (linkPath) => observableOf(communitiesEndpoint)
|
||||||
|
};
|
||||||
|
|
||||||
function initMockCommunityDataService(): CommunityDataService {
|
function initMockCommunityDataService(): CommunityDataService {
|
||||||
return jasmine.createSpyObj('responseCache', {
|
return jasmine.createSpyObj('responseCache', {
|
||||||
getEndpoint: hot('--a-', { a: communitiesEndpoint }),
|
getEndpoint: hot('--a-', { a: communitiesEndpoint }),
|
||||||
getFindByIDHref: cold('b-', { b: communityEndpoint })
|
getIDHref: cold('b-', { b: communityEndpoint })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,15 +106,27 @@ describe('ComColDataService', () => {
|
|||||||
return new TestService(
|
return new TestService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
|
dataBuildService,
|
||||||
store,
|
store,
|
||||||
EnvConfig,
|
EnvConfig,
|
||||||
cds,
|
cds,
|
||||||
halService,
|
|
||||||
objectCache,
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator,
|
||||||
LINK_NAME
|
LINK_NAME
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cds = initMockCommunityDataService();
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
objectCache = initMockObjectCacheService();
|
||||||
|
halService = mockHalService;
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
describe('getBrowseEndpoint', () => {
|
describe('getBrowseEndpoint', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
@@ -156,4 +185,5 @@ describe('ComColDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -20,8 +20,9 @@ import { NormalizedObject } from '../cache/models/normalized-object.model';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { getResponseFromEntry } from '../shared/operators';
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
|
||||||
export abstract class ComColDataService<TNormalized extends NormalizedObject, TDomain> extends DataService<TNormalized, TDomain> {
|
export abstract class ComColDataService<TNormalized extends NormalizedObject, TDomain extends CacheableObject> extends DataService<TNormalized, TDomain> {
|
||||||
protected abstract cds: CommunityDataService;
|
protected abstract cds: CommunityDataService;
|
||||||
protected abstract objectCache: ObjectCacheService;
|
protected abstract objectCache: ObjectCacheService;
|
||||||
protected abstract halService: HALEndpointService;
|
protected abstract halService: HALEndpointService;
|
||||||
@@ -41,7 +42,7 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
|
|||||||
return this.halService.getEndpoint(linkPath);
|
return this.halService.getEndpoint(linkPath);
|
||||||
} else {
|
} else {
|
||||||
const scopeCommunityHrefObs = this.cds.getEndpoint().pipe(
|
const scopeCommunityHrefObs = this.cds.getEndpoint().pipe(
|
||||||
mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID)),
|
mergeMap((endpoint: string) => this.cds.getIDHref(endpoint, options.scopeID)),
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
take(1),
|
take(1),
|
||||||
tap((href: string) => {
|
tap((href: string) => {
|
||||||
|
@@ -15,6 +15,10 @@ import { RemoteData } from './remote-data';
|
|||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
|
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
|
||||||
@@ -25,9 +29,13 @@ export class CommunityDataService extends ComColDataService<NormalizedCommunity,
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected objectCache: ObjectCacheService
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@@ -161,7 +161,13 @@ describe('ConfigResponseParsingService', () => {
|
|||||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
}, statusCode: '500'
|
}, statusCode: '500'
|
||||||
};
|
};
|
||||||
const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 4, totalElements: 4, totalPages: 1, currentPage: 1 });
|
const pageinfo = Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: 4,
|
||||||
|
totalElements: 4,
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1,
|
||||||
|
self: 'https://rest.api/config/submissiondefinitions/traditional/sections'
|
||||||
|
});
|
||||||
const definitions =
|
const definitions =
|
||||||
Object.assign(new SubmissionDefinitionsModel(), {
|
Object.assign(new SubmissionDefinitionsModel(), {
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
|
@@ -5,13 +5,17 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
|||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { FindAllOptions } from './request.models';
|
import { FindAllOptions } from './request.models';
|
||||||
import { SortOptions, SortDirection } from '../cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
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 { ChangeAnalyzer } from './change-analyzer';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { compare } from 'fast-json-patch';
|
||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
@@ -23,10 +27,14 @@ class TestService extends DataService<NormalizedTestObject, any> {
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
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 http: HttpClient,
|
||||||
|
protected comparator: ChangeAnalyzer<NormalizedTestObject>
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -36,12 +44,24 @@ class TestService extends DataService<NormalizedTestObject, any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DummyChangeAnalyzer implements ChangeAnalyzer<NormalizedTestObject> {
|
||||||
|
diff(object1: NormalizedTestObject, object2: NormalizedTestObject): Operation[] {
|
||||||
|
return compare((object1 as any).metadata, (object2 as any).metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
describe('DataService', () => {
|
describe('DataService', () => {
|
||||||
let service: TestService;
|
let service: TestService;
|
||||||
let options: FindAllOptions;
|
let options: FindAllOptions;
|
||||||
const requestService = {} as RequestService;
|
const requestService = {} as RequestService;
|
||||||
const halService = {} as HALEndpointService;
|
const halService = {} as HALEndpointService;
|
||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = new DummyChangeAnalyzer() as any;
|
||||||
|
const dataBuildService = {
|
||||||
|
normalize: (object) => object
|
||||||
|
} as NormalizedObjectBuildService;
|
||||||
const objectCache = {
|
const objectCache = {
|
||||||
addPatch: () => {
|
addPatch: () => {
|
||||||
/* empty */
|
/* empty */
|
||||||
@@ -56,13 +76,16 @@ describe('DataService', () => {
|
|||||||
return new TestService(
|
return new TestService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
|
dataBuildService,
|
||||||
store,
|
store,
|
||||||
endpoint,
|
endpoint,
|
||||||
halService,
|
halService,
|
||||||
objectCache
|
objectCache,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
|
|
||||||
describe('getFindAllHref', () => {
|
describe('getFindAllHref', () => {
|
||||||
@@ -120,7 +143,7 @@ describe('DataService', () => {
|
|||||||
elementsPerPage: 10,
|
elementsPerPage: 10,
|
||||||
sort: sortOptions,
|
sort: sortOptions,
|
||||||
startsWith: 'ab'
|
startsWith: 'ab'
|
||||||
}
|
};
|
||||||
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
|
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
|
||||||
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
|
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
|
||||||
|
|
||||||
@@ -134,7 +157,7 @@ describe('DataService', () => {
|
|||||||
let selfLink;
|
let selfLink;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation];
|
operations = [{ op: 'replace', path: '/metadata/dc.title', value: 'random string' } as Operation];
|
||||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
spyOn(objectCache, 'addPatch');
|
spyOn(objectCache, 'addPatch');
|
||||||
});
|
});
|
||||||
@@ -153,28 +176,29 @@ describe('DataService', () => {
|
|||||||
const name1 = 'random string';
|
const name1 = 'random string';
|
||||||
const name2 = 'another random string';
|
const name2 = 'another random string';
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
operations = [{ op: 'replace', path: '/name', value: name2 } as Operation];
|
operations = [{ op: 'replace', path: '/0/value', value: name2 } as Operation];
|
||||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
|
||||||
dso = new DSpaceObject();
|
dso = new DSpaceObject();
|
||||||
dso.self = selfLink;
|
dso.self = selfLink;
|
||||||
dso.name = name1;
|
dso.metadata = [{ key: 'dc.title', value: name1 }];
|
||||||
|
|
||||||
dso2 = new DSpaceObject();
|
dso2 = new DSpaceObject();
|
||||||
dso2.self = selfLink;
|
dso2.self = selfLink;
|
||||||
dso2.name = name2;
|
dso2.metadata = [{ key: 'dc.title', value: name2 }];
|
||||||
|
|
||||||
spyOn(objectCache, 'getBySelfLink').and.returnValue(dso);
|
spyOn(service, 'findById').and.returnValues(observableOf(dso));
|
||||||
|
spyOn(objectCache, 'getBySelfLink').and.returnValues(observableOf(dso));
|
||||||
spyOn(objectCache, 'addPatch');
|
spyOn(objectCache, 'addPatch');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call addPatch on the object cache with the right parameters when there are differences', () => {
|
it('should call addPatch on the object cache with the right parameters when there are differences', () => {
|
||||||
service.update(dso2);
|
service.update(dso2).subscribe();
|
||||||
expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations);
|
expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call addPatch on the object cache with the right parameters when there are no differences', () => {
|
it('should not call addPatch on the object cache with the right parameters when there are no differences', () => {
|
||||||
service.update(dso);
|
service.update(dso).subscribe();
|
||||||
expect(objectCache.addPatch).not.toHaveBeenCalled();
|
expect(objectCache.addPatch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,28 +1,58 @@
|
|||||||
import { delay, distinctUntilChanged, filter, find, first, map, take, tap } from 'rxjs/operators';
|
import {
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
find,
|
||||||
|
first,
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
switchMap,
|
||||||
|
take
|
||||||
|
} from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
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 { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models';
|
import {
|
||||||
|
CreateRequest,
|
||||||
|
DeleteByIDRequest,
|
||||||
|
FindAllOptions,
|
||||||
|
FindAllRequest,
|
||||||
|
FindByIDRequest,
|
||||||
|
GetRequest
|
||||||
|
} 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 { of } from 'rxjs/internal/observable/of';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { configureRequest, getResponseFromEntry } from '../shared/operators';
|
||||||
|
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||||
|
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.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> {
|
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: 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 http: HttpClient;
|
||||||
|
protected abstract comparator: ChangeAnalyzer<TNormalized>;
|
||||||
|
|
||||||
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
|
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
|
||||||
|
|
||||||
@@ -65,13 +95,18 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
|
|||||||
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs) as Observable<RemoteData<PaginatedList<TDomain>>>;
|
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs) as Observable<RemoteData<PaginatedList<TDomain>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFindByIDHref(endpoint, resourceID): string {
|
/**
|
||||||
|
* Create the HREF for a specific object based on its identifier
|
||||||
|
* @param endpoint The base endpoint for the type of object
|
||||||
|
* @param resourceID The identifier for the object
|
||||||
|
*/
|
||||||
|
getIDHref(endpoint, resourceID): string {
|
||||||
return `${endpoint}/${resourceID}`;
|
return `${endpoint}/${resourceID}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
findById(id: string): Observable<RemoteData<TDomain>> {
|
findById(id: string): Observable<RemoteData<TDomain>> {
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getFindByIDHref(endpoint, id)));
|
map((endpoint: string) => this.getIDHref(endpoint, id)));
|
||||||
|
|
||||||
hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
find((href: string) => hasValue(href)))
|
find((href: string) => hasValue(href)))
|
||||||
@@ -102,29 +137,96 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
|
|||||||
* The patch is derived from the differences between the given object and its version in the object cache
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
* @param {DSpaceObject} object The given object
|
* @param {DSpaceObject} object The given object
|
||||||
*/
|
*/
|
||||||
update(object: DSpaceObject) {
|
update(object: TDomain): Observable<RemoteData<TDomain>> {
|
||||||
const oldVersion = this.objectCache.getBySelfLink(object.self);
|
const oldVersion$ = this.objectCache.getBySelfLink(object.self);
|
||||||
const operations = compare(oldVersion, object);
|
return oldVersion$.pipe(first(), mergeMap((oldVersion: TNormalized) => {
|
||||||
if (isNotEmpty(operations)) {
|
const newVersion = this.dataBuildService.normalize<TDomain, TNormalized>(object);
|
||||||
this.objectCache.addPatch(object.self, operations);
|
const operations = this.comparator.diff(oldVersion, newVersion);
|
||||||
}
|
if (isNotEmpty(operations)) {
|
||||||
|
this.objectCache.addPatch(object.self, operations);
|
||||||
|
}
|
||||||
|
return this.findById(object.uuid);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpoint: string) => parentUUID ? `${endpoint}?parentCommunity=${parentUUID}` : endpoint)
|
||||||
|
);
|
||||||
|
|
||||||
|
const normalizedObject: TNormalized = this.dataBuildService.normalize<TDomain, TNormalized>(dso);
|
||||||
|
const serializedDso = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(dso.type)).serialize(normalizedObject);
|
||||||
|
|
||||||
|
const request$ = endpoint$.pipe(
|
||||||
|
take(1),
|
||||||
|
map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute the post request
|
||||||
|
request$.pipe(
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
// Resolve self link for new object
|
||||||
|
const selfLink$ = this.requestService.getByUUID(requestId).pipe(
|
||||||
|
getResponseFromEntry(),
|
||||||
|
map((response: RestResponse) => {
|
||||||
|
if (!response.isSuccessful && response instanceof ErrorResponse) {
|
||||||
|
this.notificationsService.error('Server Error:', response.errorMessage, new NotificationOptions(-1));
|
||||||
|
} else {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
map((response: any) => {
|
||||||
|
if (isNotEmpty(response.resourceSelfLinks)) {
|
||||||
|
return response.resourceSelfLinks[0];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
distinctUntilChanged()
|
||||||
|
) as Observable<string>;
|
||||||
|
|
||||||
|
return selfLink$.pipe(
|
||||||
|
switchMap((selfLink: string) => this.findByHref(selfLink)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing DSpace Object on the server
|
||||||
|
* @param dso The DSpace Object to be removed
|
||||||
|
* Return an observable that emits true when the deletion was successful, false when it failed
|
||||||
|
*/
|
||||||
|
delete(dso: TDomain): Observable<boolean> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, dso.uuid)));
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new DeleteByIDRequest(requestId, href, dso.uuid);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((request: RequestEntry) => request.completed),
|
||||||
|
map((request: RequestEntry) => request.response.isSuccessful)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO implement, after the structure of the REST server's POST response is finalized
|
|
||||||
// create(dso: DSpaceObject): Observable<RemoteData<TDomain>> {
|
|
||||||
// const postHrefObs = this.getEndpoint();
|
|
||||||
//
|
|
||||||
// // TODO ID is unknown at this point
|
|
||||||
// const idHrefObs = postHrefObs.map((href: string) => this.getFindByIDHref(href, dso.id));
|
|
||||||
//
|
|
||||||
// postHrefObs
|
|
||||||
// .filter((href: string) => hasValue(href))
|
|
||||||
// .take(1)
|
|
||||||
// .subscribe((href: string) => {
|
|
||||||
// const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.POST, dso);
|
|
||||||
// this.requestService.configure(request);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// return this.rdbService.buildSingle<TNormalized, TDomain>(idHrefObs, this.normalizedResourceType);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
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 }));
|
||||||
|
}
|
||||||
|
}
|
@@ -7,6 +7,9 @@ import { FindByIDRequest } from './request.models';
|
|||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { DSpaceObjectDataService } from './dspace-object-data.service';
|
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 { HttpClient } from '@angular/common/http';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
|
||||||
describe('DSpaceObjectDataService', () => {
|
describe('DSpaceObjectDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -40,12 +43,20 @@ describe('DSpaceObjectDataService', () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
objectCache = {} as ObjectCacheService;
|
objectCache = {} as ObjectCacheService;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||||
|
|
||||||
service = new DSpaceObjectDataService(
|
service = new DSpaceObjectDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
|
dataBuildService,
|
||||||
|
objectCache,
|
||||||
halService,
|
halService,
|
||||||
objectCache
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -11,6 +11,10 @@ import { RemoteData } from './remote-data';
|
|||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { FindAllOptions } from './request.models';
|
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 { HttpClient } from '@angular/common/http';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
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> {
|
||||||
@@ -19,9 +23,13 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected objectCache: ObjectCacheService) {
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +37,7 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
|
|||||||
return this.halService.getEndpoint(linkPath);
|
return this.halService.getEndpoint(linkPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFindByIDHref(endpoint, resourceID): string {
|
getIDHref(endpoint, resourceID): string {
|
||||||
return endpoint.replace(/\{\?uuid\}/,`?uuid=${resourceID}`);
|
return endpoint.replace(/\{\?uuid\}/,`?uuid=${resourceID}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,9 +50,13 @@ export class DSpaceObjectDataService {
|
|||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected objectCache: ObjectCacheService) {
|
protected notificationsService: NotificationsService,
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, halService, objectCache);
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer) {
|
||||||
|
this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
findById(uuid: string): Observable<RemoteData<DSpaceObject>> {
|
findById(uuid: string): Observable<RemoteData<DSpaceObject>> {
|
||||||
|
@@ -7,10 +7,15 @@ 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 { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { FindAllOptions, RestRequest } from './request.models';
|
import { FindAllOptions, RestRequest } from './request.models';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
import { RestResponse } from '../cache/response.models';
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
describe('ItemDataService', () => {
|
describe('ItemDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -22,12 +27,17 @@ describe('ItemDataService', () => {
|
|||||||
},
|
},
|
||||||
configure(request: RestRequest) {
|
configure(request: RestRequest) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
},
|
||||||
|
getByHref(requestHref: string) {
|
||||||
|
const responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.response = new RestResponse(true, '200');
|
||||||
|
return observableOf(responseCacheEntry);
|
||||||
}
|
}
|
||||||
} as RequestService;
|
} 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 objectCache = {} as ObjectCacheService;
|
||||||
const halEndpointService = {
|
const halEndpointService = {
|
||||||
getEndpoint(linkPath: string): Observable<string> {
|
getEndpoint(linkPath: string): Observable<string> {
|
||||||
return cold('a', {a: itemEndpoint});
|
return cold('a', {a: itemEndpoint});
|
||||||
@@ -48,12 +58,16 @@ describe('ItemDataService', () => {
|
|||||||
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
|
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
|
||||||
const serviceEndpoint = `https://rest.api/core/items`;
|
const serviceEndpoint = `https://rest.api/core/items`;
|
||||||
const browseError = new Error('getBrowseURL failed');
|
const browseError = new Error('getBrowseURL failed');
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||||
const itemEndpoint = 'https://rest.api/core/items';
|
const itemEndpoint = 'https://rest.api/core/items';
|
||||||
const ScopedItemEndpoint = `https://rest.api/core/items/${scopeID}`;
|
const ScopedItemEndpoint = `https://rest.api/core/items/${scopeID}`;
|
||||||
|
|
||||||
function initMockBrowseService(isSuccessful: boolean) {
|
function initMockBrowseService(isSuccessful: boolean) {
|
||||||
const obs = isSuccessful ?
|
const obs = isSuccessful ?
|
||||||
cold('--a-', {a: itemBrowseEndpoint}) :
|
cold('--a-', { a: itemBrowseEndpoint }) :
|
||||||
cold('--#-', undefined, browseError);
|
cold('--#-', undefined, browseError);
|
||||||
return jasmine.createSpyObj('bs', {
|
return jasmine.createSpyObj('bs', {
|
||||||
getBrowseURLFor: obs
|
getBrowseURLFor: obs
|
||||||
@@ -64,10 +78,14 @@ describe('ItemDataService', () => {
|
|||||||
return new ItemDataService(
|
return new ItemDataService(
|
||||||
requestService,
|
requestService,
|
||||||
rdbService,
|
rdbService,
|
||||||
|
dataBuildService,
|
||||||
store,
|
store,
|
||||||
bs,
|
bs,
|
||||||
|
objectCache,
|
||||||
halEndpointService,
|
halEndpointService,
|
||||||
objectCache
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +99,7 @@ describe('ItemDataService', () => {
|
|||||||
service = initTestService();
|
service = initTestService();
|
||||||
|
|
||||||
const result = service.getBrowseEndpoint(options);
|
const result = service.getBrowseEndpoint(options);
|
||||||
const expected = cold('--b-', {b: scopedEndpoint});
|
const expected = cold('--b-', { b: scopedEndpoint });
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -144,25 +162,4 @@ describe('ItemDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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,9 +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 { DeleteRequest, FindAllOptions, PatchRequest, RestRequest } from './request.models';
|
import { FindAllOptions, PatchRequest, RestRequest } from './request.models';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { configureRequest, getResponseFromEntry } from '../shared/operators';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
import { configureRequest, getRequestFromRequestHref } from '../shared/operators';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||||
@@ -25,10 +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: NormalizedObjectBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
private bs: BrowseService,
|
private bs: BrowseService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected objectCache: ObjectCacheService) {
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +64,7 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
|||||||
*/
|
*/
|
||||||
public getItemWithdrawEndpoint(itemId: string): Observable<string> {
|
public getItemWithdrawEndpoint(itemId: string): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getFindByIDHref(endpoint, itemId))
|
map((endpoint: string) => this.getIDHref(endpoint, itemId))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,17 +74,7 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
|||||||
*/
|
*/
|
||||||
public getItemDiscoverableEndpoint(itemId: string): Observable<string> {
|
public getItemDiscoverableEndpoint(itemId: string): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getFindByIDHref(endpoint, itemId))
|
map((endpoint: string) => this.getIDHref(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))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +93,9 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
|||||||
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
||||||
),
|
),
|
||||||
configureRequest(this.requestService),
|
configureRequest(this.requestService),
|
||||||
getResponseFromEntry()
|
map((request: RestRequest) => request.href),
|
||||||
|
getRequestFromRequestHref(this.requestService),
|
||||||
|
map((requestEntry: RequestEntry) => requestEntry.response)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,23 +114,9 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
|||||||
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
||||||
),
|
),
|
||||||
configureRequest(this.requestService),
|
configureRequest(this.requestService),
|
||||||
getResponseFromEntry()
|
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),
|
|
||||||
getResponseFromEntry()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -81,4 +81,12 @@ export class PaginatedList<T> {
|
|||||||
set last(last: string) {
|
set last(last: string) {
|
||||||
this.pageInfo.last = last;
|
this.pageInfo.last = last;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get self(): string {
|
||||||
|
return this.pageInfo.self;
|
||||||
|
}
|
||||||
|
|
||||||
|
set self(self: string) {
|
||||||
|
this.pageInfo.self = self;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -260,6 +260,29 @@ export class UpdateMetadataFieldRequest extends PutRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CreateRequest extends PostRequest {
|
||||||
|
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||||
|
super(uuid, href, body, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return DSOResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to delete an object based on its identifier
|
||||||
|
*/
|
||||||
|
export class DeleteByIDRequest extends DeleteRequest {
|
||||||
|
constructor(
|
||||||
|
uuid: string,
|
||||||
|
href: string,
|
||||||
|
public resourceID: string
|
||||||
|
) {
|
||||||
|
super(uuid, href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class RequestError extends Error {
|
export class RequestError extends Error {
|
||||||
statusText: string;
|
statusText: string;
|
||||||
}
|
}
|
||||||
|
@@ -90,7 +90,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],
|
||||||
|
@@ -100,7 +100,7 @@ export class RequestService {
|
|||||||
this.store.pipe(select(this.entryFromUUIDSelector(uuid))),
|
this.store.pipe(select(this.entryFromUUIDSelector(uuid))),
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(this.originalUUIDFromUUIDSelector(uuid)),
|
select(this.originalUUIDFromUUIDSelector(uuid)),
|
||||||
switchMap((originalUUID) => {
|
mergeMap((originalUUID) => {
|
||||||
return this.store.pipe(select(this.entryFromUUIDSelector(originalUUID)))
|
return this.store.pipe(select(this.entryFromUUIDSelector(originalUUID)))
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
@@ -2,6 +2,7 @@ import { TestBed, inject } from '@angular/core/testing';
|
|||||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
|
||||||
import { DSpaceRESTv2Service } from './dspace-rest-v2.service';
|
import { DSpaceRESTv2Service } from './dspace-rest-v2.service';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
|
||||||
describe('DSpaceRESTv2Service', () => {
|
describe('DSpaceRESTv2Service', () => {
|
||||||
let dSpaceRESTv2Service: DSpaceRESTv2Service;
|
let dSpaceRESTv2Service: DSpaceRESTv2Service;
|
||||||
@@ -65,4 +66,15 @@ describe('DSpaceRESTv2Service', () => {
|
|||||||
expect(req.request.method).toBe('GET');
|
expect(req.request.method).toBe('GET');
|
||||||
req.error(mockError);
|
req.error(mockError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('buildFormData', () => {
|
||||||
|
it('should return the correct data', () => {
|
||||||
|
const name = 'testname';
|
||||||
|
const dso: DSpaceObject = {
|
||||||
|
name: name
|
||||||
|
} as DSpaceObject;
|
||||||
|
const formdata = dSpaceRESTv2Service.buildFormData(dso);
|
||||||
|
expect(formdata.get('name')).toBe(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -6,6 +6,8 @@ import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/comm
|
|||||||
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
|
||||||
import { HttpObserve } from '@angular/common/http/src/client';
|
import { HttpObserve } from '@angular/common/http/src/client';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
|
||||||
export interface HttpOptions {
|
export interface HttpOptions {
|
||||||
body?: any;
|
body?: any;
|
||||||
@@ -59,6 +61,9 @@ export class DSpaceRESTv2Service {
|
|||||||
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable<DSpaceRESTV2Response> {
|
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable<DSpaceRESTV2Response> {
|
||||||
const requestOptions: HttpOptions = {};
|
const requestOptions: HttpOptions = {};
|
||||||
requestOptions.body = body;
|
requestOptions.body = body;
|
||||||
|
if (method === RestRequestMethod.POST && isNotEmpty(body) && isNotEmpty(body.name)) {
|
||||||
|
requestOptions.body = this.buildFormData(body);
|
||||||
|
}
|
||||||
requestOptions.observe = 'response';
|
requestOptions.observe = 'response';
|
||||||
if (options && options.headers) {
|
if (options && options.headers) {
|
||||||
requestOptions.headers = Object.assign(new HttpHeaders(), options.headers);
|
requestOptions.headers = Object.assign(new HttpHeaders(), options.headers);
|
||||||
@@ -74,4 +79,25 @@ export class DSpaceRESTv2Service {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a FormData object from a DSpaceObject
|
||||||
|
*
|
||||||
|
* @param {DSpaceObject} dso
|
||||||
|
* the DSpaceObject
|
||||||
|
* @return {FormData}
|
||||||
|
* the result
|
||||||
|
*/
|
||||||
|
buildFormData(dso: DSpaceObject): FormData {
|
||||||
|
const form: FormData = new FormData();
|
||||||
|
form.append('name', dso.name);
|
||||||
|
if (dso.metadata) {
|
||||||
|
for (const i of Object.keys(dso.metadata)) {
|
||||||
|
if (isNotEmpty(dso.metadata[i].value)) {
|
||||||
|
form.append(dso.metadata[i].key, dso.metadata[i].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { filter, map, tap } from 'rxjs/operators';
|
import { filter, map } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
|
|
||||||
@@ -12,11 +12,6 @@ import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'
|
|||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { IndexName } from './index.reducer';
|
import { IndexName } from './index.reducer';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import {
|
|
||||||
AddMenuSectionAction,
|
|
||||||
MenuActionTypes,
|
|
||||||
RemoveMenuSectionAction
|
|
||||||
} from '../../shared/menu/menu.actions';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UUIDIndexEffects {
|
export class UUIDIndexEffects {
|
||||||
|
@@ -23,15 +23,57 @@ describe('IntegrationResponseParsingService', () => {
|
|||||||
const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
||||||
const integrationEndpoint = 'https://rest.api/integration/authorities';
|
const integrationEndpoint = 'https://rest.api/integration/authorities';
|
||||||
const entriesEndpoint = `${integrationEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`;
|
const entriesEndpoint = `${integrationEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`;
|
||||||
|
let validRequest;
|
||||||
|
|
||||||
beforeEach(() => {
|
let validResponse;
|
||||||
service = new IntegrationResponseParsingService(EnvConfig, objectCacheService);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parse', () => {
|
let invalidResponse1;
|
||||||
const validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint);
|
let invalidResponse2;
|
||||||
|
let pageInfo;
|
||||||
|
let definitions;
|
||||||
|
|
||||||
const validResponse = {
|
function initVars() {
|
||||||
|
pageInfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1, self: 'https://rest.api/integration/authorities/type/entries'});
|
||||||
|
definitions = new PaginatedList(pageInfo,[
|
||||||
|
Object.assign({}, new AuthorityValueModel(), {
|
||||||
|
type: 'authority',
|
||||||
|
display: 'One',
|
||||||
|
id: 'One',
|
||||||
|
otherInformation: undefined,
|
||||||
|
value: 'One'
|
||||||
|
}),
|
||||||
|
Object.assign({}, new AuthorityValueModel(), {
|
||||||
|
type: 'authority',
|
||||||
|
display: 'Two',
|
||||||
|
id: 'Two',
|
||||||
|
otherInformation: undefined,
|
||||||
|
value: 'Two'
|
||||||
|
}),
|
||||||
|
Object.assign({}, new AuthorityValueModel(), {
|
||||||
|
type: 'authority',
|
||||||
|
display: 'Three',
|
||||||
|
id: 'Three',
|
||||||
|
otherInformation: undefined,
|
||||||
|
value: 'Three'
|
||||||
|
}),
|
||||||
|
Object.assign({}, new AuthorityValueModel(), {
|
||||||
|
type: 'authority',
|
||||||
|
display: 'Four',
|
||||||
|
id: 'Four',
|
||||||
|
otherInformation: undefined,
|
||||||
|
value: 'Four'
|
||||||
|
}),
|
||||||
|
Object.assign({}, new AuthorityValueModel(), {
|
||||||
|
type: 'authority',
|
||||||
|
display: 'Five',
|
||||||
|
id: 'Five',
|
||||||
|
otherInformation: undefined,
|
||||||
|
value: 'Five'
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint);
|
||||||
|
|
||||||
|
validResponse = {
|
||||||
payload: {
|
payload: {
|
||||||
page: {
|
page: {
|
||||||
number: 0,
|
number: 0,
|
||||||
@@ -86,12 +128,12 @@ describe('IntegrationResponseParsingService', () => {
|
|||||||
statusCode: '200'
|
statusCode: '200'
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidResponse1 = {
|
invalidResponse1 = {
|
||||||
payload: {},
|
payload: {},
|
||||||
statusCode: '200'
|
statusCode: '200'
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidResponse2 = {
|
invalidResponse2 = {
|
||||||
payload: {
|
payload: {
|
||||||
page: {
|
page: {
|
||||||
number: 0,
|
number: 0,
|
||||||
@@ -143,45 +185,13 @@ describe('IntegrationResponseParsingService', () => {
|
|||||||
},
|
},
|
||||||
statusCode: '200'
|
statusCode: '200'
|
||||||
};
|
};
|
||||||
const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1 });
|
}
|
||||||
const definitions = new PaginatedList(pageinfo,[
|
beforeEach(() => {
|
||||||
Object.assign({}, new AuthorityValueModel(), {
|
initVars();
|
||||||
type: 'authority',
|
service = new IntegrationResponseParsingService(EnvConfig, objectCacheService);
|
||||||
display: 'One',
|
});
|
||||||
id: 'One',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'One'
|
|
||||||
}),
|
|
||||||
Object.assign({}, new AuthorityValueModel(), {
|
|
||||||
type: 'authority',
|
|
||||||
display: 'Two',
|
|
||||||
id: 'Two',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'Two'
|
|
||||||
}),
|
|
||||||
Object.assign({}, new AuthorityValueModel(), {
|
|
||||||
type: 'authority',
|
|
||||||
display: 'Three',
|
|
||||||
id: 'Three',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'Three'
|
|
||||||
}),
|
|
||||||
Object.assign({}, new AuthorityValueModel(), {
|
|
||||||
type: 'authority',
|
|
||||||
display: 'Four',
|
|
||||||
id: 'Four',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'Four'
|
|
||||||
}),
|
|
||||||
Object.assign({}, new AuthorityValueModel(), {
|
|
||||||
type: 'authority',
|
|
||||||
display: 'Five',
|
|
||||||
id: 'Five',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'Five'
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
it('should return a IntegrationSuccessResponse if data contains a valid endpoint response', () => {
|
it('should return a IntegrationSuccessResponse if data contains a valid endpoint response', () => {
|
||||||
const response = service.parse(validRequest, validResponse);
|
const response = service.parse(validRequest, validResponse);
|
||||||
expect(response.constructor).toBe(IntegrationSuccessResponse);
|
expect(response.constructor).toBe(IntegrationSuccessResponse);
|
||||||
|
@@ -31,9 +31,12 @@ import { MockItem } from '../../shared/mocks/mock-item';
|
|||||||
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
||||||
import { BrowseService } from '../browse/browse.service';
|
import { BrowseService } from '../browse/browse.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { EmptyError } from 'rxjs/internal-compatibility';
|
import { EmptyError } from 'rxjs/internal-compatibility';
|
||||||
import { MockStore } from '../../shared/testing/mock-store';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { IndexState } from '../index/index.reducer';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@Component({
|
@Component({
|
||||||
@@ -68,6 +71,7 @@ describe('MetadataService', () => {
|
|||||||
let uuidService: UUIDService;
|
let uuidService: UUIDService;
|
||||||
let remoteDataBuildService: RemoteDataBuildService;
|
let remoteDataBuildService: RemoteDataBuildService;
|
||||||
let itemDataService: ItemDataService;
|
let itemDataService: ItemDataService;
|
||||||
|
let authService: AuthService;
|
||||||
|
|
||||||
let location: Location;
|
let location: Location;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
@@ -112,6 +116,11 @@ describe('MetadataService', () => {
|
|||||||
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
||||||
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
||||||
{ provide: HALEndpointService, useValue: {}},
|
{ provide: HALEndpointService, useValue: {}},
|
||||||
|
{ provide: AuthService, useValue: {} },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
{ provide: HttpClient, useValue: {} },
|
||||||
|
{ provide: NormalizedObjectBuildService, useValue: {} },
|
||||||
|
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||||
Meta,
|
Meta,
|
||||||
Title,
|
Title,
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
@@ -124,6 +133,7 @@ describe('MetadataService', () => {
|
|||||||
title = TestBed.get(Title);
|
title = TestBed.get(Title);
|
||||||
itemDataService = TestBed.get(ItemDataService);
|
itemDataService = TestBed.get(ItemDataService);
|
||||||
metadataService = TestBed.get(MetadataService);
|
metadataService = TestBed.get(MetadataService);
|
||||||
|
authService = TestBed.get(AuthService);
|
||||||
|
|
||||||
envConfig = TestBed.get(GLOBAL_CONFIG);
|
envConfig = TestBed.get(GLOBAL_CONFIG);
|
||||||
|
|
||||||
|
@@ -67,7 +67,7 @@ export class MetadataService {
|
|||||||
public processRemoteData(remoteData: Observable<RemoteData<CacheableObject>>): void {
|
public processRemoteData(remoteData: Observable<RemoteData<CacheableObject>>): void {
|
||||||
remoteData.pipe(map((rd: RemoteData<CacheableObject>) => rd.payload),
|
remoteData.pipe(map((rd: RemoteData<CacheableObject>) => rd.payload),
|
||||||
filter((co: CacheableObject) => hasValue(co)),
|
filter((co: CacheableObject) => hasValue(co)),
|
||||||
take(1),)
|
take(1))
|
||||||
.subscribe((dspaceObject: DSpaceObject) => {
|
.subscribe((dspaceObject: DSpaceObject) => {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
this.initialize(dspaceObject);
|
this.initialize(dspaceObject);
|
||||||
@@ -277,7 +277,7 @@ export class MetadataService {
|
|||||||
.subscribe((bitstreams: Bitstream[]) => {
|
.subscribe((bitstreams: Bitstream[]) => {
|
||||||
for (const bitstream of bitstreams) {
|
for (const bitstream of bitstreams) {
|
||||||
bitstream.format.pipe(
|
bitstream.format.pipe(
|
||||||
take(1),
|
first(),
|
||||||
catchError((error: Error) => {
|
catchError((error: Error) => {
|
||||||
console.debug(error.message);
|
console.debug(error.message);
|
||||||
return []
|
return []
|
||||||
|
@@ -34,14 +34,15 @@ export class DSpaceObject implements CacheableObject, ListableObject {
|
|||||||
/**
|
/**
|
||||||
* The name for this DSpaceObject
|
* The name for this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
get name(): string {
|
||||||
name: string;
|
return this.findMetadata('dc.title');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array containing all metadata of this DSpaceObject
|
* An array containing all metadata of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
metadata: Metadatum[];
|
metadata: Metadatum[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
||||||
|
@@ -8,11 +8,14 @@ import {
|
|||||||
configureRequest,
|
configureRequest,
|
||||||
filterSuccessfulResponses,
|
filterSuccessfulResponses,
|
||||||
getAllSucceededRemoteData,
|
getAllSucceededRemoteData,
|
||||||
getRemoteDataPayload, getRequestFromRequestHref, getRequestFromRequestUUID,
|
getRemoteDataPayload,
|
||||||
getResourceLinksFromResponse, getResponseFromEntry,
|
getRequestFromRequestHref,
|
||||||
|
getRequestFromRequestUUID,
|
||||||
|
getResourceLinksFromResponse,
|
||||||
|
getResponseFromEntry,
|
||||||
getSucceededRemoteData
|
getSucceededRemoteData
|
||||||
} from './operators';
|
} from './operators';
|
||||||
import {RemoteData} from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
|
||||||
describe('Core Module - RxJS Operators', () => {
|
describe('Core Module - RxJS Operators', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -188,8 +191,24 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
.toEqual(new RemoteData(false, false, true, null, 'd')));
|
.toEqual(new RemoteData(false, false, true, null, 'd')));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getResponseFromEntry', () => {
|
||||||
|
it('should return the response for all not empty request entries, when they have a value', () => {
|
||||||
|
const source = hot('abcdefg', testRCEs);
|
||||||
|
const result = source.pipe(getResponseFromEntry());
|
||||||
|
const expected = cold('abcde--', {
|
||||||
|
a: testRCEs.a.response,
|
||||||
|
b: testRCEs.b.response,
|
||||||
|
c: testRCEs.c.response,
|
||||||
|
d: testRCEs.d.response,
|
||||||
|
e: testRCEs.e.response
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getAllSucceededRemoteData', () => {
|
describe('getAllSucceededRemoteData', () => {
|
||||||
it('should return all hasSucceeded RemoteData Observables', () => {
|
it('should return all hasSucceeded RemoteData Observables', () => {
|
||||||
const testRD = {
|
const testRD = {
|
||||||
|
@@ -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';
|
||||||
@@ -62,6 +62,10 @@ export const getSucceededRemoteData = () =>
|
|||||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded));
|
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded));
|
||||||
|
|
||||||
|
export const getFinishedRemoteData = () =>
|
||||||
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
|
source.pipe(find((rd: RemoteData<T>) => !rd.isLoading));
|
||||||
|
|
||||||
export const getAllSucceededRemoteData = () =>
|
export const getAllSucceededRemoteData = () =>
|
||||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
source.pipe(filter((rd: RemoteData<T>) => rd.hasSucceeded));
|
source.pipe(filter((rd: RemoteData<T>) => rd.hasSucceeded));
|
||||||
|
@@ -39,4 +39,7 @@ export class PageInfo {
|
|||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
first: string;
|
first: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
self: string;
|
||||||
}
|
}
|
||||||
|
@@ -3,12 +3,13 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
import { HeaderComponent } from './header.component';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { MenuServiceStub } from '../shared/testing/menu-service-stub';
|
import { MenuServiceStub } from '../shared/testing/menu-service-stub';
|
||||||
import { HeaderComponent } from './header.component';
|
|
||||||
|
|
||||||
let comp: HeaderComponent;
|
let comp: HeaderComponent;
|
||||||
let fixture: ComponentFixture<HeaderComponent>;
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
<ds-form *ngIf="formModel"
|
||||||
|
[formId]="'comcol-form-id'"
|
||||||
|
[formModel]="formModel" (submitForm)="onSubmit()"></ds-form>
|
@@ -0,0 +1,115 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core';
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
|
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||||
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
import { ResourceType } from '../../../core/shared/resource-type';
|
||||||
|
import { ComColFormComponent } from './comcol-form.component';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { hasValue } from '../../empty.util';
|
||||||
|
import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||||
|
|
||||||
|
describe('ComColFormComponent', () => {
|
||||||
|
let comp: ComColFormComponent<DSpaceObject>;
|
||||||
|
let fixture: ComponentFixture<ComColFormComponent<DSpaceObject>>;
|
||||||
|
let location: Location;
|
||||||
|
const formServiceStub: any = {
|
||||||
|
createFormGroup: (fModel: DynamicFormControlModel[]) => {
|
||||||
|
const controls = {};
|
||||||
|
if (hasValue(fModel)) {
|
||||||
|
fModel.forEach((controlModel) => {
|
||||||
|
controls[controlModel.id] = new FormControl((controlModel as any).value);
|
||||||
|
});
|
||||||
|
return new FormGroup(controls);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const titleMD = { key: 'dc.title', value: 'Community Title' } as Metadatum;
|
||||||
|
const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' } as Metadatum;
|
||||||
|
const abstractMD = {
|
||||||
|
key: 'dc.description.abstract',
|
||||||
|
value: 'Community description'
|
||||||
|
} as Metadatum;
|
||||||
|
const newTitleMD = { key: 'dc.title', value: 'New Community Title' } as Metadatum;
|
||||||
|
const formModel = [
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'title',
|
||||||
|
name: newTitleMD.key,
|
||||||
|
value: 'New Community Title'
|
||||||
|
}),
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'abstract',
|
||||||
|
name: abstractMD.key,
|
||||||
|
value: abstractMD.value
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
const locationStub = {
|
||||||
|
back: () => {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
||||||
|
declarations: [ComColFormComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: Location, useValue: locationStub },
|
||||||
|
{ provide: DynamicFormService, useValue: formServiceStub }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ComColFormComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.formModel = [];
|
||||||
|
comp.dso = new Community();
|
||||||
|
fixture.detectChanges();
|
||||||
|
location = (comp as any).location;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onSubmit', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp.submitForm, 'emit');
|
||||||
|
comp.formModel = formModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit the new version of the community', () => {
|
||||||
|
comp.dso = Object.assign(
|
||||||
|
new Community(),
|
||||||
|
{
|
||||||
|
metadata: [
|
||||||
|
titleMD,
|
||||||
|
randomMD
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
comp.onSubmit();
|
||||||
|
|
||||||
|
expect(comp.submitForm.emit).toHaveBeenCalledWith(
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
new Community(),
|
||||||
|
{
|
||||||
|
metadata: [
|
||||||
|
randomMD,
|
||||||
|
newTitleMD,
|
||||||
|
abstractMD
|
||||||
|
],
|
||||||
|
type: ResourceType.Community
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
115
src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts
Normal file
115
src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import {
|
||||||
|
DynamicFormService,
|
||||||
|
DynamicInputModel
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { isNotEmpty } from '../../empty.util';
|
||||||
|
import { ResourceType } from '../../../core/shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A form for creating and editing Communities or Collections
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-comcol-form',
|
||||||
|
styleUrls: ['./comcol-form.component.scss'],
|
||||||
|
templateUrl: './comcol-form.component.html'
|
||||||
|
})
|
||||||
|
export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
|
||||||
|
/**
|
||||||
|
* DSpaceObject that the form represents
|
||||||
|
*/
|
||||||
|
@Input() dso: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of DSpaceObject that the form represents
|
||||||
|
*/
|
||||||
|
protected type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string} Key prefix used to generate form labels
|
||||||
|
*/
|
||||||
|
LABEL_KEY_PREFIX = '.form.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string} Key prefix used to generate form error messages
|
||||||
|
*/
|
||||||
|
ERROR_KEY_PREFIX = '.form.errors.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form model that represents the fields in the form
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form group of this form
|
||||||
|
*/
|
||||||
|
formGroup: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits DSO when the form is submitted
|
||||||
|
* @type {EventEmitter<any>}
|
||||||
|
*/
|
||||||
|
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
public constructor(private location: Location,
|
||||||
|
private formService: DynamicFormService,
|
||||||
|
private translate: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.formModel.forEach(
|
||||||
|
(fieldModel: DynamicInputModel) => {
|
||||||
|
fieldModel.value = this.dso.findMetadata(fieldModel.name);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.formGroup = this.formService.createFormGroup(this.formModel);
|
||||||
|
|
||||||
|
this.updateFieldTranslations();
|
||||||
|
this.translate.onLangChange
|
||||||
|
.subscribe(() => {
|
||||||
|
this.updateFieldTranslations();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks which new fields where added and sends the updated version of the DSO to the parent component
|
||||||
|
*/
|
||||||
|
onSubmit() {
|
||||||
|
const metadata = this.formModel.map(
|
||||||
|
(fieldModel: DynamicInputModel) => {
|
||||||
|
return { key: fieldModel.name, value: fieldModel.value }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const filteredOldMetadata = this.dso.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key));
|
||||||
|
const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value));
|
||||||
|
|
||||||
|
const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata];
|
||||||
|
const updatedDSO = Object.assign({}, this.dso, {
|
||||||
|
metadata: newMetadata,
|
||||||
|
type: ResourceType.Community
|
||||||
|
});
|
||||||
|
this.submitForm.emit(updatedDSO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used the update translations of errors and labels on init and on language change
|
||||||
|
*/
|
||||||
|
private updateFieldTranslations() {
|
||||||
|
this.formModel.forEach(
|
||||||
|
(fieldModel: DynamicInputModel) => {
|
||||||
|
fieldModel.label = this.translate.instant(this.type + this.LABEL_KEY_PREFIX + fieldModel.id);
|
||||||
|
if (isNotEmpty(fieldModel.validators)) {
|
||||||
|
fieldModel.errorMessages = {};
|
||||||
|
Object.keys(fieldModel.validators).forEach((key) => {
|
||||||
|
fieldModel.errorMessages[key] = this.translate.instant(this.type + this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,119 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { CommunityDataService } from '../../../core/data/community-data.service';
|
||||||
|
import { RouteService } from '../../services/route.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
import { SharedModule } from '../../shared.module';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model';
|
||||||
|
import { CreateComColPageComponent } from './create-comcol-page.component';
|
||||||
|
import { DataService } from '../../../core/data/data.service';
|
||||||
|
|
||||||
|
describe('CreateComColPageComponent', () => {
|
||||||
|
let comp: CreateComColPageComponent<DSpaceObject, NormalizedDSpaceObject>;
|
||||||
|
let fixture: ComponentFixture<CreateComColPageComponent<DSpaceObject, NormalizedDSpaceObject>>;
|
||||||
|
let communityDataService: CommunityDataService;
|
||||||
|
let dsoDataService: CommunityDataService;
|
||||||
|
let routeService: RouteService;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
let community;
|
||||||
|
let newCommunity;
|
||||||
|
let communityDataServiceStub;
|
||||||
|
let routeServiceStub;
|
||||||
|
let routerStub;
|
||||||
|
|
||||||
|
function initializeVars() {
|
||||||
|
community = Object.assign(new Community(), {
|
||||||
|
uuid: 'a20da287-e174-466a-9926-f66b9300d347',
|
||||||
|
metadata: [{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'test community'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
newCommunity = Object.assign(new Community(), {
|
||||||
|
uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48',
|
||||||
|
metadata: [{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'new community'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
communityDataServiceStub = {
|
||||||
|
findById: (uuid) => observableOf(new RemoteData(false, false, true, null, Object.assign(new Community(), {
|
||||||
|
uuid: uuid,
|
||||||
|
metadata: [{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: community.name
|
||||||
|
}]
|
||||||
|
}))),
|
||||||
|
create: (com, uuid?) => observableOf(new RemoteData(false, false, true, undefined, newCommunity))
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
routeServiceStub = {
|
||||||
|
getQueryParameterValue: (param) => observableOf(community.uuid)
|
||||||
|
};
|
||||||
|
routerStub = {
|
||||||
|
navigate: (commands) => commands
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
initializeVars();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: DataService, useValue: communityDataServiceStub },
|
||||||
|
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
|
{ provide: Router, useValue: routerStub },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CreateComColPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
dsoDataService = (comp as any).dsoDataService;
|
||||||
|
communityDataService = (comp as any).communityDataService;
|
||||||
|
routeService = (comp as any).routeService;
|
||||||
|
router = (comp as any).router;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onSubmit', () => {
|
||||||
|
let data;
|
||||||
|
beforeEach(() => {
|
||||||
|
data = Object.assign(new Community(), {
|
||||||
|
metadata: [{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'test'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should navigate when successful', () => {
|
||||||
|
spyOn(router, 'navigate');
|
||||||
|
comp.onSubmit(data);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(router.navigate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not navigate on failure', () => {
|
||||||
|
spyOn(router, 'navigate');
|
||||||
|
spyOn(dsoDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity)));
|
||||||
|
comp.onSubmit(data);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,73 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
import { CommunityDataService } from '../../../core/data/community-data.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RouteService } from '../../services/route.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { isNotEmpty, isNotUndefined } from '../../empty.util';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { DataService } from '../../../core/data/data.service';
|
||||||
|
import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component representing the create page for communities and collections
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-create-comcol',
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
export class CreateComColPageComponent<TDomain extends DSpaceObject, TNormalized extends NormalizedDSpaceObject> implements OnInit {
|
||||||
|
/**
|
||||||
|
* Frontend endpoint for this type of DSO
|
||||||
|
*/
|
||||||
|
protected frontendURL: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The provided UUID for the parent community
|
||||||
|
*/
|
||||||
|
public parentUUID$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parent community of the object that is to be created
|
||||||
|
*/
|
||||||
|
public parentRD$: Observable<RemoteData<Community>>;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected dsoDataService: DataService<TNormalized, TDomain>,
|
||||||
|
protected parentDataService: CommunityDataService,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected router: Router
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.parentUUID$ = this.routeService.getQueryParameterValue('parent');
|
||||||
|
this.parentUUID$.pipe(take(1)).subscribe((parentID: string) => {
|
||||||
|
if (isNotEmpty(parentID)) {
|
||||||
|
this.parentRD$ = this.parentDataService.findById(parentID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {TDomain} dso The updated version of the DSO
|
||||||
|
* Creates a new DSO based on the submitted user data and navigates to the new object's home page
|
||||||
|
*/
|
||||||
|
onSubmit(dso: TDomain) {
|
||||||
|
this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => {
|
||||||
|
this.dsoDataService.create(dso, uuid)
|
||||||
|
.pipe(getSucceededRemoteData())
|
||||||
|
.subscribe((dsoRD: RemoteData<TDomain>) => {
|
||||||
|
if (isNotUndefined(dsoRD)) {
|
||||||
|
const newUUID = dsoRD.payload.uuid;
|
||||||
|
this.router.navigate([this.frontendURL + newUUID]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user