Edit and create communities

This commit is contained in:
lotte
2018-12-20 15:54:47 +01:00
parent cd52c0cc06
commit 7a12332d70
39 changed files with 427 additions and 151 deletions

View File

@@ -42,6 +42,7 @@
"head": "Collections of this Community" "head": "Collections of this Community"
}, },
"edit": { "edit": {
"head": "Edit collction",
"name": "Name", "name": "Name",
"description": "Short Description", "description": "Short Description",
"introductory": "Introductory text (HTML)", "introductory": "Introductory text (HTML)",

View File

@@ -38,7 +38,7 @@ export class CreateCollectionPageComponent {
onSubmit(data: any) { onSubmit(data: any) {
this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => {
const collection = Object.assign(new NormalizedCollection(), { const collection = Object.assign(new Collection(), {
name: data.name, name: data.name,
metadata: [ metadata: [
{ key: 'dc.description', value: data.introductory }, { key: 'dc.description', value: data.introductory },

View File

@@ -1,27 +1,4 @@
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row" action="/"> <ds-form *ngIf="formModel" #formRef="formComponent"
<div class="col-12 form-group"> [formId]="'community-form-id'"
<label for="community-name" class="font-weight-bold">{{ 'community.edit.name' | translate }}</label> [formModel]="formModel" (submit)="onSubmit($event)"></ds-form>
<input type="text" [(ngModel)]="name" name="name" id="community-name" class="form-control" [ngClass]="{'is-invalid' : !name && nameRequiredError}" aria-label="Community Name" />
<div class="invalid-feedback">{{ 'community.edit.required.name' | translate }}</div>
</div>
<div class="col-12 form-group">
<label for="community-description" class="font-weight-bold">{{ 'community.edit.description' | translate }}</label>
<input type="text" [(ngModel)]="description" name="description" id="community-description" class="form-control" aria-label="Community Short Description" />
</div>
<div class="col-12 form-group">
<label for="community-introductory" class="font-weight-bold">{{ 'community.edit.introductory' | translate }}</label>
<textarea [(ngModel)]="introductory" name="introductory" id="community-introductory" class="form-control" aria-label="Community Introductory Text" rows="6" ></textarea>
</div>
<div class="col-12 form-group">
<label for="community-copyright" class="font-weight-bold">{{ 'community.edit.copyright' | translate }}</label>
<textarea [(ngModel)]="copyright" name="copyright" id="community-copyright" class="form-control" aria-label="Community Copyright Text" rows="6" ></textarea>
</div>
<div class="col-12 form-group">
<label for="community-news" class="font-weight-bold">{{ 'community.edit.news' | translate }}</label>
<textarea [(ngModel)]="news" name="news" id="community-news" class="form-control" aria-label="Community News" rows="6" ></textarea>
</div>
<div class="col-12 form-group">
<button type="button" class="btn btn-secondary" id="community-cancel" (click)="cancel()">{{ 'community.edit.cancel' | translate }}</button>
<button type="submit" class="btn btn-secondary" id="community-submit">{{ 'community.edit.submit' | translate }}</button>
</div>
</form>

View File

@@ -1,39 +1,93 @@
import { Component, EventEmitter, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { isNotEmpty } from '../../shared/empty.util';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import {
DynamicFormService,
DynamicInputModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
import { 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 { hasValue, isNotEmpty } from '../../shared/empty.util';
@Component({ @Component({
selector: 'ds-community-form', selector: 'ds-community-form',
styleUrls: ['./community-form.component.scss'], styleUrls: ['./community-form.component.scss'],
templateUrl: './community-form.component.html' templateUrl: './community-form.component.html'
}) })
export class CommunityFormComponent { export class CommunityFormComponent implements OnInit {
name: string; @Input() community: Community = new Community();
description: string; formModel: DynamicFormControlModel[] = [
introductory: string; new DynamicInputModel({
copyright: string; id: 'title',
news: string; name: 'dc.title',
label: 'Name',
required: true,
validators: {
required: null
},
errorMessages: {
required: 'Please enter a name for this title'
}
}),
new DynamicTextAreaModel({
id: 'description',
name: 'dc.description',
label: 'Introductory text (HTML)',
}),
new DynamicTextAreaModel({
id: 'abstract',
name: 'dc.description.abstract',
label: 'Short Description',
}),
new DynamicTextAreaModel({
id: 'rights',
name: 'dc.rights',
label: 'Copyright text (HTML)',
}),
new DynamicTextAreaModel({
id: 'tableofcontents',
name: 'dc.description.tableofcontents',
label: 'News (HTML)',
}),
];
nameRequiredError = false; formGroup: FormGroup;
@Output() submitted: EventEmitter<any> = new EventEmitter(); @Output() submitForm: EventEmitter<any> = new EventEmitter();
public constructor(private location: Location) {
public constructor(private location: Location, private formService: DynamicFormService) {
} }
onSubmit(data: any) { ngOnInit(): void {
if (isNotEmpty(data.name)) { this.formModel.forEach(
this.submitted.emit(data); (fieldModel: DynamicInputModel) => {
this.nameRequiredError = false; fieldModel.value = this.community.findMetadata(fieldModel.name);
} else { }
this.nameRequiredError = true; );
} this.formGroup = this.formService.createFormGroup(this.formModel);
}
onSubmit(event: Event) {
event.stopPropagation();
const metadata = this.formModel.map(
(fieldModel: DynamicInputModel) => {
return { key: fieldModel.name, value: fieldModel.value }
}
);
const filteredOldMetadata = this.community.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key));
const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value));
const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata];
const updatedCommunity = Object.assign({}, this.community, {
metadata: newMetadata,
type: ResourceType.Community
});
this.submitForm.emit(updatedCommunity);
} }
cancel() { cancel() {
this.location.back(); this.location.back();
} }
} }

View File

@@ -5,13 +5,23 @@ 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 { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{ path: 'create', { path: 'create',
component: CreateCommunityPageComponent, component: CreateCommunityPageComponent,
canActivate: [AuthenticatedGuard] }, canActivate: [AuthenticatedGuard]
},
{ path: ':id/edit',
pathMatch: 'full',
component: EditCommunityPageComponent,
canActivate: [AuthenticatedGuard],
resolve: {
community: CommunityPageResolver
}
},
{ {
path: ':id', path: ':id',
component: CommunityPageComponent, component: CommunityPageComponent,

View File

@@ -28,6 +28,8 @@
[community]="communityPayload"></ds-community-page-sub-collection-list> [community]="communityPayload"></ds-community-page-sub-collection-list>
</div> </div>
</div> </div>
<a [routerLink]="'edit'">Edit</a>
<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" <ds-loading *ngIf="communityRD?.isLoading"
message="{{'loading.community' | translate}}"></ds-loading> message="{{'loading.community' | translate}}"></ds-loading>

View File

@@ -24,8 +24,6 @@ import { hasValue } from '../shared/empty.util';
export class CommunityPageComponent implements OnInit, OnDestroy { export class CommunityPageComponent implements OnInit, OnDestroy {
communityRD$: Observable<RemoteData<Community>>; communityRD$: Observable<RemoteData<Community>>;
logoRD$: Observable<RemoteData<Bitstream>>; logoRD$: Observable<RemoteData<Bitstream>>;
private subs: Subscription[] = []; private subs: Subscription[] = [];
constructor( constructor(
@@ -42,14 +40,9 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
map((rd: RemoteData<Community>) => rd.payload), map((rd: RemoteData<Community>) => rd.payload),
filter((community: Community) => hasValue(community)), filter((community: Community) => hasValue(community)),
mergeMap((community: Community) => community.logo)); mergeMap((community: Community) => community.logo));
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
} }
} }

View File

@@ -8,6 +8,7 @@ import { CommunityPageSubCollectionListComponent } from './sub-collection-list/c
import { CommunityPageRoutingModule } from './community-page-routing.module'; import { CommunityPageRoutingModule } from './community-page-routing.module';
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
import { CommunityFormComponent } from './community-form/community-form.component'; import { CommunityFormComponent } from './community-form/community-form.component';
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -19,6 +20,7 @@ import { CommunityFormComponent } from './community-form/community-form.componen
CommunityPageComponent, CommunityPageComponent,
CommunityPageSubCollectionListComponent, CommunityPageSubCollectionListComponent,
CreateCommunityPageComponent, CreateCommunityPageComponent,
EditCommunityPageComponent,
CommunityFormComponent CommunityFormComponent
] ]
}) })

View File

@@ -7,5 +7,5 @@
</ng-container> </ng-container>
</div> </div>
</div> </div>
<ds-community-form (submitted)="onSubmit($event)"></ds-community-form> <ds-community-form (submitForm)="onSubmit($event)"></ds-community-form>
</div> </div>

View File

@@ -8,12 +8,10 @@ import { Observable } from 'rxjs/Observable';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models';
import { BrowserModule } from '@angular/platform-browser';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CommunityFormComponent } from '../community-form/community-form.component'; import { CommunityFormComponent } from '../community-form/community-form.component';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { RequestError } from '../../core/data/request.models';
describe('CreateCommunityPageComponent', () => { describe('CreateCommunityPageComponent', () => {
let comp: CreateCommunityPageComponent; let comp: CreateCommunityPageComponent;

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -7,55 +7,46 @@ import { Router } from '@angular/router';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
import { ResourceType } from '../../core/shared/resource-type'; import { getSucceededRemoteData } from '../../core/shared/operators';
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
@Component({ @Component({
selector: 'ds-create-community', selector: 'ds-create-community',
styleUrls: ['./create-community-page.component.scss'], styleUrls: ['./create-community-page.component.scss'],
templateUrl: './create-community-page.component.html' templateUrl: './create-community-page.component.html'
}) })
export class CreateCommunityPageComponent { export class CreateCommunityPageComponent implements OnInit {
public parentUUID$: Observable<string>; public parentUUID$: Observable<string>;
public communityRDObs: Observable<RemoteData<Community>>; public parentRD$: Observable<RemoteData<Community>>;
public constructor( public constructor(
private communityDataService: CommunityDataService, private communityDataService: CommunityDataService,
private routeService: RouteService, private routeService: RouteService,
private router: Router private router: Router
) { ) {
}
ngOnInit(): void {
this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); this.parentUUID$ = this.routeService.getQueryParameterValue('parent');
this.parentUUID$.subscribe((uuid: string) => { this.parentUUID$.subscribe((parentID: string) => {
if (isNotEmpty(uuid)) { if (isNotEmpty(parentID)) {
this.communityRDObs = this.communityDataService.findById(uuid); this.parentRD$ = this.communityDataService.findById(parentID);
} }
}); });
} }
onSubmit(data: any) { onSubmit(community: Community) {
this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => {
const community = Object.assign(new NormalizedCommunity(), { this.communityDataService.create(community, uuid)
name: data.name, .pipe(getSucceededRemoteData())
metadata: [ .subscribe((communityRD: RemoteData<Community>) => {
{ key: 'dc.description', value: data.introductory }, const newUUID = communityRD.payload.uuid;
{ key: 'dc.description.abstract', value: data.description }, this.router.navigate(['/communities/' + newUUID]);
{ key: 'dc.rights', value: data.copyright }
// TODO: metadata for news
],
type: ResourceType.Community
});
this.communityDataService.create(community, uuid).pipe(take(1)).subscribe((rd: RemoteData<DSpaceObject>) => {
if (rd.hasSucceeded) {
if (uuid) {
this.router.navigate(['communities', uuid]);
} else {
this.router.navigate([]);
}
}
}); });
}); });
} }
} }

View File

@@ -0,0 +1,11 @@
<div class="container">
<div class="row">
<div class="col-12 pb-4">
<ng-container *ngVar="(parentUUID$ | async)?.payload as parent">
<h2 *ngIf="!parent" id="header" class="border-bottom pb-2">{{ 'community.edit.head' | translate }}</h2>
<h2 *ngIf="parent" id="sub-header" class="border-bottom pb-2">{{ 'community.edit.sub-head' | translate:{ parent: parent.name } }}</h2>
</ng-container>
</div>
</div>
<ds-community-form (submitForm)="onSubmit($event)" [community]="(communityRD$ | async)?.payload"></ds-community-form>
</div>

View File

@@ -0,0 +1,90 @@
import { CreateCommunityPageComponent } from './create-community-page.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CommunityDataService } from '../../core/data/community-data.service';
import { RouteService } from '../../shared/services/route.service';
import { Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable';
import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model';
import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models';
import { BrowserModule } from '@angular/platform-browser';
import { SharedModule } from '../../shared/shared.module';
import { CommonModule } from '@angular/common';
import { CommunityFormComponent } from '../community-form/community-form.component';
import { RouterTestingModule } from '@angular/router/testing';
import { RequestError } from '../../core/data/request.models';
describe('CreateCommunityPageComponent', () => {
let comp: CreateCommunityPageComponent;
let fixture: ComponentFixture<CreateCommunityPageComponent>;
let communityDataService: CommunityDataService;
let routeService: RouteService;
let router: Router;
const community = Object.assign(new Community(), {
uuid: 'a20da287-e174-466a-9926-f66b9300d347',
name: 'test community'
});
const newCommunity = Object.assign(new Community(), {
uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48',
name: 'new community'
});
const communityDataServiceStub = {
findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), {
uuid: uuid,
name: community.name
}))),
create: (com, uuid?) => Observable.of(new RemoteData(false, false, true, undefined, newCommunity))
};
const routeServiceStub = {
getQueryParameterValue: (param) => Observable.of(community.uuid)
};
const routerStub = {
navigate: (commands) => commands
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
declarations: [CreateCommunityPageComponent, CommunityFormComponent],
providers: [
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: Router, useValue: routerStub }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CreateCommunityPageComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
communityDataService = (comp as any).communityDataService;
routeService = (comp as any).routeService;
router = (comp as any).router;
});
describe('onSubmit', () => {
const data = {
name: '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(communityDataService, 'create').and.returnValue(Observable.of(new RemoteData(true, true, false, undefined, newCommunity)));
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,46 @@
import { Component } 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 '../../shared/services/route.service';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../core/data/remote-data';
import { isNotEmpty } from '../../shared/empty.util';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { first, map, take, tap } from 'rxjs/operators';
import { ResourceType } from '../../core/shared/resource-type';
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
import { getSucceededRemoteData } from '../../core/shared/operators';
@Component({
selector: 'ds-edit-community',
styleUrls: ['./edit-community-page.component.scss'],
templateUrl: './edit-community-page.component.html'
})
export class EditCommunityPageComponent {
public parentUUID$: Observable<string>;
public parentRD$: Observable<RemoteData<Community>>;
public communityRD$: Observable<RemoteData<Community>>;
public constructor(
private communityDataService: CommunityDataService,
private routeService: RouteService,
private router: Router,
private route: ActivatedRoute
) {
}
ngOnInit(): void {
this.communityRD$ = this.route.data.pipe(first(), map((data) => data.community));
}
onSubmit(community: Community) {
this.communityDataService.update(community)
.pipe(getSucceededRemoteData())
.subscribe((communityRD: RemoteData<Community>) => {
const newUUID = communityRD.payload.uuid;
this.router.navigate(['/communities/' + newUUID]);
});
}
}

View File

@@ -0,0 +1,36 @@
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 { map, take } from 'rxjs/operators';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list';
export function isRestDataObject(halObj: any) {
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
}
export function isRestPaginatedList(halObj: any) {
return hasValue(halObj.page) && hasValue(halObj._embedded);
}
export function isPaginatedList(halObj: any) {
return hasValue(halObj.page) && hasValue(halObj.pageInfo);
}
@Injectable()
export class DataBuildService {
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;
}
}

View File

@@ -51,10 +51,7 @@ export class RemoteDataBuildService {
const requestEntry$ = observableRace( const requestEntry$ = observableRace(
href$.pipe(getRequestFromRequestHref(this.requestService)), href$.pipe(getRequestFromRequestHref(this.requestService)),
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)), requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)),
).pipe(
take(1)
); );
// 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(

View File

@@ -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[];

View File

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

View File

@@ -63,6 +63,8 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
import { UploaderService } from '../shared/uploader/uploader.service'; import { UploaderService } from '../shared/uploader/uploader.service';
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service';
import { DataBuildService } from './cache/builders/data-build.service';
import { DSOUpdateComparator } from './data/dso-update-comparator';
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -99,6 +101,7 @@ const PROVIDERS = [
ObjectCacheService, ObjectCacheService,
PaginationComponentOptions, PaginationComponentOptions,
RegistryService, RegistryService,
DataBuildService,
RemoteDataBuildService, RemoteDataBuildService,
RequestService, RequestService,
EndpointMapResponseParsingService, EndpointMapResponseParsingService,
@@ -126,6 +129,7 @@ const PROVIDERS = [
UploaderService, UploaderService,
UUIDService, UUIDService,
DSpaceObjectDataService, DSpaceObjectDataService,
DSOUpdateComparator,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,

View File

@@ -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/data-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[] {

View File

@@ -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';
@@ -10,9 +10,10 @@ 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 { AuthService } from '../auth/auth.service'; import { AuthService } from '../auth/auth.service';
import { Community } from '../shared/community.model';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { DataBuildService } from '../cache/builders/data-build.service';
import { DSOUpdateComparator } from './dso-update-comparator';
@Injectable() @Injectable()
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> { export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
@@ -21,13 +22,15 @@ export class CollectionDataService extends ComColDataService<NormalizedCollectio
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected dataBuildService: DataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
protected cds: CommunityDataService, protected cds: CommunityDataService,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected authService: AuthService, protected authService: AuthService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected http: HttpClient protected http: HttpClient,
protected comparator: DSOUpdateComparator
) { ) {
super(); super();
} }

View File

@@ -14,10 +14,12 @@ 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 { Community } from '../shared/community.model';
import { AuthService } from '../auth/auth.service'; import { AuthService } from '../auth/auth.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { DataBuildService } from '../cache/builders/data-build.service';
import { DSOUpdateComparator } from './dso-update-comparator';
import { UpdateComparator } from './update-comparator';
const LINK_NAME = 'test'; const LINK_NAME = 'test';
@@ -30,6 +32,7 @@ class TestService extends ComColDataService<NormalizedTestObject, any> {
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected dataBuildService: DataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
protected EnvConfig: GlobalConfig, protected EnvConfig: GlobalConfig,
protected cds: CommunityDataService, protected cds: CommunityDataService,
@@ -38,6 +41,7 @@ class TestService extends ComColDataService<NormalizedTestObject, any> {
protected authService: AuthService, protected authService: AuthService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected http: HttpClient, protected http: HttpClient,
protected comparator: DSOUpdateComparator,
protected linkPath: string protected linkPath: string
) { ) {
super(); super();
@@ -60,6 +64,8 @@ describe('ComColDataService', () => {
const EnvConfig = {} as GlobalConfig; const EnvConfig = {} as GlobalConfig;
const notificationsService = {} as NotificationsService; const notificationsService = {} as NotificationsService;
const http = {} as HttpClient; const http = {} as HttpClient;
const comparator = {} as any;
const dataBuildService = {} as DataBuildService;
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(), {
@@ -78,7 +84,7 @@ describe('ComColDataService', () => {
const authHeader = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiJhNjA4NmIzNC0zOTE4LTQ1YjctOGRkZC05MzI5YTcwMmEyNmEiLCJzZyI6W10sImV4cCI6MTUzNDk0MDcyNX0.RV5GAtiX6cpwBN77P_v16iG9ipeyiO7faNYSNMzq_sQ'; const authHeader = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiJhNjA4NmIzNC0zOTE4LTQ1YjctOGRkZC05MzI5YTcwMmEyNmEiLCJzZyI6W10sImV4cCI6MTUzNDk0MDcyNX0.RV5GAtiX6cpwBN77P_v16iG9ipeyiO7faNYSNMzq_sQ';
const mockHalService = { const mockHalService = {
getEndpoint: (linkPath) => Observable.of(communitiesEndpoint) getEndpoint: (linkPath) => observableOf(communitiesEndpoint)
}; };
function initMockCommunityDataService(): CommunityDataService { function initMockCommunityDataService(): CommunityDataService {
@@ -112,6 +118,7 @@ describe('ComColDataService', () => {
return new TestService( return new TestService(
requestService, requestService,
rdbService, rdbService,
dataBuildService,
store, store,
EnvConfig, EnvConfig,
cds, cds,
@@ -120,6 +127,7 @@ describe('ComColDataService', () => {
authService, authService,
notificationsService, notificationsService,
http, http,
comparator,
LINK_NAME LINK_NAME
); );
} }

View File

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

View File

@@ -18,6 +18,8 @@ import { Observable } from 'rxjs';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { DataBuildService } from '../cache/builders/data-build.service';
import { DSOUpdateComparator } from './dso-update-comparator';
@Injectable() @Injectable()
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> { export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
@@ -28,12 +30,14 @@ export class CommunityDataService extends ComColDataService<NormalizedCommunity,
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected dataBuildService: DataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected authService: AuthService, protected authService: AuthService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected http: HttpClient protected http: HttpClient,
protected comparator: DSOUpdateComparator
) { ) {
super(); super();
} }

View File

@@ -177,7 +177,7 @@ describe('ConfigResponseParsingService', () => {
'https://rest.api/config/submissionsections/traditionalpagetwo', 'https://rest.api/config/submissionsections/traditionalpagetwo',
'https://rest.api/config/submissionsections/upload', 'https://rest.api/config/submissionsections/upload',
'https://rest.api/config/submissionsections/license' 'https://rest.api/config/submissionsections/license'
]) ], 'https://rest.api/config/submissiondefinitions/traditional/sections')
}); });
it('should return a ConfigSuccessResponse if data contains a valid config endpoint response', () => { it('should return a ConfigSuccessResponse if data contains a valid config endpoint response', () => {

View File

@@ -1,4 +1,13 @@
import { delay, distinctUntilChanged, filter, find, switchMap, map, take, tap } from 'rxjs/operators'; import {
delay,
distinctUntilChanged,
filter,
find,
switchMap,
map,
take,
tap, first, mergeMap
} from 'rxjs/operators';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
@@ -32,10 +41,14 @@ import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/respon
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
import { CacheableObject } from '../cache/object-cache.reducer';
import { DataBuildService } from '../cache/builders/data-build.service';
import { UpdateComparator } from './update-comparator';
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: DataBuildService;
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;
@@ -43,6 +56,7 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
protected abstract authService: AuthService; protected abstract authService: AuthService;
protected abstract notificationsService: NotificationsService; protected abstract notificationsService: NotificationsService;
protected abstract http: HttpClient; protected abstract http: HttpClient;
protected abstract comparator: UpdateComparator<TNormalized>;
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string> public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
@@ -122,15 +136,21 @@ 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.compare(oldVersion, newVersion);
} if (isNotEmpty(operations)) {
this.objectCache.addPatch(object.self, operations);
}
return this.findById(object.uuid);
}
));
} }
create(dso: TNormalized, parentUUID: string): Observable<RemoteData<TDomain>> { create(dso: TDomain, parentUUID: string): Observable<RemoteData<TDomain>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
isNotEmptyOperator(), isNotEmptyOperator(),
@@ -138,7 +158,8 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint)
); );
const serializedDso = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(dso.type)).serialize(dso); const normalizedObject: TNormalized = this.dataBuildService.normalize<TDomain, TNormalized>(dso);
const serializedDso = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(dso.type)).serialize(normalizedObject);
const request$ = endpoint$.pipe( const request$ = endpoint$.pipe(
take(1), take(1),
@@ -150,6 +171,7 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
configureRequest(this.requestService) configureRequest(this.requestService)
).subscribe(); ).subscribe();
// Resolve self link for new object
const selfLink$ = this.requestService.getByUUID(requestId).pipe( const selfLink$ = this.requestService.getByUUID(requestId).pipe(
getResponseFromEntry(), getResponseFromEntry(),
map((response: RestResponse) => { map((response: RestResponse) => {

View File

@@ -0,0 +1,12 @@
import { Operation } from 'fast-json-patch/lib/core';
import { compare } from 'fast-json-patch';
import { UpdateComparator } from './update-comparator';
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
import { Injectable } from '@angular/core';
@Injectable()
export class DSOUpdateComparator implements UpdateComparator<NormalizedDSpaceObject> {
compare(object1: NormalizedDSpaceObject, object2: NormalizedDSpaceObject): Operation[] {
return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path }));
}
}

View File

@@ -14,6 +14,8 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { AuthService } from '../auth/auth.service'; import { AuthService } from '../auth/auth.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { DataBuildService } from '../cache/builders/data-build.service';
import { DSOUpdateComparator } from './dso-update-comparator';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject> { class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject> {
@@ -22,12 +24,14 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected dataBuildService: DataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected authService: AuthService, protected authService: AuthService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected http: HttpClient) { protected http: HttpClient,
protected comparator: DSOUpdateComparator) {
super(); super();
} }
@@ -48,12 +52,14 @@ export class DSpaceObjectDataService {
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected dataBuildService: DataBuildService,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected authService: AuthService, protected authService: AuthService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected http: HttpClient) { protected http: HttpClient,
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, authService, notificationsService, http); protected comparator: DSOUpdateComparator) {
this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, authService, notificationsService, http, comparator);
} }
findById(uuid: string): Observable<RemoteData<DSpaceObject>> { findById(uuid: string): Observable<RemoteData<DSpaceObject>> {

View File

@@ -19,6 +19,8 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { AuthService } from '../auth/auth.service'; import { AuthService } from '../auth/auth.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { DataBuildService } from '../cache/builders/data-build.service';
import { DSOUpdateComparator } from './dso-update-comparator';
@Injectable() @Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> { export class ItemDataService extends DataService<NormalizedItem, Item> {
@@ -27,13 +29,15 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected dataBuildService: DataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
private bs: BrowseService, private bs: BrowseService,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected authService: AuthService, protected authService: AuthService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected http: HttpClient) { protected http: HttpClient,
protected comparator: DSOUpdateComparator) {
super(); super();
} }

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { Operation } from 'fast-json-patch/lib/core';
export interface UpdateComparator<TNormalized extends NormalizedObject> {
compare(object1: TNormalized, object2: TNormalized): Operation[];
}

View File

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

View File

@@ -39,4 +39,7 @@ export class PageInfo {
@autoserialize @autoserialize
first: string; first: string;
@autoserialize
self: string;
} }

View File

@@ -96,7 +96,6 @@ export class DsDynamicFormControlComponent extends DynamicFormControlContainerCo
} }
static getFormControlType(model: DynamicFormControlModel): Type<DynamicFormControl> | null { static getFormControlType(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
switch (model.type) { switch (model.type) {
case DYNAMIC_FORM_CONTROL_TYPE_ARRAY: case DYNAMIC_FORM_CONTROL_TYPE_ARRAY:

View File

@@ -51,7 +51,7 @@
<div class="col text-right"> <div class="col text-right">
<button type="reset" class="btn btn-default" (click)="reset()">{{'form.cancel' | translate}}</button> <button type="reset" class="btn btn-default" (click)="reset()">{{'form.cancel' | translate}}</button>
<button type="submit" class="btn btn-primary" (click)="onSubmit($event)" <button type="submit" class="btn btn-primary" (click)="onSubmit()"
[disabled]="!(isValid() | async)">{{'form.submit' | translate}} [disabled]="!(isValid() | async)">{{'form.submit' | translate}}
</button> </button>
</div> </div>

View File

@@ -73,7 +73,7 @@ export class FormComponent implements OnDestroy, OnInit {
* An event fired when form is valid and submitted . * An event fired when form is valid and submitted .
* Event's payload equals to the form content. * Event's payload equals to the form content.
*/ */
@Output() submit: EventEmitter<Observable<any>> = new EventEmitter<Observable<any>>(); @Output() submitForm: EventEmitter<Observable<any>> = new EventEmitter<Observable<any>>();
/** /**
* An object of FormGroup type * An object of FormGroup type
@@ -264,7 +264,7 @@ export class FormComponent implements OnDestroy, OnInit {
*/ */
onSubmit(): void { onSubmit(): void {
if (this.getFormGroupValidStatus()) { if (this.getFormGroupValidStatus()) {
this.submit.emit(this.formService.getFormData(this.formId)); this.submitForm.emit(this.formService.getFormData(this.formId));
} else { } else {
this.formService.validateAllFormFields(this.formGroup); this.formService.validateAllFormFields(this.formGroup);
} }

View File

@@ -98,7 +98,7 @@ export class FormService {
const errorKey = this.getValidatorNameFromMap(message); const errorKey = this.getValidatorNameFromMap(message);
let errorMsg = message; let errorMsg = message;
// if form control model has not errorMessages object, create it // if form control model has no errorMessages object, create it
if (!model.errorMessages) { if (!model.errorMessages) {
model.errorMessages = {}; model.errorMessages = {};
} }