mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'master' into Fix-context-sensitive-menus
This commit is contained in:
@@ -104,7 +104,7 @@ DSPACE_REST_SSL # Whether the angular REST uses SSL [true/false]
|
|||||||
|
|
||||||
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
||||||
```bash
|
```bash
|
||||||
export DSPACE_HOST=https://dspace7.4science.cloud/server
|
export DSPACE_HOST=dspace7.4science.cloud
|
||||||
```
|
```
|
||||||
|
|
||||||
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides **`environment.(prod, dev or test).ts`** overrides **`environment.common.ts`**
|
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides **`environment.(prod, dev or test).ts`** overrides **`environment.common.ts`**
|
||||||
|
@@ -91,6 +91,7 @@
|
|||||||
"express": "4.16.2",
|
"express": "4.16.2",
|
||||||
"fast-json-patch": "^2.0.7",
|
"fast-json-patch": "^2.0.7",
|
||||||
"file-saver": "^1.3.8",
|
"file-saver": "^1.3.8",
|
||||||
|
"filesize": "^6.1.0",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"https": "1.0.0",
|
"https": "1.0.0",
|
||||||
"js-cookie": "2.2.0",
|
"js-cookie": "2.2.0",
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
"moment": "^2.22.1",
|
"moment": "^2.22.1",
|
||||||
"morgan": "^1.9.1",
|
"morgan": "^1.9.1",
|
||||||
"ng-mocks": "^8.1.0",
|
"ng-mocks": "^8.1.0",
|
||||||
"ng2-file-upload": "1.2.1",
|
"ng2-file-upload": "1.4.0",
|
||||||
"ng2-nouislider": "^1.8.2",
|
"ng2-nouislider": "^1.8.2",
|
||||||
"ngx-bootstrap": "^5.3.2",
|
"ngx-bootstrap": "^5.3.2",
|
||||||
"ngx-infinite-scroll": "6.0.1",
|
"ngx-infinite-scroll": "6.0.1",
|
||||||
|
@@ -4,6 +4,7 @@ import { NgModule } from '@angular/core';
|
|||||||
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { getRegistriesModulePath } from '../admin-routing.module';
|
import { getRegistriesModulePath } from '../admin-routing.module';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
|
const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
|
||||||
|
|
||||||
@@ -14,16 +15,28 @@ export function getBitstreamFormatsModulePath() {
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{path: 'metadata', component: MetadataRegistryComponent, data: {title: 'admin.registries.metadata.title'}},
|
|
||||||
{
|
{
|
||||||
path: 'metadata/:schemaName',
|
path: 'metadata',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
data: {title: 'admin.registries.metadata.title', breadcrumbKey: 'admin.registries.metadata'},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: MetadataRegistryComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':schemaName',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
component: MetadataSchemaComponent,
|
component: MetadataSchemaComponent,
|
||||||
data: {title: 'admin.registries.schema.title'}
|
data: {title: 'admin.registries.schema.title', breadcrumbKey: 'admin.registries.schema'}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: BITSTREAMFORMATS_MODULE_PATH,
|
path: BITSTREAMFORMATS_MODULE_PATH,
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule',
|
loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule',
|
||||||
data: {title: 'admin.registries.bitstream-formats.title'}
|
data: {title: 'admin.registries.bitstream-formats.title', breadcrumbKey: 'admin.registries.bitstream-formats'}
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
@@ -4,6 +4,7 @@ import { BitstreamFormatsResolver } from './bitstream-formats.resolver';
|
|||||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||||
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||||
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
|
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
|
||||||
const BITSTREAMFORMAT_ADD_PATH = 'add';
|
const BITSTREAMFORMAT_ADD_PATH = 'add';
|
||||||
@@ -17,14 +18,18 @@ const BITSTREAMFORMAT_ADD_PATH = 'add';
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: BITSTREAMFORMAT_ADD_PATH,
|
path: BITSTREAMFORMAT_ADD_PATH,
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
component: AddBitstreamFormatComponent,
|
component: AddBitstreamFormatComponent,
|
||||||
|
data: {breadcrumbKey: 'admin.registries.bitstream-formats.create'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: BITSTREAMFORMAT_EDIT_PATH,
|
path: BITSTREAMFORMAT_EDIT_PATH,
|
||||||
component: EditBitstreamFormatComponent,
|
component: EditBitstreamFormatComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
bitstreamFormat: BitstreamFormatsResolver
|
bitstreamFormat: BitstreamFormatsResolver,
|
||||||
}
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: {breadcrumbKey: 'admin.registries.bitstream-formats.edit'}
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@@ -11,7 +11,6 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="(metadataSchemas | async)?.payload"
|
|
||||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
|
@@ -4,7 +4,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
|||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
import { zip } from 'rxjs/internal/observable/zip';
|
import { zip } from 'rxjs/internal/observable/zip';
|
||||||
@@ -12,6 +12,8 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
|||||||
import { Route, Router } from '@angular/router';
|
import { Route, Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-metadata-registry',
|
selector: 'ds-metadata-registry',
|
||||||
@@ -37,6 +39,11 @@ export class MetadataRegistryComponent {
|
|||||||
pageSize: 25
|
pageSize: 25
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the list of MetadataSchemas needs an update
|
||||||
|
*/
|
||||||
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
constructor(private registryService: RegistryService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -50,14 +57,17 @@ export class MetadataRegistryComponent {
|
|||||||
*/
|
*/
|
||||||
onPageChange(event) {
|
onPageChange(event) {
|
||||||
this.config.currentPage = event;
|
this.config.currentPage = event;
|
||||||
this.updateSchemas();
|
this.forceUpdateSchemas();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the list of schemas by fetching it from the rest api or cache
|
* Update the list of schemas by fetching it from the rest api or cache
|
||||||
*/
|
*/
|
||||||
private updateSchemas() {
|
private updateSchemas() {
|
||||||
this.metadataSchemas = this.registryService.getMetadataSchemas(this.config);
|
this.metadataSchemas = this.needsUpdate$.pipe(
|
||||||
|
filter((update) => update === true),
|
||||||
|
switchMap(() => this.registryService.getMetadataSchemas(toFindListOptions(this.config)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,8 +75,7 @@ export class MetadataRegistryComponent {
|
|||||||
* a new REST call
|
* a new REST call
|
||||||
*/
|
*/
|
||||||
public forceUpdateSchemas() {
|
public forceUpdateSchemas() {
|
||||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
this.needsUpdate$.next(true);
|
||||||
this.updateSchemas();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +134,7 @@ export class MetadataRegistryComponent {
|
|||||||
* Delete all the selected metadata schemas
|
* Delete all the selected metadata schemas
|
||||||
*/
|
*/
|
||||||
deleteSchemas() {
|
deleteSchemas() {
|
||||||
|
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||||
(schemas) => {
|
(schemas) => {
|
||||||
const tasks$ = [];
|
const tasks$ = [];
|
||||||
|
@@ -21,7 +21,8 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
getActiveMetadataSchema: () => observableOf(undefined),
|
||||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
||||||
cancelEditMetadataSchema: () => {}
|
cancelEditMetadataSchema: () => {},
|
||||||
|
clearMetadataSchemaRequests: () => observableOf(undefined)
|
||||||
};
|
};
|
||||||
const formBuilderServiceStub = {
|
const formBuilderServiceStub = {
|
||||||
createFormGroup: () => {
|
createFormGroup: () => {
|
||||||
|
@@ -128,6 +128,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created schema using the EventEmitter submitForm
|
* Emit the updated/created schema using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
|
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||||
(schema) => {
|
(schema) => {
|
||||||
const values = {
|
const values = {
|
||||||
@@ -139,7 +140,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
this.submitForm.emit(newSchema);
|
this.submitForm.emit(newSchema);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), {
|
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||||
id: schema.id,
|
id: schema.id,
|
||||||
prefix: (values.prefix ? values.prefix : schema.prefix),
|
prefix: (values.prefix ? values.prefix : schema.prefix),
|
||||||
namespace: (values.namespace ? values.namespace : schema.namespace)
|
namespace: (values.namespace ? values.namespace : schema.namespace)
|
||||||
@@ -148,6 +149,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.clearFields();
|
this.clearFields();
|
||||||
|
this.registryService.cancelEditMetadataSchema();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -30,6 +30,7 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
||||||
cancelEditMetadataField: () => {},
|
cancelEditMetadataField: () => {},
|
||||||
cancelEditMetadataSchema: () => {},
|
cancelEditMetadataSchema: () => {},
|
||||||
|
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||||
};
|
};
|
||||||
const formBuilderServiceStub = {
|
const formBuilderServiceStub = {
|
||||||
createFormGroup: () => {
|
createFormGroup: () => {
|
||||||
|
@@ -153,6 +153,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created field using the EventEmitter submitForm
|
* Emit the updated/created field using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
|
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||||
(field) => {
|
(field) => {
|
||||||
const values = {
|
const values = {
|
||||||
@@ -166,7 +167,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
this.submitForm.emit(newField);
|
this.submitForm.emit(newField);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), {
|
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), field, {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
schema: this.metadataSchema,
|
schema: this.metadataSchema,
|
||||||
element: (values.element ? values.element : field.element),
|
element: (values.element ? values.element : field.element),
|
||||||
@@ -177,6 +178,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.clearFields();
|
this.clearFields();
|
||||||
|
this.registryService.cancelEditMetadataField();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +1,23 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="metadata-schema row">
|
<div class="metadata-schema row">
|
||||||
<div class="col-12">
|
<div class="col-12" *ngVar="(metadataSchema$ | async) as schema">
|
||||||
|
|
||||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{(metadataSchema | async)?.payload?.prefix}}"</h2>
|
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{schema?.prefix}}"</h2>
|
||||||
|
|
||||||
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:namespace }}</p>
|
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:{ namespace: schema?.namespace } }}</p>
|
||||||
|
|
||||||
<ds-metadata-field-form
|
<ds-metadata-field-form
|
||||||
[metadataSchema]="(metadataSchema | async)?.payload"
|
[metadataSchema]="schema"
|
||||||
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
||||||
|
|
||||||
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
|
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
|
||||||
|
|
||||||
|
<ng-container *ngVar="(metadataFields$ | async)?.payload as fields">
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(metadataFields | async)?.payload?.totalElements > 0"
|
*ngIf="fields?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="(metadataFields | async)?.payload"
|
[pageInfoState]="fields"
|
||||||
[collectionSize]="(metadataFields | async)?.payload?.totalElements"
|
[collectionSize]="fields?.totalElements"
|
||||||
[hideGear]="false"
|
[hideGear]="false"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
(pageChange)="onPageChange($event)">
|
(pageChange)="onPageChange($event)">
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let field of (metadataFields | async)?.payload?.page"
|
<tr *ngFor="let field of fields?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label>
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
(change)="selectMetadataField(field, $event)">
|
(change)="selectMetadataField(field, $event)">
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{(metadataSchema | async)?.payload?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -47,14 +48,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
|
|
||||||
<div *ngIf="(metadataFields | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
<div *ngIf="fields?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||||
{{'admin.registries.schema.fields.no-items' | translate}}
|
{{'admin.registries.schema.fields.no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
||||||
<button *ngIf="(metadataFields | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
<button *ngIf="fields?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -22,6 +22,7 @@ import { RestResponse } from '../../../core/cache/response.models';
|
|||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
|
||||||
describe('MetadataSchemaComponent', () => {
|
describe('MetadataSchemaComponent', () => {
|
||||||
let comp: MetadataSchemaComponent;
|
let comp: MetadataSchemaComponent;
|
||||||
@@ -124,7 +125,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe],
|
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryServiceStub },
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
@@ -5,7 +5,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
|||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
import { zip } from 'rxjs/internal/observable/zip';
|
import { zip } from 'rxjs/internal/observable/zip';
|
||||||
@@ -13,6 +13,10 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-metadata-schema',
|
selector: 'ds-metadata-schema',
|
||||||
@@ -24,21 +28,15 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
|||||||
* The admin can create, edit or delete metadata fields here.
|
* The admin can create, edit or delete metadata fields here.
|
||||||
*/
|
*/
|
||||||
export class MetadataSchemaComponent implements OnInit {
|
export class MetadataSchemaComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
|
||||||
* The namespace of the metadata schema
|
|
||||||
*/
|
|
||||||
namespace;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadata schema
|
* The metadata schema
|
||||||
*/
|
*/
|
||||||
metadataSchema: Observable<RemoteData<MetadataSchema>>;
|
metadataSchema$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all the fields attached to this metadata schema
|
* A list of all the fields attached to this metadata schema
|
||||||
*/
|
*/
|
||||||
metadataFields: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
metadataFields$: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination config used to display the list of metadata fields
|
* Pagination config used to display the list of metadata fields
|
||||||
@@ -49,6 +47,11 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
pageSizeOptions: [25, 50, 100, 200]
|
pageSizeOptions: [25, 50, 100, 200]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the list of MetadataFields needs an update
|
||||||
|
*/
|
||||||
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
constructor(private registryService: RegistryService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
@@ -68,7 +71,7 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
* @param params
|
* @param params
|
||||||
*/
|
*/
|
||||||
initialize(params) {
|
initialize(params) {
|
||||||
this.metadataSchema = this.registryService.getMetadataSchemaByName(params.schemaName);
|
this.metadataSchema$ = this.registryService.getMetadataSchemaByName(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||||
this.updateFields();
|
this.updateFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,18 +81,20 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
onPageChange(event) {
|
onPageChange(event) {
|
||||||
this.config.currentPage = event;
|
this.config.currentPage = event;
|
||||||
this.updateFields();
|
this.forceUpdateFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the list of fields by fetching it from the rest api or cache
|
* Update the list of fields by fetching it from the rest api or cache
|
||||||
*/
|
*/
|
||||||
private updateFields() {
|
private updateFields() {
|
||||||
this.metadataSchema.subscribe((schemaData) => {
|
this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
|
||||||
const schema = schemaData.payload;
|
switchMap(([schema, update]: [MetadataSchema, boolean]) => {
|
||||||
this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, this.config);
|
if (update) {
|
||||||
this.namespace = {namespace: schemaData.payload.namespace};
|
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config));
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,8 +102,7 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
* a new REST call
|
* a new REST call
|
||||||
*/
|
*/
|
||||||
public forceUpdateFields() {
|
public forceUpdateFields() {
|
||||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
this.needsUpdate$.next(true);
|
||||||
this.updateFields();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,6 +161,7 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
* Delete all the selected metadata fields
|
* Delete all the selected metadata fields
|
||||||
*/
|
*/
|
||||||
deleteFields() {
|
deleteFields() {
|
||||||
|
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
||||||
(fields) => {
|
(fields) => {
|
||||||
const tasks$ = [];
|
const tasks$ = [];
|
||||||
|
@@ -145,11 +145,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
this.modalService.open(CreateItemParentSelectorComponent);
|
this.modalService.open(CreateItemParentSelectorComponent);
|
||||||
}
|
}
|
||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
// model: {
|
},
|
||||||
// type: MenuItemType.LINK,
|
{
|
||||||
// text: 'menu.section.new_item',
|
id: 'new_process',
|
||||||
// link: '/submit'
|
parentID: 'new',
|
||||||
// } as LinkMenuItemModel,
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.new_process',
|
||||||
|
link: '/processes/new'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new_item_version',
|
id: 'new_item_version',
|
||||||
@@ -439,6 +445,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
icon: 'cogs',
|
icon: 'cogs',
|
||||||
index: 9
|
index: 9
|
||||||
},
|
},
|
||||||
|
/* Processes */
|
||||||
|
{
|
||||||
|
id: 'processes',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.processes',
|
||||||
|
link: '/processes'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'terminal',
|
||||||
|
index: 10
|
||||||
|
},
|
||||||
/* Workflow */
|
/* Workflow */
|
||||||
{
|
{
|
||||||
id: 'workflow',
|
id: 'workflow',
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
::ng-deep {
|
:host ::ng-deep {
|
||||||
.fa-chevron-right {
|
.fa-chevron-right {
|
||||||
padding-left: $spacer/2;
|
padding-left: $spacer/2;
|
||||||
font-size: 0.5rem;
|
font-size: 0.5rem;
|
||||||
|
@@ -32,6 +32,7 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version
|
|||||||
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
|
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
|
||||||
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||||
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||||
|
import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module that contains all components related to the Edit Item page administrator functionality
|
* Module that contains all components related to the Edit Item page administrator functionality
|
||||||
@@ -75,7 +76,8 @@ import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/cr
|
|||||||
ResourcePolicyCreateComponent,
|
ResourcePolicyCreateComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
BundleDataService
|
BundleDataService,
|
||||||
|
ObjectValuesPipe
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditItemPageModule {
|
export class EditItemPageModule {
|
||||||
|
@@ -36,7 +36,8 @@
|
|||||||
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
|
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
|
||||||
[bundle]="bundle"
|
[bundle]="bundle"
|
||||||
[item]="item"
|
[item]="item"
|
||||||
[columnSizes]="columnSizes">
|
[columnSizes]="columnSizes"
|
||||||
|
(dropObject)="dropBitstream(bundle, $event)">
|
||||||
</ds-item-edit-bitstream-bundle>
|
</ds-item-edit-bitstream-bundle>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="bundles?.length === 0"
|
<div *ngIf="bundles?.length === 0"
|
||||||
|
@@ -188,8 +188,21 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => {
|
it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => {
|
||||||
expect(bitstreamService.deleteAndReturnResponse).not.toHaveBeenCalledWith(bitstream1.id);
|
expect(bitstreamService.deleteAndReturnResponse).not.toHaveBeenCalledWith(bitstream1.id);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should send out a patch for the move operations', () => {
|
describe('when dropBitstream is called', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
comp.dropBitstream(bundle, {
|
||||||
|
fromIndex: 0,
|
||||||
|
toIndex: 50,
|
||||||
|
// tslint:disable-next-line:no-empty
|
||||||
|
finish: () => {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send out a patch for the move operation', () => {
|
||||||
expect(bundleService.patch).toHaveBeenCalled();
|
expect(bundleService.patch).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { Subscription } from 'rxjs/internal/Subscription';
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
@@ -9,8 +9,8 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { zip as observableZip, combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
import { zip as observableZip, of as observableOf } from 'rxjs';
|
||||||
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
||||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
@@ -22,8 +22,6 @@ import { Bundle } from '../../../core/shared/bundle.model';
|
|||||||
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||||
import { Operation } from 'fast-json-patch';
|
|
||||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
|
||||||
import { BundleDataService } from '../../../core/data/bundle-data.service';
|
import { BundleDataService } from '../../../core/data/bundle-data.service';
|
||||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||||
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
|
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||||
@@ -90,7 +88,8 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
public objectCache: ObjectCacheService,
|
public objectCache: ObjectCacheService,
|
||||||
public requestService: RequestService,
|
public requestService: RequestService,
|
||||||
public cdRef: ChangeDetectorRef,
|
public cdRef: ChangeDetectorRef,
|
||||||
public bundleService: BundleDataService
|
public bundleService: BundleDataService,
|
||||||
|
public zone: NgZone
|
||||||
) {
|
) {
|
||||||
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
||||||
}
|
}
|
||||||
@@ -143,7 +142,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit the current changes
|
* Submit the current changes
|
||||||
* Bitstreams that were dragged around send out a patch request with move operations to the rest API
|
|
||||||
* Bitstreams marked as deleted send out a delete request to the rest API
|
* Bitstreams marked as deleted send out a delete request to the rest API
|
||||||
* Display notifications and reset the current item/updates
|
* Display notifications and reset the current item/updates
|
||||||
*/
|
*/
|
||||||
@@ -151,32 +149,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
this.submitting = true;
|
this.submitting = true;
|
||||||
const bundlesOnce$ = this.bundles$.pipe(take(1));
|
const bundlesOnce$ = this.bundles$.pipe(take(1));
|
||||||
|
|
||||||
// Fetch all move operations for each bundle
|
|
||||||
const moveOperations$ = bundlesOnce$.pipe(
|
|
||||||
switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) =>
|
|
||||||
this.objectUpdatesService.getMoveOperations(bundle.self).pipe(
|
|
||||||
take(1),
|
|
||||||
map((operations: MoveOperation[]) => [...operations.map((operation: MoveOperation) => Object.assign(operation, {
|
|
||||||
from: `/_links/bitstreams${operation.from}/href`,
|
|
||||||
path: `/_links/bitstreams${operation.path}/href`
|
|
||||||
}))])
|
|
||||||
)
|
|
||||||
)))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send out an immediate patch request for each bundle
|
|
||||||
const patchResponses$ = observableCombineLatest(bundlesOnce$, moveOperations$).pipe(
|
|
||||||
switchMap(([bundles, moveOperationList]: [Bundle[], Operation[][]]) =>
|
|
||||||
observableZip(...bundles.map((bundle: Bundle, index: number) => {
|
|
||||||
if (isNotEmpty(moveOperationList[index])) {
|
|
||||||
return this.bundleService.patch(bundle, moveOperationList[index]);
|
|
||||||
} else {
|
|
||||||
return observableOf(undefined);
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch all removed bitstreams from the object update service
|
// Fetch all removed bitstreams from the object update service
|
||||||
const removedBitstreams$ = bundlesOnce$.pipe(
|
const removedBitstreams$ = bundlesOnce$.pipe(
|
||||||
switchMap((bundles: Bundle[]) => observableZip(
|
switchMap((bundles: Bundle[]) => observableZip(
|
||||||
@@ -201,19 +173,42 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Perform the setup actions from above in order and display notifications
|
// Perform the setup actions from above in order and display notifications
|
||||||
patchResponses$.pipe(
|
removedResponses$.pipe(take(1)).subscribe((responses: RestResponse[]) => {
|
||||||
switchMap((responses: RestResponse[]) => {
|
|
||||||
this.displayNotifications('item.edit.bitstreams.notifications.move', responses);
|
|
||||||
return removedResponses$
|
|
||||||
}),
|
|
||||||
take(1)
|
|
||||||
).subscribe((responses: RestResponse[]) => {
|
|
||||||
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
|
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
|
||||||
this.reset();
|
this.reset();
|
||||||
this.submitting = false;
|
this.submitting = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
|
||||||
|
* refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
|
||||||
|
* navigate the user to the correct page)
|
||||||
|
* @param bundle The bundle to send patch requests to
|
||||||
|
* @param event The event containing the index the bitstream came from and was dropped to
|
||||||
|
*/
|
||||||
|
dropBitstream(bundle: Bundle, event: any) {
|
||||||
|
this.zone.runOutsideAngular(() => {
|
||||||
|
if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) {
|
||||||
|
const moveOperation = Object.assign({
|
||||||
|
op: 'move',
|
||||||
|
from: `/_links/bitstreams/${event.fromIndex}/href`,
|
||||||
|
path: `/_links/bitstreams/${event.toIndex}/href`
|
||||||
|
});
|
||||||
|
this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RestResponse) => {
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.displayNotifications('item.edit.bitstreams.notifications.move', [response]);
|
||||||
|
// Remove all cached requests from this bundle and call the event's callback when the requests are cleared
|
||||||
|
this.requestService.removeByHrefSubstring(bundle.self).pipe(
|
||||||
|
filter((isCached) => isCached),
|
||||||
|
take(1)
|
||||||
|
).subscribe(() => event.finish());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display notifications
|
* Display notifications
|
||||||
* - Error notification for each failed response with their message
|
* - Error notification for each failed response with their message
|
||||||
|
@@ -17,5 +17,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-paginated-drag-and-drop-bitstream-list [bundle]="bundle" [columnSizes]="columnSizes"></ds-paginated-drag-and-drop-bitstream-list>
|
<ds-paginated-drag-and-drop-bitstream-list [bundle]="bundle" [columnSizes]="columnSizes" (dropObject)="dropObject.emit($event)"></ds-paginated-drag-and-drop-bitstream-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core';
|
||||||
import { Bundle } from '../../../../core/shared/bundle.model';
|
import { Bundle } from '../../../../core/shared/bundle.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||||
@@ -36,6 +36,13 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() columnSizes: ResponsiveTableSizes;
|
@Input() columnSizes: ResponsiveTableSizes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an event when the user drops an object on the pagination
|
||||||
|
* The event contains details about the index the object came from and is dropped to (across the entirety of the list,
|
||||||
|
* not just within a single page)
|
||||||
|
*/
|
||||||
|
@Output() dropObject: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bootstrap sizes used for the Bundle Name column
|
* The bootstrap sizes used for the Bundle Name column
|
||||||
* This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit
|
* This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit
|
||||||
|
@@ -7,18 +7,20 @@
|
|||||||
[collectionSize]="(objectsRD$ | async)?.payload?.totalElements"
|
[collectionSize]="(objectsRD$ | async)?.payload?.totalElements"
|
||||||
[disableRouteParameterUpdate]="true"
|
[disableRouteParameterUpdate]="true"
|
||||||
(pageChange)="switchPage($event)">
|
(pageChange)="switchPage($event)">
|
||||||
|
<ng-container *ngIf="!(loading$ | async)">
|
||||||
<div [id]="bundle.id" class="bundle-bitstreams-list"
|
<div [id]="bundle.id" class="bundle-bitstreams-list"
|
||||||
[ngClass]="{'mb-3': (objectsRD$ | async)?.payload?.totalElements > pageSize}"
|
[ngClass]="{'mb-3': (objectsRD$ | async)?.payload?.totalElements > pageSize}"
|
||||||
*ngVar="((updates$ | async) | dsObjectValues) as updateValues" cdkDropList (cdkDropListDropped)="drop($event)">
|
*ngVar="(updates$ | async) as updates" cdkDropList (cdkDropListDropped)="drop($event)">
|
||||||
<div class="row bitstream-row" *ngFor="let updateValue of updateValues" cdkDrag
|
<ng-container *ngIf="updates">
|
||||||
[id]="updateValue.field.uuid"
|
<div class="row bitstream-row" *ngFor="let uuid of customOrder" cdkDrag
|
||||||
|
[id]="uuid"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'table-warning': updateValue.changeType === 0,
|
'table-warning': updates[uuid].changeType === 0,
|
||||||
'table-danger': updateValue.changeType === 2,
|
'table-danger': updates[uuid].changeType === 2,
|
||||||
'table-success': updateValue.changeType === 1,
|
'table-success': updates[uuid].changeType === 1,
|
||||||
'bg-white': updateValue.changeType === undefined
|
'bg-white': updates[uuid].changeType === undefined
|
||||||
}">
|
}">
|
||||||
<ds-item-edit-bitstream [fieldUpdate]="updateValue"
|
<ds-item-edit-bitstream [fieldUpdate]="updates[uuid]"
|
||||||
[bundleUrl]="bundle.self"
|
[bundleUrl]="bundle.self"
|
||||||
[columnSizes]="columnSizes">
|
[columnSizes]="columnSizes">
|
||||||
<div class="d-flex align-items-center" slot="drag-handle" cdkDragHandle>
|
<div class="d-flex align-items-center" slot="drag-handle" cdkDragHandle>
|
||||||
@@ -26,5 +28,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</ds-item-edit-bitstream>
|
</ds-item-edit-bitstream>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ds-loading *ngIf="(loading$ | async)" [message]="'loading.bitstreams' | translate"></ds-loading>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
|
@@ -16,12 +16,15 @@ import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-siz
|
|||||||
import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../../../shared/testing/utils.test';
|
||||||
|
import { RequestService } from '../../../../../core/data/request.service';
|
||||||
|
|
||||||
describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
||||||
let comp: PaginatedDragAndDropBitstreamListComponent;
|
let comp: PaginatedDragAndDropBitstreamListComponent;
|
||||||
let fixture: ComponentFixture<PaginatedDragAndDropBitstreamListComponent>;
|
let fixture: ComponentFixture<PaginatedDragAndDropBitstreamListComponent>;
|
||||||
let objectUpdatesService: ObjectUpdatesService;
|
let objectUpdatesService: ObjectUpdatesService;
|
||||||
let bundleService: BundleDataService;
|
let bundleService: BundleDataService;
|
||||||
|
let objectValuesPipe: ObjectValuesPipe;
|
||||||
|
let requestService: RequestService;
|
||||||
|
|
||||||
const columnSizes = new ResponsiveTableSizes([
|
const columnSizes = new ResponsiveTableSizes([
|
||||||
new ResponsiveColumnSizes(2, 2, 3, 4, 4),
|
new ResponsiveColumnSizes(2, 2, 3, 4, 4),
|
||||||
@@ -97,15 +100,24 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
bundleService = jasmine.createSpyObj('bundleService', {
|
bundleService = jasmine.createSpyObj('bundleService', {
|
||||||
getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2]))
|
getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])),
|
||||||
|
getBitstreamsEndpoint: observableOf('')
|
||||||
|
});
|
||||||
|
|
||||||
|
objectValuesPipe = new ObjectValuesPipe();
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
hasByHrefObservable: observableOf(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective, ObjectValuesPipe],
|
declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
{ provide: BundleDataService, useValue: bundleService }
|
{ provide: BundleDataService, useValue: bundleService },
|
||||||
|
{ provide: ObjectValuesPipe, useValue: objectValuesPipe },
|
||||||
|
{ provide: RequestService, useValue: requestService }
|
||||||
], schemas: [
|
], schemas: [
|
||||||
NO_ERRORS_SCHEMA
|
NO_ERRORS_SCHEMA
|
||||||
]
|
]
|
||||||
|
@@ -8,6 +8,8 @@ import { switchMap } from 'rxjs/operators';
|
|||||||
import { PaginatedSearchOptions } from '../../../../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../../../../shared/search/paginated-search-options.model';
|
||||||
import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes';
|
import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe';
|
||||||
|
import { RequestService } from '../../../../../core/data/request.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-paginated-drag-and-drop-bitstream-list',
|
selector: 'ds-paginated-drag-and-drop-bitstream-list',
|
||||||
@@ -33,8 +35,10 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
|
|||||||
|
|
||||||
constructor(protected objectUpdatesService: ObjectUpdatesService,
|
constructor(protected objectUpdatesService: ObjectUpdatesService,
|
||||||
protected elRef: ElementRef,
|
protected elRef: ElementRef,
|
||||||
protected bundleService: BundleDataService) {
|
protected objectValuesPipe: ObjectValuesPipe,
|
||||||
super(objectUpdatesService, elRef);
|
protected bundleService: BundleDataService,
|
||||||
|
protected requestService: RequestService) {
|
||||||
|
super(objectUpdatesService, elRef, objectValuesPipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -46,12 +50,18 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
|
|||||||
*/
|
*/
|
||||||
initializeObjectsRD(): void {
|
initializeObjectsRD(): void {
|
||||||
this.objectsRD$ = this.currentPage$.pipe(
|
this.objectsRD$ = this.currentPage$.pipe(
|
||||||
switchMap((page: number) => this.bundleService.getBitstreams(
|
switchMap((page: number) => {
|
||||||
|
const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })});
|
||||||
|
return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe(
|
||||||
|
switchMap((href) => this.requestService.hasByHrefObservable(href)),
|
||||||
|
switchMap(() => this.bundleService.getBitstreams(
|
||||||
this.bundle.id,
|
this.bundle.id,
|
||||||
new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}),
|
paginatedOptions,
|
||||||
followLink('format')
|
followLink('format')
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -21,9 +21,9 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<a [href]="file._links.content.href" [download]="file.name">
|
<ds-file-download-link [href]="file._links.content.href" [download]="file.name">
|
||||||
{{"item.page.filesection.download" | translate}}
|
{{"item.page.filesection.download" | translate}}
|
||||||
</a>
|
</ds-file-download-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
||||||
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
||||||
<div class="file-section">
|
<div class="file-section">
|
||||||
<a *ngFor="let file of bitstreams; let last=last;" [href]="file?._links.content.href" [download]="file?.name">
|
<ds-file-download-link *ngFor="let file of bitstreams; let last=last;" [href]="file?._links.content.href" [download]="file?.name">
|
||||||
<span>{{file?.name}}</span>
|
<span>{{file?.name}}</span>
|
||||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||||
</a>
|
</ds-file-download-link>
|
||||||
</div>
|
</div>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="add">
|
<div class="add">
|
||||||
<a class="btn btn-lg btn-primary mt-1 ml-2" [routerLink]="['/submit']" role="button">
|
<button class="btn btn-lg btn-primary mt-1 ml-2" (click)="openDialog()" role="button">
|
||||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> {{'mydspace.new-submission' | translate}}
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> {{'mydspace.new-submission' | translate}}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, inject, TestBed, tick, fakeAsync } from '@angular/core/testing';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -21,6 +21,8 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
|
|||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock';
|
import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock';
|
||||||
import { UploaderService } from '../../shared/uploader/uploader.service';
|
import { UploaderService } from '../../shared/uploader/uploader.service';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
describe('MyDSpaceNewSubmissionComponent test', () => {
|
describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||||
|
|
||||||
@@ -54,6 +56,11 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
|||||||
{ provide: ScrollToService, useValue: getMockScrollToService() },
|
{ provide: ScrollToService, useValue: getMockScrollToService() },
|
||||||
{ provide: Store, useValue: store },
|
{ provide: Store, useValue: store },
|
||||||
{ provide: TranslateService, useValue: translateService },
|
{ provide: TranslateService, useValue: translateService },
|
||||||
|
{
|
||||||
|
provide: NgbModal, useValue: {
|
||||||
|
open: () => {/*comment*/}
|
||||||
|
}
|
||||||
|
},
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
MyDSpaceNewSubmissionComponent,
|
MyDSpaceNewSubmissionComponent,
|
||||||
UploaderService
|
UploaderService
|
||||||
@@ -86,6 +93,25 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
let fixture: ComponentFixture<MyDSpaceNewSubmissionComponent>;
|
||||||
|
let comp: MyDSpaceNewSubmissionComponent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MyDSpaceNewSubmissionComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call app.openDialog', () => {
|
||||||
|
spyOn(comp, 'openDialog');
|
||||||
|
const submissionButton = fixture.debugElement.query(By.css('button.btn-primary'));
|
||||||
|
submissionButton.triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(comp.openDialog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// declare a test component
|
// declare a test component
|
||||||
|
@@ -15,6 +15,9 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
|||||||
import { NotificationType } from '../../shared/notifications/models/notification-type';
|
import { NotificationType } from '../../shared/notifications/models/notification-type';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { SearchResult } from '../../shared/search/search-result.model';
|
import { SearchResult } from '../../shared/search/search-result.model';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { CreateItemParentSelectorComponent } from 'src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents the whole mydspace page header
|
* This component represents the whole mydspace page header
|
||||||
@@ -55,7 +58,9 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
|
|||||||
private halService: HALEndpointService,
|
private halService: HALEndpointService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private store: Store<SubmissionState>,
|
private store: Store<SubmissionState>,
|
||||||
private translate: TranslateService) {
|
private translate: TranslateService,
|
||||||
|
private router: Router,
|
||||||
|
private modalService: NgbModal) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,6 +110,14 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
|
|||||||
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
|
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called on clicking the button "New Submition", It opens a dialog for
|
||||||
|
* select a collection.
|
||||||
|
*/
|
||||||
|
openDialog() {
|
||||||
|
this.modalService.open(CreateItemParentSelectorComponent);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from the subscription
|
* Unsubscribe from the subscription
|
||||||
*/
|
*/
|
||||||
|
@@ -45,6 +45,20 @@ export function getProfileModulePath() {
|
|||||||
return `/${PROFILE_MODULE_PATH}`;
|
return `/${PROFILE_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REGISTER_PATH = 'register';
|
||||||
|
|
||||||
|
export function getRegisterPath() {
|
||||||
|
return `/${REGISTER_PATH}`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORGOT_PASSWORD_PATH = 'forgot';
|
||||||
|
|
||||||
|
export function getForgotPasswordPath() {
|
||||||
|
return `/${FORGOT_PASSWORD_PATH}`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const WORKFLOW_ITEM_MODULE_PATH = 'workflowitems';
|
const WORKFLOW_ITEM_MODULE_PATH = 'workflowitems';
|
||||||
|
|
||||||
export function getWorkflowItemModulePath() {
|
export function getWorkflowItemModulePath() {
|
||||||
@@ -71,6 +85,8 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
||||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||||
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||||
|
{ path: REGISTER_PATH, loadChildren: './register-page/register-page.module#RegisterPageModule' },
|
||||||
|
{ path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule' },
|
||||||
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||||
@@ -98,6 +114,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
path: PROFILE_MODULE_PATH,
|
path: PROFILE_MODULE_PATH,
|
||||||
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
||||||
},
|
},
|
||||||
|
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { NgZone } from '@angular/core';
|
||||||
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
import { CommunityListService, FlatNode } from './community-list-service';
|
import { CommunityListService, FlatNode } from './community-list-service';
|
||||||
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
|
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
|
||||||
import { BehaviorSubject, Observable, } from 'rxjs';
|
import { BehaviorSubject, Observable, } from 'rxjs';
|
||||||
@@ -14,21 +16,23 @@ export class CommunityListDatasource implements DataSource<FlatNode> {
|
|||||||
private communityList$ = new BehaviorSubject<FlatNode[]>([]);
|
private communityList$ = new BehaviorSubject<FlatNode[]>([]);
|
||||||
public loading$ = new BehaviorSubject<boolean>(false);
|
public loading$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
constructor(private communityListService: CommunityListService) {
|
constructor(private communityListService: CommunityListService,
|
||||||
|
private zone: NgZone) {
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
|
connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
|
||||||
return this.communityList$.asObservable();
|
return this.communityList$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCommunities(expandedNodes: FlatNode[]) {
|
loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]) {
|
||||||
this.loading$.next(true);
|
this.loading$.next(true);
|
||||||
|
this.zone.runOutsideAngular(() => {
|
||||||
this.communityListService.loadCommunities(expandedNodes).pipe(
|
this.communityListService.loadCommunities(findOptions, expandedNodes).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
finalize(() => this.loading$.next(false)),
|
finalize(() => this.zone.run(() => this.loading$.next(false))),
|
||||||
).subscribe((flatNodes: FlatNode[]) => {
|
).subscribe((flatNodes: FlatNode[]) => {
|
||||||
this.communityList$.next(flatNodes);
|
this.zone.run(() => this.communityList$.next(flatNodes));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,21 +1,19 @@
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { inject, TestBed } from '@angular/core/testing';
|
||||||
import { TestBed, inject, async } from '@angular/core/testing';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
import { StoreMock } from '../shared/testing/store.mock';
|
import { StoreMock } from '../shared/testing/store.mock';
|
||||||
import { CommunityListService, FlatNode, toFlatNode } from './community-list-service';
|
import { CommunityListService, FlatNode, toFlatNode } from './community-list-service';
|
||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
|
||||||
import { PageInfo } from '../core/shared/page-info.model';
|
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import {
|
|
||||||
createFailedRemoteDataObject$,
|
|
||||||
createSuccessfulRemoteDataObject$
|
|
||||||
} from '../shared/remote-data.utils';
|
|
||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { Collection } from '../core/shared/collection.model';
|
import { Collection } from '../core/shared/collection.model';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { FindListOptions } from '../core/data/request.models';
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
|
import { PageInfo } from '../core/shared/page-info.model';
|
||||||
|
|
||||||
describe('CommunityListService', () => {
|
describe('CommunityListService', () => {
|
||||||
let store: StoreMock<AppState>;
|
let store: StoreMock<AppState>;
|
||||||
@@ -210,13 +208,18 @@ describe('CommunityListService', () => {
|
|||||||
let flatNodeList;
|
let flatNodeList;
|
||||||
describe('None expanded: should return list containing only flatnodes of the test top communities page 1 and 2', () => {
|
describe('None expanded: should return list containing only flatnodes of the test top communities page 1 and 2', () => {
|
||||||
let findTopSpy;
|
let findTopSpy;
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
findTopSpy = spyOn(communityDataServiceStub, 'findTop').and.callThrough();
|
findTopSpy = spyOn(communityDataServiceStub, 'findTop').and.callThrough();
|
||||||
service.getNextPageTopCommunities();
|
|
||||||
|
|
||||||
const sub = service.loadCommunities(null)
|
service.loadCommunities({
|
||||||
.subscribe((value) => flatNodeList = value);
|
currentPage: 2,
|
||||||
sub.unsubscribe();
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, null)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('flatnode list should contain just flatnodes of top community list page 1 and 2', () => {
|
it('flatnode list should contain just flatnodes of top community list page 1 and 2', () => {
|
||||||
expect(findTopSpy).toHaveBeenCalled();
|
expect(findTopSpy).toHaveBeenCalled();
|
||||||
@@ -236,10 +239,16 @@ describe('CommunityListService', () => {
|
|||||||
describe('should transform all communities in a list of flatnodes with possible subcoms and collections as subflatnodes if they\'re expanded', () => {
|
describe('should transform all communities in a list of flatnodes with possible subcoms and collections as subflatnodes if they\'re expanded', () => {
|
||||||
let flatNodeList;
|
let flatNodeList;
|
||||||
describe('None expanded: should return list containing only flatnodes of the test top communities', () => {
|
describe('None expanded: should return list containing only flatnodes of the test top communities', () => {
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
const sub = service.loadCommunities(null)
|
service.loadCommunities({
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
currentPage: 1,
|
||||||
sub.unsubscribe();
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, null)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('length of flatnode list should be as big as top community list', () => {
|
it('length of flatnode list should be as big as top community list', () => {
|
||||||
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length);
|
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length);
|
||||||
@@ -256,7 +265,7 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
const expandedNodes = [];
|
const expandedNodes = [];
|
||||||
mockListOfTopCommunitiesPage1.map((community: Community) => {
|
mockListOfTopCommunitiesPage1.map((community: Community) => {
|
||||||
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
|
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
|
||||||
@@ -264,9 +273,15 @@ describe('CommunityListService', () => {
|
|||||||
communityFlatNode.currentCommunityPage = 1;
|
communityFlatNode.currentCommunityPage = 1;
|
||||||
expandedNodes.push(communityFlatNode);
|
expandedNodes.push(communityFlatNode);
|
||||||
});
|
});
|
||||||
const sub = service.loadCommunities(expandedNodes)
|
service.loadCommunities({
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
currentPage: 1,
|
||||||
sub.unsubscribe();
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, expandedNodes)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('length of flatnode list should be as big as top community list and size of its possible page-limited children', () => {
|
it('length of flatnode list should be as big as top community list and size of its possible page-limited children', () => {
|
||||||
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
|
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
|
||||||
@@ -281,14 +296,20 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Just first top comm expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
describe('Just first top comm expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[0], observableOf(true), 0, true, null);
|
const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[0], observableOf(true), 0, true, null);
|
||||||
communityFlatNode.currentCollectionPage = 1;
|
communityFlatNode.currentCollectionPage = 1;
|
||||||
communityFlatNode.currentCommunityPage = 1;
|
communityFlatNode.currentCommunityPage = 1;
|
||||||
const expandedNodes = [communityFlatNode];
|
const expandedNodes = [communityFlatNode];
|
||||||
const sub = service.loadCommunities(expandedNodes)
|
service.loadCommunities({
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
currentPage: 1,
|
||||||
sub.unsubscribe();
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, expandedNodes)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('length of flatnode list should be as big as top community list and size of page-limited children of first top community', () => {
|
it('length of flatnode list should be as big as top community list and size of page-limited children of first top community', () => {
|
||||||
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length);
|
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length);
|
||||||
@@ -300,14 +321,20 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Just second top comm expanded, collections at page 2: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
describe('Just second top comm expanded, collections at page 2: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[1], observableOf(true), 0, true, null);
|
const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[1], observableOf(true), 0, true, null);
|
||||||
communityFlatNode.currentCollectionPage = 2;
|
communityFlatNode.currentCollectionPage = 2;
|
||||||
communityFlatNode.currentCommunityPage = 1;
|
communityFlatNode.currentCommunityPage = 1;
|
||||||
const expandedNodes = [communityFlatNode];
|
const expandedNodes = [communityFlatNode];
|
||||||
const sub = service.loadCommunities(expandedNodes)
|
service.loadCommunities({
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
currentPage: 1,
|
||||||
sub.unsubscribe();
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}, expandedNodes)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('length of flatnode list should be as big as top community list and size of page-limited children of second top community', () => {
|
it('length of flatnode list should be as big as top community list and size of page-limited children of second top community', () => {
|
||||||
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockCollectionsPage1.length + mockCollectionsPage2.length);
|
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockCollectionsPage1.length + mockCollectionsPage2.length);
|
||||||
@@ -333,10 +360,13 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
let flatNodeList;
|
let flatNodeList;
|
||||||
describe('None expanded: should return list containing only flatnodes of the communities in the test list', () => {
|
describe('None expanded: should return list containing only flatnodes of the communities in the test list', () => {
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null)
|
service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null)
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
.pipe(take(1))
|
||||||
sub.unsubscribe();
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('length of flatnode list should be as big as community test list', () => {
|
it('length of flatnode list should be as big as community test list', () => {
|
||||||
expect(flatNodeList.length).toEqual(listOfCommunities.length);
|
expect(flatNodeList.length).toEqual(listOfCommunities.length);
|
||||||
@@ -353,7 +383,7 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
const expandedNodes = [];
|
const expandedNodes = [];
|
||||||
listOfCommunities.map((community: Community) => {
|
listOfCommunities.map((community: Community) => {
|
||||||
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
|
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
|
||||||
@@ -361,9 +391,12 @@ describe('CommunityListService', () => {
|
|||||||
communityFlatNode.currentCommunityPage = 1;
|
communityFlatNode.currentCommunityPage = 1;
|
||||||
expandedNodes.push(communityFlatNode);
|
expandedNodes.push(communityFlatNode);
|
||||||
});
|
});
|
||||||
const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes)
|
service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes)
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
.pipe(take(1))
|
||||||
sub.unsubscribe();
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('length of flatnode list should be as big as community test list and size of its possible children', () => {
|
it('length of flatnode list should be as big as community test list and size of its possible children', () => {
|
||||||
expect(flatNodeList.length).toEqual(listOfCommunities.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
|
expect(flatNodeList.length).toEqual(listOfCommunities.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
|
||||||
@@ -397,10 +430,13 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
let flatNodeList;
|
let flatNodeList;
|
||||||
describe('should return list containing only flatnode corresponding to that community', () => {
|
describe('should return list containing only flatnode corresponding to that community', () => {
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
const sub = service.transformCommunity(communityWithNoSubcomsOrColls, 0, null, null)
|
service.transformCommunity(communityWithNoSubcomsOrColls, 0, null, null)
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
.pipe(take(1))
|
||||||
sub.unsubscribe();
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('length of flatnode list should be 1', () => {
|
it('length of flatnode list should be 1', () => {
|
||||||
expect(flatNodeList.length).toEqual(1);
|
expect(flatNodeList.length).toEqual(1);
|
||||||
@@ -426,10 +462,14 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
let flatNodeList;
|
let flatNodeList;
|
||||||
describe('should return list containing only flatnode corresponding to that community', () => {
|
describe('should return list containing only flatnode corresponding to that community', () => {
|
||||||
beforeAll(() => {
|
beforeAll((done) => {
|
||||||
const sub = service.transformCommunity(communityWithSubcoms, 0, null, null)
|
service.transformCommunity(communityWithSubcoms, 0, null, null)
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
.pipe(take(1))
|
||||||
sub.unsubscribe();
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
it('length of flatnode list should be 1', () => {
|
it('length of flatnode list should be 1', () => {
|
||||||
expect(flatNodeList.length).toEqual(1);
|
expect(flatNodeList.length).toEqual(1);
|
||||||
@@ -455,14 +495,17 @@ describe('CommunityListService', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let flatNodeList;
|
let flatNodeList;
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
const communityFlatNode = toFlatNode(communityWithSubcoms, observableOf(true), 0, true, null);
|
const communityFlatNode = toFlatNode(communityWithSubcoms, observableOf(true), 0, true, null);
|
||||||
communityFlatNode.currentCollectionPage = 1;
|
communityFlatNode.currentCollectionPage = 1;
|
||||||
communityFlatNode.currentCommunityPage = 1;
|
communityFlatNode.currentCommunityPage = 1;
|
||||||
const expandedNodes = [communityFlatNode];
|
const expandedNodes = [communityFlatNode];
|
||||||
const sub = service.transformCommunity(communityWithSubcoms, 0, null, expandedNodes)
|
service.transformCommunity(communityWithSubcoms, 0, null, expandedNodes)
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
.pipe(take(1))
|
||||||
sub.unsubscribe();
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('list of flatnodes is length is 1 + nrOfSubcoms & first flatnode is of expanded test community', () => {
|
it('list of flatnodes is length is 1 + nrOfSubcoms & first flatnode is of expanded test community', () => {
|
||||||
expect(flatNodeList.length).toEqual(1 + mockSubcommunities1Page1.length);
|
expect(flatNodeList.length).toEqual(1 + mockSubcommunities1Page1.length);
|
||||||
@@ -485,7 +528,7 @@ describe('CommunityListService', () => {
|
|||||||
describe('should return list containing flatnodes of that community, its collections of the first two pages', () => {
|
describe('should return list containing flatnodes of that community, its collections of the first two pages', () => {
|
||||||
let communityWithCollections;
|
let communityWithCollections;
|
||||||
let flatNodeList;
|
let flatNodeList;
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
communityWithCollections = Object.assign(new Community(), {
|
communityWithCollections = Object.assign(new Community(), {
|
||||||
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
@@ -500,9 +543,12 @@ describe('CommunityListService', () => {
|
|||||||
communityFlatNode.currentCollectionPage = 2;
|
communityFlatNode.currentCollectionPage = 2;
|
||||||
communityFlatNode.currentCommunityPage = 1;
|
communityFlatNode.currentCommunityPage = 1;
|
||||||
const expandedNodes = [communityFlatNode];
|
const expandedNodes = [communityFlatNode];
|
||||||
const sub = service.transformCommunity(communityWithCollections, 0, null, expandedNodes)
|
service.transformCommunity(communityWithCollections, 0, null, expandedNodes)
|
||||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
.pipe(take(1))
|
||||||
sub.unsubscribe();
|
.subscribe((value) => {
|
||||||
|
flatNodeList = value;
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('list of flatnodes is length is 1 + nrOfCollections & first flatnode is of expanded test community', () => {
|
it('list of flatnodes is length is 1 + nrOfCollections & first flatnode is of expanded test community', () => {
|
||||||
expect(flatNodeList.length).toEqual(1 + mockCollectionsPage1.length + mockCollectionsPage2.length);
|
expect(flatNodeList.length).toEqual(1 + mockCollectionsPage1.length + mockCollectionsPage2.length);
|
||||||
@@ -533,7 +579,7 @@ describe('CommunityListService', () => {
|
|||||||
|
|
||||||
describe('getIsExpandable', () => {
|
describe('getIsExpandable', () => {
|
||||||
describe('should return true', () => {
|
describe('should return true', () => {
|
||||||
it('if community has subcommunities', () => {
|
it('if community has subcommunities', (done) => {
|
||||||
const communityWithSubcoms = Object.assign(new Community(), {
|
const communityWithSubcoms = Object.assign(new Community(), {
|
||||||
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||||
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||||
@@ -546,9 +592,10 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
service.getIsExpandable(communityWithSubcoms).pipe(take(1)).subscribe((result) => {
|
service.getIsExpandable(communityWithSubcoms).pipe(take(1)).subscribe((result) => {
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('if community has collections', () => {
|
it('if community has collections', (done) => {
|
||||||
const communityWithCollections = Object.assign(new Community(), {
|
const communityWithCollections = Object.assign(new Community(), {
|
||||||
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
@@ -561,11 +608,12 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
service.getIsExpandable(communityWithCollections).pipe(take(1)).subscribe((result) => {
|
service.getIsExpandable(communityWithCollections).pipe(take(1)).subscribe((result) => {
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('should return false', () => {
|
describe('should return false', () => {
|
||||||
it('if community has neither subcommunities nor collections', () => {
|
it('if community has neither subcommunities nor collections', (done) => {
|
||||||
const communityWithNoSubcomsOrColls = Object.assign(new Community(), {
|
const communityWithNoSubcomsOrColls = Object.assign(new Community(), {
|
||||||
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
|
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
|
||||||
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
|
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
|
||||||
@@ -578,6 +626,7 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
service.getIsExpandable(communityWithNoSubcomsOrColls).pipe(take(1)).subscribe((result) => {
|
service.getIsExpandable(communityWithNoSubcomsOrColls).pipe(take(1)).subscribe((result) => {
|
||||||
expect(result).toEqual(false);
|
expect(result).toEqual(false);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,11 +4,12 @@ import { combineLatest as observableCombineLatest } from 'rxjs/internal/observab
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
import { map, flatMap } from 'rxjs/operators';
|
||||||
import { catchError, filter, map, switchMap, take } from 'rxjs/operators';
|
|
||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { Collection } from '../core/shared/collection.model';
|
import { Collection } from '../core/shared/collection.model';
|
||||||
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { PageInfo } from '../core/shared/page-info.model';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
@@ -46,8 +47,7 @@ export class ShowMoreFlatNode {
|
|||||||
// Helper method to combine an flatten an array of observables of flatNode arrays
|
// Helper method to combine an flatten an array of observables of flatNode arrays
|
||||||
export const combineAndFlatten = (obsList: Array<Observable<FlatNode[]>>): Observable<FlatNode[]> =>
|
export const combineAndFlatten = (obsList: Array<Observable<FlatNode[]>>): Observable<FlatNode[]> =>
|
||||||
observableCombineLatest(...obsList).pipe(
|
observableCombineLatest(...obsList).pipe(
|
||||||
map((matrix: FlatNode[][]) =>
|
map((matrix: any[][]) => [].concat(...matrix))
|
||||||
matrix.reduce((combinedList, currentList: FlatNode[]) => [...combinedList, ...currentList]))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,6 +99,8 @@ const communityListStateSelector = (state: AppState) => state.communityList;
|
|||||||
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
|
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
|
||||||
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
|
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
|
||||||
|
|
||||||
|
export const MAX_COMCOLS_PER_PAGE = 50;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
|
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
|
||||||
* and connection to the store to retrieve and save the state of the community list
|
* and connection to the store to retrieve and save the state of the community list
|
||||||
@@ -107,26 +109,8 @@ const loadingNodeSelector = createSelector(communityListStateSelector, (communit
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityListService {
|
export class CommunityListService {
|
||||||
|
|
||||||
// page-limited list of top-level communities
|
|
||||||
payloads$: Array<Observable<PaginatedList<Community>>>;
|
|
||||||
|
|
||||||
topCommunitiesConfig: PaginationComponentOptions;
|
|
||||||
topCommunitiesSortConfig: SortOptions;
|
|
||||||
|
|
||||||
maxSubCommunitiesPerPage: number;
|
|
||||||
maxCollectionsPerPage: number;
|
|
||||||
|
|
||||||
constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService,
|
constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService,
|
||||||
private store: Store<any>) {
|
private store: Store<any>) {
|
||||||
this.topCommunitiesConfig = new PaginationComponentOptions();
|
|
||||||
this.topCommunitiesConfig.id = 'top-level-pagination';
|
|
||||||
this.topCommunitiesConfig.pageSize = 10;
|
|
||||||
this.topCommunitiesConfig.currentPage = 1;
|
|
||||||
this.topCommunitiesSortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
|
||||||
this.initTopCommunityList();
|
|
||||||
|
|
||||||
this.maxSubCommunitiesPerPage = 3;
|
|
||||||
this.maxCollectionsPerPage = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void {
|
saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void {
|
||||||
@@ -141,57 +125,46 @@ export class CommunityListService {
|
|||||||
return this.store.select(loadingNodeSelector);
|
return this.store.select(loadingNodeSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Increases the payload so it contains the next page of top level communities
|
|
||||||
*/
|
|
||||||
getNextPageTopCommunities(): void {
|
|
||||||
this.topCommunitiesConfig.currentPage = this.topCommunitiesConfig.currentPage + 1;
|
|
||||||
this.payloads$ = [...this.payloads$, this.communityDataService.findTop({
|
|
||||||
currentPage: this.topCommunitiesConfig.currentPage,
|
|
||||||
elementsPerPage: this.topCommunitiesConfig.pageSize,
|
|
||||||
sort: {
|
|
||||||
field: this.topCommunitiesSortConfig.field,
|
|
||||||
direction: this.topCommunitiesSortConfig.direction
|
|
||||||
}
|
|
||||||
}).pipe(
|
|
||||||
take(1),
|
|
||||||
map((results) => results.payload),
|
|
||||||
)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all top communities, limited by page, and transforms this in a list of flatNodes.
|
* Gets all top communities, limited by page, and transforms this in a list of flatNodes.
|
||||||
* @param expandedNodes List of expanded nodes; if a node is not expanded its subCommunities and collections need
|
* @param expandedNodes List of expanded nodes; if a node is not expanded its subCommunities and collections need
|
||||||
* not be added to the list
|
* not be added to the list
|
||||||
*/
|
*/
|
||||||
loadCommunities(expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
||||||
const res = this.payloads$.map((payload) => {
|
const currentPage = findOptions.currentPage;
|
||||||
return payload.pipe(
|
const topCommunities = [];
|
||||||
take(1),
|
for (let i = 1; i <= currentPage; i++) {
|
||||||
switchMap((result: PaginatedList<Community>) => {
|
const pagination: FindListOptions = Object.assign({}, findOptions, { currentPage: i });
|
||||||
return this.transformListOfCommunities(result, 0, null, expandedNodes);
|
topCommunities.push(this.getTopCommunities(pagination));
|
||||||
}),
|
}
|
||||||
catchError(() => observableOf([])),
|
const topComs$ = observableCombineLatest(...topCommunities).pipe(
|
||||||
|
map((coms: Array<PaginatedList<Community>>) => {
|
||||||
|
const newPages: Community[][] = coms.map((unit: PaginatedList<Community>) => unit.page);
|
||||||
|
const newPage: Community[] = [].concat(...newPages);
|
||||||
|
let newPageInfo = new PageInfo();
|
||||||
|
if (coms && coms.length > 0) {
|
||||||
|
newPageInfo = Object.assign({}, coms[0].pageInfo, { currentPage })
|
||||||
|
}
|
||||||
|
return new PaginatedList(newPageInfo, newPage);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
});
|
return topComs$.pipe(flatMap((topComs: PaginatedList<Community>) => this.transformListOfCommunities(topComs, 0, null, expandedNodes)));
|
||||||
return combineAndFlatten(res);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puts the initial top level communities in a list to be called upon
|
* Puts the initial top level communities in a list to be called upon
|
||||||
*/
|
*/
|
||||||
private initTopCommunityList(): void {
|
private getTopCommunities(options: FindListOptions): Observable<PaginatedList<Community>> {
|
||||||
this.payloads$ = [this.communityDataService.findTop({
|
return this.communityDataService.findTop({
|
||||||
currentPage: this.topCommunitiesConfig.currentPage,
|
currentPage: options.currentPage,
|
||||||
elementsPerPage: this.topCommunitiesConfig.pageSize,
|
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||||
sort: {
|
sort: {
|
||||||
field: this.topCommunitiesSortConfig.field,
|
field: options.sort.field,
|
||||||
direction: this.topCommunitiesSortConfig.direction
|
direction: options.sort.direction
|
||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
take(1),
|
|
||||||
map((results) => results.payload),
|
map((results) => results.payload),
|
||||||
)];
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,16 +179,15 @@ export class CommunityListService {
|
|||||||
parent: FlatNode,
|
parent: FlatNode,
|
||||||
expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
||||||
if (isNotEmpty(listOfPaginatedCommunities.page)) {
|
if (isNotEmpty(listOfPaginatedCommunities.page)) {
|
||||||
let currentPage = this.topCommunitiesConfig.currentPage;
|
let currentPage = listOfPaginatedCommunities.currentPage;
|
||||||
if (isNotEmpty(parent)) {
|
if (isNotEmpty(parent)) {
|
||||||
currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage;
|
currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage;
|
||||||
}
|
}
|
||||||
const isNotAllCommunities = (listOfPaginatedCommunities.totalElements > (listOfPaginatedCommunities.elementsPerPage * currentPage));
|
|
||||||
let obsList = listOfPaginatedCommunities.page
|
let obsList = listOfPaginatedCommunities.page
|
||||||
.map((community: Community) => {
|
.map((community: Community) => {
|
||||||
return this.transformCommunity(community, level, parent, expandedNodes)
|
return this.transformCommunity(community, level, parent, expandedNodes)
|
||||||
});
|
});
|
||||||
if (isNotAllCommunities && listOfPaginatedCommunities.currentPage > currentPage) {
|
if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) {
|
||||||
obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])];
|
obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,13 +224,12 @@ export class CommunityListService {
|
|||||||
let subcoms = [];
|
let subcoms = [];
|
||||||
for (let i = 1; i <= currentCommunityPage; i++) {
|
for (let i = 1; i <= currentCommunityPage; i++) {
|
||||||
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
|
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
|
||||||
elementsPerPage: this.maxSubCommunitiesPerPage,
|
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||||
currentPage: i
|
currentPage: i
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded),
|
getSucceededRemoteData(),
|
||||||
take(1),
|
flatMap((rd: RemoteData<PaginatedList<Community>>) =>
|
||||||
switchMap((rd: RemoteData<PaginatedList<Community>>) =>
|
|
||||||
this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes))
|
this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -271,16 +242,15 @@ export class CommunityListService {
|
|||||||
let collections = [];
|
let collections = [];
|
||||||
for (let i = 1; i <= currentCollectionPage; i++) {
|
for (let i = 1; i <= currentCollectionPage; i++) {
|
||||||
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
|
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
|
||||||
elementsPerPage: this.maxCollectionsPerPage,
|
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||||
currentPage: i
|
currentPage: i
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Collection>>) => rd.hasSucceeded),
|
getSucceededRemoteData(),
|
||||||
take(1),
|
|
||||||
map((rd: RemoteData<PaginatedList<Collection>>) => {
|
map((rd: RemoteData<PaginatedList<Collection>>) => {
|
||||||
let nodes = rd.payload.page
|
let nodes = rd.payload.page
|
||||||
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
||||||
if ((rd.payload.elementsPerPage * currentCollectionPage) < rd.payload.totalElements && rd.payload.currentPage > currentCollectionPage) {
|
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
|
||||||
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
|
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
@@ -305,21 +275,18 @@ export class CommunityListService {
|
|||||||
let hasColls$: Observable<boolean>;
|
let hasColls$: Observable<boolean>;
|
||||||
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded),
|
getSucceededRemoteData(),
|
||||||
take(1),
|
|
||||||
map((results) => results.payload.totalElements > 0),
|
map((results) => results.payload.totalElements > 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Collection>>) => rd.hasSucceeded),
|
getSucceededRemoteData(),
|
||||||
take(1),
|
|
||||||
map((results) => results.payload.totalElements > 0),
|
map((results) => results.payload.totalElements > 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
let hasChildren$: Observable<boolean>;
|
let hasChildren$: Observable<boolean>;
|
||||||
hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe(
|
hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe(
|
||||||
take(1),
|
|
||||||
map(([hasSubcoms, hasColls]: [boolean, boolean]) => {
|
map(([hasSubcoms, hasColls]: [boolean, boolean]) => {
|
||||||
if (hasSubcoms || hasColls) {
|
if (hasSubcoms || hasColls) {
|
||||||
return true;
|
return true;
|
||||||
|
@@ -114,15 +114,9 @@ describe('CommunityListComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
communityListServiceStub = {
|
communityListServiceStub = {
|
||||||
topPageSize: 2,
|
pageSize: 2,
|
||||||
topCurrentPage: 1,
|
|
||||||
collectionPageSize: 2,
|
|
||||||
subcommunityPageSize: 2,
|
|
||||||
expandedNodes: [],
|
expandedNodes: [],
|
||||||
loadingNode: null,
|
loadingNode: null,
|
||||||
getNextPageTopCommunities() {
|
|
||||||
this.topCurrentPage++;
|
|
||||||
},
|
|
||||||
getLoadingNodeFromStore() {
|
getLoadingNodeFromStore() {
|
||||||
return observableOf(this.loadingNode);
|
return observableOf(this.loadingNode);
|
||||||
},
|
},
|
||||||
@@ -133,12 +127,12 @@ describe('CommunityListComponent', () => {
|
|||||||
this.expandedNodes = expandedNodes;
|
this.expandedNodes = expandedNodes;
|
||||||
this.loadingNode = loadingNode;
|
this.loadingNode = loadingNode;
|
||||||
},
|
},
|
||||||
loadCommunities(expandedNodes) {
|
loadCommunities(options, expandedNodes) {
|
||||||
let flatnodes;
|
let flatnodes;
|
||||||
let showMoreTopComNode = false;
|
let showMoreTopComNode = false;
|
||||||
flatnodes = [...mockTopFlatnodesUnexpanded];
|
flatnodes = [...mockTopFlatnodesUnexpanded];
|
||||||
const currentPage = this.topCurrentPage;
|
const currentPage = options.currentPage;
|
||||||
const elementsPerPage = this.topPageSize;
|
const elementsPerPage = this.pageSize;
|
||||||
let endPageIndex = (currentPage * elementsPerPage);
|
let endPageIndex = (currentPage * elementsPerPage);
|
||||||
if (endPageIndex >= flatnodes.length) {
|
if (endPageIndex >= flatnodes.length) {
|
||||||
endPageIndex = flatnodes.length;
|
endPageIndex = flatnodes.length;
|
||||||
@@ -171,14 +165,14 @@ describe('CommunityListComponent', () => {
|
|||||||
collFlatnodes = [...collFlatnodes, toFlatNode(coll, observableOf(false), topNode.level + 1, false, topNode)];
|
collFlatnodes = [...collFlatnodes, toFlatNode(coll, observableOf(false), topNode.level + 1, false, topNode)];
|
||||||
});
|
});
|
||||||
if (isNotEmpty(subComFlatnodes)) {
|
if (isNotEmpty(subComFlatnodes)) {
|
||||||
const endSubComIndex = this.subcommunityPageSize * expandedParent.currentCommunityPage;
|
const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage;
|
||||||
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
|
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
|
||||||
if (subComFlatnodes.length > endSubComIndex) {
|
if (subComFlatnodes.length > endSubComIndex) {
|
||||||
flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)];
|
flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isNotEmpty(collFlatnodes)) {
|
if (isNotEmpty(collFlatnodes)) {
|
||||||
const endColIndex = this.collectionPageSize * expandedParent.currentCollectionPage;
|
const endColIndex = this.pageSize * expandedParent.currentCollectionPage;
|
||||||
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
|
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
|
||||||
if (collFlatnodes.length > endColIndex) {
|
if (collFlatnodes.length > endColIndex) {
|
||||||
flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)];
|
flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)];
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { FindListOptions } from '../../core/data/request.models';
|
||||||
import { CommunityListService, FlatNode } from '../community-list-service';
|
import { CommunityListService, FlatNode } from '../community-list-service';
|
||||||
import { CommunityListDatasource } from '../community-list-datasource';
|
import { CommunityListDatasource } from '../community-list-datasource';
|
||||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||||
@@ -27,17 +29,24 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
dataSource: CommunityListDatasource;
|
dataSource: CommunityListDatasource;
|
||||||
|
|
||||||
constructor(private communityListService: CommunityListService) {
|
paginationConfig: FindListOptions;
|
||||||
|
|
||||||
|
constructor(private communityListService: CommunityListService,
|
||||||
|
private zone: NgZone) {
|
||||||
|
this.paginationConfig = new FindListOptions();
|
||||||
|
this.paginationConfig.elementsPerPage = 2;
|
||||||
|
this.paginationConfig.currentPage = 1;
|
||||||
|
this.paginationConfig.sort = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.dataSource = new CommunityListDatasource(this.communityListService);
|
this.dataSource = new CommunityListDatasource(this.communityListService, this.zone);
|
||||||
this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => {
|
this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => {
|
||||||
this.loadingNode = result;
|
this.loadingNode = result;
|
||||||
});
|
});
|
||||||
this.communityListService.getExpandedNodesFromStore().pipe(take(1)).subscribe((result) => {
|
this.communityListService.getExpandedNodesFromStore().pipe(take(1)).subscribe((result) => {
|
||||||
this.expandedNodes = [...result];
|
this.expandedNodes = [...result];
|
||||||
this.dataSource.loadCommunities(this.expandedNodes);
|
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +83,7 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
node.currentCommunityPage = 1;
|
node.currentCommunityPage = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.dataSource.loadCommunities(this.expandedNodes);
|
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,10 +103,10 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
||||||
parentNodeInExpandedNodes.currentCommunityPage++;
|
parentNodeInExpandedNodes.currentCommunityPage++;
|
||||||
}
|
}
|
||||||
this.dataSource.loadCommunities(this.expandedNodes);
|
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||||
} else {
|
} else {
|
||||||
this.communityListService.getNextPageTopCommunities();
|
this.paginationConfig.currentPage++;
|
||||||
this.dataSource.loadCommunities(this.expandedNodes);
|
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,12 +1,18 @@
|
|||||||
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest } from '../data/request.models';
|
import {
|
||||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
AuthGetRequest,
|
||||||
|
AuthPostRequest,
|
||||||
|
GetRequest,
|
||||||
|
PostRequest,
|
||||||
|
RestRequest,
|
||||||
|
TokenPostRequest
|
||||||
|
} from '../data/request.models';
|
||||||
|
import { AuthStatusResponse, ErrorResponse, TokenResponse } from '../cache/response.models';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { getResponseFromEntry } from '../shared/operators';
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
@@ -15,6 +21,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
export class AuthRequestService {
|
export class AuthRequestService {
|
||||||
protected linkName = 'authn';
|
protected linkName = 'authn';
|
||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
|
protected shortlivedtokensEndpoint = 'shortlivedtokens';
|
||||||
|
|
||||||
constructor(protected halService: HALEndpointService,
|
constructor(protected halService: HALEndpointService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -67,4 +74,19 @@ export class AuthRequestService {
|
|||||||
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
||||||
distinctUntilChanged());
|
distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a POST request to retrieve a short-lived token which provides download access of restricted files
|
||||||
|
*/
|
||||||
|
public getShortlivedToken(): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(`${this.linkName}/${this.shortlivedtokensEndpoint}`).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => new TokenPostRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
tap((request: PostRequest) => this.requestService.configure(request)),
|
||||||
|
switchMap((request: PostRequest) => this.requestService.getByUUID(request.uuid)),
|
||||||
|
getResponseFromEntry(),
|
||||||
|
map((response: TokenResponse) => response.token)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,6 @@ import { Action } from '@ngrx/store';
|
|||||||
// import type function
|
// import type function
|
||||||
import { type } from '../../shared/ngrx/type';
|
import { type } from '../../shared/ngrx/type';
|
||||||
// import models
|
// import models
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
@@ -31,9 +30,6 @@ export const AuthActionTypes = {
|
|||||||
LOG_OUT: type('dspace/auth/LOG_OUT'),
|
LOG_OUT: type('dspace/auth/LOG_OUT'),
|
||||||
LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'),
|
LOG_OUT_ERROR: type('dspace/auth/LOG_OUT_ERROR'),
|
||||||
LOG_OUT_SUCCESS: type('dspace/auth/LOG_OUT_SUCCESS'),
|
LOG_OUT_SUCCESS: type('dspace/auth/LOG_OUT_SUCCESS'),
|
||||||
REGISTRATION: type('dspace/auth/REGISTRATION'),
|
|
||||||
REGISTRATION_ERROR: type('dspace/auth/REGISTRATION_ERROR'),
|
|
||||||
REGISTRATION_SUCCESS: type('dspace/auth/REGISTRATION_SUCCESS'),
|
|
||||||
SET_REDIRECT_URL: type('dspace/auth/SET_REDIRECT_URL'),
|
SET_REDIRECT_URL: type('dspace/auth/SET_REDIRECT_URL'),
|
||||||
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
||||||
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
||||||
@@ -263,48 +259,6 @@ export class RetrieveTokenAction implements Action {
|
|||||||
public type: string = AuthActionTypes.RETRIEVE_TOKEN;
|
public type: string = AuthActionTypes.RETRIEVE_TOKEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign up.
|
|
||||||
* @class RegistrationAction
|
|
||||||
* @implements {Action}
|
|
||||||
*/
|
|
||||||
export class RegistrationAction implements Action {
|
|
||||||
public type: string = AuthActionTypes.REGISTRATION;
|
|
||||||
payload: EPerson;
|
|
||||||
|
|
||||||
constructor(user: EPerson) {
|
|
||||||
this.payload = user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign up error.
|
|
||||||
* @class RegistrationErrorAction
|
|
||||||
* @implements {Action}
|
|
||||||
*/
|
|
||||||
export class RegistrationErrorAction implements Action {
|
|
||||||
public type: string = AuthActionTypes.REGISTRATION_ERROR;
|
|
||||||
payload: Error;
|
|
||||||
|
|
||||||
constructor(payload: Error) {
|
|
||||||
this.payload = payload;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign up success.
|
|
||||||
* @class RegistrationSuccessAction
|
|
||||||
* @implements {Action}
|
|
||||||
*/
|
|
||||||
export class RegistrationSuccessAction implements Action {
|
|
||||||
public type: string = AuthActionTypes.REGISTRATION_SUCCESS;
|
|
||||||
payload: EPerson;
|
|
||||||
|
|
||||||
constructor(user: EPerson) {
|
|
||||||
this.payload = user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add uthentication message.
|
* Add uthentication message.
|
||||||
* @class AddAuthenticationMessageAction
|
* @class AddAuthenticationMessageAction
|
||||||
@@ -439,9 +393,6 @@ export type AuthActions
|
|||||||
| CheckAuthenticationTokenCookieAction
|
| CheckAuthenticationTokenCookieAction
|
||||||
| RedirectWhenAuthenticationIsRequiredAction
|
| RedirectWhenAuthenticationIsRequiredAction
|
||||||
| RedirectWhenTokenExpiredAction
|
| RedirectWhenTokenExpiredAction
|
||||||
| RegistrationAction
|
|
||||||
| RegistrationErrorAction
|
|
||||||
| RegistrationSuccessAction
|
|
||||||
| AddAuthenticationMessageAction
|
| AddAuthenticationMessageAction
|
||||||
| RefreshTokenAction
|
| RefreshTokenAction
|
||||||
| RefreshTokenErrorAction
|
| RefreshTokenErrorAction
|
||||||
|
@@ -150,7 +150,8 @@ describe('AuthEffects', () => {
|
|||||||
|
|
||||||
describe('authenticatedSuccess$', () => {
|
describe('authenticatedSuccess$', () => {
|
||||||
|
|
||||||
it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', () => {
|
it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', (done) => {
|
||||||
|
spyOn((authEffects as any).authService, 'storeToken');
|
||||||
actions = hot('--a-', {
|
actions = hot('--a-', {
|
||||||
a: {
|
a: {
|
||||||
type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: {
|
type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: {
|
||||||
@@ -163,8 +164,14 @@ describe('AuthEffects', () => {
|
|||||||
|
|
||||||
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) });
|
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) });
|
||||||
|
|
||||||
expect(authEffects.authenticatedSuccess$).toBeObservable(expected);
|
authEffects.authenticatedSuccess$.subscribe(() => {
|
||||||
|
expect(authServiceStub.storeToken).toHaveBeenCalledWith(token);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(authEffects.authenticatedSuccess$).toBeObservable(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkToken$', () => {
|
describe('checkToken$', () => {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
// import @ngrx
|
// import @ngrx
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
import { Action, select, Store } from '@ngrx/store';
|
import { Action, select, Store } from '@ngrx/store';
|
||||||
@@ -30,9 +30,6 @@ import {
|
|||||||
RefreshTokenAction,
|
RefreshTokenAction,
|
||||||
RefreshTokenErrorAction,
|
RefreshTokenErrorAction,
|
||||||
RefreshTokenSuccessAction,
|
RefreshTokenSuccessAction,
|
||||||
RegistrationAction,
|
|
||||||
RegistrationErrorAction,
|
|
||||||
RegistrationSuccessAction,
|
|
||||||
RetrieveAuthenticatedEpersonAction,
|
RetrieveAuthenticatedEpersonAction,
|
||||||
RetrieveAuthenticatedEpersonErrorAction,
|
RetrieveAuthenticatedEpersonErrorAction,
|
||||||
RetrieveAuthenticatedEpersonSuccessAction,
|
RetrieveAuthenticatedEpersonSuccessAction,
|
||||||
@@ -65,7 +62,6 @@ export class AuthEffects {
|
|||||||
@Effect()
|
@Effect()
|
||||||
public authenticateSuccess$: Observable<Action> = this.actions$.pipe(
|
public authenticateSuccess$: Observable<Action> = this.actions$.pipe(
|
||||||
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
|
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
|
||||||
tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)),
|
|
||||||
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,6 +78,7 @@ export class AuthEffects {
|
|||||||
@Effect()
|
@Effect()
|
||||||
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
|
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
|
||||||
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
||||||
|
tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)),
|
||||||
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -136,18 +133,6 @@ export class AuthEffects {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@Effect()
|
|
||||||
public createUser$: Observable<Action> = this.actions$.pipe(
|
|
||||||
ofType(AuthActionTypes.REGISTRATION),
|
|
||||||
debounceTime(500), // to remove when functionality is implemented
|
|
||||||
switchMap((action: RegistrationAction) => {
|
|
||||||
return this.authService.create(action.payload).pipe(
|
|
||||||
map((user: EPerson) => new RegistrationSuccessAction(user)),
|
|
||||||
catchError((error) => observableOf(new RegistrationErrorAction(error)))
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
@Effect()
|
@Effect()
|
||||||
public retrieveToken$: Observable<Action> = this.actions$.pipe(
|
public retrieveToken$: Observable<Action> = this.actions$.pipe(
|
||||||
ofType(AuthActionTypes.RETRIEVE_TOKEN),
|
ofType(AuthActionTypes.RETRIEVE_TOKEN),
|
||||||
|
@@ -115,7 +115,6 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.AUTHENTICATE_ERROR:
|
case AuthActionTypes.AUTHENTICATE_ERROR:
|
||||||
case AuthActionTypes.REGISTRATION_ERROR:
|
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
@@ -157,18 +156,6 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
userId: undefined
|
userId: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.REGISTRATION:
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
authenticated: false,
|
|
||||||
authToken: undefined,
|
|
||||||
error: undefined,
|
|
||||||
loading: true,
|
|
||||||
info: undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
case AuthActionTypes.REGISTRATION_SUCCESS:
|
|
||||||
return state;
|
|
||||||
|
|
||||||
case AuthActionTypes.REFRESH_TOKEN:
|
case AuthActionTypes.REFRESH_TOKEN:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
refreshing: true,
|
refreshing: true,
|
||||||
|
@@ -1,17 +1,14 @@
|
|||||||
import { async, inject, TestBed } from '@angular/core/testing';
|
import { async, inject, TestBed } from '@angular/core/testing';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { authReducer, AuthState } from './auth.reducer';
|
import { authReducer, AuthState } from './auth.reducer';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { AuthService, IMPERSONATING_COOKIE } from './auth.service';
|
import { AuthService, IMPERSONATING_COOKIE } from './auth.service';
|
||||||
import { RouterStub } from '../../shared/testing/router.stub';
|
import { RouterStub } from '../../shared/testing/router.stub';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
|
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service.stub';
|
import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service.stub';
|
||||||
import { AuthRequestService } from './auth-request.service';
|
import { AuthRequestService } from './auth-request.service';
|
||||||
@@ -49,6 +46,7 @@ describe('AuthService test', () => {
|
|||||||
let storage: CookieService;
|
let storage: CookieService;
|
||||||
let token: AuthTokenInfo;
|
let token: AuthTokenInfo;
|
||||||
let authenticatedState;
|
let authenticatedState;
|
||||||
|
let unAuthenticatedState;
|
||||||
let linkService;
|
let linkService;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@@ -67,6 +65,13 @@ describe('AuthService test', () => {
|
|||||||
authToken: token,
|
authToken: token,
|
||||||
user: EPersonMock
|
user: EPersonMock
|
||||||
};
|
};
|
||||||
|
unAuthenticatedState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
authToken: undefined,
|
||||||
|
user: undefined
|
||||||
|
};
|
||||||
authRequest = new AuthRequestServiceStub();
|
authRequest = new AuthRequestServiceStub();
|
||||||
routeStub = new ActivatedRouteStub();
|
routeStub = new ActivatedRouteStub();
|
||||||
linkService = {
|
linkService = {
|
||||||
@@ -214,6 +219,12 @@ describe('AuthService test', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return the shortlived token when user is logged in', () => {
|
||||||
|
authService.getShortlivedToken().subscribe((shortlivedToken: string) => {
|
||||||
|
expect(shortlivedToken).toEqual(authRequest.mockShortLivedToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should return token object when it is valid', () => {
|
it('should return token object when it is valid', () => {
|
||||||
authService.hasValidAuthenticationToken().subscribe((tokenState: AuthTokenInfo) => {
|
authService.hasValidAuthenticationToken().subscribe((tokenState: AuthTokenInfo) => {
|
||||||
expect(tokenState).toBe(token);
|
expect(tokenState).toBe(token);
|
||||||
@@ -448,4 +459,44 @@ describe('AuthService test', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when user is not logged in', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
StoreModule.forRoot({ authReducer }, {
|
||||||
|
runtimeChecks: {
|
||||||
|
strictStateImmutability: false,
|
||||||
|
strictActionImmutability: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthRequestService, useValue: authRequest },
|
||||||
|
{ provide: REQUEST, useValue: {} },
|
||||||
|
{ provide: Router, useValue: routerStub },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
|
{ provide: RemoteDataBuildService, useValue: linkService },
|
||||||
|
CookieService,
|
||||||
|
AuthService
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = unAuthenticatedState;
|
||||||
|
});
|
||||||
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should return null for the shortlived token', () => {
|
||||||
|
authService.getShortlivedToken().subscribe((shortlivedToken: string) => {
|
||||||
|
expect(shortlivedToken).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -270,18 +270,6 @@ export class AuthService {
|
|||||||
return observableOf(authMethods);
|
return observableOf(authMethods);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new user
|
|
||||||
* @returns {User}
|
|
||||||
*/
|
|
||||||
public create(user: EPerson): Observable<EPerson> {
|
|
||||||
// Normally you would do an HTTP request to POST the user
|
|
||||||
// details and then return the new user object
|
|
||||||
// but, let's just return the new user for this example.
|
|
||||||
// this._authenticated = true;
|
|
||||||
return observableOf(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End session
|
* End session
|
||||||
* @returns {Observable<boolean>}
|
* @returns {Observable<boolean>}
|
||||||
@@ -546,4 +534,14 @@ export class AuthService {
|
|||||||
return this.getImpersonateID() === epersonId;
|
return this.getImpersonateID() === epersonId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a short-lived token for appending to download urls of restricted files
|
||||||
|
* Returns null if the user isn't authenticated
|
||||||
|
*/
|
||||||
|
getShortlivedToken(): Observable<string> {
|
||||||
|
return this.isAuthenticated().pipe(
|
||||||
|
switchMap((authenticated) => authenticated ? this.authRequestService.getShortlivedToken() : observableOf(null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
45
src/app/core/auth/token-response-parsing.service.spec.ts
Normal file
45
src/app/core/auth/token-response-parsing.service.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { TokenResponseParsingService } from './token-response-parsing.service';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { TokenResponse } from '../cache/response.models';
|
||||||
|
|
||||||
|
describe('TokenResponseParsingService', () => {
|
||||||
|
let service: TokenResponseParsingService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new TokenResponseParsingService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
|
it('should return a TokenResponse containing the token', () => {
|
||||||
|
const data = {
|
||||||
|
payload: {
|
||||||
|
token: 'valid-token'
|
||||||
|
},
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
const expected = new TokenResponse(data.payload.token, true, 200, 'OK');
|
||||||
|
expect(service.parse(undefined, data)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty TokenResponse when payload doesn\'t contain a token', () => {
|
||||||
|
const data = {
|
||||||
|
payload: {},
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'OK'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
const expected = new TokenResponse(null, false, 200, 'OK');
|
||||||
|
expect(service.parse(undefined, data)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error TokenResponse when the response failed', () => {
|
||||||
|
const data = {
|
||||||
|
payload: {},
|
||||||
|
statusCode: 400,
|
||||||
|
statusText: 'BAD REQUEST'
|
||||||
|
} as DSpaceRESTV2Response;
|
||||||
|
const expected = new TokenResponse(null, false, 400, 'BAD REQUEST');
|
||||||
|
expect(service.parse(undefined, data)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
23
src/app/core/auth/token-response-parsing.service.ts
Normal file
23
src/app/core/auth/token-response-parsing.service.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ResponseParsingService } from '../data/parsing.service';
|
||||||
|
import { RestRequest } from '../data/request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { RestResponse, TokenResponse } from '../cache/response.models';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a token string
|
||||||
|
* wrapped in a TokenResponse
|
||||||
|
*/
|
||||||
|
export class TokenResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload.token) && (data.statusCode === 200)) {
|
||||||
|
return new TokenResponse(data.payload.token, true, data.statusCode, data.statusText);
|
||||||
|
} else {
|
||||||
|
return new TokenResponse(null, false, data.statusCode, data.statusText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,21 +1,38 @@
|
|||||||
import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
describe('I18nBreadcrumbResolver', () => {
|
describe('I18nBreadcrumbResolver', () => {
|
||||||
describe('resolve', () => {
|
describe('resolve', () => {
|
||||||
let resolver: I18nBreadcrumbResolver;
|
let resolver: I18nBreadcrumbResolver;
|
||||||
let i18nBreadcrumbService: any;
|
let i18nBreadcrumbService: any;
|
||||||
let i18nKey: string;
|
let i18nKey: string;
|
||||||
let path: string;
|
let route: any;
|
||||||
|
let parentSegment;
|
||||||
|
let segment;
|
||||||
|
let expectedPath;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
i18nKey = 'example.key';
|
i18nKey = 'example.key';
|
||||||
path = 'rest.com/path/to/breadcrumb';
|
parentSegment = 'path';
|
||||||
|
segment = 'breadcrumb';
|
||||||
|
route = {
|
||||||
|
data: { breadcrumbKey: i18nKey },
|
||||||
|
routeConfig: {
|
||||||
|
path: segment
|
||||||
|
},
|
||||||
|
parent: {
|
||||||
|
routeConfig: {
|
||||||
|
path: parentSegment
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
};
|
||||||
|
expectedPath = new URLCombiner(parentSegment, segment).toString();
|
||||||
i18nBreadcrumbService = {};
|
i18nBreadcrumbService = {};
|
||||||
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService);
|
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the breadcrumb config', () => {
|
it('should resolve the breadcrumb config', () => {
|
||||||
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path] } as any, {} as any);
|
const resolvedConfig = resolver.resolve(route, {} as any);
|
||||||
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path };
|
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath };
|
||||||
expect(resolvedConfig).toEqual(expectedConfig);
|
expect(resolvedConfig).toEqual(expectedConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
|
||||||
import { hasNoValue } from '../../shared/empty.util';
|
import { hasNoValue } from '../../shared/empty.util';
|
||||||
|
import { currentPathFromSnapshot } from '../../shared/utils/route.utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
||||||
@@ -25,7 +26,7 @@ export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>>
|
|||||||
if (hasNoValue(key)) {
|
if (hasNoValue(key)) {
|
||||||
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
|
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
|
||||||
}
|
}
|
||||||
const fullPath = route.url.join('');
|
const fullPath = currentPathFromSnapshot(route);
|
||||||
return { provider: this.breadcrumbService, key: key, url: fullPath };
|
return { provider: this.breadcrumbService, key: key, url: fullPath };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -150,12 +150,7 @@ export class RemoteDataBuildService {
|
|||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((response: DSOSuccessResponse) => {
|
map((response: DSOSuccessResponse) => {
|
||||||
if (hasValue((response as DSOSuccessResponse).pageInfo)) {
|
if (hasValue((response as DSOSuccessResponse).pageInfo)) {
|
||||||
const resPageInfo = (response as DSOSuccessResponse).pageInfo;
|
return (response as DSOSuccessResponse).pageInfo;
|
||||||
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
|
||||||
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
|
|
||||||
} else {
|
|
||||||
return resPageInfo;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
73
src/app/core/cache/response.models.ts
vendored
73
src/app/core/cache/response.models.ts
vendored
@@ -6,15 +6,13 @@ import { ConfigObject } from '../config/models/config.model';
|
|||||||
import { FacetValue } from '../../shared/search/facet-value.model';
|
import { FacetValue } from '../../shared/search/facet-value.model';
|
||||||
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
|
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
|
||||||
import { IntegrationModel } from '../integration/models/integration.model';
|
import { IntegrationModel } from '../integration/models/integration.model';
|
||||||
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
|
|
||||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
|
||||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { SubmissionObject } from '../submission/models/submission-object.model';
|
import { SubmissionObject } from '../submission/models/submission-object.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
import { ContentSource } from '../shared/content-source.model';
|
import { ContentSource } from '../shared/content-source.model';
|
||||||
|
import { Registration } from '../shared/registration.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
@@ -40,48 +38,6 @@ export class DSOSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A successful response containing a list of MetadataSchemas wrapped in a RegistryMetadataschemasResponse
|
|
||||||
*/
|
|
||||||
export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
|
||||||
constructor(
|
|
||||||
public metadataschemasResponse: RegistryMetadataschemasResponse,
|
|
||||||
public statusCode: number,
|
|
||||||
public statusText: string,
|
|
||||||
public pageInfo?: PageInfo
|
|
||||||
) {
|
|
||||||
super(true, statusCode, statusText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A successful response containing a list of MetadataFields wrapped in a RegistryMetadatafieldsResponse
|
|
||||||
*/
|
|
||||||
export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
|
||||||
constructor(
|
|
||||||
public metadatafieldsResponse: RegistryMetadatafieldsResponse,
|
|
||||||
public statusCode: number,
|
|
||||||
public statusText: string,
|
|
||||||
public pageInfo?: PageInfo
|
|
||||||
) {
|
|
||||||
super(true, statusCode, statusText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A successful response containing a list of BitstreamFormats wrapped in a RegistryBitstreamformatsResponse
|
|
||||||
*/
|
|
||||||
export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
|
||||||
constructor(
|
|
||||||
public bitstreamformatsResponse: RegistryBitstreamformatsResponse,
|
|
||||||
public statusCode: number,
|
|
||||||
public statusText: string,
|
|
||||||
public pageInfo?: PageInfo
|
|
||||||
) {
|
|
||||||
super(true, statusCode, statusText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A successful response containing exactly one MetadataSchema
|
* A successful response containing exactly one MetadataSchema
|
||||||
*/
|
*/
|
||||||
@@ -211,6 +167,20 @@ export class AuthStatusResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A REST Response containing a token
|
||||||
|
*/
|
||||||
|
export class TokenResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public token: string,
|
||||||
|
public isSuccessful: boolean,
|
||||||
|
public statusCode: number,
|
||||||
|
public statusText: string
|
||||||
|
) {
|
||||||
|
super(isSuccessful, statusCode, statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class IntegrationSuccessResponse extends RestResponse {
|
export class IntegrationSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public dataDefinition: PaginatedList<IntegrationModel>,
|
public dataDefinition: PaginatedList<IntegrationModel>,
|
||||||
@@ -302,4 +272,17 @@ export class ContentSourceSuccessResponse extends RestResponse {
|
|||||||
super(true, statusCode, statusText);
|
super(true, statusCode, statusText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A successful response containing a Registration
|
||||||
|
*/
|
||||||
|
export class RegistrationSuccessResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public registration: Registration,
|
||||||
|
public statusCode: number,
|
||||||
|
public statusText: string,
|
||||||
|
) {
|
||||||
|
super(true, statusCode, statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||||
|
|
||||||
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
|
||||||
@@ -69,13 +68,8 @@ import { ItemDataService } from './data/item-data.service';
|
|||||||
import { LicenseDataService } from './data/license-data.service';
|
import { LicenseDataService } from './data/license-data.service';
|
||||||
import { LookupRelationService } from './data/lookup-relation.service';
|
import { LookupRelationService } from './data/lookup-relation.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||||
import { MetadatafieldParsingService } from './data/metadatafield-parsing.service';
|
|
||||||
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
|
|
||||||
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
|
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
|
||||||
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
|
||||||
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
|
|
||||||
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
|
|
||||||
import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service';
|
|
||||||
import { RelationshipTypeService } from './data/relationship-type.service';
|
import { RelationshipTypeService } from './data/relationship-type.service';
|
||||||
import { RelationshipService } from './data/relationship.service';
|
import { RelationshipService } from './data/relationship.service';
|
||||||
import { ResourcePolicyService } from './resource-policy/resource-policy.service';
|
import { ResourcePolicyService } from './resource-policy/resource-policy.service';
|
||||||
@@ -143,8 +137,17 @@ import { VersionDataService } from './data/version-data.service';
|
|||||||
import { VersionHistoryDataService } from './data/version-history-data.service';
|
import { VersionHistoryDataService } from './data/version-history-data.service';
|
||||||
import { Version } from './shared/version.model';
|
import { Version } from './shared/version.model';
|
||||||
import { VersionHistory } from './shared/version-history.model';
|
import { VersionHistory } from './shared/version-history.model';
|
||||||
|
import { Script } from '../process-page/scripts/script.model';
|
||||||
|
import { Process } from '../process-page/processes/process.model';
|
||||||
|
import { ProcessDataService } from './data/processes/process-data.service';
|
||||||
|
import { ScriptDataService } from './data/processes/script-data.service';
|
||||||
|
import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service';
|
||||||
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
||||||
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||||
|
import { Registration } from './shared/registration.model';
|
||||||
|
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||||
|
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||||
|
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -201,9 +204,6 @@ const PROVIDERS = [
|
|||||||
FacetValueResponseParsingService,
|
FacetValueResponseParsingService,
|
||||||
FacetValueMapResponseParsingService,
|
FacetValueMapResponseParsingService,
|
||||||
FacetConfigResponseParsingService,
|
FacetConfigResponseParsingService,
|
||||||
RegistryMetadataschemasResponseParsingService,
|
|
||||||
RegistryMetadatafieldsResponseParsingService,
|
|
||||||
RegistryBitstreamformatsResponseParsingService,
|
|
||||||
MappedCollectionsReponseParsingService,
|
MappedCollectionsReponseParsingService,
|
||||||
DebugResponseParsingService,
|
DebugResponseParsingService,
|
||||||
SearchResponseParsingService,
|
SearchResponseParsingService,
|
||||||
@@ -223,8 +223,6 @@ const PROVIDERS = [
|
|||||||
JsonPatchOperationsBuilder,
|
JsonPatchOperationsBuilder,
|
||||||
AuthorityService,
|
AuthorityService,
|
||||||
IntegrationResponseParsingService,
|
IntegrationResponseParsingService,
|
||||||
MetadataschemaParsingService,
|
|
||||||
MetadatafieldParsingService,
|
|
||||||
UploaderService,
|
UploaderService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
@@ -264,6 +262,12 @@ const PROVIDERS = [
|
|||||||
LicenseDataService,
|
LicenseDataService,
|
||||||
ItemTypeDataService,
|
ItemTypeDataService,
|
||||||
WorkflowActionDataService,
|
WorkflowActionDataService,
|
||||||
|
ProcessDataService,
|
||||||
|
ScriptDataService,
|
||||||
|
ProcessFilesResponseParsingService,
|
||||||
|
MetadataSchemaDataService,
|
||||||
|
MetadataFieldDataService,
|
||||||
|
TokenResponseParsingService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
@@ -312,9 +316,12 @@ export const models =
|
|||||||
ItemType,
|
ItemType,
|
||||||
ExternalSource,
|
ExternalSource,
|
||||||
ExternalSourceEntry,
|
ExternalSourceEntry,
|
||||||
|
Script,
|
||||||
|
Process,
|
||||||
Version,
|
Version,
|
||||||
VersionHistory,
|
VersionHistory,
|
||||||
WorkflowAction
|
WorkflowAction,
|
||||||
|
Registration
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -88,10 +88,12 @@ export class BundleDataService extends DataService<Bundle> {
|
|||||||
/**
|
/**
|
||||||
* Get the bitstreams endpoint for a bundle
|
* Get the bitstreams endpoint for a bundle
|
||||||
* @param bundleId
|
* @param bundleId
|
||||||
|
* @param searchOptions
|
||||||
*/
|
*/
|
||||||
getBitstreamsEndpoint(bundleId: string): Observable<string> {
|
getBitstreamsEndpoint(bundleId: string, searchOptions?: PaginatedSearchOptions): Observable<string> {
|
||||||
return this.getBrowseEndpoint().pipe(
|
return this.getBrowseEndpoint().pipe(
|
||||||
switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`))
|
switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`)),
|
||||||
|
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +104,8 @@ export class BundleDataService extends DataService<Bundle> {
|
|||||||
* @param linksToFollow The {@link FollowLinkConfig}s for the request
|
* @param linksToFollow The {@link FollowLinkConfig}s for the request
|
||||||
*/
|
*/
|
||||||
getBitstreams(bundleId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array<FollowLinkConfig<Bitstream>>): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
getBitstreams(bundleId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array<FollowLinkConfig<Bitstream>>): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||||
const hrefObs = this.getBitstreamsEndpoint(bundleId).pipe(
|
const hrefObs = this.getBitstreamsEndpoint(bundleId, searchOptions);
|
||||||
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
|
|
||||||
);
|
|
||||||
hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
take(1)
|
take(1)
|
||||||
).subscribe((href) => {
|
).subscribe((href) => {
|
||||||
|
@@ -13,13 +13,19 @@ import { RequestEntry } from './request.reducer';
|
|||||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { Collection } from '../shared/collection.model';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
|
||||||
|
import { hot, getTestScheduler, cold } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
const url = 'fake-url';
|
const url = 'fake-url';
|
||||||
const collectionId = 'fake-collection-id';
|
const collectionId = 'fake-collection-id';
|
||||||
|
|
||||||
describe('CollectionDataService', () => {
|
describe('CollectionDataService', () => {
|
||||||
let service: CollectionDataService;
|
let service: CollectionDataService;
|
||||||
|
let scheduler: TestScheduler;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let translate: TranslateService;
|
let translate: TranslateService;
|
||||||
let notificationsService: any;
|
let notificationsService: any;
|
||||||
@@ -27,6 +33,44 @@ describe('CollectionDataService', () => {
|
|||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
let halService: any;
|
let halService: any;
|
||||||
|
|
||||||
|
const mockCollection1: Collection = Object.assign(new Collection(), {
|
||||||
|
id: 'test-collection-1-1',
|
||||||
|
name: 'test-collection-1',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/collections/test-collection-1-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCollection2: Collection = Object.assign(new Collection(), {
|
||||||
|
id: 'test-collection-2-2',
|
||||||
|
name: 'test-collection-2',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/collections/test-collection-2-2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCollection3: Collection = Object.assign(new Collection(), {
|
||||||
|
id: 'test-collection-3-3',
|
||||||
|
name: 'test-collection-3',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/collections/test-collection-3-3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryString = 'test-string';
|
||||||
|
const communityId = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
|
||||||
|
const pageInfo = new PageInfo();
|
||||||
|
const array = [mockCollection1, mockCollection2, mockCollection3];
|
||||||
|
const paginatedList = new PaginatedList(pageInfo, array);
|
||||||
|
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
|
||||||
|
|
||||||
describe('when the requests are successful', () => {
|
describe('when the requests are successful', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createService();
|
createService();
|
||||||
@@ -74,6 +118,43 @@ describe('CollectionDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when calling getAuthorizedCollection', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
spyOn(service, 'getAuthorizedCollection').and.callThrough();
|
||||||
|
spyOn(service, 'getAuthorizedCollectionByCommunity').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should proxy the call to getAuthorizedCollection', () => {
|
||||||
|
scheduler.schedule(() => service.getAuthorizedCollection(queryString));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(service.getAuthorizedCollection).toHaveBeenCalledWith(queryString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollection', () => {
|
||||||
|
const result = service.getAuthorizedCollection(queryString)
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: paginatedListRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should proxy the call to getAuthorizedCollectionByCommunity', () => {
|
||||||
|
scheduler.schedule(() => service.getAuthorizedCollectionByCommunity(communityId, queryString));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(service.getAuthorizedCollectionByCommunity).toHaveBeenCalledWith(communityId, queryString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollectionByCommunity', () => {
|
||||||
|
const result = service.getAuthorizedCollectionByCommunity(communityId, queryString)
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: paginatedListRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the requests are unsuccessful', () => {
|
describe('when the requests are unsuccessful', () => {
|
||||||
@@ -117,7 +198,9 @@ describe('CollectionDataService', () => {
|
|||||||
function createService(requestEntry$?) {
|
function createService(requestEntry$?) {
|
||||||
requestService = getMockRequestService(requestEntry$);
|
requestService = getMockRequestService(requestEntry$);
|
||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
buildList: jasmine.createSpy('buildList')
|
buildList: hot('a|', {
|
||||||
|
a: paginatedListRD
|
||||||
|
})
|
||||||
});
|
});
|
||||||
objectCache = jasmine.createSpyObj('objectCache', {
|
objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
remove: jasmine.createSpy('remove')
|
remove: jasmine.createSpy('remove')
|
||||||
|
@@ -72,14 +72,18 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
/**
|
/**
|
||||||
* Get all collections the user is authorized to submit to
|
* Get all collections the user is authorized to submit to
|
||||||
*
|
*
|
||||||
|
* @param query limit the returned collection to those with metadata values matching the query terms.
|
||||||
* @param options The [[FindListOptions]] object
|
* @param options The [[FindListOptions]] object
|
||||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||||
* collection list
|
* collection list
|
||||||
*/
|
*/
|
||||||
getAuthorizedCollection(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Collection>>): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||||
const searchHref = 'findAuthorized';
|
const searchHref = 'findSubmitAuthorized';
|
||||||
|
options = Object.assign({}, options, {
|
||||||
|
searchParams: [new RequestParam('query', query)]
|
||||||
|
});
|
||||||
|
|
||||||
return this.searchBy(searchHref, options).pipe(
|
return this.searchBy(searchHref, options, ...linksToFollow).pipe(
|
||||||
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
|
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,14 +91,18 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
* Get all collections the user is authorized to submit to, by community
|
* Get all collections the user is authorized to submit to, by community
|
||||||
*
|
*
|
||||||
* @param communityId The community id
|
* @param communityId The community id
|
||||||
|
* @param query limit the returned collection to those with metadata values matching the query terms.
|
||||||
* @param options The [[FindListOptions]] object
|
* @param options The [[FindListOptions]] object
|
||||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||||
* collection list
|
* collection list
|
||||||
*/
|
*/
|
||||||
getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||||
const searchHref = 'findAuthorizedByCommunity';
|
const searchHref = 'findSubmitAuthorizedByCommunity';
|
||||||
options = Object.assign({}, options, {
|
options = Object.assign({}, options, {
|
||||||
searchParams: [new RequestParam('uuid', communityId)]
|
searchParams: [
|
||||||
|
new RequestParam('uuid', communityId),
|
||||||
|
new RequestParam('query', query)
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.searchBy(searchHref, options).pipe(
|
return this.searchBy(searchHref, options).pipe(
|
||||||
@@ -108,7 +116,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
* true if the user has at least one collection to submit to
|
* true if the user has at least one collection to submit to
|
||||||
*/
|
*/
|
||||||
hasAuthorizedCollection(): Observable<boolean> {
|
hasAuthorizedCollection(): Observable<boolean> {
|
||||||
const searchHref = 'findAuthorized';
|
const searchHref = 'findSubmitAuthorized';
|
||||||
const options = new FindListOptions();
|
const options = new FindListOptions();
|
||||||
options.elementsPerPage = 1;
|
options.elementsPerPage = 1;
|
||||||
|
|
||||||
|
@@ -45,11 +45,12 @@ import {
|
|||||||
FindListOptions,
|
FindListOptions,
|
||||||
FindListRequest,
|
FindListRequest,
|
||||||
GetRequest,
|
GetRequest,
|
||||||
PatchRequest
|
PatchRequest, PutRequest
|
||||||
} from './request.models';
|
} from './request.models';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
|
||||||
export abstract class DataService<T extends CacheableObject> {
|
export abstract class DataService<T extends CacheableObject> {
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
@@ -343,7 +344,9 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
tap((href: string) => {
|
tap((href: string) => {
|
||||||
this.requestService.removeByHrefSubstring(href);
|
this.requestService.removeByHrefSubstring(href);
|
||||||
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
||||||
request.responseMsToLive = 10 * 1000;
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
}
|
}
|
||||||
@@ -381,6 +384,28 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a PUT request for the specified object
|
||||||
|
*
|
||||||
|
* @param object The object to send a put request for.
|
||||||
|
*/
|
||||||
|
put(object: T): Observable<RemoteData<T>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
||||||
|
const request = new PutRequest(requestId, object._links.self.href, serializedObject);
|
||||||
|
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestService.configure(request);
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((re: RequestEntry) => hasValue(re) && re.completed),
|
||||||
|
switchMap(() => this.findByHref(object._links.self.href))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new patch to the object cache
|
* Add a new patch to the object cache
|
||||||
* 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
|
||||||
|
89
src/app/core/data/eperson-registration.service.spec.ts
Normal file
89
src/app/core/data/eperson-registration.service.spec.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { EpersonRegistrationService } from './eperson-registration.service';
|
||||||
|
import { RegistrationSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { PostRequest } from './request.models';
|
||||||
|
import { Registration } from '../shared/registration.model';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
|
||||||
|
describe('EpersonRegistrationService', () => {
|
||||||
|
let service: EpersonRegistrationService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
|
||||||
|
let halService: any;
|
||||||
|
|
||||||
|
const registration = new Registration();
|
||||||
|
registration.email = 'test@mail.org';
|
||||||
|
|
||||||
|
const registrationWithUser = new Registration();
|
||||||
|
registrationWithUser.email = 'test@mail.org';
|
||||||
|
registrationWithUser.user = 'test-uuid';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
halService = new HALEndpointServiceStub('rest-url');
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
configure: {},
|
||||||
|
getByUUID: cold('a',
|
||||||
|
{a: Object.assign(new RequestEntry(), {response: new RestResponse(true, 200, 'Success')})})
|
||||||
|
});
|
||||||
|
service = new EpersonRegistrationService(
|
||||||
|
requestService,
|
||||||
|
halService
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRegistrationEndpoint', () => {
|
||||||
|
it('should retrieve the registration endpoint', () => {
|
||||||
|
const expected = service.getRegistrationEndpoint();
|
||||||
|
|
||||||
|
expected.subscribe(((value) => {
|
||||||
|
expect(value).toEqual('rest-url/registrations');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTokenSearchEndpoint', () => {
|
||||||
|
it('should return the token search endpoint for a specified token', () => {
|
||||||
|
const expected = service.getTokenSearchEndpoint('test-token');
|
||||||
|
|
||||||
|
expected.subscribe(((value) => {
|
||||||
|
expect(value).toEqual('rest-url/registrations/search/findByToken?token=test-token');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('registerEmail', () => {
|
||||||
|
it('should send an email registration', () => {
|
||||||
|
|
||||||
|
const expected = service.registerEmail('test@mail.org');
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration));
|
||||||
|
expect(expected).toBeObservable(cold('a', {a: new RestResponse(true, 200, 'Success')}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchByToken', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(requestService.getByUUID as jasmine.Spy).and.returnValue(
|
||||||
|
cold('a',
|
||||||
|
{a: Object.assign(new RequestEntry(), {response: new RegistrationSuccessResponse(registrationWithUser, 200, 'Success')})})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should return a registration corresponding to the provided token', () => {
|
||||||
|
const expected = service.searchByToken('test-token');
|
||||||
|
|
||||||
|
expect(expected).toBeObservable(cold('(a|)', {
|
||||||
|
a: Object.assign(new Registration(), {
|
||||||
|
email: registrationWithUser.email,
|
||||||
|
token: 'test-token',
|
||||||
|
user: registrationWithUser.user
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
108
src/app/core/data/eperson-registration.service.ts
Normal file
108
src/app/core/data/eperson-registration.service.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { GetRequest, PostRequest } from './request.models';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { filter, find, map, take } from 'rxjs/operators';
|
||||||
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { Registration } from '../shared/registration.model';
|
||||||
|
import { filterSuccessfulResponses, getResponseFromEntry } from '../shared/operators';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { RegistrationResponseParsingService } from './registration-response-parsing.service';
|
||||||
|
import { RegistrationSuccessResponse } from '../cache/response.models';
|
||||||
|
|
||||||
|
@Injectable(
|
||||||
|
{
|
||||||
|
providedIn: 'root',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
/**
|
||||||
|
* Service that will register a new email address and request a token
|
||||||
|
*/
|
||||||
|
export class EpersonRegistrationService {
|
||||||
|
|
||||||
|
protected linkPath = 'registrations';
|
||||||
|
protected searchByTokenPath = '/search/findByToken?token=';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the Registration endpoint
|
||||||
|
*/
|
||||||
|
getRegistrationEndpoint(): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the endpoint to search by registration token
|
||||||
|
*/
|
||||||
|
getTokenSearchEndpoint(token: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
map((href: string) => `${href}${this.searchByTokenPath}${token}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new email address
|
||||||
|
* @param email
|
||||||
|
*/
|
||||||
|
registerEmail(email: string) {
|
||||||
|
const registration = new Registration();
|
||||||
|
registration.email = email;
|
||||||
|
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const hrefObs = this.getRegistrationEndpoint();
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new PostRequest(requestId, href, registration);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
getResponseFromEntry()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search a registration based on the provided token
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
searchByToken(token: string): Observable<Registration> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const hrefObs = this.getTokenSearchEndpoint(token);
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new GetRequest(requestId, href);
|
||||||
|
Object.assign(request, {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return RegistrationResponseParsingService;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.requestService.configure(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((restResponse: RegistrationSuccessResponse) => {
|
||||||
|
return Object.assign(new Registration(), {email: restResponse.registration.email, token: token, user: restResponse.registration.user});
|
||||||
|
}),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
114
src/app/core/data/metadata-field-data.service.spec.ts
Normal file
114
src/app/core/data/metadata-field-data.service.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { CreateRequest, FindListOptions, PutRequest } from './request.models';
|
||||||
|
import { MetadataFieldDataService } from './metadata-field-data.service';
|
||||||
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
|
||||||
|
describe('MetadataFieldDataService', () => {
|
||||||
|
let metadataFieldService: MetadataFieldDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let schema: MetadataSchema;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
|
||||||
|
const endpoint = 'api/metadatafield/endpoint';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
schema = Object.assign(new MetadataSchema(), {
|
||||||
|
prefix: 'dc',
|
||||||
|
namespace: 'namespace',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
|
||||||
|
configure: {},
|
||||||
|
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
halService = Object.assign(new HALEndpointServiceStub(endpoint));
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||||
|
error: {}
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: createSuccessfulRemoteDataObject$(undefined)
|
||||||
|
});
|
||||||
|
metadataFieldService = new MetadataFieldDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findBySchema', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(metadataFieldService, 'searchBy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call searchBy with the correct arguments', () => {
|
||||||
|
metadataFieldService.findBySchema(schema);
|
||||||
|
const expectedOptions = Object.assign(new FindListOptions(), {
|
||||||
|
searchParams: [new RequestParam('schema', schema.prefix)]
|
||||||
|
});
|
||||||
|
expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createOrUpdateMetadataField', () => {
|
||||||
|
let field: MetadataField;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
field = Object.assign(new MetadataField(), {
|
||||||
|
element: 'identifier',
|
||||||
|
qualifier: undefined,
|
||||||
|
schema: schema,
|
||||||
|
_links: {
|
||||||
|
self: { href: 'selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('called with a new metadata field', () => {
|
||||||
|
it('should send a CreateRequest', (done) => {
|
||||||
|
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('called with an existing metadata field', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
field = Object.assign(field, {
|
||||||
|
id: 'id-of-existing-field'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a PutRequest', (done) => {
|
||||||
|
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearRequests', () => {
|
||||||
|
it('should remove requests on the data service\'s endpoint', (done) => {
|
||||||
|
metadataFieldService.clearRequests().subscribe(() => {
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataFieldService as any).linkPath}`);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
89
src/app/core/data/metadata-field-data.service.ts
Normal file
89
src/app/core/data/metadata-field-data.service.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { DataService } from './data.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
|
||||||
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
|
import { FindListOptions, FindListRequest } from './request.models';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { find, skipWhile, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(METADATA_FIELD)
|
||||||
|
export class MetadataFieldDataService extends DataService<MetadataField> {
|
||||||
|
protected linkPath = 'metadatafields';
|
||||||
|
protected searchBySchemaLinkPath = 'bySchema';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<MetadataField>,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected notificationsService: NotificationsService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find metadata fields belonging to a metadata schema
|
||||||
|
* @param schema The metadata schema to list fields for
|
||||||
|
* @param options The options info used to retrieve the fields
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
findBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>) {
|
||||||
|
const optionsWithSchema = Object.assign(new FindListOptions(), options, {
|
||||||
|
searchParams: [new RequestParam('schema', schema.prefix)]
|
||||||
|
});
|
||||||
|
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or Update a MetadataField
|
||||||
|
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
||||||
|
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||||
|
* - On creation, a CreateRequest is used
|
||||||
|
* - On update, a PutRequest is used
|
||||||
|
* @param field The MetadataField to create or update
|
||||||
|
*/
|
||||||
|
createOrUpdateMetadataField(field: MetadataField): Observable<RemoteData<MetadataField>> {
|
||||||
|
const isUpdate = hasValue(field.id);
|
||||||
|
|
||||||
|
if (isUpdate) {
|
||||||
|
return this.put(field);
|
||||||
|
} else {
|
||||||
|
return this.create(field, new RequestParam('schemaId', field.schema.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all metadata field requests
|
||||||
|
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
||||||
|
*/
|
||||||
|
clearRequests(): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
tap((href: string) => {
|
||||||
|
this.requestService.removeByHrefSubstring(href);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
89
src/app/core/data/metadata-schema-data.service.spec.ts
Normal file
89
src/app/core/data/metadata-schema-data.service.spec.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { MetadataSchemaDataService } from './metadata-schema-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
|
import { CreateRequest, PutRequest } from './request.models';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
|
||||||
|
describe('MetadataSchemaDataService', () => {
|
||||||
|
let metadataSchemaService: MetadataSchemaDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
|
||||||
|
const endpoint = 'api/metadataschema/endpoint';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
|
||||||
|
configure: {},
|
||||||
|
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
halService = Object.assign(new HALEndpointServiceStub(endpoint));
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||||
|
error: {}
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: createSuccessfulRemoteDataObject$(undefined)
|
||||||
|
});
|
||||||
|
metadataSchemaService = new MetadataSchemaDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createOrUpdateMetadataSchema', () => {
|
||||||
|
let schema: MetadataSchema;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
schema = Object.assign(new MetadataSchema(), {
|
||||||
|
prefix: 'dc',
|
||||||
|
namespace: 'namespace',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('called with a new metadata schema', () => {
|
||||||
|
it('should send a CreateRequest', (done) => {
|
||||||
|
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('called with an existing metadata schema', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
schema = Object.assign(schema, {
|
||||||
|
id: 'id-of-existing-schema'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a PutRequest', (done) => {
|
||||||
|
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearRequests', () => {
|
||||||
|
it('should remove requests on the data service\'s endpoint', (done) => {
|
||||||
|
metadataSchemaService.clearRequests().subscribe(() => {
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataSchemaService as any).linkPath}`);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -9,37 +9,21 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type';
|
import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
|
||||||
|
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
/* tslint:disable:max-classes-per-file */
|
import { hasValue } from '../../shared/empty.util';
|
||||||
class DataServiceImpl extends DataService<MetadataSchema> {
|
import { tap } from 'rxjs/operators';
|
||||||
protected linkPath = 'metadataschemas';
|
import { RemoteData } from './remote-data';
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected requestService: RequestService,
|
|
||||||
protected rdbService: RemoteDataBuildService,
|
|
||||||
protected store: Store<CoreState>,
|
|
||||||
protected objectCache: ObjectCacheService,
|
|
||||||
protected halService: HALEndpointService,
|
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected http: HttpClient,
|
|
||||||
protected comparator: ChangeAnalyzer<MetadataSchema>) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint
|
* A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(METADATA_SCHEMA)
|
@dataService(METADATA_SCHEMA)
|
||||||
export class MetadataSchemaDataService {
|
export class MetadataSchemaDataService extends DataService<MetadataSchema> {
|
||||||
private dataService: DataServiceImpl;
|
protected linkPath = 'metadataschemas';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -50,6 +34,35 @@ export class MetadataSchemaDataService {
|
|||||||
protected comparator: DefaultChangeAnalyzer<MetadataSchema>,
|
protected comparator: DefaultChangeAnalyzer<MetadataSchema>,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected notificationsService: NotificationsService) {
|
protected notificationsService: NotificationsService) {
|
||||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or Update a MetadataSchema
|
||||||
|
* If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead
|
||||||
|
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||||
|
* - On creation, a CreateRequest is used
|
||||||
|
* - On update, a PutRequest is used
|
||||||
|
* @param schema The MetadataSchema to create or update
|
||||||
|
*/
|
||||||
|
createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<RemoteData<MetadataSchema>> {
|
||||||
|
const isUpdate = hasValue(schema.id);
|
||||||
|
|
||||||
|
if (isUpdate) {
|
||||||
|
return this.put(schema);
|
||||||
|
} else {
|
||||||
|
return this.create(schema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all metadata schema requests
|
||||||
|
* Used for refreshing lists after adding/updating/removing a metadata schema in the registry
|
||||||
|
*/
|
||||||
|
clearRequests(): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { MetadatafieldSuccessResponse, RestResponse } from '../cache/response.models';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
|
||||||
import { ResponseParsingService } from './parsing.service';
|
|
||||||
import { RestRequest } from './request.models';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A service responsible for parsing DSpaceRESTV2Response data related to a single MetadataField to a valid RestResponse
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class MetadatafieldParsingService implements ResponseParsingService {
|
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
|
||||||
const payload = data.payload;
|
|
||||||
|
|
||||||
const deserialized = new DSpaceSerializer(MetadataField).deserialize(payload);
|
|
||||||
return new MetadatafieldSuccessResponse(deserialized, data.statusCode, data.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
|
||||||
import { ResponseParsingService } from './parsing.service';
|
|
||||||
import { RestRequest } from './request.models';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class MetadataschemaParsingService implements ResponseParsingService {
|
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
|
||||||
const payload = data.payload;
|
|
||||||
|
|
||||||
const deserialized = new DSpaceSerializer(MetadataSchema).deserialize(payload);
|
|
||||||
return new MetadataschemaSuccessResponse(deserialized, data.statusCode, data.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -8,7 +8,6 @@ import {INotification} from '../../../shared/notifications/models/notification.m
|
|||||||
*/
|
*/
|
||||||
export const ObjectUpdatesActionTypes = {
|
export const ObjectUpdatesActionTypes = {
|
||||||
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
||||||
ADD_PAGE_TO_CUSTOM_ORDER: type('dspace/core/cache/object-updates/ADD_PAGE_TO_CUSTOM_ORDER'),
|
|
||||||
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
||||||
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
|
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
|
||||||
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
||||||
@@ -17,8 +16,7 @@ export const ObjectUpdatesActionTypes = {
|
|||||||
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
||||||
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
||||||
REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'),
|
REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'),
|
||||||
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'),
|
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD')
|
||||||
MOVE: type('dspace/core/cache/object-updates/MOVE'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -29,8 +27,7 @@ export const ObjectUpdatesActionTypes = {
|
|||||||
export enum FieldChangeType {
|
export enum FieldChangeType {
|
||||||
UPDATE = 0,
|
UPDATE = 0,
|
||||||
ADD = 1,
|
ADD = 1,
|
||||||
REMOVE = 2,
|
REMOVE = 2
|
||||||
MOVE = 3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,10 +38,7 @@ export class InitializeFieldsAction implements Action {
|
|||||||
payload: {
|
payload: {
|
||||||
url: string,
|
url: string,
|
||||||
fields: Identifiable[],
|
fields: Identifiable[],
|
||||||
lastModified: Date,
|
lastModified: Date
|
||||||
order: string[],
|
|
||||||
pageSize: number,
|
|
||||||
page: number
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,42 +55,9 @@ export class InitializeFieldsAction implements Action {
|
|||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
fields: Identifiable[],
|
fields: Identifiable[],
|
||||||
lastModified: Date,
|
lastModified: Date
|
||||||
order: string[] = [],
|
|
||||||
pageSize: number = 9999,
|
|
||||||
page: number = 0
|
|
||||||
) {
|
) {
|
||||||
this.payload = { url, fields, lastModified, order, pageSize, page };
|
this.payload = { url, fields, lastModified };
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to initialize a new page's fields in the ObjectUpdates state
|
|
||||||
*/
|
|
||||||
export class AddPageToCustomOrderAction implements Action {
|
|
||||||
type = ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER;
|
|
||||||
payload: {
|
|
||||||
url: string,
|
|
||||||
fields: Identifiable[],
|
|
||||||
order: string[],
|
|
||||||
page: number
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new AddPageToCustomOrderAction
|
|
||||||
*
|
|
||||||
* @param url The unique url of the page for which the fields are being added
|
|
||||||
* @param fields The identifiable fields of which the updates are kept track of
|
|
||||||
* @param order A custom order to keep track of objects moving around
|
|
||||||
* @param page The page to populate in the custom order
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
url: string,
|
|
||||||
fields: Identifiable[],
|
|
||||||
order: string[] = [],
|
|
||||||
page: number = 0
|
|
||||||
) {
|
|
||||||
this.payload = { url, fields, order, page };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,43 +281,6 @@ export class RemoveFieldUpdateAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to remove a single field update in the ObjectUpdates state for a certain page url and field uuid
|
|
||||||
*/
|
|
||||||
export class MoveFieldUpdateAction implements Action {
|
|
||||||
type = ObjectUpdatesActionTypes.MOVE;
|
|
||||||
payload: {
|
|
||||||
url: string,
|
|
||||||
from: number,
|
|
||||||
to: number,
|
|
||||||
fromPage: number,
|
|
||||||
toPage: number,
|
|
||||||
field?: Identifiable
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new RemoveObjectUpdatesAction
|
|
||||||
*
|
|
||||||
* @param url
|
|
||||||
* the unique url of the page for which a field's change should be removed
|
|
||||||
* @param from The index of the object to move
|
|
||||||
* @param to The index to move the object to
|
|
||||||
* @param fromPage The page to move the object from
|
|
||||||
* @param toPage The page to move the object to
|
|
||||||
* @param field Optional field to add to the fieldUpdates list (useful when we want to track updates across multiple pages)
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
url: string,
|
|
||||||
from: number,
|
|
||||||
to: number,
|
|
||||||
fromPage: number,
|
|
||||||
toPage: number,
|
|
||||||
field?: Identifiable
|
|
||||||
) {
|
|
||||||
this.payload = { url, from, to, fromPage, toPage, field };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -369,8 +293,6 @@ export type ObjectUpdatesAction
|
|||||||
| ReinstateObjectUpdatesAction
|
| ReinstateObjectUpdatesAction
|
||||||
| RemoveObjectUpdatesAction
|
| RemoveObjectUpdatesAction
|
||||||
| RemoveFieldUpdateAction
|
| RemoveFieldUpdateAction
|
||||||
| MoveFieldUpdateAction
|
|
||||||
| AddPageToCustomOrderAction
|
|
||||||
| RemoveAllObjectUpdatesAction
|
| RemoveAllObjectUpdatesAction
|
||||||
| SelectVirtualMetadataAction
|
| SelectVirtualMetadataAction
|
||||||
| SetEditableFieldUpdateAction
|
| SetEditableFieldUpdateAction
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import * as deepFreeze from 'deep-freeze';
|
import * as deepFreeze from 'deep-freeze';
|
||||||
import {
|
import {
|
||||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
AddFieldUpdateAction,
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
FieldChangeType,
|
FieldChangeType,
|
||||||
InitializeFieldsAction, MoveFieldUpdateAction,
|
InitializeFieldsAction,
|
||||||
ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction,
|
ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction,
|
||||||
RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction,
|
RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction,
|
||||||
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
||||||
@@ -85,16 +85,6 @@ describe('objectUpdatesReducer', () => {
|
|||||||
virtualMetadataSources: {
|
virtualMetadataSources: {
|
||||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||||
},
|
},
|
||||||
customOrder: {
|
|
||||||
initialOrderPages: [
|
|
||||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
|
||||||
],
|
|
||||||
newOrderPages: [
|
|
||||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
|
||||||
],
|
|
||||||
pageSize: 10,
|
|
||||||
changed: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,16 +111,6 @@ describe('objectUpdatesReducer', () => {
|
|||||||
virtualMetadataSources: {
|
virtualMetadataSources: {
|
||||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||||
},
|
},
|
||||||
customOrder: {
|
|
||||||
initialOrderPages: [
|
|
||||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
|
||||||
],
|
|
||||||
newOrderPages: [
|
|
||||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
|
||||||
],
|
|
||||||
pageSize: 10,
|
|
||||||
changed: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[url + OBJECT_UPDATES_TRASH_PATH]: {
|
[url + OBJECT_UPDATES_TRASH_PATH]: {
|
||||||
fieldStates: {
|
fieldStates: {
|
||||||
@@ -165,16 +145,6 @@ describe('objectUpdatesReducer', () => {
|
|||||||
virtualMetadataSources: {
|
virtualMetadataSources: {
|
||||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||||
},
|
},
|
||||||
customOrder: {
|
|
||||||
initialOrderPages: [
|
|
||||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
|
||||||
],
|
|
||||||
newOrderPages: [
|
|
||||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
|
||||||
],
|
|
||||||
pageSize: 10,
|
|
||||||
changed: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,7 +213,7 @@ describe('objectUpdatesReducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => {
|
it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => {
|
||||||
const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate, [identifiable1.uuid, identifiable3.uuid], 10, 0);
|
const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate);
|
||||||
|
|
||||||
const expectedState = {
|
const expectedState = {
|
||||||
[url]: {
|
[url]: {
|
||||||
@@ -261,17 +231,7 @@ describe('objectUpdatesReducer', () => {
|
|||||||
},
|
},
|
||||||
fieldUpdates: {},
|
fieldUpdates: {},
|
||||||
virtualMetadataSources: {},
|
virtualMetadataSources: {},
|
||||||
lastModified: modDate,
|
lastModified: modDate
|
||||||
customOrder: {
|
|
||||||
initialOrderPages: [
|
|
||||||
{ order: [identifiable1.uuid, identifiable3.uuid] }
|
|
||||||
],
|
|
||||||
newOrderPages: [
|
|
||||||
{ order: [identifiable1.uuid, identifiable3.uuid] }
|
|
||||||
],
|
|
||||||
pageSize: 10,
|
|
||||||
changed: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const newState = objectUpdatesReducer(testState, action);
|
const newState = objectUpdatesReducer(testState, action);
|
||||||
@@ -337,30 +297,4 @@ describe('objectUpdatesReducer', () => {
|
|||||||
const newState = objectUpdatesReducer(testState, action);
|
const newState = objectUpdatesReducer(testState, action);
|
||||||
expect(newState[url].fieldUpdates[uuid]).toBeUndefined();
|
expect(newState[url].fieldUpdates[uuid]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move the custom order from the state when the MOVE action is dispatched', () => {
|
|
||||||
const action = new MoveFieldUpdateAction(url, 0, 1, 0, 0);
|
|
||||||
|
|
||||||
const newState = objectUpdatesReducer(testState, action);
|
|
||||||
expect(newState[url].customOrder.newOrderPages[0].order[0]).toEqual(testState[url].customOrder.newOrderPages[0].order[1]);
|
|
||||||
expect(newState[url].customOrder.newOrderPages[0].order[1]).toEqual(testState[url].customOrder.newOrderPages[0].order[0]);
|
|
||||||
expect(newState[url].customOrder.changed).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add a new page to the custom order and add empty pages in between when the ADD_PAGE_TO_CUSTOM_ORDER action is dispatched', () => {
|
|
||||||
const identifiable4 = {
|
|
||||||
uuid: 'a23eae5a-7857-4ef9-8e52-989436ad2955',
|
|
||||||
key: 'dc.description.abstract',
|
|
||||||
language: null,
|
|
||||||
value: 'Extra value'
|
|
||||||
};
|
|
||||||
const action = new AddPageToCustomOrderAction(url, [identifiable4], [identifiable4.uuid], 2);
|
|
||||||
|
|
||||||
const newState = objectUpdatesReducer(testState, action);
|
|
||||||
// Confirm the page in between the two pages (index 1) has been filled with 10 (page size) undefined values
|
|
||||||
expect(newState[url].customOrder.newOrderPages[1].order.length).toEqual(10);
|
|
||||||
expect(newState[url].customOrder.newOrderPages[1].order[0]).toBeUndefined();
|
|
||||||
// Verify the new page is correct
|
|
||||||
expect(newState[url].customOrder.newOrderPages[2].order[0]).toEqual(identifiable4.uuid);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
AddFieldUpdateAction,
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
FieldChangeType,
|
FieldChangeType,
|
||||||
InitializeFieldsAction, MoveFieldUpdateAction,
|
InitializeFieldsAction,
|
||||||
ObjectUpdatesAction,
|
ObjectUpdatesAction,
|
||||||
ObjectUpdatesActionTypes,
|
ObjectUpdatesActionTypes,
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
@@ -12,9 +12,7 @@ import {
|
|||||||
SetValidFieldUpdateAction,
|
SetValidFieldUpdateAction,
|
||||||
SelectVirtualMetadataAction,
|
SelectVirtualMetadataAction,
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
|
||||||
import { from } from 'rxjs/internal/observable/from';
|
|
||||||
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,20 +81,6 @@ export interface DeleteRelationship extends Relationship {
|
|||||||
keepRightVirtualMetadata: boolean,
|
keepRightVirtualMetadata: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom order given to the list of objects
|
|
||||||
*/
|
|
||||||
export interface CustomOrder {
|
|
||||||
initialOrderPages: OrderPage[],
|
|
||||||
newOrderPages: OrderPage[],
|
|
||||||
pageSize: number;
|
|
||||||
changed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrderPage {
|
|
||||||
order: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The updated state of a single page
|
* The updated state of a single page
|
||||||
*/
|
*/
|
||||||
@@ -105,7 +89,6 @@ export interface ObjectUpdatesEntry {
|
|||||||
fieldUpdates: FieldUpdates;
|
fieldUpdates: FieldUpdates;
|
||||||
virtualMetadataSources: VirtualMetadataSources;
|
virtualMetadataSources: VirtualMetadataSources;
|
||||||
lastModified: Date;
|
lastModified: Date;
|
||||||
customOrder: CustomOrder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,9 +121,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
|||||||
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
||||||
return initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
return initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER: {
|
|
||||||
return addPageToCustomOrder(state, action as AddPageToCustomOrderAction);
|
|
||||||
}
|
|
||||||
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
||||||
return addFieldUpdate(state, action as AddFieldUpdateAction);
|
return addFieldUpdate(state, action as AddFieldUpdateAction);
|
||||||
}
|
}
|
||||||
@@ -168,9 +148,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
|||||||
case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
|
case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
|
||||||
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
|
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.MOVE: {
|
|
||||||
return moveFieldUpdate(state, action as MoveFieldUpdateAction);
|
|
||||||
}
|
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -186,50 +163,18 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
|||||||
const url: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
const fields: Identifiable[] = action.payload.fields;
|
const fields: Identifiable[] = action.payload.fields;
|
||||||
const lastModifiedServer: Date = action.payload.lastModified;
|
const lastModifiedServer: Date = action.payload.lastModified;
|
||||||
const order = action.payload.order;
|
|
||||||
const pageSize = action.payload.pageSize;
|
|
||||||
const page = action.payload.page;
|
|
||||||
const fieldStates = createInitialFieldStates(fields);
|
const fieldStates = createInitialFieldStates(fields);
|
||||||
const initialOrderPages = addOrderToPages([], order, pageSize, page);
|
|
||||||
const newPageState = Object.assign(
|
const newPageState = Object.assign(
|
||||||
{},
|
{},
|
||||||
state[url],
|
state[url],
|
||||||
{ fieldStates: fieldStates },
|
{ fieldStates: fieldStates },
|
||||||
{ fieldUpdates: {} },
|
{ fieldUpdates: {} },
|
||||||
{ virtualMetadataSources: {} },
|
{ virtualMetadataSources: {} },
|
||||||
{ lastModified: lastModifiedServer },
|
{ lastModified: lastModifiedServer }
|
||||||
{ customOrder: {
|
|
||||||
initialOrderPages: initialOrderPages,
|
|
||||||
newOrderPages: initialOrderPages,
|
|
||||||
pageSize: pageSize,
|
|
||||||
changed: false }
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return Object.assign({}, state, { [url]: newPageState });
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a page of objects to the state of a specific url and update a specific page of the custom order
|
|
||||||
* @param state The current state
|
|
||||||
* @param action The action to perform on the current state
|
|
||||||
*/
|
|
||||||
function addPageToCustomOrder(state: any, action: AddPageToCustomOrderAction) {
|
|
||||||
const url: string = action.payload.url;
|
|
||||||
const fields: Identifiable[] = action.payload.fields;
|
|
||||||
const fieldStates = createInitialFieldStates(fields);
|
|
||||||
const order = action.payload.order;
|
|
||||||
const page = action.payload.page;
|
|
||||||
const pageState: ObjectUpdatesEntry = state[url] || {};
|
|
||||||
const newPageState = Object.assign({}, pageState, {
|
|
||||||
fieldStates: Object.assign({}, pageState.fieldStates, fieldStates),
|
|
||||||
customOrder: Object.assign({}, pageState.customOrder, {
|
|
||||||
newOrderPages: addOrderToPages(pageState.customOrder.newOrderPages, order, pageState.customOrder.pageSize, page),
|
|
||||||
initialOrderPages: addOrderToPages(pageState.customOrder.initialOrderPages, order, pageState.customOrder.pageSize, page)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
return Object.assign({}, state, { [url]: newPageState });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new update for a specific field to the store
|
* Add a new update for a specific field to the store
|
||||||
* @param state The current state
|
* @param state The current state
|
||||||
@@ -338,19 +283,9 @@ function discardObjectUpdatesFor(url: string, state: any) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCustomOrder = Object.assign({}, pageState.customOrder);
|
|
||||||
if (pageState.customOrder.changed) {
|
|
||||||
const initialOrder = pageState.customOrder.initialOrderPages;
|
|
||||||
if (isNotEmpty(initialOrder)) {
|
|
||||||
newCustomOrder.newOrderPages = initialOrder;
|
|
||||||
newCustomOrder.changed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const discardedPageState = Object.assign({}, pageState, {
|
const discardedPageState = Object.assign({}, pageState, {
|
||||||
fieldUpdates: {},
|
fieldUpdates: {},
|
||||||
fieldStates: newFieldStates,
|
fieldStates: newFieldStates
|
||||||
customOrder: newCustomOrder
|
|
||||||
});
|
});
|
||||||
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
|
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
|
||||||
}
|
}
|
||||||
@@ -504,121 +439,3 @@ function createInitialFieldStates(fields: Identifiable[]) {
|
|||||||
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
||||||
return fieldStates;
|
return fieldStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to add a list of objects to an existing FieldStates object
|
|
||||||
* @param fieldStates FieldStates to add states to
|
|
||||||
* @param fields Identifiable objects The list of objects to add to the FieldStates
|
|
||||||
*/
|
|
||||||
function addFieldStates(fieldStates: FieldStates, fields: Identifiable[]) {
|
|
||||||
const uuids = fields.map((field: Identifiable) => field.uuid);
|
|
||||||
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
|
||||||
return fieldStates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move an object within the custom order of a page state
|
|
||||||
* @param state The current state
|
|
||||||
* @param action The move action to perform
|
|
||||||
*/
|
|
||||||
function moveFieldUpdate(state: any, action: MoveFieldUpdateAction) {
|
|
||||||
const url = action.payload.url;
|
|
||||||
const fromIndex = action.payload.from;
|
|
||||||
const toIndex = action.payload.to;
|
|
||||||
const fromPage = action.payload.fromPage;
|
|
||||||
const toPage = action.payload.toPage;
|
|
||||||
const field = action.payload.field;
|
|
||||||
|
|
||||||
const pageState: ObjectUpdatesEntry = state[url];
|
|
||||||
const initialOrderPages = pageState.customOrder.initialOrderPages;
|
|
||||||
const customOrderPages = [...pageState.customOrder.newOrderPages];
|
|
||||||
|
|
||||||
// Create a copy of the custom orders for the from- and to-pages
|
|
||||||
const fromPageOrder = [...customOrderPages[fromPage].order];
|
|
||||||
const toPageOrder = [...customOrderPages[toPage].order];
|
|
||||||
if (fromPage === toPage) {
|
|
||||||
if (isNotEmpty(customOrderPages[fromPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex]) && isNotEmpty(customOrderPages[fromPage].order[toIndex])) {
|
|
||||||
// Move an item from one index to another within the same page
|
|
||||||
moveItemInArray(fromPageOrder, fromIndex, toIndex);
|
|
||||||
// Update the custom order for this page
|
|
||||||
customOrderPages[fromPage] = { order: fromPageOrder };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isNotEmpty(customOrderPages[fromPage]) && hasValue(customOrderPages[toPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex])) {
|
|
||||||
// Move an item from one index of one page to an index in another page
|
|
||||||
transferArrayItem(fromPageOrder, toPageOrder, fromIndex, toIndex);
|
|
||||||
// Update the custom order for both pages
|
|
||||||
customOrderPages[fromPage] = { order: fromPageOrder };
|
|
||||||
customOrderPages[toPage] = { order: toPageOrder };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a field update if it doesn't exist for this field yet
|
|
||||||
let fieldUpdate = {};
|
|
||||||
if (hasValue(field)) {
|
|
||||||
fieldUpdate = pageState.fieldUpdates[field.uuid];
|
|
||||||
if (hasNoValue(fieldUpdate)) {
|
|
||||||
fieldUpdate = { field: field, changeType: undefined }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the store's state with new values and return
|
|
||||||
return Object.assign({}, state, { [url]: Object.assign({}, pageState, {
|
|
||||||
fieldUpdates: Object.assign({}, pageState.fieldUpdates, hasValue(field) ? { [field.uuid]: fieldUpdate } : {}),
|
|
||||||
customOrder: Object.assign({}, pageState.customOrder, { newOrderPages: customOrderPages, changed: checkForOrderChanges(initialOrderPages, customOrderPages) })
|
|
||||||
})})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare two lists of OrderPage objects and return whether there's at least one change in the order of objects within
|
|
||||||
* @param initialOrderPages The initial list of OrderPages
|
|
||||||
* @param customOrderPages The changed list of OrderPages
|
|
||||||
*/
|
|
||||||
function checkForOrderChanges(initialOrderPages: OrderPage[], customOrderPages: OrderPage[]) {
|
|
||||||
let changed = false;
|
|
||||||
initialOrderPages.forEach((orderPage: OrderPage, page: number) => {
|
|
||||||
if (isNotEmpty(orderPage) && isNotEmpty(orderPage.order) && isNotEmpty(customOrderPages[page]) && isNotEmpty(customOrderPages[page].order)) {
|
|
||||||
orderPage.order.forEach((id: string, index: number) => {
|
|
||||||
if (id !== customOrderPages[page].order[index]) {
|
|
||||||
changed = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (changed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a custom order page by providing the list of all pages, a list of UUIDs, pageSize and the page to populate
|
|
||||||
* @param initialPages The initial list of OrderPage objects
|
|
||||||
* @param order The list of UUIDs to create a page for
|
|
||||||
* @param pageSize The pageSize used to populate empty spacer pages
|
|
||||||
* @param page The index of the page to add
|
|
||||||
*/
|
|
||||||
function addOrderToPages(initialPages: OrderPage[], order: string[], pageSize: number, page: number): OrderPage[] {
|
|
||||||
const result = [...initialPages];
|
|
||||||
const orderPage: OrderPage = { order: order };
|
|
||||||
if (page < result.length) {
|
|
||||||
// The page we're trying to add already exists in the list. Overwrite it.
|
|
||||||
result[page] = orderPage;
|
|
||||||
} else if (page === result.length) {
|
|
||||||
// The page we're trying to add is the next page in the list, add it.
|
|
||||||
result.push(orderPage);
|
|
||||||
} else {
|
|
||||||
// The page we're trying to add is at least one page ahead of the list, fill the list with empty pages before adding the page.
|
|
||||||
const emptyOrder = [];
|
|
||||||
for (let i = 0; i < pageSize; i++) {
|
|
||||||
emptyOrder.push(undefined);
|
|
||||||
}
|
|
||||||
const emptyOrderPage: OrderPage = { order: emptyOrder };
|
|
||||||
for (let i = result.length; i < page; i++) {
|
|
||||||
result.push(emptyOrderPage);
|
|
||||||
}
|
|
||||||
result.push(orderPage);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
@@ -2,7 +2,6 @@ import { Store } from '@ngrx/store';
|
|||||||
import { CoreState } from '../../core.reducers';
|
import { CoreState } from '../../core.reducers';
|
||||||
import { ObjectUpdatesService } from './object-updates.service';
|
import { ObjectUpdatesService } from './object-updates.service';
|
||||||
import {
|
import {
|
||||||
AddPageToCustomOrderAction,
|
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
FieldChangeType,
|
FieldChangeType,
|
||||||
InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction,
|
InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction,
|
||||||
@@ -13,8 +12,6 @@ import { Notification } from '../../../shared/notifications/models/notification.
|
|||||||
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||||
import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer';
|
import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer';
|
||||||
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
|
||||||
import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service';
|
|
||||||
|
|
||||||
describe('ObjectUpdatesService', () => {
|
describe('ObjectUpdatesService', () => {
|
||||||
let service: ObjectUpdatesService;
|
let service: ObjectUpdatesService;
|
||||||
@@ -47,7 +44,7 @@ describe('ObjectUpdatesService', () => {
|
|||||||
};
|
};
|
||||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||||
spyOn(store, 'dispatch');
|
spyOn(store, 'dispatch');
|
||||||
service = new ObjectUpdatesService(store, new ArrayMoveChangeAnalyzer<string>());
|
service = new ObjectUpdatesService(store);
|
||||||
|
|
||||||
spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry));
|
spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry));
|
||||||
spyOn(service as any, 'getFieldState').and.callFake((uuid) => {
|
spyOn(service as any, 'getFieldState').and.callFake((uuid) => {
|
||||||
@@ -63,25 +60,6 @@ describe('ObjectUpdatesService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('initializeWithCustomOrder', () => {
|
|
||||||
const pageSize = 20;
|
|
||||||
const page = 0;
|
|
||||||
|
|
||||||
it('should dispatch an INITIALIZE action with the correct URL, initial identifiables, last modified , custom order, page size and page', () => {
|
|
||||||
service.initializeWithCustomOrder(url, identifiables, modDate, pageSize, page);
|
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new InitializeFieldsAction(url, identifiables, modDate, identifiables.map((identifiable) => identifiable.uuid), pageSize, page));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('addPageToCustomOrder', () => {
|
|
||||||
const page = 2;
|
|
||||||
|
|
||||||
it('should dispatch an ADD_PAGE_TO_CUSTOM_ORDER action with the correct URL, identifiables, custom order and page number to add', () => {
|
|
||||||
service.addPageToCustomOrder(url, identifiables, page);
|
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new AddPageToCustomOrderAction(url, identifiables, identifiables.map((identifiable) => identifiable.uuid), page));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getFieldUpdates', () => {
|
describe('getFieldUpdates', () => {
|
||||||
it('should return the list of all fields, including their update if there is one', () => {
|
it('should return the list of all fields, including their update if there is one', () => {
|
||||||
const result$ = service.getFieldUpdates(url, identifiables);
|
const result$ = service.getFieldUpdates(url, identifiables);
|
||||||
@@ -116,49 +94,6 @@ describe('ObjectUpdatesService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getFieldUpdatesByCustomOrder', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const fieldStates = {
|
|
||||||
[identifiable1.uuid]: { editable: false, isNew: false, isValid: true },
|
|
||||||
[identifiable2.uuid]: { editable: true, isNew: false, isValid: false },
|
|
||||||
[identifiable3.uuid]: { editable: true, isNew: true, isValid: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
const customOrder = {
|
|
||||||
initialOrderPages: [{
|
|
||||||
order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid]
|
|
||||||
}],
|
|
||||||
newOrderPages: [{
|
|
||||||
order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid]
|
|
||||||
}],
|
|
||||||
pageSize: 20,
|
|
||||||
changed: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const objectEntry = {
|
|
||||||
fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder
|
|
||||||
};
|
|
||||||
|
|
||||||
(service as any).getObjectEntry.and.returnValue(observableOf(objectEntry))
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the list of all fields, including their update if there is one, ordered by their custom order', (done) => {
|
|
||||||
const result$ = service.getFieldUpdatesByCustomOrder(url, identifiables);
|
|
||||||
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
|
||||||
|
|
||||||
const expectedResult = {
|
|
||||||
[identifiable2.uuid]: { field: identifiable2, changeType: undefined },
|
|
||||||
[identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD },
|
|
||||||
[identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }
|
|
||||||
};
|
|
||||||
|
|
||||||
result$.subscribe((result) => {
|
|
||||||
expect(result).toEqual(expectedResult);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isEditable', () => {
|
describe('isEditable', () => {
|
||||||
it('should return false if this identifiable is currently not editable in the store', () => {
|
it('should return false if this identifiable is currently not editable in the store', () => {
|
||||||
const result$ = service.isEditable(url, identifiable1.uuid);
|
const result$ = service.isEditable(url, identifiable1.uuid);
|
||||||
@@ -274,11 +209,7 @@ describe('ObjectUpdatesService', () => {
|
|||||||
});
|
});
|
||||||
describe('when updates are emtpy', () => {
|
describe('when updates are emtpy', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(service as any).getObjectEntry.and.returnValue(observableOf({
|
(service as any).getObjectEntry.and.returnValue(observableOf({}))
|
||||||
customOrder: {
|
|
||||||
changed: false
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when there are no updates', () => {
|
it('should return false when there are no updates', () => {
|
||||||
@@ -346,44 +277,4 @@ describe('ObjectUpdatesService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getMoveOperations', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const fieldStates = {
|
|
||||||
[identifiable1.uuid]: { editable: false, isNew: false, isValid: true },
|
|
||||||
[identifiable2.uuid]: { editable: true, isNew: false, isValid: false },
|
|
||||||
[identifiable3.uuid]: { editable: true, isNew: true, isValid: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
const customOrder = {
|
|
||||||
initialOrderPages: [{
|
|
||||||
order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid]
|
|
||||||
}],
|
|
||||||
newOrderPages: [{
|
|
||||||
order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid]
|
|
||||||
}],
|
|
||||||
pageSize: 20,
|
|
||||||
changed: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const objectEntry = {
|
|
||||||
fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder
|
|
||||||
};
|
|
||||||
|
|
||||||
(service as any).getObjectEntry.and.returnValue(observableOf(objectEntry))
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the expected move operations', (done) => {
|
|
||||||
const result$ = service.getMoveOperations(url);
|
|
||||||
|
|
||||||
const expectedResult = [
|
|
||||||
{ op: 'move', from: '/0', path: '/2' }
|
|
||||||
] as MoveOperation[];
|
|
||||||
|
|
||||||
result$.subscribe((result) => {
|
|
||||||
expect(result).toEqual(expectedResult);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -8,16 +8,15 @@ import {
|
|||||||
Identifiable,
|
Identifiable,
|
||||||
OBJECT_UPDATES_TRASH_PATH,
|
OBJECT_UPDATES_TRASH_PATH,
|
||||||
ObjectUpdatesEntry,
|
ObjectUpdatesEntry,
|
||||||
ObjectUpdatesState, OrderPage,
|
ObjectUpdatesState,
|
||||||
VirtualMetadataSource
|
VirtualMetadataSource
|
||||||
} from './object-updates.reducer';
|
} from './object-updates.reducer';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
AddFieldUpdateAction,
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
FieldChangeType,
|
FieldChangeType,
|
||||||
InitializeFieldsAction,
|
InitializeFieldsAction,
|
||||||
MoveFieldUpdateAction,
|
|
||||||
ReinstateObjectUpdatesAction,
|
ReinstateObjectUpdatesAction,
|
||||||
RemoveFieldUpdateAction,
|
RemoveFieldUpdateAction,
|
||||||
SelectVirtualMetadataAction,
|
SelectVirtualMetadataAction,
|
||||||
@@ -25,11 +24,8 @@ import {
|
|||||||
SetValidFieldUpdateAction
|
SetValidFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
||||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||||
import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service';
|
|
||||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
|
||||||
import { flatten } from '@angular/compiler';
|
|
||||||
|
|
||||||
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
||||||
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
||||||
@@ -52,9 +48,7 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ObjectUpdatesService {
|
export class ObjectUpdatesService {
|
||||||
constructor(private store: Store<CoreState>,
|
constructor(private store: Store<CoreState>) {
|
||||||
private comparator: ArrayMoveChangeAnalyzer<string>) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,28 +61,6 @@ export class ObjectUpdatesService {
|
|||||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to dispatch an InitializeFieldsAction to the store and keeping track of the order objects are stored
|
|
||||||
* @param url The page's URL for which the changes are being mapped
|
|
||||||
* @param fields The initial fields for the page's object
|
|
||||||
* @param lastModified The date the object was last modified
|
|
||||||
* @param pageSize The page size to use for adding pages to the custom order
|
|
||||||
* @param page The first page to populate the custom order with
|
|
||||||
*/
|
|
||||||
initializeWithCustomOrder(url, fields: Identifiable[], lastModified: Date, pageSize = 9999, page = 0): void {
|
|
||||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, fields.map((field) => field.uuid), pageSize, page));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to dispatch an AddPageToCustomOrderAction, adding a new page to an already existing custom order tracking
|
|
||||||
* @param url The URL for which the changes are being mapped
|
|
||||||
* @param fields The fields to add a new page for
|
|
||||||
* @param page The page number (starting from index 0)
|
|
||||||
*/
|
|
||||||
addPageToCustomOrder(url, fields: Identifiable[], page: number): void {
|
|
||||||
this.store.dispatch(new AddPageToCustomOrderAction(url, fields, fields.map((field) => field.uuid), page));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to dispatch an AddFieldUpdateAction to the store
|
* Method to dispatch an AddFieldUpdateAction to the store
|
||||||
* @param url The page's URL for which the changes are saved
|
* @param url The page's URL for which the changes are saved
|
||||||
@@ -153,7 +125,7 @@ export class ObjectUpdatesService {
|
|||||||
*/
|
*/
|
||||||
getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
||||||
const objectUpdates = this.getObjectEntry(url);
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
return objectUpdates.pipe(map((objectEntry) => {
|
return objectUpdates.pipe(isNotEmptyOperator(), map((objectEntry) => {
|
||||||
const fieldUpdates: FieldUpdates = {};
|
const fieldUpdates: FieldUpdates = {};
|
||||||
for (const object of initialFields) {
|
for (const object of initialFields) {
|
||||||
let fieldUpdate = objectEntry.fieldUpdates[object.uuid];
|
let fieldUpdate = objectEntry.fieldUpdates[object.uuid];
|
||||||
@@ -166,31 +138,6 @@ export class ObjectUpdatesService {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method that combines the state's updates with the initial values (when there's no update),
|
|
||||||
* sorted by their custom order to create a FieldUpdates object
|
|
||||||
* @param url The URL of the page for which the FieldUpdates should be requested
|
|
||||||
* @param initialFields The initial values of the fields
|
|
||||||
* @param page The page to retrieve
|
|
||||||
*/
|
|
||||||
getFieldUpdatesByCustomOrder(url: string, initialFields: Identifiable[], page = 0): Observable<FieldUpdates> {
|
|
||||||
const objectUpdates = this.getObjectEntry(url);
|
|
||||||
return objectUpdates.pipe(map((objectEntry) => {
|
|
||||||
const fieldUpdates: FieldUpdates = {};
|
|
||||||
if (hasValue(objectEntry) && hasValue(objectEntry.customOrder) && isNotEmpty(objectEntry.customOrder.newOrderPages) && page < objectEntry.customOrder.newOrderPages.length) {
|
|
||||||
for (const uuid of objectEntry.customOrder.newOrderPages[page].order) {
|
|
||||||
let fieldUpdate = objectEntry.fieldUpdates[uuid];
|
|
||||||
if (isEmpty(fieldUpdate)) {
|
|
||||||
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
|
||||||
fieldUpdate = {field: identifiable, changeType: undefined};
|
|
||||||
}
|
|
||||||
fieldUpdates[uuid] = fieldUpdate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fieldUpdates;
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to check if a specific field is currently editable in the store
|
* Method to check if a specific field is currently editable in the store
|
||||||
* @param url The URL of the page on which the field resides
|
* @param url The URL of the page on which the field resides
|
||||||
@@ -260,19 +207,6 @@ export class ObjectUpdatesService {
|
|||||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches a MoveFieldUpdateAction
|
|
||||||
* @param url The page's URL for which the changes are saved
|
|
||||||
* @param from The index of the object to move
|
|
||||||
* @param to The index to move the object to
|
|
||||||
* @param fromPage The page to move the object from
|
|
||||||
* @param toPage The page to move the object to
|
|
||||||
* @param field Optional field to add to the fieldUpdates list (useful if we want to track updates across multiple pages)
|
|
||||||
*/
|
|
||||||
saveMoveFieldUpdate(url: string, from: number, to: number, fromPage = 0, toPage = 0, field?: Identifiable) {
|
|
||||||
this.store.dispatch(new MoveFieldUpdateAction(url, from, to, fromPage, toPage, field));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
||||||
* @param url The URL of the page on which the field resides
|
* @param url The URL of the page on which the field resides
|
||||||
@@ -387,7 +321,7 @@ export class ObjectUpdatesService {
|
|||||||
* @param url The page's url to check for in the store
|
* @param url The page's url to check for in the store
|
||||||
*/
|
*/
|
||||||
hasUpdates(url: string): Observable<boolean> {
|
hasUpdates(url: string): Observable<boolean> {
|
||||||
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && (isNotEmpty(objectEntry.fieldUpdates) || objectEntry.customOrder.changed)));
|
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && isNotEmpty(objectEntry.fieldUpdates)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -405,19 +339,4 @@ export class ObjectUpdatesService {
|
|||||||
getLastModified(url: string): Observable<Date> {
|
getLastModified(url: string): Observable<Date> {
|
||||||
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified));
|
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get move operations based on the custom order
|
|
||||||
* @param url The page's url
|
|
||||||
*/
|
|
||||||
getMoveOperations(url: string): Observable<MoveOperation[]> {
|
|
||||||
return this.getObjectEntry(url).pipe(
|
|
||||||
map((objectEntry) => objectEntry.customOrder),
|
|
||||||
map((customOrder) => this.comparator.diff(
|
|
||||||
flatten(customOrder.initialOrderPages.map((orderPage: OrderPage) => orderPage.order)),
|
|
||||||
flatten(customOrder.newOrderPages.map((orderPage: OrderPage) => orderPage.order)))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
41
src/app/core/data/process-files-response-parsing.service.ts
Normal file
41
src/app/core/data/process-files-response-parsing.service.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { GenericSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||||
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a GenericSuccessResponse
|
||||||
|
* containing a PaginatedList of a process's output files
|
||||||
|
*/
|
||||||
|
export class ProcessFilesResponseParsingService implements ResponseParsingService {
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
const payload = data.payload;
|
||||||
|
|
||||||
|
let page;
|
||||||
|
if (isNotEmpty(payload._embedded) && isNotEmpty(Object.keys(payload._embedded))) {
|
||||||
|
const bitstreams = new DSpaceSerializer(Bitstream).deserializeArray(payload._embedded[Object.keys(payload._embedded)[0]]);
|
||||||
|
|
||||||
|
if (isNotEmpty(bitstreams)) {
|
||||||
|
page = new PaginatedList(Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: bitstreams.length,
|
||||||
|
totalElements: bitstreams.length,
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1
|
||||||
|
}), bitstreams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(page)) {
|
||||||
|
page = new PaginatedList(new PageInfo(), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GenericSuccessResponse(page, data.statusCode, data.statusText);
|
||||||
|
}
|
||||||
|
}
|
70
src/app/core/data/processes/process-data.service.ts
Normal file
70
src/app/core/data/processes/process-data.service.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DataService } from '../data.service';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
|
||||||
|
import { Process } from '../../../process-page/processes/process.model';
|
||||||
|
import { dataService } from '../../cache/builders/build-decorators';
|
||||||
|
import { PROCESS } from '../../../process-page/processes/process.resource-type';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { ProcessFilesRequest, RestRequest } from '../request.models';
|
||||||
|
import { configureRequest, filterSuccessfulResponses } from '../../shared/operators';
|
||||||
|
import { GenericSuccessResponse } from '../../cache/response.models';
|
||||||
|
import { PaginatedList } from '../paginated-list';
|
||||||
|
import { Bitstream } from '../../shared/bitstream.model';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(PROCESS)
|
||||||
|
export class ProcessDataService extends DataService<Process> {
|
||||||
|
protected linkPath = 'processes';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<Process>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for a process his files
|
||||||
|
* @param processId The ID of the process
|
||||||
|
*/
|
||||||
|
getFilesEndpoint(processId: string): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
switchMap((href) => this.halService.getEndpoint('files', `${href}/${processId}`))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a process his output files
|
||||||
|
* @param processId The ID of the process
|
||||||
|
*/
|
||||||
|
getFiles(processId: string): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||||
|
const request$ = this.getFilesEndpoint(processId).pipe(
|
||||||
|
map((href) => new ProcessFilesRequest(this.requestService.generateRequestId(), href)),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
);
|
||||||
|
const requestEntry$ = request$.pipe(
|
||||||
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
|
);
|
||||||
|
const payload$ = requestEntry$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((response: GenericSuccessResponse<PaginatedList<Bitstream>>) => response.payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rdbService.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
|
}
|
||||||
|
}
|
61
src/app/core/data/processes/script-data.service.ts
Normal file
61
src/app/core/data/processes/script-data.service.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DataService } from '../data.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
|
||||||
|
import { Script } from '../../../process-page/scripts/script.model';
|
||||||
|
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
|
||||||
|
import { find, map, switchMap } from 'rxjs/operators';
|
||||||
|
import { URLCombiner } from '../../url-combiner/url-combiner';
|
||||||
|
import { MultipartPostRequest, RestRequest } from '../request.models';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RequestEntry } from '../request.reducer';
|
||||||
|
import { dataService } from '../../cache/builders/build-decorators';
|
||||||
|
import { SCRIPT } from '../../../process-page/scripts/script.resource-type';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(SCRIPT)
|
||||||
|
export class ScriptDataService extends DataService<Script> {
|
||||||
|
protected linkPath = 'scripts';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<Script>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public invoke(scriptName: string, parameters: ProcessParameter[], files: File[]): Observable<RequestEntry> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
map((endpoint: string) => new URLCombiner(endpoint, scriptName, 'processes').toString()),
|
||||||
|
map((endpoint: string) => {
|
||||||
|
const body = this.getInvocationFormData(parameters, files);
|
||||||
|
return new MultipartPostRequest(requestId, endpoint, body)
|
||||||
|
}),
|
||||||
|
map((request: RestRequest) => this.requestService.configure(request)),
|
||||||
|
switchMap(() => this.requestService.getByUUID(requestId)),
|
||||||
|
find((request: RequestEntry) => request.completed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInvocationFormData(parameters: ProcessParameter[], files: File[]): FormData {
|
||||||
|
const form: FormData = new FormData();
|
||||||
|
form.set('properties', JSON.stringify(parameters));
|
||||||
|
files.forEach((file: File) => {
|
||||||
|
form.append('file', file);
|
||||||
|
});
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
import { Registration } from '../shared/registration.model';
|
||||||
|
import { RegistrationResponseParsingService } from './registration-response-parsing.service';
|
||||||
|
import { RegistrationSuccessResponse } from '../cache/response.models';
|
||||||
|
|
||||||
|
describe('RegistrationResponseParsingService', () => {
|
||||||
|
describe('parse', () => {
|
||||||
|
const registration = Object.assign(new Registration(), {email: 'test@email.org', token: 'test-token'});
|
||||||
|
const registrationResponseParsingService = new RegistrationResponseParsingService();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
payload: {email: 'test@email.org', token: 'test-token'},
|
||||||
|
statusCode: 200,
|
||||||
|
statusText: 'Success'
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should parse a registration response', () => {
|
||||||
|
const expected = registrationResponseParsingService.parse({} as any, data);
|
||||||
|
|
||||||
|
expect(expected).toEqual(new RegistrationSuccessResponse(registration, 200, 'Success'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
24
src/app/core/data/registration-response-parsing.service.ts
Normal file
24
src/app/core/data/registration-response-parsing.service.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { RegistrationSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { Registration } from '../shared/registration.model';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Parsing service responsible for parsing a Registration response
|
||||||
|
*/
|
||||||
|
export class RegistrationResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
const payload = data.payload;
|
||||||
|
|
||||||
|
const registration = Object.assign(new Registration(), payload);
|
||||||
|
|
||||||
|
return new RegistrationSuccessResponse(registration, data.statusCode, data.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,41 +0,0 @@
|
|||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import {
|
|
||||||
RegistryBitstreamformatsSuccessResponse
|
|
||||||
} from '../cache/response.models';
|
|
||||||
import { RegistryBitstreamformatsResponseParsingService } from './registry-bitstreamformats-response-parsing.service';
|
|
||||||
|
|
||||||
describe('RegistryBitstreamformatsResponseParsingService', () => {
|
|
||||||
let service: RegistryBitstreamformatsResponseParsingService;
|
|
||||||
|
|
||||||
const mockDSOParser = Object.assign({
|
|
||||||
processPageInfo: () => new PageInfo()
|
|
||||||
}) as DSOResponseParsingService;
|
|
||||||
|
|
||||||
const data = Object.assign({
|
|
||||||
payload: {
|
|
||||||
_embedded: {
|
|
||||||
bitstreamformats: [
|
|
||||||
{
|
|
||||||
uuid: 'uuid-1',
|
|
||||||
description: 'a description'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uuid: 'uuid-2',
|
|
||||||
description: 'another description'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as DSpaceRESTV2Response;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
service = new RegistryBitstreamformatsResponseParsingService(mockDSOParser);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse the data correctly', () => {
|
|
||||||
const response = service.parse(null, data);
|
|
||||||
expect(response.constructor).toBe(RegistryBitstreamformatsSuccessResponse);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,25 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response.models';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
|
||||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
|
||||||
import { ResponseParsingService } from './parsing.service';
|
|
||||||
import { RestRequest } from './request.models';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RegistryBitstreamformatsResponseParsingService implements ResponseParsingService {
|
|
||||||
constructor(private dsoParser: DSOResponseParsingService) {
|
|
||||||
}
|
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
|
||||||
const payload = data.payload;
|
|
||||||
|
|
||||||
const bitstreamformats = payload._embedded.bitstreamformats;
|
|
||||||
payload.bitstreamformats = bitstreamformats;
|
|
||||||
|
|
||||||
const deserialized = new DSpaceSerializer(RegistryBitstreamformatsResponse).deserialize(payload);
|
|
||||||
return new RegistryBitstreamformatsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload.page));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,68 +0,0 @@
|
|||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import {
|
|
||||||
RegistryMetadatafieldsSuccessResponse
|
|
||||||
} from '../cache/response.models';
|
|
||||||
import { RegistryMetadatafieldsResponseParsingService } from './registry-metadatafields-response-parsing.service';
|
|
||||||
|
|
||||||
describe('RegistryMetadatafieldsResponseParsingService', () => {
|
|
||||||
let service: RegistryMetadatafieldsResponseParsingService;
|
|
||||||
|
|
||||||
const mockDSOParser = Object.assign({
|
|
||||||
processPageInfo: () => new PageInfo()
|
|
||||||
}) as DSOResponseParsingService;
|
|
||||||
|
|
||||||
const data = Object.assign({
|
|
||||||
payload: {
|
|
||||||
_embedded: {
|
|
||||||
metadatafields: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
element: 'element',
|
|
||||||
qualifier: 'qualifier',
|
|
||||||
scopeNote: 'a scope note',
|
|
||||||
_embedded: {
|
|
||||||
schema: {
|
|
||||||
id: 1,
|
|
||||||
prefix: 'test',
|
|
||||||
namespace: 'test namespace'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
element: 'secondelement',
|
|
||||||
qualifier: 'secondqualifier',
|
|
||||||
scopeNote: 'a second scope note',
|
|
||||||
_embedded: {
|
|
||||||
schema: {
|
|
||||||
id: 1,
|
|
||||||
prefix: 'test',
|
|
||||||
namespace: 'test namespace'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as DSpaceRESTV2Response;
|
|
||||||
|
|
||||||
const emptyData = Object.assign({
|
|
||||||
payload: {}
|
|
||||||
}) as DSpaceRESTV2Response;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
service = new RegistryMetadatafieldsResponseParsingService(mockDSOParser);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse the data correctly', () => {
|
|
||||||
const response = service.parse(null, data);
|
|
||||||
expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not produce an error and parse the data correctly when the data is empty', () => {
|
|
||||||
const response = service.parse(null, emptyData);
|
|
||||||
expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,34 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { hasValue } from '../../shared/empty.util';
|
|
||||||
import { RegistryMetadatafieldsSuccessResponse, RestResponse } from '../cache/response.models';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
|
||||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
|
||||||
import { ResponseParsingService } from './parsing.service';
|
|
||||||
import { RestRequest } from './request.models';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RegistryMetadatafieldsResponseParsingService implements ResponseParsingService {
|
|
||||||
constructor(private dsoParser: DSOResponseParsingService) {
|
|
||||||
}
|
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
|
||||||
const payload = data.payload;
|
|
||||||
|
|
||||||
let metadatafields = [];
|
|
||||||
|
|
||||||
if (hasValue(payload._embedded)) {
|
|
||||||
metadatafields = payload._embedded.metadatafields;
|
|
||||||
metadatafields.forEach((field) => {
|
|
||||||
field.schema = field._embedded.schema;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
payload.metadatafields = metadatafields;
|
|
||||||
|
|
||||||
const deserialized = new DSpaceSerializer(RegistryMetadatafieldsResponse).deserialize(payload);
|
|
||||||
return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,50 +0,0 @@
|
|||||||
import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service';
|
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import { RegistryMetadataschemasSuccessResponse } from '../cache/response.models';
|
|
||||||
|
|
||||||
describe('RegistryMetadataschemasResponseParsingService', () => {
|
|
||||||
let service: RegistryMetadataschemasResponseParsingService;
|
|
||||||
|
|
||||||
const mockDSOParser = Object.assign({
|
|
||||||
processPageInfo: () => new PageInfo()
|
|
||||||
}) as DSOResponseParsingService;
|
|
||||||
|
|
||||||
const data = Object.assign({
|
|
||||||
payload: {
|
|
||||||
_embedded: {
|
|
||||||
metadataschemas: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
prefix: 'test',
|
|
||||||
namespace: 'test namespace'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
prefix: 'second',
|
|
||||||
namespace: 'second test namespace'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as DSpaceRESTV2Response;
|
|
||||||
|
|
||||||
const emptyData = Object.assign({
|
|
||||||
payload: {}
|
|
||||||
}) as DSpaceRESTV2Response;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
service = new RegistryMetadataschemasResponseParsingService(mockDSOParser);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse the data correctly', () => {
|
|
||||||
const response = service.parse(null, data);
|
|
||||||
expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not produce an error and parse the data correctly when the data is empty', () => {
|
|
||||||
const response = service.parse(null, emptyData);
|
|
||||||
expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,29 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { hasValue } from '../../shared/empty.util';
|
|
||||||
import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
|
||||||
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
|
|
||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
|
||||||
import { ResponseParsingService } from './parsing.service';
|
|
||||||
import { RestRequest } from './request.models';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RegistryMetadataschemasResponseParsingService implements ResponseParsingService {
|
|
||||||
constructor(private dsoParser: DSOResponseParsingService) {
|
|
||||||
}
|
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
|
||||||
const payload = data.payload;
|
|
||||||
|
|
||||||
let metadataschemas = [];
|
|
||||||
if (hasValue(payload._embedded)) {
|
|
||||||
metadataschemas = payload._embedded.metadataschemas;
|
|
||||||
}
|
|
||||||
payload.metadataschemas = metadataschemas;
|
|
||||||
|
|
||||||
const deserialized = new DSpaceSerializer(RegistryMetadataschemasResponse).deserialize(payload);
|
|
||||||
return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -11,12 +11,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
|||||||
|
|
||||||
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||||
import {
|
import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction, ResetResponseTimestampsAction } from './request.actions';
|
||||||
RequestActionTypes,
|
|
||||||
RequestCompleteAction,
|
|
||||||
RequestExecuteAction,
|
|
||||||
ResetResponseTimestampsAction
|
|
||||||
} from './request.actions';
|
|
||||||
import { RequestError, RestRequest } from './request.models';
|
import { RequestError, RestRequest } from './request.models';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
@@ -42,12 +37,12 @@ export class RequestEffects {
|
|||||||
filter((entry: RequestEntry) => hasValue(entry)),
|
filter((entry: RequestEntry) => hasValue(entry)),
|
||||||
map((entry: RequestEntry) => entry.request),
|
map((entry: RequestEntry) => entry.request),
|
||||||
flatMap((request: RestRequest) => {
|
flatMap((request: RestRequest) => {
|
||||||
let body;
|
let body = request.body;
|
||||||
if (isNotEmpty(request.body)) {
|
if (isNotEmpty(request.body) && !request.isMultipart) {
|
||||||
const serializer = new DSpaceSerializer(getClassForType(request.body.type));
|
const serializer = new DSpaceSerializer(getClassForType(request.body.type));
|
||||||
body = serializer.serialize(request.body);
|
body = serializer.serialize(request.body);
|
||||||
}
|
}
|
||||||
return this.restApi.request(request.method, request.href, body, request.options).pipe(
|
return this.restApi.request(request.method, request.href, body, request.options, request.isMultipart).pipe(
|
||||||
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
|
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
|
||||||
addToResponseCacheAndCompleteAction(request),
|
addToResponseCacheAndCompleteAction(request),
|
||||||
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(
|
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(
|
||||||
|
@@ -14,12 +14,12 @@ import { RestRequestMethod } from './rest-request-method';
|
|||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
|
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
|
||||||
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
|
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
|
||||||
import { MetadataschemaParsingService } from './metadataschema-parsing.service';
|
|
||||||
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||||
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
|
import { ProcessFilesResponseParsingService } from './process-files-response-parsing.service';
|
||||||
|
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -32,6 +32,8 @@ export enum IdentifierType {
|
|||||||
export abstract class RestRequest {
|
export abstract class RestRequest {
|
||||||
public responseMsToLive = 10 * 1000;
|
public responseMsToLive = 10 * 1000;
|
||||||
public forceBypassCache = false;
|
public forceBypassCache = false;
|
||||||
|
public isMultipart = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
@@ -74,6 +76,21 @@ export class PostRequest extends RestRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request representing a multipart post request
|
||||||
|
*/
|
||||||
|
export class MultipartPostRequest extends RestRequest {
|
||||||
|
public isMultipart = true;
|
||||||
|
constructor(
|
||||||
|
public uuid: string,
|
||||||
|
public href: string,
|
||||||
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
|
) {
|
||||||
|
super(uuid, href, RestRequestMethod.POST, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class PutRequest extends RestRequest {
|
export class PutRequest extends RestRequest {
|
||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
@@ -209,6 +226,15 @@ export class MappedCollectionsRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to fetch the files of a process
|
||||||
|
*/
|
||||||
|
export class ProcessFilesRequest extends GetRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return ProcessFilesResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ConfigRequest extends GetRequest {
|
export class ConfigRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
||||||
super(uuid, href, null, options);
|
super(uuid, href, null, options);
|
||||||
@@ -241,6 +267,15 @@ export class AuthGetRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A POST request for retrieving a token
|
||||||
|
*/
|
||||||
|
export class TokenPostRequest extends PostRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return TokenResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class IntegrationRequest extends GetRequest {
|
export class IntegrationRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string) {
|
constructor(uuid: string, href: string) {
|
||||||
super(uuid, href);
|
super(uuid, href);
|
||||||
@@ -251,58 +286,6 @@ export class IntegrationRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Request to create a MetadataSchema
|
|
||||||
*/
|
|
||||||
export class CreateMetadataSchemaRequest extends PostRequest {
|
|
||||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
|
||||||
super(uuid, href, body, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
|
||||||
return MetadataschemaParsingService;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request to update a MetadataSchema
|
|
||||||
*/
|
|
||||||
export class UpdateMetadataSchemaRequest extends PutRequest {
|
|
||||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
|
||||||
super(uuid, href, body, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
|
||||||
return MetadataschemaParsingService;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request to create a MetadataField
|
|
||||||
*/
|
|
||||||
export class CreateMetadataFieldRequest extends PostRequest {
|
|
||||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
|
||||||
super(uuid, href, body, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
|
||||||
return MetadatafieldParsingService;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request to update a MetadataField
|
|
||||||
*/
|
|
||||||
export class UpdateMetadataFieldRequest extends PutRequest {
|
|
||||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
|
||||||
super(uuid, href, body, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
|
||||||
return MetadatafieldParsingService;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing a submission HTTP GET request object
|
* Class representing a submission HTTP GET request object
|
||||||
*/
|
*/
|
||||||
|
@@ -201,8 +201,9 @@ export class RequestService {
|
|||||||
* Remove all request cache providing (part of) the href
|
* Remove all request cache providing (part of) the href
|
||||||
* This also includes href-to-uuid index cache
|
* This also includes href-to-uuid index cache
|
||||||
* @param href A substring of the request(s) href
|
* @param href A substring of the request(s) href
|
||||||
|
* @return Returns an observable emitting whether or not the cache is removed
|
||||||
*/
|
*/
|
||||||
removeByHrefSubstring(href: string) {
|
removeByHrefSubstring(href: string): Observable<boolean> {
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||||
take(1)
|
take(1)
|
||||||
@@ -213,6 +214,11 @@ export class RequestService {
|
|||||||
});
|
});
|
||||||
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
|
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
|
||||||
this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href));
|
this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href));
|
||||||
|
|
||||||
|
return this.store.pipe(
|
||||||
|
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||||
|
map((uuids) => isEmpty(uuids))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -69,10 +69,12 @@ export class DSpaceRESTv2Service {
|
|||||||
* an optional body for the request
|
* an optional body for the request
|
||||||
* @param options
|
* @param options
|
||||||
* the HttpOptions object
|
* the HttpOptions object
|
||||||
|
* @param isMultipart
|
||||||
|
* true when this concerns a multipart request
|
||||||
* @return {Observable<string>}
|
* @return {Observable<string>}
|
||||||
* An Observable<string> containing the response from the server
|
* An Observable<string> containing the response from the server
|
||||||
*/
|
*/
|
||||||
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable<DSpaceRESTV2Response> {
|
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions, isMultipart?: boolean): Observable<DSpaceRESTV2Response> {
|
||||||
const requestOptions: HttpOptions = {};
|
const requestOptions: HttpOptions = {};
|
||||||
requestOptions.body = body;
|
requestOptions.body = body;
|
||||||
if (method === RestRequestMethod.POST && isNotEmpty(body) && isNotEmpty(body.name)) {
|
if (method === RestRequestMethod.POST && isNotEmpty(body) && isNotEmpty(body.name)) {
|
||||||
@@ -98,7 +100,7 @@ export class DSpaceRESTv2Service {
|
|||||||
requestOptions.withCredentials = options.withCredentials;
|
requestOptions.withCredentials = options.withCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!requestOptions.headers.has('Content-Type')) {
|
if (!requestOptions.headers.has('Content-Type') && !isMultipart) {
|
||||||
// Because HttpHeaders is immutable, the set method returns a new object instead of updating the existing headers
|
// Because HttpHeaders is immutable, the set method returns a new object instead of updating the existing headers
|
||||||
requestOptions.headers = requestOptions.headers.set('Content-Type', DEFAULT_CONTENT_TYPE);
|
requestOptions.headers = requestOptions.headers.set('Content-Type', DEFAULT_CONTENT_TYPE);
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { ChangeAnalyzer } from '../data/change-analyzer';
|
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { DeleteByIDRequest, FindListOptions, PatchRequest } from '../data/request.models';
|
import { DeleteByIDRequest, FindListOptions, PatchRequest, PostRequest } from '../data/request.models';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
@@ -291,6 +291,25 @@ describe('EPersonDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createEPersonForToken', () => {
|
||||||
|
it('should sent a postRquest with an eperson to the token endpoint', () => {
|
||||||
|
service.createEPersonForToken(EPersonMock, 'test-token');
|
||||||
|
|
||||||
|
const expected = new PostRequest(requestService.generateRequestId(), epersonsEndpoint + '?token=test-token', EPersonMock);
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('patchPasswordWithToken', () => {
|
||||||
|
it('should sent a patch request with an uuid, token and new password to the epersons endpoint', () => {
|
||||||
|
service.patchPasswordWithToken('test-uuid', 'test-token','test-password');
|
||||||
|
|
||||||
|
const operation = Object.assign({ op: 'replace', path: '/password', value: 'test-password' });
|
||||||
|
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]);
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
|
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
|
||||||
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
import { Operation } from 'fast-json-patch/lib/core';
|
import { Operation } from 'fast-json-patch/lib/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, take } from 'rxjs/operators';
|
import { filter, find, map, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
EPeopleRegistryCancelEPersonAction,
|
EPeopleRegistryCancelEPersonAction,
|
||||||
EPeopleRegistryEditEPersonAction
|
EPeopleRegistryEditEPersonAction
|
||||||
@@ -22,12 +22,13 @@ import { DataService } from '../data/data.service';
|
|||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { FindListOptions, FindListRequest, PatchRequest, } from '../data/request.models';
|
import { FindListOptions, FindListRequest, PatchRequest, PostRequest, } from '../data/request.models';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators';
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators';
|
||||||
import { EPerson } from './models/eperson.model';
|
import { EPerson } from './models/eperson.model';
|
||||||
import { EPERSON } from './models/eperson.resource-type';
|
import { EPERSON } from './models/eperson.resource-type';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
|
||||||
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
|
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
|
||||||
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
|
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
|
||||||
@@ -165,17 +166,17 @@ export class EPersonDataService extends DataService<EPerson> {
|
|||||||
if (hasValue(oldEPerson.email) && oldEPerson.email !== newEPerson.email) {
|
if (hasValue(oldEPerson.email) && oldEPerson.email !== newEPerson.email) {
|
||||||
operations = [...operations, {
|
operations = [...operations, {
|
||||||
op: 'replace', path: '/email', value: newEPerson.email
|
op: 'replace', path: '/email', value: newEPerson.email
|
||||||
}]
|
}];
|
||||||
}
|
}
|
||||||
if (hasValue(oldEPerson.requireCertificate) && oldEPerson.requireCertificate !== newEPerson.requireCertificate) {
|
if (hasValue(oldEPerson.requireCertificate) && oldEPerson.requireCertificate !== newEPerson.requireCertificate) {
|
||||||
operations = [...operations, {
|
operations = [...operations, {
|
||||||
op: 'replace', path: '/certificate', value: newEPerson.requireCertificate
|
op: 'replace', path: '/certificate', value: newEPerson.requireCertificate
|
||||||
}]
|
}];
|
||||||
}
|
}
|
||||||
if (hasValue(oldEPerson.canLogIn) && oldEPerson.canLogIn !== newEPerson.canLogIn) {
|
if (hasValue(oldEPerson.canLogIn) && oldEPerson.canLogIn !== newEPerson.canLogIn) {
|
||||||
operations = [...operations, {
|
operations = [...operations, {
|
||||||
op: 'replace', path: '/canLogIn', value: newEPerson.canLogIn
|
op: 'replace', path: '/canLogIn', value: newEPerson.canLogIn
|
||||||
}]
|
}];
|
||||||
}
|
}
|
||||||
return operations;
|
return operations;
|
||||||
}
|
}
|
||||||
@@ -200,7 +201,7 @@ export class EPersonDataService extends DataService<EPerson> {
|
|||||||
* Method to retrieve the eperson that is currently being edited
|
* Method to retrieve the eperson that is currently being edited
|
||||||
*/
|
*/
|
||||||
public getActiveEPerson(): Observable<EPerson> {
|
public getActiveEPerson(): Observable<EPerson> {
|
||||||
return this.store.pipe(select(editEPersonSelector))
|
return this.store.pipe(select(editEPersonSelector));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -249,4 +250,54 @@ export class EPersonDataService extends DataService<EPerson> {
|
|||||||
return '/admin/access-control/epeople';
|
return '/admin/access-control/epeople';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new EPerson using a token
|
||||||
|
* @param eperson
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
public createEPersonForToken(eperson: EPerson, token: string) {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const hrefObs = this.getBrowseEndpoint().pipe(
|
||||||
|
map((href: string) => `${href}?token=${token}`));
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new PostRequest(requestId, href, eperson);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.fetchResponse(requestId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a patch request to update an epersons password based on a forgot password token
|
||||||
|
* @param uuid Uuid of the eperson
|
||||||
|
* @param token The forgot password token
|
||||||
|
* @param password The new password value
|
||||||
|
*/
|
||||||
|
patchPasswordWithToken(uuid: string, token: string, password: string): Observable<RestResponse> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const operation = Object.assign({ op: 'replace', path: '/password', value: password });
|
||||||
|
|
||||||
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, uuid)),
|
||||||
|
map((href: string) => `${href}?token=${token}`));
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new PatchRequest(requestId, href, [operation]);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((request: RequestEntry) => request.completed),
|
||||||
|
map((request: RequestEntry) => request.response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -57,6 +57,12 @@ export class EPerson extends DSpaceObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
public selfRegistered: boolean;
|
public selfRegistered: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The password of this EPerson
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
public password: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter to retrieve the EPerson's full name as a string
|
* Getter to retrieve the EPerson's full name as a string
|
||||||
*/
|
*/
|
||||||
|
@@ -1,24 +0,0 @@
|
|||||||
import { autoserialize, deserialize } from 'cerialize';
|
|
||||||
import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type';
|
|
||||||
import { HALLink } from '../shared/hal-link.model';
|
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
|
||||||
import { link } from '../cache/builders/build-decorators';
|
|
||||||
|
|
||||||
export class RegistryBitstreamformatsResponse {
|
|
||||||
@autoserialize
|
|
||||||
page: PageInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link HALLink}s for this RegistryBitstreamformatsResponse
|
|
||||||
*/
|
|
||||||
@deserialize
|
|
||||||
_links: {
|
|
||||||
self: HALLink;
|
|
||||||
bitstreamformats: HALLink;
|
|
||||||
};
|
|
||||||
|
|
||||||
@link(BITSTREAM_FORMAT)
|
|
||||||
bitstreamformats?: BitstreamFormat[];
|
|
||||||
|
|
||||||
}
|
|
@@ -1,46 +0,0 @@
|
|||||||
import { autoserialize, deserialize } from 'cerialize';
|
|
||||||
import { typedObject } from '../cache/builders/build-decorators';
|
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
|
||||||
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
|
|
||||||
import { HALLink } from '../shared/hal-link.model';
|
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { ResourceType } from '../shared/resource-type';
|
|
||||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that represents a response with a registry's metadata fields
|
|
||||||
*/
|
|
||||||
@typedObject
|
|
||||||
export class RegistryMetadatafieldsResponse {
|
|
||||||
static type = METADATA_FIELD;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The object type
|
|
||||||
*/
|
|
||||||
@excludeFromEquals
|
|
||||||
@autoserialize
|
|
||||||
type: ResourceType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of metadata fields in the response
|
|
||||||
*/
|
|
||||||
@deserialize
|
|
||||||
metadatafields: MetadataField[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page info of this response
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
page: PageInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The REST link to this response
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
self: string;
|
|
||||||
|
|
||||||
@deserialize
|
|
||||||
_links: {
|
|
||||||
self: HALLink,
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { autoserialize, deserialize } from 'cerialize';
|
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
|
||||||
|
|
||||||
export class RegistryMetadataschemasResponse {
|
|
||||||
@deserialize
|
|
||||||
metadataschemas: MetadataSchema[];
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
page: PageInfo;
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
self: string;
|
|
||||||
}
|
|
@@ -3,8 +3,7 @@ import { Component } from '@angular/core';
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
|
||||||
import {
|
import {
|
||||||
MetadataRegistryCancelFieldAction,
|
MetadataRegistryCancelFieldAction,
|
||||||
MetadataRegistryCancelSchemaAction,
|
MetadataRegistryCancelSchemaAction,
|
||||||
@@ -17,30 +16,20 @@ import {
|
|||||||
MetadataRegistrySelectFieldAction,
|
MetadataRegistrySelectFieldAction,
|
||||||
MetadataRegistrySelectSchemaAction
|
MetadataRegistrySelectSchemaAction
|
||||||
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
|
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { StoreMock } from '../../shared/testing/store.mock';
|
import { StoreMock } from '../../shared/testing/store.mock';
|
||||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
|
|
||||||
import {
|
import { RestResponse } from '../cache/response.models';
|
||||||
RegistryMetadatafieldsSuccessResponse,
|
|
||||||
RegistryMetadataschemasSuccessResponse,
|
|
||||||
RestResponse
|
|
||||||
} from '../cache/response.models';
|
|
||||||
import { RemoteData } from '../data/remote-data';
|
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
|
||||||
import { RequestService } from '../data/request.service';
|
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
|
||||||
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
|
||||||
import { RegistryService } from './registry.service';
|
import { RegistryService } from './registry.service';
|
||||||
import { storeModuleConfig } from '../../app.reducer';
|
import { storeModuleConfig } from '../../app.reducer';
|
||||||
|
import { FindListOptions } from '../data/request.models';
|
||||||
|
import { MetadataSchemaDataService } from '../data/metadata-schema-data.service';
|
||||||
|
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
|
||||||
@Component({ template: '' })
|
@Component({ template: '' })
|
||||||
class DummyComponent {
|
class DummyComponent {
|
||||||
@@ -49,12 +38,20 @@ class DummyComponent {
|
|||||||
describe('RegistryService', () => {
|
describe('RegistryService', () => {
|
||||||
let registryService: RegistryService;
|
let registryService: RegistryService;
|
||||||
let mockStore;
|
let mockStore;
|
||||||
const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
let metadataSchemaService: MetadataSchemaDataService;
|
||||||
id: 'registry-service-spec-pagination',
|
let metadataFieldService: MetadataFieldDataService;
|
||||||
pageSize: 20
|
|
||||||
|
let options: FindListOptions;
|
||||||
|
let mockSchemasList: MetadataSchema[];
|
||||||
|
let mockFieldsList: MetadataField[];
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
options = Object.assign(new FindListOptions(), {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: 20
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockSchemasList = [
|
mockSchemasList = [
|
||||||
Object.assign(new MetadataSchema(), {
|
Object.assign(new MetadataSchema(), {
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -74,7 +71,8 @@ describe('RegistryService', () => {
|
|||||||
type: MetadataSchema.type
|
type: MetadataSchema.type
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
const mockFieldsList = [
|
|
||||||
|
mockFieldsList = [
|
||||||
Object.assign(new MetadataField(),
|
Object.assign(new MetadataField(),
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -125,135 +123,84 @@ describe('RegistryService', () => {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const pageInfo = new PageInfo();
|
metadataSchemaService = jasmine.createSpyObj('metadataSchemaService', {
|
||||||
pageInfo.elementsPerPage = 20;
|
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)),
|
||||||
pageInfo.currentPage = 1;
|
findById: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||||
|
createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||||
|
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
||||||
|
clearRequests: observableOf('href')
|
||||||
|
});
|
||||||
|
|
||||||
const endpoint = 'path';
|
metadataFieldService = jasmine.createSpyObj('metadataFieldService', {
|
||||||
const endpointWithParams = `${endpoint}?size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`;
|
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockFieldsList)),
|
||||||
const fieldEndpointWithParams = `${endpoint}?schema=${mockSchemasList[0].prefix}&size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`;
|
findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||||
|
createOrUpdateMetadataField: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||||
const halServiceStub = {
|
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
||||||
getEndpoint: (link: string) => observableOf(endpoint)
|
clearRequests: observableOf('href')
|
||||||
};
|
});
|
||||||
|
|
||||||
const rdbStub = {
|
|
||||||
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
|
|
||||||
return observableCombineLatest(requestEntryObs,
|
|
||||||
payloadObs).pipe(map(([req, pay]) => {
|
|
||||||
return { req, pay };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
aggregate: (input: Array<Observable<RemoteData<any>>>): Observable<RemoteData<any[]>> => {
|
|
||||||
return createSuccessfulRemoteDataObject$([]);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot()],
|
imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot()],
|
||||||
declarations: [
|
declarations: [
|
||||||
DummyComponent
|
DummyComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RequestService, useValue: getMockRequestService() },
|
|
||||||
{ provide: RemoteDataBuildService, useValue: rdbStub },
|
|
||||||
{ provide: HALEndpointService, useValue: halServiceStub },
|
|
||||||
{ provide: Store, useClass: StoreMock },
|
{ provide: Store, useClass: StoreMock },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: MetadataSchemaDataService, useValue: metadataSchemaService },
|
||||||
|
{ provide: MetadataFieldDataService, useValue: metadataFieldService },
|
||||||
RegistryService
|
RegistryService
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
registryService = TestBed.get(RegistryService);
|
registryService = TestBed.get(RegistryService);
|
||||||
mockStore = TestBed.get(Store);
|
mockStore = TestBed.get(Store);
|
||||||
spyOn((registryService as any).halService, 'getEndpoint').and.returnValue(observableOf(endpoint));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when requesting metadataschemas', () => {
|
describe('when requesting metadataschemas', () => {
|
||||||
const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), {
|
let result;
|
||||||
metadataschemas: mockSchemasList,
|
|
||||||
page: pageInfo
|
|
||||||
});
|
|
||||||
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
|
||||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
result = registryService.getMetadataSchemas(options);
|
||||||
/* tslint:disable:no-empty */
|
|
||||||
registryService.getMetadataSchemas(pagination).subscribe((value) => {
|
|
||||||
});
|
|
||||||
/* tslint:enable:no-empty */
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getEndpoint on the halService', () => {
|
it('should call metadataSchemaService.findAll', (done) => {
|
||||||
expect((registryService as any).halService.getEndpoint).toHaveBeenCalled();
|
result.subscribe(() => {
|
||||||
|
expect(metadataSchemaService.findAll).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send out the request on the request service', () => {
|
|
||||||
expect((registryService as any).requestService.configure).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call getByHref on the request service with the correct request url', () => {
|
|
||||||
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when requesting metadataschema by name', () => {
|
describe('when requesting metadataschema by name', () => {
|
||||||
const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), {
|
let result;
|
||||||
metadataschemas: mockSchemasList,
|
|
||||||
page: pageInfo
|
|
||||||
});
|
|
||||||
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
|
||||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
result = registryService.getMetadataSchemaByName(mockSchemasList[0].prefix);
|
||||||
/* tslint:disable:no-empty */
|
|
||||||
registryService.getMetadataSchemaByName(mockSchemasList[0].prefix).subscribe((value) => {
|
|
||||||
});
|
|
||||||
/* tslint:enable:no-empty */
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getEndpoint on the halService', () => {
|
it('should call metadataSchemaService.findById with the correct ID', (done) => {
|
||||||
expect((registryService as any).halService.getEndpoint).toHaveBeenCalled();
|
result.subscribe(() => {
|
||||||
|
expect(metadataSchemaService.findById).toHaveBeenCalledWith(`${mockSchemasList[0].id}`);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send out the request on the request service', () => {
|
|
||||||
expect((registryService as any).requestService.configure).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call getByHref on the request service with the correct request url', () => {
|
|
||||||
expect((registryService as any).requestService.getByHref.calls.argsFor(0)[0]).toContain(endpoint);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when requesting metadatafields', () => {
|
describe('when requesting metadatafields', () => {
|
||||||
const queryResponse = Object.assign(new RegistryMetadatafieldsResponse(), {
|
let result;
|
||||||
metadatafields: mockFieldsList,
|
|
||||||
page: pageInfo
|
|
||||||
});
|
|
||||||
const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
|
||||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
result = registryService.getAllMetadataFields();
|
||||||
/* tslint:disable:no-empty */
|
|
||||||
registryService.getMetadataFieldsBySchema(mockSchemasList[0], pagination).subscribe((value) => {
|
|
||||||
});
|
|
||||||
/* tslint:enable:no-empty */
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getEndpoint on the halService', () => {
|
it('should call metadataFieldService.findAll', (done) => {
|
||||||
expect((registryService as any).halService.getEndpoint).toHaveBeenCalled();
|
result.subscribe(() => {
|
||||||
|
expect(metadataFieldService.findAll).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send out the request on the request service', () => {
|
|
||||||
expect((registryService as any).requestService.configure).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call getByHref on the request service with the correct request url', () => {
|
|
||||||
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(fieldEndpointWithParams);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -370,9 +317,10 @@ describe('RegistryService', () => {
|
|||||||
result = registryService.createOrUpdateMetadataSchema(mockSchemasList[0]);
|
result = registryService.createOrUpdateMetadataSchema(mockSchemasList[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the created/updated metadata schema', () => {
|
it('should return the created/updated metadata schema', (done) => {
|
||||||
result.subscribe((schema: MetadataSchema) => {
|
result.subscribe((schema: MetadataSchema) => {
|
||||||
expect(schema).toEqual(mockSchemasList[0]);
|
expect(schema).toEqual(mockSchemasList[0]);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -384,9 +332,10 @@ describe('RegistryService', () => {
|
|||||||
result = registryService.createOrUpdateMetadataField(mockFieldsList[0]);
|
result = registryService.createOrUpdateMetadataField(mockFieldsList[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the created/updated metadata field', () => {
|
it('should return the created/updated metadata field', (done) => {
|
||||||
result.subscribe((field: MetadataField) => {
|
result.subscribe((field: MetadataField) => {
|
||||||
expect(field).toEqual(mockFieldsList[0]);
|
expect(field).toEqual(mockFieldsList[0]);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -425,7 +374,7 @@ describe('RegistryService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should remove the requests related to metadata schemas from cache', () => {
|
it('should remove the requests related to metadata schemas from cache', () => {
|
||||||
expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled();
|
expect(metadataSchemaService.clearRequests).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -435,7 +384,7 @@ describe('RegistryService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should remove the requests related to metadata fields from cache', () => {
|
it('should remove the requests related to metadata fields from cache', () => {
|
||||||
expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled();
|
expect(metadataFieldService.clearRequests).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -2,37 +2,18 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import {
|
import { FindListOptions } from '../data/request.models';
|
||||||
CreateMetadataFieldRequest,
|
|
||||||
CreateMetadataSchemaRequest,
|
|
||||||
DeleteRequest,
|
|
||||||
GetRequest,
|
|
||||||
RestRequest,
|
|
||||||
UpdateMetadataFieldRequest,
|
|
||||||
UpdateMetadataSchemaRequest
|
|
||||||
} from '../data/request.models';
|
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
|
||||||
import { ResponseParsingService } from '../data/parsing.service';
|
|
||||||
import { RegistryMetadataschemasResponseParsingService } from '../data/registry-metadataschemas-response-parsing.service';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
|
||||||
import {
|
import {
|
||||||
MetadatafieldSuccessResponse,
|
MetadatafieldSuccessResponse,
|
||||||
MetadataschemaSuccessResponse,
|
MetadataschemaSuccessResponse,
|
||||||
RegistryMetadatafieldsSuccessResponse,
|
|
||||||
RegistryMetadataschemasSuccessResponse,
|
|
||||||
RestResponse
|
RestResponse
|
||||||
} from '../cache/response.models';
|
} from '../cache/response.models';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service';
|
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||||
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { configureRequest, getResponseFromEntry } from '../shared/operators';
|
|
||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
||||||
@@ -48,15 +29,14 @@ import {
|
|||||||
MetadataRegistrySelectFieldAction,
|
MetadataRegistrySelectFieldAction,
|
||||||
MetadataRegistrySelectSchemaAction
|
MetadataRegistrySelectSchemaAction
|
||||||
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
|
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
|
||||||
import { distinctUntilChanged, flatMap, map, take, tap } from 'rxjs/operators';
|
import { flatMap, map, tap } from 'rxjs/operators';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
|
||||||
import { HttpHeaders } from '@angular/common/http';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
import { getClassForType } from '../cache/builders/build-decorators';
|
import { MetadataSchemaDataService } from '../data/metadata-schema-data.service';
|
||||||
|
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
||||||
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
||||||
@@ -70,221 +50,64 @@ const selectedMetadataFieldsSelector = createSelector(metadataRegistryStateSelec
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class RegistryService {
|
export class RegistryService {
|
||||||
|
|
||||||
private metadataSchemasPath = 'metadataschemas';
|
constructor(private store: Store<AppState>,
|
||||||
private metadataFieldsPath = 'metadatafields';
|
|
||||||
|
|
||||||
// private bitstreamFormatsPath = 'bitstreamformats';
|
|
||||||
|
|
||||||
constructor(protected requestService: RequestService,
|
|
||||||
private rdb: RemoteDataBuildService,
|
|
||||||
private halService: HALEndpointService,
|
|
||||||
private store: Store<AppState>,
|
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private translateService: TranslateService) {
|
private translateService: TranslateService,
|
||||||
|
private metadataSchemaService: MetadataSchemaDataService,
|
||||||
|
private metadataFieldService: MetadataFieldDataService) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all metadata schemas
|
* Retrieves all metadata schemas
|
||||||
* @param pagination The pagination info used to retrieve the schemas
|
* @param options The options used to retrieve the schemas
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
public getMetadataSchemas(pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataSchema>>> {
|
public getMetadataSchemas(options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataSchema>>): Observable<RemoteData<PaginatedList<MetadataSchema>>> {
|
||||||
const requestObs = this.getMetadataSchemasRequestObs(pagination);
|
return this.metadataSchemaService.findAll(options, ...linksToFollow);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
|
||||||
);
|
|
||||||
|
|
||||||
const rmrObs: Observable<RegistryMetadataschemasResponse> = requestEntryObs.pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse)
|
|
||||||
);
|
|
||||||
|
|
||||||
const metadataschemasObs: Observable<MetadataSchema[]> = rmrObs.pipe(
|
|
||||||
map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas)
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
map((response: RegistryMetadataschemasSuccessResponse) => response.pageInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const payloadObs = observableCombineLatest(metadataschemasObs, pageInfoObs).pipe(
|
|
||||||
map(([metadataschemas, pageInfo]) => {
|
|
||||||
return new PaginatedList(pageInfo, metadataschemas);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a metadata schema by its name
|
* Retrieves a metadata schema by its name
|
||||||
* @param schemaName The name of the schema to find
|
* @param schemaName The name of the schema to find
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
public getMetadataSchemaByName(schemaName: string): Observable<RemoteData<MetadataSchema>> {
|
public getMetadataSchemaByName(schemaName: string, ...linksToFollow: Array<FollowLinkConfig<MetadataSchema>>): Observable<RemoteData<MetadataSchema>> {
|
||||||
// Temporary pagination to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema
|
// Temporary options to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema
|
||||||
const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
const options: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
id: 'all-metadatafields-pagination',
|
elementsPerPage: 10000
|
||||||
pageSize: 10000
|
|
||||||
});
|
});
|
||||||
const requestObs = this.getMetadataSchemasRequestObs(pagination);
|
return this.getMetadataSchemas(options).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
const requestEntryObs = requestObs.pipe(
|
map((schemas: PaginatedList<MetadataSchema>) => schemas.page),
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
isNotEmptyOperator(),
|
||||||
|
map((schemas: MetadataSchema[]) => schemas.filter((schema) => schema.prefix === schemaName)[0]),
|
||||||
|
flatMap((schema: MetadataSchema) => this.metadataSchemaService.findById(`${schema.id}`, ...linksToFollow))
|
||||||
);
|
);
|
||||||
|
|
||||||
const rmrObs: Observable<RegistryMetadataschemasResponse> = requestEntryObs.pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse)
|
|
||||||
);
|
|
||||||
|
|
||||||
const metadataschemaObs: Observable<MetadataSchema> = rmrObs.pipe(
|
|
||||||
map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas),
|
|
||||||
map((metadataSchemas: MetadataSchema[]) => metadataSchemas.filter((value) => value.prefix === schemaName)[0])
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, metadataschemaObs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* retrieves all metadata fields that belong to a certain metadata schema
|
* retrieves all metadata fields that belong to a certain metadata schema
|
||||||
* @param schema The schema to filter by
|
* @param schema The schema to filter by
|
||||||
* @param pagination The pagination info used to retrieve the fields
|
* @param options The options info used to retrieve the fields
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
public getMetadataFieldsBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
const requestObs = this.getMetadataFieldsBySchemaRequestObs(pagination, schema);
|
return this.metadataFieldService.findBySchema(schema, options, ...linksToFollow);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
|
||||||
);
|
|
||||||
|
|
||||||
const rmrObs: Observable<RegistryMetadatafieldsResponse> = requestEntryObs.pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse)
|
|
||||||
);
|
|
||||||
|
|
||||||
const metadatafieldsObs: Observable<MetadataField[]> = rmrObs.pipe(
|
|
||||||
map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields)
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
|
|
||||||
map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe(
|
|
||||||
map(([metadatafields, pageInfo]) => {
|
|
||||||
return new PaginatedList(pageInfo, metadatafields);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all existing metadata fields as a paginated list
|
* Retrieve all existing metadata fields as a paginated list
|
||||||
* @param pagination Pagination options to determine which page of metadata fields should be requested
|
* @param options Options to determine which page of metadata fields should be requested
|
||||||
* When no pagination is provided, all metadata fields are requested in one large page
|
* When no options are provided, all metadata fields are requested in one large page
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
* @returns an observable that emits a remote data object with a page of metadata fields
|
* @returns an observable that emits a remote data object with a page of metadata fields
|
||||||
*/
|
*/
|
||||||
public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
public getAllMetadataFields(options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
if (hasNoValue(pagination)) {
|
if (hasNoValue(options)) {
|
||||||
pagination = {currentPage: 1, pageSize: 10000} as any;
|
options = {currentPage: 1, elementsPerPage: 10000} as any;
|
||||||
}
|
}
|
||||||
const requestObs = this.getMetadataFieldsRequestObs(pagination);
|
return this.metadataFieldService.findAll(options, ...linksToFollow);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
|
||||||
);
|
|
||||||
|
|
||||||
const rmrObs: Observable<RegistryMetadatafieldsResponse> = requestEntryObs.pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse)
|
|
||||||
);
|
|
||||||
|
|
||||||
const metadatafieldsObs: Observable<MetadataField[]> = rmrObs.pipe(
|
|
||||||
map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields),
|
|
||||||
/* Make sure to explicitly cast this into a MetadataField object, on first page loads this object comes from the object cache created by the server and its prototype is unknown */
|
|
||||||
map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => Object.assign(new MetadataField(), metadataField)))
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
|
|
||||||
map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe(
|
|
||||||
map(([metadatafields, pageInfo]) => {
|
|
||||||
return new PaginatedList(pageInfo, metadatafields);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
|
||||||
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
|
||||||
map((url: string) => {
|
|
||||||
const args: string[] = [];
|
|
||||||
args.push(`size=${pagination.pageSize}`);
|
|
||||||
args.push(`page=${pagination.currentPage - 1}`);
|
|
||||||
if (isNotEmpty(args)) {
|
|
||||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
|
||||||
}
|
|
||||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
|
||||||
return Object.assign(request, {
|
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
|
||||||
return RegistryMetadataschemasResponseParsingService;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMetadataFieldsBySchemaRequestObs(pagination: PaginationComponentOptions, schema: MetadataSchema): Observable<RestRequest> {
|
|
||||||
return this.halService.getEndpoint(this.metadataFieldsPath + '/search/bySchema').pipe(
|
|
||||||
// return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
|
||||||
map((url: string) => {
|
|
||||||
const args: string[] = [];
|
|
||||||
args.push(`schema=${schema.prefix}`);
|
|
||||||
args.push(`size=${pagination.pageSize}`);
|
|
||||||
args.push(`page=${pagination.currentPage - 1}`);
|
|
||||||
if (isNotEmpty(args)) {
|
|
||||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
|
||||||
}
|
|
||||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
|
||||||
return Object.assign(request, {
|
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
|
||||||
return RegistryMetadatafieldsResponseParsingService;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMetadataFieldsRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
|
||||||
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
|
||||||
map((url: string) => {
|
|
||||||
const args: string[] = [];
|
|
||||||
args.push(`size=${pagination.pageSize}`);
|
|
||||||
args.push(`page=${pagination.currentPage - 1}`);
|
|
||||||
if (isNotEmpty(args)) {
|
|
||||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
|
||||||
}
|
|
||||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
|
||||||
return Object.assign(request, {
|
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
|
||||||
return RegistryMetadatafieldsResponseParsingService;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public editMetadataSchema(schema: MetadataSchema) {
|
public editMetadataSchema(schema: MetadataSchema) {
|
||||||
@@ -386,59 +209,17 @@ export class RegistryService {
|
|||||||
* Create or Update a MetadataSchema
|
* Create or Update a MetadataSchema
|
||||||
* If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead
|
* If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead
|
||||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||||
* - On creation, a CreateMetadataSchemaRequest is used
|
* - On creation, a CreateRequest is used
|
||||||
* - On update, a UpdateMetadataSchemaRequest is used
|
* - On update, a PutRequest is used
|
||||||
* @param schema The MetadataSchema to create or update
|
* @param schema The MetadataSchema to create or update
|
||||||
*/
|
*/
|
||||||
public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<MetadataSchema> {
|
public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<MetadataSchema> {
|
||||||
const isUpdate = hasValue(schema.id);
|
const isUpdate = hasValue(schema.id);
|
||||||
const requestId = this.requestService.generateRequestId();
|
return this.metadataSchemaService.createOrUpdateMetadataSchema(schema).pipe(
|
||||||
const endpoint$ = this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
getFirstSucceededRemoteDataPayload(),
|
||||||
isNotEmptyOperator(),
|
hasValueOperator(),
|
||||||
map((endpoint: string) => (isUpdate ? `${endpoint}/${schema.id}` : endpoint)),
|
tap(() => {
|
||||||
distinctUntilChanged()
|
|
||||||
);
|
|
||||||
|
|
||||||
const serializedSchema = new DSpaceSerializer(getClassForType(MetadataSchema.type)).serialize(schema);
|
|
||||||
|
|
||||||
const request$ = endpoint$.pipe(
|
|
||||||
take(1),
|
|
||||||
map((endpoint: string) => {
|
|
||||||
if (isUpdate) {
|
|
||||||
const options: HttpOptions = Object.create({});
|
|
||||||
let headers = new HttpHeaders();
|
|
||||||
headers = headers.append('Content-Type', 'application/json');
|
|
||||||
options.headers = headers;
|
|
||||||
return new UpdateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema), options);
|
|
||||||
} else {
|
|
||||||
return new CreateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute the post/put request
|
|
||||||
request$.pipe(
|
|
||||||
configureRequest(this.requestService)
|
|
||||||
).subscribe();
|
|
||||||
|
|
||||||
// Return created/updated schema
|
|
||||||
return this.requestService.getByUUID(requestId).pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
map((response: RestResponse) => {
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
if (hasValue((response as any).errorMessage)) {
|
|
||||||
this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.showNotifications(true, isUpdate, false, {prefix: schema.prefix});
|
this.showNotifications(true, isUpdate, false, {prefix: schema.prefix});
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
isNotEmptyOperator(),
|
|
||||||
map((response: MetadataschemaSuccessResponse) => {
|
|
||||||
if (isNotEmpty(response.metadataschema)) {
|
|
||||||
return response.metadataschema;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -448,74 +229,32 @@ export class RegistryService {
|
|||||||
* @param id The id of the metadata schema to delete
|
* @param id The id of the metadata schema to delete
|
||||||
*/
|
*/
|
||||||
public deleteMetadataSchema(id: number): Observable<RestResponse> {
|
public deleteMetadataSchema(id: number): Observable<RestResponse> {
|
||||||
return this.delete(this.metadataSchemasPath, id);
|
return this.metadataSchemaService.deleteAndReturnResponse(`${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that clears a cached metadata schema request and returns its REST url
|
* Method that clears a cached metadata schema request and returns its REST url
|
||||||
*/
|
*/
|
||||||
public clearMetadataSchemaRequests(): Observable<string> {
|
public clearMetadataSchemaRequests(): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
return this.metadataSchemaService.clearRequests();
|
||||||
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create or Update a MetadataField
|
* Create or Update a MetadataField
|
||||||
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
||||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||||
* - On creation, a CreateMetadataFieldRequest is used
|
* - On creation, a CreateRequest is used
|
||||||
* - On update, a UpdateMetadataFieldRequest is used
|
* - On update, a PutRequest is used
|
||||||
* @param field The MetadataField to create or update
|
* @param field The MetadataField to create or update
|
||||||
*/
|
*/
|
||||||
public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> {
|
public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> {
|
||||||
const isUpdate = hasValue(field.id);
|
const isUpdate = hasValue(field.id);
|
||||||
const requestId = this.requestService.generateRequestId();
|
return this.metadataFieldService.createOrUpdateMetadataField(field).pipe(
|
||||||
const endpoint$ = this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
getFirstSucceededRemoteDataPayload(),
|
||||||
isNotEmptyOperator(),
|
hasValueOperator(),
|
||||||
map((endpoint: string) => (isUpdate ? `${endpoint}/${field.id}` : `${endpoint}?schemaId=${field.schema.id}`)),
|
tap(() => {
|
||||||
distinctUntilChanged()
|
|
||||||
);
|
|
||||||
|
|
||||||
const request$ = endpoint$.pipe(
|
|
||||||
take(1),
|
|
||||||
map((endpoint: string) => {
|
|
||||||
if (isUpdate) {
|
|
||||||
const options: HttpOptions = Object.create({});
|
|
||||||
let headers = new HttpHeaders();
|
|
||||||
headers = headers.append('Content-Type', 'application/json');
|
|
||||||
options.headers = headers;
|
|
||||||
return new UpdateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field), options);
|
|
||||||
} else {
|
|
||||||
return new CreateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute the post/put request
|
|
||||||
request$.pipe(
|
|
||||||
configureRequest(this.requestService)
|
|
||||||
).subscribe();
|
|
||||||
|
|
||||||
// Return created/updated field
|
|
||||||
return this.requestService.getByUUID(requestId).pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
map((response: RestResponse) => {
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
if (hasValue((response as any).errorMessage)) {
|
|
||||||
this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`;
|
const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`;
|
||||||
this.showNotifications(true, isUpdate, true, {field: fieldString});
|
this.showNotifications(true, isUpdate, true, {field: fieldString});
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
isNotEmptyOperator(),
|
|
||||||
map((response: MetadatafieldSuccessResponse) => {
|
|
||||||
if (isNotEmpty(response.metadatafield)) {
|
|
||||||
return response.metadatafield;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -525,38 +264,13 @@ export class RegistryService {
|
|||||||
* @param id The id of the metadata field to delete
|
* @param id The id of the metadata field to delete
|
||||||
*/
|
*/
|
||||||
public deleteMetadataField(id: number): Observable<RestResponse> {
|
public deleteMetadataField(id: number): Observable<RestResponse> {
|
||||||
return this.delete(this.metadataFieldsPath, id);
|
return this.metadataFieldService.deleteAndReturnResponse(`${id}`);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Method that clears a cached metadata field request and returns its REST url
|
* Method that clears a cached metadata field request and returns its REST url
|
||||||
*/
|
*/
|
||||||
public clearMetadataFieldRequests(): Observable<string> {
|
public clearMetadataFieldRequests(): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
return this.metadataFieldService.clearRequests();
|
||||||
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private delete(path: string, id: number): Observable<RestResponse> {
|
|
||||||
const requestId = this.requestService.generateRequestId();
|
|
||||||
const endpoint$ = this.halService.getEndpoint(path).pipe(
|
|
||||||
isNotEmptyOperator(),
|
|
||||||
map((endpoint: string) => `${endpoint}/${id}`),
|
|
||||||
distinctUntilChanged()
|
|
||||||
);
|
|
||||||
|
|
||||||
const request$ = endpoint$.pipe(
|
|
||||||
take(1),
|
|
||||||
map((endpoint: string) => new DeleteRequest(requestId, endpoint))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute the delete request
|
|
||||||
request$.pipe(
|
|
||||||
configureRequest(this.requestService)
|
|
||||||
).subscribe();
|
|
||||||
|
|
||||||
return this.requestService.getByUUID(requestId).pipe(
|
|
||||||
getResponseFromEntry()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private showNotifications(success: boolean, edited: boolean, isField: boolean, options: any) {
|
private showNotifications(success: boolean, edited: boolean, isField: boolean, options: any) {
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { HttpHeaders } from '@angular/common/http';
|
|
||||||
|
|
||||||
import { DSpaceRESTv2Service, HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides utility methods to save files on the client-side.
|
* Provides utility methods to save files on the client-side.
|
||||||
@@ -12,21 +12,19 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileService {
|
export class FileService {
|
||||||
constructor(
|
constructor(
|
||||||
private restService: DSpaceRESTv2Service
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
|
private authService: AuthService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a HTTP Get request to download a file
|
* Combines an URL with a short-lived token and sets the current URL to the newly created one
|
||||||
*
|
*
|
||||||
* @param url
|
* @param url
|
||||||
* file url
|
* file url
|
||||||
*/
|
*/
|
||||||
downloadFile(url: string) {
|
downloadFile(url: string) {
|
||||||
const headers = new HttpHeaders();
|
this.authService.getShortlivedToken().pipe(take(1)).subscribe((token) => {
|
||||||
const options: HttpOptions = Object.create({headers, responseType: 'blob'});
|
this._window.nativeWindow.location.href = hasValue(token) ? new URLCombiner(url, `?authentication-token=${token}`).toString() : url;
|
||||||
return this.restService.request(RestRequestMethod.GET, url, null, options)
|
|
||||||
.subscribe((data) => {
|
|
||||||
saveAs(data.payload as Blob, this.getFileNameFromResponseContentDisposition(data));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
src/app/core/shared/registration.model.ts
Normal file
26
src/app/core/shared/registration.model.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Model representing a registration
|
||||||
|
*/
|
||||||
|
export class Registration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The email linked to the registration
|
||||||
|
*/
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user linked to the registration
|
||||||
|
*/
|
||||||
|
user: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The token linked to the registration
|
||||||
|
*/
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,3 @@
|
|||||||
|
<ds-register-email-form
|
||||||
|
[MESSAGE_PREFIX]="'forgot-email.form'">
|
||||||
|
</ds-register-email-form>
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { ForgotEmailComponent } from './forgot-email.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
describe('ForgotEmailComponent', () => {
|
||||||
|
let comp: ForgotEmailComponent;
|
||||||
|
let fixture: ComponentFixture<ForgotEmailComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, TranslateModule.forRoot(), ReactiveFormsModule],
|
||||||
|
declarations: [ForgotEmailComponent],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ForgotEmailComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(comp).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-forgot-email',
|
||||||
|
templateUrl: './forgot-email.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component responsible the forgot password email step
|
||||||
|
*/
|
||||||
|
export class ForgotEmailComponent {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h3 class="mb-4">{{'forgot-password.form.head' | translate}}</h3>
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">{{'forgot-password.form.identification.header' | translate}}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="font-weight-bold"
|
||||||
|
for="email">{{'forgot-password.form.identification.email' | translate}}</label>
|
||||||
|
<span id="email">{{(registration$ |async).email}}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">{{'forgot-password.form.card.security' | translate}}</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<ds-profile-page-security-form
|
||||||
|
[passwordCanBeEmpty]="false"
|
||||||
|
[FORM_PREFIX]="'forgot-password.form.'"
|
||||||
|
(isInvalid)="setInValid($event)"
|
||||||
|
(passwordValue)="setPasswordValue($event)"
|
||||||
|
></ds-profile-page-security-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<button
|
||||||
|
[disabled]="isInValid"
|
||||||
|
class="btn btn-default btn-primary"
|
||||||
|
(click)="submit()">{{'forgot-password.form.submit' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,117 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RouterStub } from '../../shared/testing/router.stub';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { RestResponse } from '../../core/cache/response.models';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { CoreState } from '../../core/core.reducers';
|
||||||
|
import { Registration } from '../../core/shared/registration.model';
|
||||||
|
import { ForgotPasswordFormComponent } from './forgot-password-form.component';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { AuthenticateAction } from '../../core/auth/auth.actions';
|
||||||
|
|
||||||
|
describe('ForgotPasswordFormComponent', () => {
|
||||||
|
let comp: ForgotPasswordFormComponent;
|
||||||
|
let fixture: ComponentFixture<ForgotPasswordFormComponent>;
|
||||||
|
|
||||||
|
let router;
|
||||||
|
let route;
|
||||||
|
let ePersonDataService: EPersonDataService;
|
||||||
|
let notificationsService;
|
||||||
|
let store: Store<CoreState>;
|
||||||
|
|
||||||
|
const registration = Object.assign(new Registration(), {
|
||||||
|
email: 'test@email.org',
|
||||||
|
user: 'test-uuid',
|
||||||
|
token: 'test-token'
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
|
||||||
|
route = {data: observableOf({registration: registration})};
|
||||||
|
router = new RouterStub();
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
ePersonDataService = jasmine.createSpyObj('ePersonDataService', {
|
||||||
|
patchPasswordWithToken: observableOf(new RestResponse(true, 200, 'Success'))
|
||||||
|
});
|
||||||
|
|
||||||
|
store = jasmine.createSpyObj('store', {
|
||||||
|
dispatch: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule],
|
||||||
|
declarations: [ForgotPasswordFormComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: Router, useValue: router},
|
||||||
|
{provide: ActivatedRoute, useValue: route},
|
||||||
|
{provide: Store, useValue: store},
|
||||||
|
{provide: EPersonDataService, useValue: ePersonDataService},
|
||||||
|
{provide: FormBuilder, useValue: new FormBuilder()},
|
||||||
|
{provide: NotificationsService, useValue: notificationsService},
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ForgotPasswordFormComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('init', () => {
|
||||||
|
it('should initialise mail address', () => {
|
||||||
|
const elem = fixture.debugElement.queryAll(By.css('span#email'))[0].nativeElement;
|
||||||
|
expect(elem.innerHTML).toContain('test@email.org');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('submit', () => {
|
||||||
|
|
||||||
|
it('should submit a patch request for the user uuid and log in on success', () => {
|
||||||
|
comp.password = 'password';
|
||||||
|
comp.isInValid = false;
|
||||||
|
|
||||||
|
comp.submit();
|
||||||
|
|
||||||
|
expect(ePersonDataService.patchPasswordWithToken).toHaveBeenCalledWith('test-uuid', 'test-token', 'password');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new AuthenticateAction('test@email.org', 'password'));
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/home']);
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should submit a patch request for the user uuid and stay on page on error', () => {
|
||||||
|
|
||||||
|
(ePersonDataService.patchPasswordWithToken as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 500, 'Error')));
|
||||||
|
|
||||||
|
comp.password = 'password';
|
||||||
|
comp.isInValid = false;
|
||||||
|
|
||||||
|
comp.submit();
|
||||||
|
|
||||||
|
expect(ePersonDataService.patchPasswordWithToken).toHaveBeenCalledWith('test-uuid', 'test-token', 'password');
|
||||||
|
expect(store.dispatch).not.toHaveBeenCalled();
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('should submit a patch request for the user uuid when the form is invalid', () => {
|
||||||
|
|
||||||
|
comp.password = 'password';
|
||||||
|
comp.isInValid = true;
|
||||||
|
|
||||||
|
comp.submit();
|
||||||
|
|
||||||
|
expect(ePersonDataService.patchPasswordWithToken).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
@@ -0,0 +1,87 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { ErrorResponse, RestResponse } from '../../core/cache/response.models';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Registration } from '../../core/shared/registration.model';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { AuthenticateAction } from '../../core/auth/auth.actions';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core/core.reducers';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-forgot-password-form',
|
||||||
|
templateUrl: './forgot-password-form.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for a user to enter a new password for a forgot token.
|
||||||
|
*/
|
||||||
|
export class ForgotPasswordFormComponent {
|
||||||
|
|
||||||
|
registration$: Observable<Registration>;
|
||||||
|
|
||||||
|
token: string;
|
||||||
|
email: string;
|
||||||
|
user: string;
|
||||||
|
|
||||||
|
isInValid = true;
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for the notification messages of this component
|
||||||
|
*/
|
||||||
|
NOTIFICATIONS_PREFIX = 'forgot-password.form.notification';
|
||||||
|
|
||||||
|
constructor(private ePersonDataService: EPersonDataService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private store: Store<CoreState>,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.registration$ = this.route.data.pipe(
|
||||||
|
map((data) => data.registration as Registration),
|
||||||
|
);
|
||||||
|
this.registration$.subscribe((registration: Registration) => {
|
||||||
|
this.email = registration.email;
|
||||||
|
this.token = registration.token;
|
||||||
|
this.user = registration.user;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInValid($event: boolean) {
|
||||||
|
this.isInValid = $event;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPasswordValue($event: string) {
|
||||||
|
this.password = $event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the password to the eperson service to be updated.
|
||||||
|
* The submission will not be made when the form is not valid.
|
||||||
|
*/
|
||||||
|
submit() {
|
||||||
|
if (!this.isInValid) {
|
||||||
|
this.ePersonDataService.patchPasswordWithToken(this.user, this.token, this.password).subscribe((response: RestResponse) => {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
this.notificationsService.success(
|
||||||
|
this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.success.title'),
|
||||||
|
this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.success.content')
|
||||||
|
);
|
||||||
|
this.store.dispatch(new AuthenticateAction(this.email, this.password));
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(
|
||||||
|
this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.error.title'), (response as ErrorResponse).errorMessage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
src/app/forgot-password/forgot-password-routing.module.ts
Normal file
32
src/app/forgot-password/forgot-password-routing.module.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { ItemPageResolver } from '../+item-page/item-page.resolver';
|
||||||
|
import { RegistrationResolver } from '../register-email-form/registration.resolver';
|
||||||
|
import { ForgotPasswordFormComponent } from './forgot-password-form/forgot-password-form.component';
|
||||||
|
import { ForgotEmailComponent } from './forgot-password-email/forgot-email.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ForgotEmailComponent,
|
||||||
|
data: {title: 'forgot-password.title'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':token',
|
||||||
|
component: ForgotPasswordFormComponent,
|
||||||
|
resolve: {registration: RegistrationResolver}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
RegistrationResolver,
|
||||||
|
ItemPageResolver
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This module defines the routing to the components related to the forgot password components.
|
||||||
|
*/
|
||||||
|
export class ForgotPasswordRoutingModule {
|
||||||
|
}
|
31
src/app/forgot-password/forgot-password.module.ts
Normal file
31
src/app/forgot-password/forgot-password.module.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { ForgotEmailComponent } from './forgot-password-email/forgot-email.component';
|
||||||
|
import { ForgotPasswordRoutingModule } from './forgot-password-routing.module';
|
||||||
|
import { RegisterEmailFormModule } from '../register-email-form/register-email-form.module';
|
||||||
|
import { ForgotPasswordFormComponent } from './forgot-password-form/forgot-password-form.component';
|
||||||
|
import { ProfilePageModule } from '../profile-page/profile-page.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
ForgotPasswordRoutingModule,
|
||||||
|
RegisterEmailFormModule,
|
||||||
|
ProfilePageModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
ForgotEmailComponent,
|
||||||
|
ForgotPasswordFormComponent
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
entryComponents: []
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module related to the Forgot Password components
|
||||||
|
*/
|
||||||
|
export class ForgotPasswordModule {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,2 @@
|
|||||||
|
<h4 class="mt-4">{{title | translate}}</h4>
|
||||||
|
<ng-content></ng-content>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user