+
{{'search.filters.filter.' + filter.name + '.head'| translate}}
-
+ [ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'">
+
\ No newline at end of file
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
index 4e45f49468..1db5e9a1b2 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
@@ -1,7 +1,7 @@
@import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';
-:host {
+:host .facet-filter {
border: 1px solid map-get($theme-colors, light);
cursor: pointer;
.search-filter-wrapper.closed {
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts
index caa5a6febc..30ef349675 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts
@@ -10,6 +10,7 @@ import { SearchService } from '../../search-service/search.service';
import { SearchFilterComponent } from './search-filter.component';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { FilterType } from '../../search-service/filter-type.model';
+import { SearchConfigurationService } from '../../search-service/search-configuration.service';
describe('SearchFilterComponent', () => {
let comp: SearchFilterComponent;
@@ -33,9 +34,7 @@ describe('SearchFilterComponent', () => {
},
expand: (filter) => {
},
- initialCollapse: (filter) => {
- },
- initialExpand: (filter) => {
+ initializeFilter: (filter) => {
},
getSelectedValuesForFilter: (filter) => {
return observableOf([filterName1, filterName2, filterName3])
@@ -55,6 +54,8 @@ describe('SearchFilterComponent', () => {
getFacetValuesFor: (filter) => mockResults
};
+ const searchConfigServiceStub = {};
+
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
@@ -65,6 +66,7 @@ describe('SearchFilterComponent', () => {
provide: SearchFilterService,
useValue: mockFilterService
},
+ { provide: SearchConfigurationService, useValue: searchConfigServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(SearchFilterComponent, {
@@ -91,32 +93,21 @@ describe('SearchFilterComponent', () => {
});
});
- describe('when the initialCollapse method is triggered', () => {
+ describe('when the initializeFilter method is triggered', () => {
beforeEach(() => {
- spyOn(filterService, 'initialCollapse');
- comp.initialCollapse();
+ spyOn(filterService, 'initializeFilter');
+ comp.initializeFilter();
});
it('should call initialCollapse with the correct filter configuration name', () => {
- expect(filterService.initialCollapse).toHaveBeenCalledWith(mockFilterConfig.name)
- });
- });
-
- describe('when the initialExpand method is triggered', () => {
- beforeEach(() => {
- spyOn(filterService, 'initialExpand');
- comp.initialExpand();
- });
-
- it('should call initialCollapse with the correct filter configuration name', () => {
- expect(filterService.initialExpand).toHaveBeenCalledWith(mockFilterConfig.name)
+ expect(filterService.initializeFilter).toHaveBeenCalledWith(mockFilterConfig)
});
});
describe('when getSelectedValues is called', () => {
let valuesObservable: Observable
;
beforeEach(() => {
- valuesObservable = comp.getSelectedValues();
+ valuesObservable = (comp as any).getSelectedValues();
});
it('should return an observable containing the existing filters', () => {
@@ -141,7 +132,7 @@ describe('SearchFilterComponent', () => {
let isActive: Observable;
beforeEach(() => {
filterService.isCollapsed = () => observableOf(true);
- isActive = comp.isCollapsed();
+ isActive = (comp as any).isCollapsed();
});
it('should return an observable containing true', () => {
@@ -156,7 +147,7 @@ describe('SearchFilterComponent', () => {
let isActive: Observable;
beforeEach(() => {
filterService.isCollapsed = () => observableOf(false);
- isActive = comp.isCollapsed();
+ isActive = (comp as any).isCollapsed();
});
it('should return an observable containing false', () => {
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts
index dcc01f2b46..14ba8f0b76 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts
@@ -1,11 +1,12 @@
-
-import { take } from 'rxjs/operators';
+import { filter, first, map, startWith, switchMap, take } from 'rxjs/operators';
import { Component, Input, OnInit } from '@angular/core';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { SearchFilterService } from './search-filter.service';
-import { Observable } from 'rxjs';
+import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { slide } from '../../../shared/animations/slide';
import { isNotEmpty } from '../../../shared/empty.util';
+import { SearchService } from '../../search-service/search.service';
+import { SearchConfigurationService } from '../../search-service/search-configuration.service';
@Component({
selector: 'ds-search-filter',
@@ -26,9 +27,24 @@ export class SearchFilterComponent implements OnInit {
/**
* True when the filter is 100% collapsed in the UI
*/
- collapsed;
+ closed = true;
- constructor(private filterService: SearchFilterService) {
+ /**
+ * Emits true when the filter is currently collapsed in the store
+ */
+ collapsed$: Observable;
+
+ /**
+ * Emits all currently selected values for this filter
+ */
+ selectedValues$: Observable;
+
+ /**
+ * Emits true when the current filter is supposed to be shown
+ */
+ active$: Observable;
+
+ constructor(private filterService: SearchFilterService, private searchService: SearchService, private searchConfigService: SearchConfigurationService) {
}
/**
@@ -37,11 +53,13 @@ export class SearchFilterComponent implements OnInit {
* Else, the filter should initially be collapsed
*/
ngOnInit() {
- this.getSelectedValues().pipe(take(1)).subscribe((isActive) => {
- if (this.filter.isOpenByDefault || isNotEmpty(isActive)) {
- this.initialExpand();
- } else {
- this.initialCollapse();
+ this.selectedValues$ = this.getSelectedValues();
+ this.active$ = this.isActive();
+ this.collapsed$ = this.isCollapsed();
+ this.initializeFilter();
+ this.selectedValues$.pipe(take(1)).subscribe((selectedValues) => {
+ if (isNotEmpty(selectedValues)) {
+ this.filterService.expand(this.filter.name);
}
});
}
@@ -57,30 +75,21 @@ export class SearchFilterComponent implements OnInit {
* Checks if the filter is currently collapsed
* @returns {Observable} Emits true when the current state of the filter is collapsed, false when it's expanded
*/
- isCollapsed(): Observable {
+ private isCollapsed(): Observable {
return this.filterService.isCollapsed(this.filter.name);
}
/**
- * Changes the initial state to collapsed
+ * Sets the initial state of the filter
*/
- initialCollapse() {
- this.filterService.initialCollapse(this.filter.name);
- this.collapsed = true;
- }
-
- /**
- * Changes the initial state to expanded
- */
- initialExpand() {
- this.filterService.initialExpand(this.filter.name);
- this.collapsed = false;
+ initializeFilter() {
+ this.filterService.initializeFilter(this.filter);
}
/**
* @returns {Observable} Emits a list of all values that are currently active for this filter
*/
- getSelectedValues(): Observable {
+ private getSelectedValues(): Observable {
return this.filterService.getSelectedValuesForFilter(this.filter);
}
@@ -90,7 +99,7 @@ export class SearchFilterComponent implements OnInit {
*/
finishSlide(event: any): void {
if (event.fromState === 'collapsed') {
- this.collapsed = false;
+ this.closed = false;
}
}
@@ -100,7 +109,31 @@ export class SearchFilterComponent implements OnInit {
*/
startSlide(event: any): void {
if (event.toState === 'collapsed') {
- this.collapsed = true;
+ this.closed = true;
}
}
+
+ /**
+ * Check if a given filter is supposed to be shown or not
+ * @returns {Observable} Emits true whenever a given filter config should be shown
+ */
+ private isActive(): Observable {
+ return this.selectedValues$.pipe(
+ switchMap((isActive) => {
+ if (isNotEmpty(isActive)) {
+ return observableOf(true);
+ } else {
+ return this.searchConfigService.searchOptions.pipe(
+ switchMap((options) => {
+ return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe(
+ filter((RD) => !RD.isLoading),
+ map((valuesRD) => {
+ return valuesRD.payload.totalElements > 0
+ }),)
+ }
+ ))
+ }
+ }),
+ startWith(true));
+ }
}
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.reducer.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.spec.ts
index 8fbfbf2e65..2f3268fba5 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.reducer.spec.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.spec.ts
@@ -1,10 +1,8 @@
import * as deepFreeze from 'deep-freeze';
import {
SearchFilterCollapseAction, SearchFilterExpandAction, SearchFilterIncrementPageAction,
- SearchFilterInitialCollapseAction,
- SearchFilterInitialExpandAction,
SearchFilterToggleAction,
- SearchFilterDecrementPageAction, SearchFilterResetPageAction
+ SearchFilterDecrementPageAction, SearchFilterResetPageAction, SearchFilterInitializeAction
} from './search-filter.actions';
import { filterReducer } from './search-filter.reducer';
@@ -98,35 +96,39 @@ describe('filterReducer', () => {
filterReducer(state, action);
});
- it('should set filterCollapsed to true in response to the INITIAL_COLLAPSE action when no state has been set for this filter', () => {
+ it('should set filterCollapsed to true in response to the INITIALIZE action with isOpenByDefault to false when no state has been set for this filter', () => {
const state = {};
state[filterName2] = { filterCollapsed: false, page: 1 };
- const action = new SearchFilterInitialCollapseAction(filterName1);
+ const filterConfig = {isOpenByDefault: false, name: filterName1} as any;
+ const action = new SearchFilterInitializeAction(filterConfig);
const newState = filterReducer(state, action);
expect(newState[filterName1].filterCollapsed).toEqual(true);
});
- it('should set filterCollapsed to true in response to the INITIAL_EXPAND action when no state has been set for this filter', () => {
+ it('should set filterCollapsed to false in response to the INITIALIZE action with isOpenByDefault to true when no state has been set for this filter', () => {
const state = {};
state[filterName2] = { filterCollapsed: true, page: 1 };
- const action = new SearchFilterInitialExpandAction(filterName1);
+ const filterConfig = {isOpenByDefault: true, name: filterName1} as any;
+ const action = new SearchFilterInitializeAction(filterConfig);
const newState = filterReducer(state, action);
expect(newState[filterName1].filterCollapsed).toEqual(false);
});
- it('should not change the state in response to the INITIAL_COLLAPSE action when the state has already been set for this filter', () => {
+ it('should not change the state in response to the INITIALIZE action with isOpenByDefault to false when the state has already been set for this filter', () => {
const state = {};
state[filterName1] = { filterCollapsed: false, page: 1 };
- const action = new SearchFilterInitialCollapseAction(filterName1);
+ const filterConfig = { isOpenByDefault: true, name: filterName1 } as any;
+ const action = new SearchFilterInitializeAction(filterConfig);
const newState = filterReducer(state, action);
expect(newState).toEqual(state);
});
- it('should not change the state in response to the INITIAL_EXPAND action when the state has already been set for this filter', () => {
+ it('should not change the state in response to the INITIALIZE action with isOpenByDefault to true when the state has already been set for this filter', () => {
const state = {};
state[filterName1] = { filterCollapsed: true, page: 1 };
- const action = new SearchFilterInitialExpandAction(filterName1);
+ const filterConfig = { isOpenByDefault: false, name: filterName1 } as any;
+ const action = new SearchFilterInitializeAction(filterConfig);
const newState = filterReducer(state, action);
expect(newState).toEqual(state);
});
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts
index f7e064fcc7..187bcd50d0 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts
@@ -1,5 +1,9 @@
-import { SearchFilterAction, SearchFilterActionTypes } from './search-filter.actions';
-import { isEmpty } from '../../../shared/empty.util';
+import {
+ SearchFilterAction,
+ SearchFilterActionTypes,
+ SearchFilterInitializeAction
+} from './search-filter.actions';
+import { isEmpty, isNotUndefined } from '../../../shared/empty.util';
/**
* Interface that represents the state for a single filters
@@ -28,27 +32,14 @@ export function filterReducer(state = initialState, action: SearchFilterAction):
switch (action.type) {
- case SearchFilterActionTypes.INITIAL_COLLAPSE: {
- if (isEmpty(state) || isEmpty(state[action.filterName])) {
- return Object.assign({}, state, {
- [action.filterName]: {
- filterCollapsed: true,
- page: 1
- }
- });
- }
- return state;
- }
-
- case SearchFilterActionTypes.INITIAL_EXPAND: {
- if (isEmpty(state) || isEmpty(state[action.filterName])) {
- return Object.assign({}, state, {
- [action.filterName]: {
- filterCollapsed: false,
- page: 1
- }
- });
- }
+ case SearchFilterActionTypes.INITIALIZE: {
+ const initAction = (action as SearchFilterInitializeAction);
+ return Object.assign({}, state, {
+ [action.filterName]: {
+ filterCollapsed: !initAction.initiallyExpanded,
+ page: 1
+ }
+ });
return state;
}
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts
index 92739dbf2c..5d45e8155e 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts
@@ -5,8 +5,7 @@ import {
SearchFilterDecrementPageAction,
SearchFilterExpandAction,
SearchFilterIncrementPageAction,
- SearchFilterInitialCollapseAction,
- SearchFilterInitialExpandAction,
+ SearchFilterInitializeAction,
SearchFilterResetPageAction,
SearchFilterToggleAction
} from './search-filter.actions';
@@ -74,23 +73,13 @@ describe('SearchFilterService', () => {
service = new SearchFilterService(store, routeServiceStub, mockFixedFilterService);
});
- describe('when the initialCollapse method is triggered', () => {
+ describe('when the initializeFilter method is triggered', () => {
beforeEach(() => {
- service.initialCollapse(mockFilterConfig.name);
+ service.initializeFilter(mockFilterConfig);
});
- it('SearchFilterInitialCollapseAction should be dispatched to the store', () => {
- expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitialCollapseAction(mockFilterConfig.name));
- });
- });
-
- describe('when the initialExpand method is triggered', () => {
- beforeEach(() => {
- service.initialExpand(mockFilterConfig.name);
- });
-
- it('SearchFilterInitialExpandAction should be dispatched to the store', () => {
- expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitialExpandAction(mockFilterConfig.name));
+ it('SearchFilterInitializeAction should be dispatched to the store', () => {
+ expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitializeAction(mockFilterConfig));
});
});
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
index e0c189e26f..19617bb5aa 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
@@ -8,8 +8,7 @@ import {
SearchFilterDecrementPageAction,
SearchFilterExpandAction,
SearchFilterIncrementPageAction,
- SearchFilterInitialCollapseAction,
- SearchFilterInitialExpandAction,
+ SearchFilterInitializeAction,
SearchFilterResetPageAction,
SearchFilterToggleAction
} from './search-filter.actions';
@@ -22,7 +21,7 @@ import { SearchOptions } from '../../search-options.model';
import { PaginatedSearchOptions } from '../../paginated-search-options.model';
import { SearchFixedFilterService } from './search-fixed-filter.service';
import { Params } from '@angular/router';
-
+// const spy = create();
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
export const FILTER_CONFIG: InjectionToken = new InjectionToken('filterConfig');
@@ -197,7 +196,7 @@ export class SearchFilterService {
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable {
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
- map((params: Params) => [].concat(...Object.values(params)))
+ map((params: Params) => [].concat(...Object.values(params))),
);
return observableCombineLatest(values$, prefixValues$).pipe(
@@ -225,13 +224,14 @@ export class SearchFilterService {
} else {
return false;
}
- })
+ }),
+ distinctUntilChanged()
);
}
/**
* Request the current page of a given filter
- * @param {string} filterName The filtername for which the page state is checked
+ * @param {string} filterName The filter name for which the page state is checked
* @returns {Observable} Emits the current page state of the given filter, if it's unavailable, return 1
*/
getPage(filterName: string): Observable {
@@ -243,7 +243,8 @@ export class SearchFilterService {
} else {
return 1;
}
- }));
+ }),
+ distinctUntilChanged());
}
/**
@@ -271,19 +272,11 @@ export class SearchFilterService {
}
/**
- * Dispatches an initial collapse action to the store for a given filter
- * @param {string} filterName The filter for which the action is dispatched
+ * Dispatches an initialize action to the store for a given filter
+ * @param {SearchFilterConfig} filter The filter for which the action is dispatched
*/
- public initialCollapse(filterName: string): void {
- this.store.dispatch(new SearchFilterInitialCollapseAction(filterName));
- }
-
- /**
- * Dispatches an initial expand action to the store for a given filter
- * @param {string} filterName The filter for which the action is dispatched
- */
- public initialExpand(filterName: string): void {
- this.store.dispatch(new SearchFilterInitialExpandAction(filterName));
+ public initializeFilter(filter: SearchFilterConfig): void {
+ this.store.dispatch(new SearchFilterInitializeAction(filter));
}
/**
diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts
index 2957b32c7f..3207345564 100644
--- a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts
@@ -18,7 +18,7 @@ describe('SearchFixedFilterService', () => {
/* tslint:enable:no-empty */
generateRequestId: () => 'fake-id',
getByUUID: () => observableOf(Object.assign(new RequestEntry(), {
- response: new FilteredDiscoveryQueryResponse(filterQuery, '200')
+ response: new FilteredDiscoveryQueryResponse(filterQuery, 200, 'OK')
}))
}) as RequestService;
const halServiceStub = Object.assign(new HALEndpointService(requestServiceStub, undefined), {
diff --git a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html
index 812f543716..b6ae0ada63 100644
--- a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html
+++ b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html
@@ -1,24 +1,9 @@
-
-
- {{value}}
-
+
diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.html
index 352c1710c0..9d35cc518a 100644
--- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.html
+++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.html
@@ -24,16 +24,7 @@
diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
index 6f3450e18e..930ea8c9fb 100644
--- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
@@ -106,16 +106,6 @@ describe('SearchRangeFilterComponent', () => {
fixture.detectChanges();
});
- describe('when the getChangeParams method is called wih a value', () => {
- it('should return the selectedValue list with the new parameter value', () => {
- const result$ = comp.getChangeParams(value3);
- result$.subscribe((result) => {
- expect(result[mockFilterConfig.paramName + minSuffix]).toEqual(['1990']);
- expect(result[mockFilterConfig.paramName + maxSuffix]).toEqual(['1992']);
- });
- });
- });
-
describe('when the onSubmit method is called with data', () => {
const searchUrl = '/search/path';
// const data = { [mockFilterConfig.paramName + minSuffix]: '1900', [mockFilterConfig.paramName + maxSuffix]: '1950' };
diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
index 6cb04c6c1f..ebdb797500 100644
--- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
@@ -1,9 +1,4 @@
-import {
- of as observableOf,
- combineLatest as observableCombineLatest,
- Observable,
- Subscription
-} from 'rxjs';
+import { combineLatest as observableCombineLatest, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { isPlatformBrowser } from '@angular/common';
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
@@ -23,16 +18,26 @@ import { RouteService } from '../../../../shared/services/route.service';
import { hasValue } from '../../../../shared/empty.util';
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
+/**
+ * The suffix for a range filters' minimum in the frontend URL
+ */
+export const RANGE_FILTER_MIN_SUFFIX = '.min';
+
+/**
+ * The suffix for a range filters' maximum in the frontend URL
+ */
+export const RANGE_FILTER_MAX_SUFFIX = '.max';
+
+/**
+ * The date formats that are possible to appear in a date filter
+ */
+const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD'];
+
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
-const minSuffix = '.min';
-const maxSuffix = '.max';
-const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD'];
-const rangeDelimiter = '-';
-
@Component({
selector: 'ds-search-range-filter',
styleUrls: ['./search-range-filter.component.scss'],
@@ -85,8 +90,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
super.ngOnInit();
this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min;
this.max = moment(this.filterConfig.maxValue, dateFormats).year() || this.max;
- const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + minSuffix).pipe(startWith(undefined));
- const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + maxSuffix).pipe(startWith(undefined));
+ const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined));
+ const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined));
this.sub = observableCombineLatest(iniMin, iniMax).pipe(
map(([min, max]) => {
const minimum = hasValue(min) ? min : this.min;
@@ -96,23 +101,6 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
).subscribe((minmax) => this.range = minmax);
}
- /**
- * Calculates the parameters that should change if a given values for this range filter would be changed
- * @param {string} value The values that are changed for this filter
- * @returns {Observable
} The changed filter parameters
- */
- getChangeParams(value: string) {
- const parts = value.split(rangeDelimiter);
- const min = parts.length > 1 ? parts[0].trim() : value;
- const max = parts.length > 1 ? parts[1].trim() : value;
- return observableOf(
- {
- [this.filterConfig.paramName + minSuffix]: [min],
- [this.filterConfig.paramName + maxSuffix]: [max],
- page: 1
- });
- }
-
/**
* Submits new custom range values to the range filter from the widget
*/
@@ -122,8 +110,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
this.router.navigate([this.getSearchLink()], {
queryParams:
{
- [this.filterConfig.paramName + minSuffix]: newMin,
- [this.filterConfig.paramName + maxSuffix]: newMax
+ [this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: newMin,
+ [this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: newMax
},
queryParamsHandling: 'merge'
});
@@ -148,8 +136,4 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
this.sub.unsubscribe();
}
}
-
- out(call) {
- console.log(call);
- }
}
diff --git a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html
index fcc2393b93..25ff8e46d3 100644
--- a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html
+++ b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html
@@ -1,26 +1,9 @@
-
-
- {{value}}
-
-
+
+
@@ -40,6 +23,5 @@
(submitSuggestion)="onSubmit($event)"
(clickSuggestion)="onClick($event)"
(findSuggestions)="findSuggestions($event)"
- ngDefaultControl
- >
+ ngDefaultControl>
diff --git a/src/app/+search-page/search-filters/search-filters.component.html b/src/app/+search-page/search-filters/search-filters.component.html
index 0522c1fba0..895765f6ac 100644
--- a/src/app/+search-page/search-filters/search-filters.component.html
+++ b/src/app/+search-page/search-filters/search-filters.component.html
@@ -1,7 +1,7 @@
{{"search.filters.head" | translate}}
-
{{"search.filters.reset" | translate}}
\ No newline at end of file
diff --git a/src/app/+search-page/search-filters/search-filters.component.ts b/src/app/+search-page/search-filters/search-filters.component.ts
index f16faff1f3..1dd747e908 100644
--- a/src/app/+search-page/search-filters/search-filters.component.ts
+++ b/src/app/+search-page/search-filters/search-filters.component.ts
@@ -1,12 +1,11 @@
-import { Observable, of as observableOf } from 'rxjs';
+import { Observable } from 'rxjs';
-import { filter, map, mergeMap, startWith, switchMap } from 'rxjs/operators';
+import { map } from 'rxjs/operators';
import { Component } from '@angular/core';
import { SearchService } from '../search-service/search.service';
import { RemoteData } from '../../core/data/remote-data';
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
import { SearchConfigurationService } from '../search-service/search-configuration.service';
-import { isNotEmpty } from '../../shared/empty.util';
import { SearchFilterService } from './search-filter/search-filter.service';
import { getSucceededRemoteData } from '../../core/shared/operators';
@@ -53,26 +52,9 @@ export class SearchFiltersComponent {
}
/**
- * Check if a given filter is supposed to be shown or not
- * @param {SearchFilterConfig} filter The filter to check for
- * @returns {Observable
} Emits true whenever a given filter config should be shown
+ * Prevent unnecessary rerendering
*/
- isActive(filterConfig: SearchFilterConfig): Observable {
- return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
- mergeMap((isActive) => {
- if (isNotEmpty(isActive)) {
- return observableOf(true);
- } else {
- return this.searchConfigService.searchOptions.pipe(
- switchMap((options) => {
- return this.searchService.getFacetValuesFor(filterConfig, 1, options).pipe(
- filter((RD) => !RD.isLoading),
- map((valuesRD) => {
- return valuesRD.payload.totalElements > 0
- }),)
- }
- ))
- }
- }),startWith(true),);
+ trackUpdate(index, config: SearchFilterConfig) {
+ return config ? config.name : undefined;
}
}
diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts
index 8db94bc8d2..b94eb39f9b 100644
--- a/src/app/+search-page/search-page.component.ts
+++ b/src/app/+search-page/search-page.component.ts
@@ -76,7 +76,6 @@ export class SearchPageComponent implements OnInit {
constructor(protected service: SearchService,
protected sidebarService: SearchSidebarService,
protected windowService: HostWindowService,
- protected filterService: SearchFilterService,
protected searchConfigService: SearchConfigurationService,
protected routeService: RouteService) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts
index d199bad724..2b2ee861da 100644
--- a/src/app/+search-page/search-page.module.ts
+++ b/src/app/+search-page/search-page.module.ts
@@ -30,6 +30,9 @@ import { SearchFacetFilterWrapperComponent } from './search-filters/search-filte
import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component';
import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component';
import { SearchConfigurationService } from './search-service/search-configuration.service';
+import { SearchFacetOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component';
+import { SearchFacetSelectedOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component';
+import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component';
const effects = [
SearchSidebarEffects
@@ -62,6 +65,9 @@ const effects = [
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
+ SearchFacetOptionComponent,
+ SearchFacetSelectedOptionComponent,
+ SearchFacetRangeOptionComponent
],
providers: [
SearchSidebarService,
@@ -83,6 +89,9 @@ const effects = [
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
+ SearchFacetOptionComponent,
+ SearchFacetSelectedOptionComponent,
+ SearchFacetRangeOptionComponent
],
exports: [
FilteredSearchPageComponent,
diff --git a/src/app/+search-page/search-service/search-configuration.service.spec.ts b/src/app/+search-page/search-service/search-configuration.service.spec.ts
index 68405dcf6b..88fd784f0b 100644
--- a/src/app/+search-page/search-service/search-configuration.service.spec.ts
+++ b/src/app/+search-page/search-service/search-configuration.service.spec.ts
@@ -121,7 +121,7 @@ describe('SearchConfigurationService', () => {
describe('when subscribeToSearchOptions is called', () => {
beforeEach(() => {
- service.subscribeToSearchOptions(defaults)
+ (service as any).subscribeToSearchOptions(defaults)
});
it('should call all getters it needs, but not call any others', () => {
expect(service.getCurrentPagination).not.toHaveBeenCalled();
@@ -135,7 +135,7 @@ describe('SearchConfigurationService', () => {
describe('when subscribeToPaginatedSearchOptions is called', () => {
beforeEach(() => {
- service.subscribeToPaginatedSearchOptions(defaults);
+ (service as any).subscribeToPaginatedSearchOptions(defaults);
});
it('should call all getters it needs', () => {
expect(service.getCurrentPagination).toHaveBeenCalled();
diff --git a/src/app/+search-page/search-service/search-configuration.service.ts b/src/app/+search-page/search-service/search-configuration.service.ts
index d642944d18..4fbd5e81bc 100644
--- a/src/app/+search-page/search-service/search-configuration.service.ts
+++ b/src/app/+search-page/search-service/search-configuration.service.ts
@@ -197,7 +197,7 @@ export class SearchConfigurationService implements OnDestroy {
* @param {SearchOptions} defaults Default values for when no parameters are available
* @returns {Subscription} The subscription to unsubscribe from
*/
- subscribeToSearchOptions(defaults: SearchOptions): Subscription {
+ private subscribeToSearchOptions(defaults: SearchOptions): Subscription {
return observableMerge(
this.getScopePart(defaults.scope),
this.getQueryPart(defaults.query),
@@ -216,7 +216,7 @@ export class SearchConfigurationService implements OnDestroy {
* @param {PaginatedSearchOptions} defaults Default values for when no parameters are available
* @returns {Subscription} The subscription to unsubscribe from
*/
- subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
+ private subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
return observableMerge(
this.getPaginationPart(defaults.pagination),
this.getSortPart(defaults.sort),
diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts
index 75708f5fd1..ee41d3d9aa 100644
--- a/src/app/+search-page/search-service/search.service.spec.ts
+++ b/src/app/+search-page/search-service/search.service.spec.ts
@@ -158,7 +158,7 @@ describe('SearchService', () => {
const endPoint = 'http://endpoint.com/test/test';
const searchOptions = new PaginatedSearchOptions({});
const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] });
- const response = new SearchSuccessResponse(queryResponse, '200');
+ const response = new SearchSuccessResponse(queryResponse, 200, 'OK');
beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
/* tslint:disable:no-empty */
@@ -183,7 +183,7 @@ describe('SearchService', () => {
describe('when getConfig is called without a scope', () => {
const endPoint = 'http://endpoint.com/test/config';
const filterConfig = [new SearchFilterConfig()];
- const response = new FacetConfigSuccessResponse(filterConfig, '200');
+ const response = new FacetConfigSuccessResponse(filterConfig, 200, 'OK');
beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
/* tslint:disable:no-empty */
@@ -210,7 +210,7 @@ describe('SearchService', () => {
const scope = 'test';
const requestUrl = endPoint + '?scope=' + scope;
const filterConfig = [new SearchFilterConfig()];
- const response = new FacetConfigSuccessResponse(filterConfig, '200');
+ const response = new FacetConfigSuccessResponse(filterConfig, 200, 'OK');
beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
/* tslint:disable:no-empty */
diff --git a/src/app/+submit-page/submit-page-routing.module.ts b/src/app/+submit-page/submit-page-routing.module.ts
new file mode 100644
index 0000000000..7a123bfc31
--- /dev/null
+++ b/src/app/+submit-page/submit-page-routing.module.ts
@@ -0,0 +1,23 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
+import { SubmissionSubmitComponent } from '../submission/submit/submission-submit.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ {
+ canActivate: [AuthenticatedGuard],
+ path: '',
+ pathMatch: 'full',
+ component: SubmissionSubmitComponent,
+ data: { title: 'submission.submit.title' }
+ }
+ ])
+ ]
+})
+/**
+ * This module defines the default component to load when navigating to the submit page path.
+ */
+export class SubmitPageRoutingModule { }
diff --git a/src/app/+submit-page/submit-page.module.ts b/src/app/+submit-page/submit-page.module.ts
new file mode 100644
index 0000000000..e43d9d36aa
--- /dev/null
+++ b/src/app/+submit-page/submit-page.module.ts
@@ -0,0 +1,20 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { SharedModule } from '../shared/shared.module';
+import { SubmitPageRoutingModule } from './submit-page-routing.module';
+import { SubmissionModule } from '../submission/submission.module';
+
+@NgModule({
+ imports: [
+ SubmitPageRoutingModule,
+ CommonModule,
+ SharedModule,
+ SubmissionModule,
+ ],
+})
+/**
+ * This module handles all modules that need to access the submit page.
+ */
+export class SubmitPageModule {
+
+}
diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts
new file mode 100644
index 0000000000..aa182eb291
--- /dev/null
+++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts
@@ -0,0 +1,23 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
+import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ { path: '', redirectTo: '/home', pathMatch: 'full' },
+ {
+ canActivate: [AuthenticatedGuard],
+ path: ':id/edit',
+ component: SubmissionEditComponent,
+ data: { title: 'submission.edit.title' }
+ }
+ ])
+ ]
+})
+/**
+ * This module defines the default component to load when navigating to the workflowitems edit page path.
+ */
+export class WorkflowitemsEditPageRoutingModule { }
diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts
new file mode 100644
index 0000000000..fbb53d8dcc
--- /dev/null
+++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts
@@ -0,0 +1,21 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { SharedModule } from '../shared/shared.module';
+import { WorkflowitemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module';
+import { SubmissionModule } from '../submission/submission.module';
+
+@NgModule({
+ imports: [
+ WorkflowitemsEditPageRoutingModule,
+ CommonModule,
+ SharedModule,
+ SubmissionModule,
+ ],
+ declarations: []
+})
+/**
+ * This module handles all modules that need to access the workflowitems edit page.
+ */
+export class WorkflowitemsEditPageModule {
+
+}
diff --git a/src/app/+workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts b/src/app/+workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts
new file mode 100644
index 0000000000..d10c53e138
--- /dev/null
+++ b/src/app/+workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts
@@ -0,0 +1,23 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
+import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ { path: '', redirectTo: '/home', pathMatch: 'full' },
+ {
+ canActivate: [AuthenticatedGuard],
+ path: ':id/edit',
+ component: SubmissionEditComponent,
+ data: { title: 'submission.edit.title' }
+ }
+ ])
+ ]
+})
+/**
+ * This module defines the default component to load when navigating to the workspaceitems edit page path
+ */
+export class WorkspaceitemsEditPageRoutingModule { }
diff --git a/src/app/+workspaceitems-edit-page/workspaceitems-edit-page.module.ts b/src/app/+workspaceitems-edit-page/workspaceitems-edit-page.module.ts
new file mode 100644
index 0000000000..65a40f3f7c
--- /dev/null
+++ b/src/app/+workspaceitems-edit-page/workspaceitems-edit-page.module.ts
@@ -0,0 +1,21 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { SharedModule } from '../shared/shared.module';
+import { WorkspaceitemsEditPageRoutingModule } from './workspaceitems-edit-page-routing.module';
+import { SubmissionModule } from '../submission/submission.module';
+
+@NgModule({
+ imports: [
+ WorkspaceitemsEditPageRoutingModule,
+ CommonModule,
+ SharedModule,
+ SubmissionModule,
+ ],
+ declarations: []
+})
+/**
+ * This module handles all modules that need to access the workspaceitems edit page.
+ */
+export class WorkspaceitemsEditPageModule {
+
+}
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 1448601067..be956ee895 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -29,6 +29,9 @@ export function getCommunityModulePath() {
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
+ { path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
+ { path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' },
+ { path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowitemsEditPageModule' },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
])
],
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index c90d35678d..fa7e7a873a 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -30,11 +30,16 @@ body {
.main-content {
z-index: $main-z-index;
- flex: 1 0 auto;
+ flex: 1 1 100%;
margin-top: $content-spacing;
margin-bottom: $content-spacing;
}
+.alert.hide {
+ padding: 0;
+ margin: 0;
+}
+
ds-header-navbar-wrapper {
z-index: $nav-z-index;
}
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index e079400e85..bd2d832c67 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -34,13 +34,16 @@ import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import { AngularticsMock } from './shared/mocks/mock-angulartics.service';
import { AuthServiceMock } from './shared/mocks/mock-auth.service';
import { AuthService } from './core/auth/auth.service';
-import { Router } from '@angular/router';
import { MenuService } from './shared/menu/menu.service';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { CSSVariableServiceStub } from './shared/testing/css-variable-service-stub';
import { MenuServiceStub } from './shared/testing/menu-service-stub';
import { HostWindowService } from './shared/host-window.service';
import { HostWindowServiceStub } from './shared/testing/host-window-service-stub';
+import { ActivatedRoute, Router } from '@angular/router';
+import { RouteService } from './shared/services/route.service';
+import { MockActivatedRoute } from './shared/mocks/mock-active-router';
+import { MockRouter } from './shared/mocks/mock-router';
let comp: AppComponent;
let fixture: ComponentFixture;
@@ -70,11 +73,13 @@ describe('App component', () => {
{ provide: MetadataService, useValue: new MockMetadataService() },
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
{ provide: AuthService, useValue: new AuthServiceMock() },
- { provide: Router, useValue: {} },
+ { provide: Router, useValue: new MockRouter() },
+ { provide: ActivatedRoute, useValue: new MockActivatedRoute() },
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
- AppComponent
+ AppComponent,
+ RouteService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 10c6643fbb..da01b1297a 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -23,6 +23,7 @@ import { NativeWindowRef, NativeWindowService } from './shared/services/window.s
import { isAuthenticated } from './core/auth/selectors';
import { AuthService } from './core/auth/auth.service';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
+import { RouteService } from './shared/services/route.service';
import variables from '../styles/_exposed_variables.scss';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service';
@@ -56,6 +57,7 @@ export class AppComponent implements OnInit, AfterViewInit {
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
private authService: AuthService,
private router: Router,
+ private routeService: RouteService,
private cssService: CSSVariableService,
private menuService: MenuService,
private windowService: HostWindowService
@@ -75,6 +77,8 @@ export class AppComponent implements OnInit, AfterViewInit {
metadata.listenForRouteChange();
+ routeService.saveRouting();
+
if (config.debug) {
console.info(config);
}
@@ -83,7 +87,6 @@ export class AppComponent implements OnInit, AfterViewInit {
}
ngOnInit() {
-
const env: string = this.config.production ? 'Production' : 'Development';
const color: string = this.config.production ? 'red' : 'green';
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 8c4126f8ed..f9d6e50dcc 100755
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -31,6 +31,7 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
import { NotificationComponent } from './shared/notifications/notification/notification.component';
import { SharedModule } from './shared/shared.module';
+import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.component';
import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
@@ -57,6 +58,7 @@ const IMPORTS = [
HttpClientModule,
AppRoutingModule,
CoreModule.forRoot(),
+ ScrollToModule.forRoot(),
NgbModule.forRoot(),
TranslateModule.forRoot(),
EffectsModule.forRoot(appEffects),
diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts
index 756bcb6f23..ea2512a974 100644
--- a/src/app/app.reducer.ts
+++ b/src/app/app.reducer.ts
@@ -22,9 +22,11 @@ import {
import { hasValue } from './shared/empty.util';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
+import { historyReducer, HistoryState } from './shared/history/history.reducer';
export interface AppState {
router: fromRouter.RouterReducerState;
+ history: HistoryState;
hostWindow: HostWindowState;
forms: FormState;
metadataRegistry: MetadataRegistryState;
@@ -38,6 +40,7 @@ export interface AppState {
export const appReducers: ActionReducerMap = {
router: fromRouter.routerReducer,
+ history: historyReducer,
hostWindow: hostWindowReducer,
forms: formReducer,
metadataRegistry: metadataRegistryReducer,
diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts
index ee9f2e571b..0b2c32fc04 100644
--- a/src/app/core/auth/auth-response-parsing.service.spec.ts
+++ b/src/app/core/auth/auth-response-parsing.service.spec.ts
@@ -1,20 +1,36 @@
-import { AuthStatusResponse } from '../cache/response.models';
+import { async, TestBed } from '@angular/core/testing';
+import { Store, StoreModule } from '@ngrx/store';
+
+import { GlobalConfig } from '../../../config/global-config.interface';
+import { AuthStatusResponse } from '../cache/response.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthResponseParsingService } from './auth-response-parsing.service';
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
import { MockStore } from '../../shared/testing/mock-store';
-import { ObjectCacheState } from '../cache/object-cache.reducer';
describe('AuthResponseParsingService', () => {
let service: AuthResponseParsingService;
- const EnvConfig = { cache: { msToLive: 1000 } } as any;
- const store = new MockStore({});
- const objectCacheService = new ObjectCacheService(store as any);
+ const EnvConfig: GlobalConfig = { cache: { msToLive: 1000 } } as any;
+ let store: any;
+ let objectCacheService: ObjectCacheService;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ StoreModule.forRoot({}),
+ ],
+ providers: [
+ { provide: Store, useClass: MockStore }
+ ]
+ }).compileComponents();
+ }));
beforeEach(() => {
+ store = TestBed.get(Store);
+ objectCacheService = new ObjectCacheService(store as any);
service = new AuthResponseParsingService(EnvConfig, objectCacheService);
});
@@ -38,12 +54,14 @@ describe('AuthResponseParsingService', () => {
expires: 1526318322000
},
} as AuthStatus,
- statusCode: '200'
+ statusCode: 200,
+ statusText: '200'
};
const validResponse1 = {
payload: {},
- statusCode: '404'
+ statusCode: 404,
+ statusText: '404'
};
const validResponse2 = {
@@ -102,7 +120,9 @@ describe('AuthResponseParsingService', () => {
}
}
},
- statusCode: '200'
+ statusCode: 200,
+ statusText: '200'
+
};
it('should return a AuthStatusResponse if data contains a valid AuthStatus object as payload', () => {
diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts
index 61559991ec..3cb00789f6 100644
--- a/src/app/core/auth/auth-response-parsing.service.ts
+++ b/src/app/core/auth/auth-response-parsing.service.ts
@@ -26,11 +26,11 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
- if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
+ if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) {
const response = this.process(data.payload, request.uuid);
- return new AuthStatusResponse(response, data.statusCode);
+ return new AuthStatusResponse(response, data.statusCode, data.statusText);
} else {
- return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
+ return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode, data.statusText);
}
}
diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts
index 0dc8abf860..8c2b4026e0 100644
--- a/src/app/core/auth/auth.effects.spec.ts
+++ b/src/app/core/auth/auth.effects.spec.ts
@@ -22,7 +22,7 @@ import {
} from './auth.actions';
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
import { AuthService } from './auth.service';
-import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
+import { AuthState } from './auth.reducer';
import { EPersonMock } from '../../shared/testing/eperson-mock';
@@ -30,7 +30,7 @@ describe('AuthEffects', () => {
let authEffects: AuthEffects;
let actions: Observable;
let authServiceStub;
- const store: Store = jasmine.createSpyObj('store', {
+ const store: Store = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */
dispatch: {},
/* tslint:enable:no-empty */
diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts
index dd9e3fb5e7..da760b8faa 100644
--- a/src/app/core/auth/auth.interceptor.ts
+++ b/src/app/core/auth/auth.interceptor.ts
@@ -17,7 +17,7 @@ import { AppState } from '../../app.reducer';
import { AuthService } from './auth.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
-import { isNotEmpty, isUndefined } from '../../shared/empty.util';
+import { isNotEmpty, isUndefined, isNotNull } from '../../shared/empty.util';
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
@@ -142,7 +142,7 @@ export class AuthInterceptor implements HttpInterceptor {
url: error.url
});
return observableOf(authResponse);
- } else if (this.isUnauthorized(error)) {
+ } else if (this.isUnauthorized(error) && isNotNull(token) && authService.isTokenExpired()) {
// The access token provided is expired, revoked, malformed, or invalid for other reasons
// Redirect to the login route
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts
index d39c0a4590..c461148eea 100644
--- a/src/app/core/auth/auth.service.spec.ts
+++ b/src/app/core/auth/auth.service.spec.ts
@@ -73,7 +73,7 @@ describe('AuthService test', () => {
{ provide: REQUEST, useValue: {} },
{ provide: Router, useValue: routerStub },
{ provide: ActivatedRoute, useValue: routeStub },
- {provide: Store, useValue: mockStore},
+ { provide: Store, useValue: mockStore },
{ provide: RemoteDataBuildService, useValue: rdbService },
CookieService,
AuthService
diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts
index 14b023e362..725b371c14 100644
--- a/src/app/core/browse/browse.service.spec.ts
+++ b/src/app/core/browse/browse.service.spec.ts
@@ -114,7 +114,7 @@ describe('BrowseService', () => {
scheduler.schedule(() => service.getBrowseDefinitions().subscribe());
scheduler.flush();
- expect(requestService.configure).toHaveBeenCalledWith(expected);
+ expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
@@ -155,7 +155,7 @@ describe('BrowseService', () => {
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush();
- expect(requestService.configure).toHaveBeenCalledWith(expected);
+ expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
@@ -174,7 +174,7 @@ describe('BrowseService', () => {
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush();
- expect(requestService.configure).toHaveBeenCalledWith(expected);
+ expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
@@ -190,7 +190,7 @@ describe('BrowseService', () => {
it('should throw an Error', () => {
const definitionID = 'invalidID';
- const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
+ const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`));
expect(service.getBrowseEntriesFor(new BrowseEntrySearchOptions(definitionID))).toBeObservable(expected);
});
@@ -303,7 +303,7 @@ describe('BrowseService', () => {
scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe());
scheduler.flush();
- expect(requestService.configure).toHaveBeenCalledWith(expected);
+ expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts
index 8d40741dc0..c0b359e7ea 100644
--- a/src/app/core/cache/builders/remote-data-build.service.ts
+++ b/src/app/core/cache/builders/remote-data-build.service.ts
@@ -1,25 +1,15 @@
-import {
- combineLatest as observableCombineLatest,
- Observable,
- of as observableOf,
- race as observableRace
-} from 'rxjs';
import { Injectable } from '@angular/core';
-import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators';
-import {
- hasValue,
- hasValueOperator,
- isEmpty,
- isNotEmpty,
- isNotUndefined
-} from '../../../shared/empty.util';
+
+import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs';
+import { distinctUntilChanged, flatMap, map, startWith, switchMap } from 'rxjs/operators';
+
+import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data';
import { RemoteDataError } from '../../data/remote-data-error';
import { GetRequest } from '../../data/request.models';
import { RequestEntry } from '../../data/request.reducer';
import { RequestService } from '../../data/request.service';
-
import { NormalizedObject } from '../models/normalized-object.model';
import { ObjectCacheService } from '../object-cache.service';
import { DSOSuccessResponse, ErrorResponse } from '../response.models';
@@ -99,7 +89,11 @@ export class RemoteDataBuildService {
isSuccessful = reqEntry.response.isSuccessful;
const errorMessage = isSuccessful === false ? (reqEntry.response as ErrorResponse).errorMessage : undefined;
if (hasValue(errorMessage)) {
- error = new RemoteDataError(reqEntry.response.statusCode, errorMessage);
+ error = new RemoteDataError(
+ (reqEntry.response as ErrorResponse).statusCode,
+ (reqEntry.response as ErrorResponse).statusText,
+ errorMessage
+ );
}
}
return new RemoteData(
@@ -232,16 +226,25 @@ export class RemoteDataBuildService {
}).filter((e: string) => hasValue(e))
.join(', ');
- const statusCode: string = arr
+ const statusText: string = arr
.map((d: RemoteData) => d.error)
.map((e: RemoteDataError, idx: number) => {
if (hasValue(e)) {
- return `[${idx}]: ${e.statusCode}`;
+ return `[${idx}]: ${e.statusText}`;
}
}).filter((c: string) => hasValue(c))
.join(', ');
- const error = new RemoteDataError(statusCode, errorMessage);
+ const statusCode: number = arr
+ .map((d: RemoteData) => d.error)
+ .map((e: RemoteDataError, idx: number) => {
+ if (hasValue(e)) {
+ return e.statusCode;
+ }
+ }).filter((c: number) => hasValue(c))
+ .reduce((acc, status) => status, undefined);
+
+ const error = new RemoteDataError(statusCode, statusText, errorMessage);
const payload: T[] = arr.map((d: RemoteData) => d.payload);
@@ -260,8 +263,10 @@ export class RemoteDataBuildService {
map((rd: RemoteData>) => {
if (Array.isArray(rd.payload)) {
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload) })
- } else {
+ } else if (isNotUndefined(rd.payload)) {
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload.page) });
+ } else {
+ return Object.assign(rd, { payload: new PaginatedList(pageInfo, []) });
}
})
);
diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts
index 4eb6e1027b..ddfcc29a2c 100644
--- a/src/app/core/cache/models/normalized-collection.model.ts
+++ b/src/app/core/cache/models/normalized-collection.model.ts
@@ -18,6 +18,20 @@ export class NormalizedCollection extends NormalizedDSpaceObject {
@autoserialize
handle: string;
+ /**
+ * The Bitstream that represents the license of this Collection
+ */
+ @autoserialize
+ @relationship(ResourceType.License, false)
+ license: string;
+
+ /**
+ * The Bitstream that represents the default Access Conditions of this Collection
+ */
+ @autoserialize
+ @relationship(ResourceType.ResourcePolicy, false)
+ defaultAccessConditions: string;
+
/**
* The Bitstream that represents the logo of this Collection
*/
diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts
index 2248b62509..e12faa4a77 100644
--- a/src/app/core/cache/models/normalized-dspace-object.model.ts
+++ b/src/app/core/cache/models/normalized-dspace-object.model.ts
@@ -31,9 +31,6 @@ export class NormalizedDSpaceObject extends NormalizedOb
/**
* The universally unique identifier of this DSpaceObject
- *
- * Repeated here to make the serialization work,
- * inheritSerialization doesn't seem to work for more than one level
*/
@autoserializeAs(String)
uuid: string;
diff --git a/src/app/core/cache/models/normalized-license.model.ts b/src/app/core/cache/models/normalized-license.model.ts
new file mode 100644
index 0000000000..02bd1808c8
--- /dev/null
+++ b/src/app/core/cache/models/normalized-license.model.ts
@@ -0,0 +1,24 @@
+import { autoserialize, inheritSerialization } from 'cerialize';
+import { mapsTo } from '../builders/build-decorators';
+import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
+import { License } from '../../shared/license.model';
+
+/**
+ * Normalized model class for a Collection License
+ */
+@mapsTo(License)
+@inheritSerialization(NormalizedDSpaceObject)
+export class NormalizedLicense extends NormalizedDSpaceObject {
+
+ /**
+ * A boolean representing if this License is custom or not
+ */
+ @autoserialize
+ custom: boolean;
+
+ /**
+ * The text of the license
+ */
+ @autoserialize
+ text: string;
+}
diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts
index c7f6bcc36a..895595ba45 100644
--- a/src/app/core/cache/models/normalized-object-factory.ts
+++ b/src/app/core/cache/models/normalized-object-factory.ts
@@ -9,12 +9,18 @@ import { GenericConstructor } from '../../shared/generic-constructor';
import { NormalizedCommunity } from './normalized-community.model';
import { ResourceType } from '../../shared/resource-type';
import { NormalizedObject } from './normalized-object.model';
-import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
+import { NormalizedLicense } from './normalized-license.model';
import { NormalizedResourcePolicy } from './normalized-resource-policy.model';
+import { NormalizedWorkspaceItem } from '../../submission/models/normalized-workspaceitem.model';
import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model';
import { NormalizedGroup } from '../../eperson/models/normalized-group.model';
+import { NormalizedWorkflowItem } from '../../submission/models/normalized-workflowitem.model';
+import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
import { NormalizedMetadataSchema } from '../../metadata/normalized-metadata-schema.model';
import { CacheableObject } from '../object-cache.reducer';
+import { NormalizedSubmissionDefinitionsModel } from '../../config/models/normalized-config-submission-definitions.model';
+import { NormalizedSubmissionFormsModel } from '../../config/models/normalized-config-submission-forms.model';
+import { NormalizedSubmissionSectionModel } from '../../config/models/normalized-config-submission-section.model';
export class NormalizedObjectFactory {
public static getConstructor(type: ResourceType): GenericConstructor> {
@@ -37,6 +43,9 @@ export class NormalizedObjectFactory {
case ResourceType.BitstreamFormat: {
return NormalizedBitstreamFormat
}
+ case ResourceType.License: {
+ return NormalizedLicense
+ }
case ResourceType.ResourcePolicy: {
return NormalizedResourcePolicy
}
@@ -61,6 +70,24 @@ export class NormalizedObjectFactory {
case ResourceType.MetadataField: {
return NormalizedGroup
}
+ case ResourceType.Workspaceitem: {
+ return NormalizedWorkspaceItem
+ }
+ case ResourceType.Workflowitem: {
+ return NormalizedWorkflowItem
+ }
+ case ResourceType.SubmissionDefinition:
+ case ResourceType.SubmissionDefinitions: {
+ return NormalizedSubmissionDefinitionsModel
+ }
+ case ResourceType.SubmissionForm:
+ case ResourceType.SubmissionForms: {
+ return NormalizedSubmissionFormsModel
+ }
+ case ResourceType.SubmissionSection:
+ case ResourceType.SubmissionSections: {
+ return NormalizedSubmissionSectionModel
+ }
default: {
return undefined;
}
diff --git a/src/app/core/cache/models/normalized-object.model.ts b/src/app/core/cache/models/normalized-object.model.ts
index de04572dcd..6ac8985d64 100644
--- a/src/app/core/cache/models/normalized-object.model.ts
+++ b/src/app/core/cache/models/normalized-object.model.ts
@@ -13,11 +13,8 @@ export abstract class NormalizedObject implements Cac
self: string;
/**
- * The universally unique identifier of this Object
+ * A string representing the kind of DSpaceObject, e.g. community, item, …
*/
- @autoserialize
- uuid: string;
-
@autoserialize
type: ResourceType;
diff --git a/src/app/core/cache/models/normalized-resource-policy.model.ts b/src/app/core/cache/models/normalized-resource-policy.model.ts
index b6c8c1369a..9438c1da0a 100644
--- a/src/app/core/cache/models/normalized-resource-policy.model.ts
+++ b/src/app/core/cache/models/normalized-resource-policy.model.ts
@@ -1,10 +1,9 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { ResourcePolicy } from '../../shared/resource-policy.model';
-import { mapsTo, relationship } from '../builders/build-decorators';
+import { mapsTo } from '../builders/build-decorators';
import { NormalizedObject } from './normalized-object.model';
import { IDToUUIDSerializer } from '../id-to-uuid-serializer';
-import { ResourceType } from '../../shared/resource-type';
import { ActionType } from './action-type.model';
/**
@@ -17,6 +16,7 @@ export class NormalizedResourcePolicy extends NormalizedObject {
/**
* The action that is allowed by this Resource Policy
*/
+ @autoserialize
action: ActionType;
/**
@@ -28,9 +28,8 @@ export class NormalizedResourcePolicy extends NormalizedObject {
/**
* The uuid of the Group this Resource Policy applies to
*/
- @relationship(ResourceType.Group, false)
- @autoserializeAs(String, 'groupUUID')
- group: string;
+ @autoserialize
+ groupUUID: string;
/**
* Identifier for this Resource Policy
@@ -46,4 +45,5 @@ export class NormalizedResourcePolicy extends NormalizedObject {
*/
@autoserializeAs(new IDToUUIDSerializer('resource-policy'), 'id')
uuid: string;
+
}
diff --git a/src/app/core/cache/models/search-param.model.ts b/src/app/core/cache/models/search-param.model.ts
new file mode 100644
index 0000000000..a33bbee5e6
--- /dev/null
+++ b/src/app/core/cache/models/search-param.model.ts
@@ -0,0 +1,9 @@
+
+/**
+ * Class representing a query parameter (query?fieldName=fieldValue) used in FindAllOptions object
+ */
+export class SearchParam {
+ constructor(public fieldName: string, public fieldValue: any) {
+
+ }
+}
diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts
index b4c37df736..483de65b98 100644
--- a/src/app/core/cache/object-cache.service.ts
+++ b/src/app/core/cache/object-cache.service.ts
@@ -1,34 +1,44 @@
+import { Injectable } from '@angular/core';
+import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
+import { applyPatch, Operation } from 'fast-json-patch';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
-import { Injectable } from '@angular/core';
-import { MemoizedSelector, select, Store } from '@ngrx/store';
-import { IndexName } from '../index/index.reducer';
-
-import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer';
+import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
+import { CoreState } from '../core.reducers';
+import { coreSelector } from '../core.selectors';
+import { RestRequestMethod } from '../data/rest-request-method';
+import { selfLinkFromUuidSelector } from '../index/index.selectors';
+import { GenericConstructor } from '../shared/generic-constructor';
+import { NormalizedObjectFactory } from './models/normalized-object-factory';
+import { NormalizedObject } from './models/normalized-object.model';
import {
AddPatchObjectCacheAction,
AddToObjectCacheAction,
ApplyPatchObjectCacheAction,
RemoveFromObjectCacheAction
} from './object-cache.actions';
-import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
-import { GenericConstructor } from '../shared/generic-constructor';
-import { coreSelector, CoreState } from '../core.reducers';
-import { pathSelector } from '../shared/selectors';
-import { NormalizedObjectFactory } from './models/normalized-object-factory';
-import { NormalizedObject } from './models/normalized-object.model';
-import { applyPatch, Operation } from 'fast-json-patch';
+
+import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
import { AddToSSBAction } from './server-sync-buffer.actions';
-import { RestRequestMethod } from '../data/rest-request-method';
-function selfLinkFromUuidSelector(uuid: string): MemoizedSelector {
- return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid);
-}
+/**
+ * The base selector function to select the object cache in the store
+ */
+const objectCacheSelector = createSelector(
+ coreSelector,
+ (state: CoreState) => state['cache/object']
+);
-function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector {
- return pathSelector(coreSelector, 'cache/object', selfLink);
-}
+/**
+ * Selector function to select an object entry by self link from the cache
+ * @param selfLink The self link of the object
+ */
+const entryFromSelfLinkSelector =
+ (selfLink: string): MemoizedSelector => createSelector(
+ objectCacheSelector,
+ (state: ObjectCacheState) => state[selfLink],
+ );
/**
* A service to interact with the object cache
diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts
index f9bdfb8a39..edebfa253e 100644
--- a/src/app/core/cache/response.models.ts
+++ b/src/app/core/cache/response.models.ts
@@ -1,7 +1,7 @@
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
import { RequestError } from '../data/request.models';
import { PageInfo } from '../shared/page-info.model';
-import { ConfigObject } from '../shared/config/config.model';
+import { ConfigObject } from '../config/models/config.model';
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
import { IntegrationModel } from '../integration/models/integration.model';
@@ -11,14 +11,19 @@ import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstream
import { AuthStatus } from '../auth/models/auth-status.model';
import { MetadataSchema } from '../metadata/metadataschema.model';
import { MetadataField } from '../metadata/metadatafield.model';
+import { PaginatedList } from '../data/paginated-list';
+import { SubmissionObject } from '../submission/models/submission-object.model';
+import { DSpaceObject } from '../shared/dspace-object.model';
/* tslint:disable:max-classes-per-file */
export class RestResponse {
+ public toCache = true;
public timeAdded: number;
constructor(
public isSuccessful: boolean,
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string
) {
}
}
@@ -26,10 +31,11 @@ export class RestResponse {
export class DSOSuccessResponse extends RestResponse {
constructor(
public resourceSelfLinks: string[],
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
@@ -39,10 +45,11 @@ export class DSOSuccessResponse extends RestResponse {
export class RegistryMetadataschemasSuccessResponse extends RestResponse {
constructor(
public metadataschemasResponse: RegistryMetadataschemasResponse,
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
@@ -52,10 +59,11 @@ export class RegistryMetadataschemasSuccessResponse extends RestResponse {
export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
constructor(
public metadatafieldsResponse: RegistryMetadatafieldsResponse,
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
@@ -65,10 +73,11 @@ export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
constructor(
public bitstreamformatsResponse: RegistryBitstreamformatsResponse,
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
@@ -78,9 +87,10 @@ export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
export class MetadataschemaSuccessResponse extends RestResponse {
constructor(
public metadataschema: MetadataSchema,
- public statusCode: string
+ public statusCode: number,
+ public statusText: string,
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
@@ -90,28 +100,31 @@ export class MetadataschemaSuccessResponse extends RestResponse {
export class MetadatafieldSuccessResponse extends RestResponse {
constructor(
public metadatafield: MetadataField,
- public statusCode: string
+ public statusCode: number,
+ public statusText: string,
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
export class SearchSuccessResponse extends RestResponse {
constructor(
public results: SearchQueryResponse,
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
export class FacetConfigSuccessResponse extends RestResponse {
constructor(
public results: SearchFilterConfig[],
- public statusCode: string
+ public statusCode: number,
+ public statusText: string,
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
@@ -122,18 +135,20 @@ export class FacetValueMap {
export class FacetValueSuccessResponse extends RestResponse {
constructor(
public results: FacetValue[],
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
export class FacetValueMapSuccessResponse extends RestResponse {
constructor(
public results: FacetValueMap,
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
@@ -144,19 +159,21 @@ export class EndpointMap {
export class EndpointMapSuccessResponse extends RestResponse {
constructor(
public endpointMap: EndpointMap,
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
export class GenericSuccessResponse extends RestResponse {
constructor(
public payload: T,
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
@@ -164,7 +181,7 @@ export class ErrorResponse extends RestResponse {
errorMessage: string;
constructor(error: RequestError) {
- super(false, error.statusText);
+ super(false, error.statusCode, error.statusText);
console.error(error);
this.errorMessage = error.message;
}
@@ -172,11 +189,12 @@ export class ErrorResponse extends RestResponse {
export class ConfigSuccessResponse extends RestResponse {
constructor(
- public configDefinition: ConfigObject[],
- public statusCode: string,
+ public configDefinition: ConfigObject,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
@@ -185,29 +203,65 @@ export class AuthStatusResponse extends RestResponse {
constructor(
public response: AuthStatus,
- public statusCode: string
+ public statusCode: number,
+ public statusText: string,
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
export class IntegrationSuccessResponse extends RestResponse {
constructor(
- public dataDefinition: IntegrationModel[],
- public statusCode: string,
+ public dataDefinition: PaginatedList,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
+ }
+}
+
+export class PostPatchSuccessResponse extends RestResponse {
+ constructor(
+ public dataDefinition: any,
+ public statusCode: number,
+ public statusText: string,
+ public pageInfo?: PageInfo
+ ) {
+ super(true, statusCode, statusText);
+ }
+}
+
+export class SubmissionSuccessResponse extends RestResponse {
+ constructor(
+ public dataDefinition: Array,
+ public statusCode: number,
+ public statusText: string,
+ public pageInfo?: PageInfo
+ ) {
+ super(true, statusCode, statusText);
+ }
+}
+
+export class EpersonSuccessResponse extends RestResponse {
+ constructor(
+ public epersonDefinition: DSpaceObject[],
+ public statusCode: number,
+ public statusText: string,
+ public pageInfo?: PageInfo
+ ) {
+ super(true, statusCode, statusText);
}
}
export class FilteredDiscoveryQueryResponse extends RestResponse {
constructor(
public filterQuery: string,
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string,
public pageInfo?: PageInfo
) {
- super(true, statusCode);
+ super(true, statusCode, statusText);
}
}
/* tslint:enable:max-classes-per-file */
diff --git a/src/app/core/cache/server-sync-buffer.effects.spec.ts b/src/app/core/cache/server-sync-buffer.effects.spec.ts
index 87d8804e9c..773e0ab60c 100644
--- a/src/app/core/cache/server-sync-buffer.effects.spec.ts
+++ b/src/app/core/cache/server-sync-buffer.effects.spec.ts
@@ -1,20 +1,17 @@
import { TestBed } from '@angular/core/testing';
+
import { Observable, of as observableOf } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles';
+
import { ServerSyncBufferEffects } from './server-sync-buffer.effects';
import { GLOBAL_CONFIG } from '../../../config';
-import {
- CommitSSBAction,
- EmptySSBAction,
- ServerSyncBufferActionTypes
-} from './server-sync-buffer.actions';
+import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions';
import { RestRequestMethod } from '../data/rest-request-method';
-import { Store } from '@ngrx/store';
+import { Store, StoreModule } from '@ngrx/store';
import { RequestService } from '../data/request.service';
import { ObjectCacheService } from './object-cache.service';
import { MockStore } from '../../shared/testing/mock-store';
-import { ObjectCacheState } from './object-cache.reducer';
import * as operators from 'rxjs/operators';
import { spyOnOperator } from '../../shared/testing/utils';
import { DSpaceObject } from '../shared/dspace-object.model';
@@ -38,8 +35,10 @@ describe('ServerSyncBufferEffects', () => {
let store;
beforeEach(() => {
- store = new MockStore({});
TestBed.configureTestingModule({
+ imports: [
+ StoreModule.forRoot({}),
+ ],
providers: [
ServerSyncBufferEffects,
provideMockActions(() => actions),
@@ -54,11 +53,12 @@ describe('ServerSyncBufferEffects', () => {
}
}
},
- { provide: Store, useValue: store }
+ { provide: Store, useClass: MockStore }
// other providers
],
});
+ store = TestBed.get(Store);
ssbEffects = TestBed.get(ServerSyncBufferEffects);
});
diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts
index 2e11f15540..3aa6ad312f 100644
--- a/src/app/core/cache/server-sync-buffer.effects.ts
+++ b/src/app/core/cache/server-sync-buffer.effects.ts
@@ -1,6 +1,7 @@
import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
+import { coreSelector } from '../core.selectors';
import {
AddToSSBAction,
CommitSSBAction,
@@ -9,7 +10,7 @@ import {
} from './server-sync-buffer.actions';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
-import { coreSelector, CoreState } from '../core.reducers';
+import { CoreState } from '../core.reducers';
import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
diff --git a/src/app/core/config/config-data.ts b/src/app/core/config/config-data.ts
index efcdb7eed4..cb40514e45 100644
--- a/src/app/core/config/config-data.ts
+++ b/src/app/core/config/config-data.ts
@@ -1,5 +1,5 @@
import { PageInfo } from '../shared/page-info.model';
-import { ConfigObject } from '../shared/config/config.model';
+import { ConfigObject } from './models/config.model';
/**
* A class to represent the data retrieved by a configuration service
@@ -7,7 +7,7 @@ import { ConfigObject } from '../shared/config/config.model';
export class ConfigData {
constructor(
public pageInfo: PageInfo,
- public payload: ConfigObject[]
+ public payload: ConfigObject
) {
}
}
diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/config/config-response-parsing.service.spec.ts
similarity index 70%
rename from src/app/core/data/config-response-parsing.service.spec.ts
rename to src/app/core/config/config-response-parsing.service.spec.ts
index a2c5cbbadc..7c69f1bdb3 100644
--- a/src/app/core/data/config-response-parsing.service.spec.ts
+++ b/src/app/core/config/config-response-parsing.service.spec.ts
@@ -2,13 +2,14 @@ import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models';
import { ConfigResponseParsingService } from './config-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
-import { ConfigRequest } from './request.models';
+import { ConfigRequest } from '../data/request.models';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
-import { SubmissionDefinitionsModel } from '../shared/config/config-submission-definitions.model';
-import { PaginatedList } from './paginated-list';
+import { PaginatedList } from '../data/paginated-list';
import { PageInfo } from '../shared/page-info.model';
+import { NormalizedSubmissionDefinitionsModel } from './models/normalized-config-submission-definitions.model';
+import { NormalizedSubmissionSectionModel } from './models/normalized-config-submission-section.model';
describe('ConfigResponseParsingService', () => {
let service: ConfigResponseParsingService;
@@ -119,7 +120,8 @@ describe('ConfigResponseParsingService', () => {
}
}
},
- statusCode: '200'
+ statusCode: 200,
+ statusText: 'OK'
};
});
@@ -128,7 +130,8 @@ describe('ConfigResponseParsingService', () => {
const invalidResponse1 = {
payload: {},
- statusCode: '200'
+ statusCode: 200,
+ statusText: 'OK'
};
const invalidResponse2 = {
@@ -152,14 +155,15 @@ describe('ConfigResponseParsingService', () => {
}
}
},
- statusCode: '200'
+ statusCode: 200,
+ statusText: 'OK'
};
const invalidResponse3 = {
payload: {
_links: { self: { href: 'https://rest.api/config/submissiondefinitions/traditional' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
- }, statusCode: '500'
+ }, statusCode: 500, statusText: 'Internal Server Error'
};
const pageinfo = Object.assign(new PageInfo(), {
elementsPerPage: 4,
@@ -169,7 +173,7 @@ describe('ConfigResponseParsingService', () => {
self: 'https://rest.api/config/submissiondefinitions/traditional/sections'
});
const definitions =
- Object.assign(new SubmissionDefinitionsModel(), {
+ Object.assign(new NormalizedSubmissionDefinitionsModel(), {
isDefault: true,
name: 'traditional',
type: 'submissiondefinition',
@@ -179,10 +183,65 @@ describe('ConfigResponseParsingService', () => {
},
self: 'https://rest.api/config/submissiondefinitions/traditional',
sections: new PaginatedList(pageinfo, [
- 'https://rest.api/config/submissionsections/traditionalpageone',
- 'https://rest.api/config/submissionsections/traditionalpagetwo',
- 'https://rest.api/config/submissionsections/upload',
- 'https://rest.api/config/submissionsections/license'
+ Object.assign(new NormalizedSubmissionSectionModel(), {
+ header: 'submit.progressbar.describe.stepone',
+ mandatory: true,
+ sectionType: 'submission-form',
+ visibility:{
+ main:null,
+ other:'READONLY'
+ },
+ type: 'submissionsection',
+ _links: {
+ self: 'https://rest.api/config/submissionsections/traditionalpageone',
+ config: 'https://rest.api/config/submissionforms/traditionalpageone'
+ },
+ self: 'https://rest.api/config/submissionsections/traditionalpageone',
+ }),
+ Object.assign(new NormalizedSubmissionSectionModel(), {
+ header: 'submit.progressbar.describe.steptwo',
+ mandatory: true,
+ sectionType: 'submission-form',
+ visibility:{
+ main:null,
+ other:'READONLY'
+ },
+ type: 'submissionsection',
+ _links: {
+ self: 'https://rest.api/config/submissionsections/traditionalpagetwo',
+ config: 'https://rest.api/config/submissionforms/traditionalpagetwo'
+ },
+ self: 'https://rest.api/config/submissionsections/traditionalpagetwo',
+ }),
+ Object.assign(new NormalizedSubmissionSectionModel(), {
+ header: 'submit.progressbar.upload',
+ mandatory: false,
+ sectionType: 'upload',
+ visibility:{
+ main:null,
+ other:'READONLY'
+ },
+ type: 'submissionsection',
+ _links: {
+ self: 'https://rest.api/config/submissionsections/upload',
+ config: 'https://rest.api/config/submissionuploads/upload'
+ },
+ self: 'https://rest.api/config/submissionsections/upload',
+ }),
+ Object.assign(new NormalizedSubmissionSectionModel(), {
+ header: 'submit.progressbar.license',
+ mandatory: true,
+ sectionType: 'license',
+ visibility:{
+ main:null,
+ other:'READONLY'
+ },
+ type: 'submissionsection',
+ _links: {
+ self: 'https://rest.api/config/submissionsections/license'
+ },
+ self: 'https://rest.api/config/submissionsections/license',
+ })
])
});
diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/config/config-response-parsing.service.ts
similarity index 69%
rename from src/app/core/data/config-response-parsing.service.ts
rename to src/app/core/config/config-response-parsing.service.ts
index 50303d0a09..b81dc07624 100644
--- a/src/app/core/data/config-response-parsing.service.ts
+++ b/src/app/core/config/config-response-parsing.service.ts
@@ -1,15 +1,15 @@
import { Inject, Injectable } from '@angular/core';
-import { ResponseParsingService } from './parsing.service';
-import { RestRequest } from './request.models';
+import { ResponseParsingService } from '../data/parsing.service';
+import { RestRequest } from '../data/request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models';
import { isNotEmpty } from '../../shared/empty.util';
-import { ConfigObjectFactory } from '../shared/config/config-object-factory';
+import { ConfigObjectFactory } from './models/config-object-factory';
-import { ConfigObject } from '../shared/config/config.model';
-import { ConfigType } from '../shared/config/config-type';
-import { BaseResponseParsingService } from './base-response-parsing.service';
+import { ConfigObject } from './models/config.model';
+import { ConfigType } from './models/config-type';
+import { BaseResponseParsingService } from '../data/base-response-parsing.service';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { ObjectCacheService } from '../cache/object-cache.service';
@@ -27,14 +27,14 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
- if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '201' || data.statusCode === '200' || data.statusCode === 'OK')) {
+ if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 201 || data.statusCode === 200)) {
const configDefinition = this.process(data.payload, request.uuid);
- return new ConfigSuccessResponse(configDefinition, data.statusCode, this.processPageInfo(data.payload));
+ return new ConfigSuccessResponse(configDefinition, data.statusCode, data.statusText, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from config endpoint'),
- {statusText: data.statusCode}
+ { statusCode: data.statusCode, statusText: data.statusText }
)
);
}
diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts
index 44cfdee358..87add6b656 100644
--- a/src/app/core/config/config.service.spec.ts
+++ b/src/app/core/config/config.service.spec.ts
@@ -1,4 +1,4 @@
-import { cold, getTestScheduler, hot } from 'jasmine-marbles';
+import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ConfigService } from './config.service';
diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts
index c6c2e2e7d2..340a7a97d6 100644
--- a/src/app/core/config/config.service.ts
+++ b/src/app/core/config/config.service.ts
@@ -6,7 +6,6 @@ import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.mode
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ConfigData } from './config-data';
-import { RequestEntry } from '../data/request.reducer';
import { getResponseFromEntry } from '../shared/operators';
export abstract class ConfigService {
diff --git a/src/app/core/config/models/config-access-condition-option.model.ts b/src/app/core/config/models/config-access-condition-option.model.ts
new file mode 100644
index 0000000000..46bf1b60ce
--- /dev/null
+++ b/src/app/core/config/models/config-access-condition-option.model.ts
@@ -0,0 +1,40 @@
+/**
+ * Model class for an Access Condition
+ */
+export class AccessConditionOption {
+
+ /**
+ * The name for this Access Condition
+ */
+ name: string;
+
+ /**
+ * The uuid of the Group this Access Condition applies to
+ */
+ groupUUID: string;
+
+ /**
+ * The uuid of the Group that contains set of groups this Resource Policy applies to
+ */
+ selectGroupUUID: string;
+
+ /**
+ * A boolean representing if this Access Condition has a start date
+ */
+ hasStartDate: boolean;
+
+ /**
+ * A boolean representing if this Access Condition has an end date
+ */
+ hasEndDate: boolean;
+
+ /**
+ * Maximum value of the start date
+ */
+ maxStartDate: string;
+
+ /**
+ * Maximum value of the end date
+ */
+ maxEndDate: string;
+}
diff --git a/src/app/core/config/models/config-object-factory.ts b/src/app/core/config/models/config-object-factory.ts
new file mode 100644
index 0000000000..44b2e377c4
--- /dev/null
+++ b/src/app/core/config/models/config-object-factory.ts
@@ -0,0 +1,36 @@
+import { GenericConstructor } from '../../shared/generic-constructor';
+import { ConfigType } from './config-type';
+import { ConfigObject } from './config.model';
+import { NormalizedSubmissionDefinitionsModel } from './normalized-config-submission-definitions.model';
+import { NormalizedSubmissionFormsModel } from './normalized-config-submission-forms.model';
+import { NormalizedSubmissionSectionModel } from './normalized-config-submission-section.model';
+import { NormalizedSubmissionUploadsModel } from './normalized-config-submission-uploads.model';
+
+/**
+ * Class to return normalized models for config objects
+ */
+export class ConfigObjectFactory {
+ public static getConstructor(type): GenericConstructor {
+ switch (type) {
+ case ConfigType.SubmissionDefinition:
+ case ConfigType.SubmissionDefinitions: {
+ return NormalizedSubmissionDefinitionsModel
+ }
+ case ConfigType.SubmissionForm:
+ case ConfigType.SubmissionForms: {
+ return NormalizedSubmissionFormsModel
+ }
+ case ConfigType.SubmissionSection:
+ case ConfigType.SubmissionSections: {
+ return NormalizedSubmissionSectionModel
+ }
+ case ConfigType.SubmissionUpload:
+ case ConfigType.SubmissionUploads: {
+ return NormalizedSubmissionUploadsModel
+ }
+ default: {
+ return undefined;
+ }
+ }
+ }
+}
diff --git a/src/app/core/shared/config/config-submission-definitions.model.ts b/src/app/core/config/models/config-submission-definitions.model.ts
similarity index 63%
rename from src/app/core/shared/config/config-submission-definitions.model.ts
rename to src/app/core/config/models/config-submission-definitions.model.ts
index 0247f13944..8bbbc90056 100644
--- a/src/app/core/shared/config/config-submission-definitions.model.ts
+++ b/src/app/core/config/models/config-submission-definitions.model.ts
@@ -1,15 +1,17 @@
-import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { ConfigObject } from './config.model';
import { SubmissionSectionModel } from './config-submission-section.model';
import { PaginatedList } from '../../data/paginated-list';
-@inheritSerialization(ConfigObject)
export class SubmissionDefinitionsModel extends ConfigObject {
- @autoserialize
+ /**
+ * A boolean representing if this submission definition is the default or not
+ */
isDefault: boolean;
- @autoserializeAs(SubmissionSectionModel)
+ /**
+ * A list of SubmissionSectionModel that are present in this submission definition
+ */
sections: PaginatedList;
}
diff --git a/src/app/core/shared/config/config-submission-forms.model.ts b/src/app/core/config/models/config-submission-forms.model.ts
similarity index 59%
rename from src/app/core/shared/config/config-submission-forms.model.ts
rename to src/app/core/config/models/config-submission-forms.model.ts
index 98d3bf9ce7..ee0962f0e9 100644
--- a/src/app/core/shared/config/config-submission-forms.model.ts
+++ b/src/app/core/config/models/config-submission-forms.model.ts
@@ -1,14 +1,20 @@
-import { autoserialize, inheritSerialization } from 'cerialize';
import { ConfigObject } from './config.model';
import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model';
+/**
+ * An interface that define a form row and its properties.
+ */
export interface FormRowModel {
fields: FormFieldModel[];
}
-@inheritSerialization(ConfigObject)
+/**
+ * A model class for a NormalizedObject.
+ */
export class SubmissionFormsModel extends ConfigObject {
- @autoserialize
+ /**
+ * An array of [FormRowModel] that are present in this form
+ */
rows: FormRowModel[];
}
diff --git a/src/app/core/config/models/config-submission-section.model.ts b/src/app/core/config/models/config-submission-section.model.ts
new file mode 100644
index 0000000000..377a8869e1
--- /dev/null
+++ b/src/app/core/config/models/config-submission-section.model.ts
@@ -0,0 +1,34 @@
+import { ConfigObject } from './config.model';
+import { SectionsType } from '../../../submission/sections/sections-type';
+
+/**
+ * An interface that define section visibility and its properties.
+ */
+export interface SubmissionSectionVisibility {
+ main: any,
+ other: any
+}
+
+export class SubmissionSectionModel extends ConfigObject {
+
+ /**
+ * The header for this section
+ */
+ header: string;
+
+ /**
+ * A boolean representing if this submission section is the mandatory or not
+ */
+ mandatory: boolean;
+
+ /**
+ * A string representing the kind of section object
+ */
+ sectionType: SectionsType;
+
+ /**
+ * The [SubmissionSectionVisibility] object for this section
+ */
+ visibility: SubmissionSectionVisibility
+
+}
diff --git a/src/app/core/config/models/config-submission-uploads.model.ts b/src/app/core/config/models/config-submission-uploads.model.ts
new file mode 100644
index 0000000000..8bb9ba7f1e
--- /dev/null
+++ b/src/app/core/config/models/config-submission-uploads.model.ts
@@ -0,0 +1,21 @@
+import { ConfigObject } from './config.model';
+import { AccessConditionOption } from './config-access-condition-option.model';
+import { SubmissionFormsModel } from './config-submission-forms.model';
+
+export class SubmissionUploadsModel extends ConfigObject {
+
+ /**
+ * A list of available bitstream access conditions
+ */
+ accessConditionOptions: AccessConditionOption[];
+
+ /**
+ * An object representing the configuration describing the bistream metadata form
+ */
+ metadata: SubmissionFormsModel;
+
+ required: boolean;
+
+ maxSize: number;
+
+}
diff --git a/src/app/core/shared/config/config-type.ts b/src/app/core/config/models/config-type.ts
similarity index 57%
rename from src/app/core/shared/config/config-type.ts
rename to src/app/core/config/models/config-type.ts
index 17ed099229..91371f10f5 100644
--- a/src/app/core/shared/config/config-type.ts
+++ b/src/app/core/config/models/config-type.ts
@@ -1,9 +1,3 @@
-/**
- * TODO replace with actual string enum after upgrade to TypeScript 2.4:
- * https://github.com/Microsoft/TypeScript/pull/15486
- */
-import { ResourceType } from '../resource-type';
-
export enum ConfigType {
SubmissionDefinitions = 'submissiondefinitions',
SubmissionDefinition = 'submissiondefinition',
@@ -11,5 +5,6 @@ export enum ConfigType {
SubmissionForms = 'submissionforms',
SubmissionSections = 'submissionsections',
SubmissionSection = 'submissionsection',
- Authority = 'authority'
+ SubmissionUploads = 'submissionuploads',
+ SubmissionUpload = 'submissionupload',
}
diff --git a/src/app/core/config/models/config.model.ts b/src/app/core/config/models/config.model.ts
new file mode 100644
index 0000000000..81f20a0b3c
--- /dev/null
+++ b/src/app/core/config/models/config.model.ts
@@ -0,0 +1,27 @@
+import { CacheableObject } from '../../cache/object-cache.reducer';
+import { ResourceType } from '../../shared/resource-type';
+
+export abstract class ConfigObject implements CacheableObject {
+
+ /**
+ * The name for this configuration
+ */
+ public name: string;
+
+ /**
+ * A string representing the kind of config object
+ */
+ public type: ResourceType;
+
+ /**
+ * The links to all related resources returned by the rest api.
+ */
+ public _links: {
+ [name: string]: string
+ };
+
+ /**
+ * The link to the rest endpoint where this config object can be found
+ */
+ self: string;
+}
diff --git a/src/app/core/config/models/normalized-config-submission-definitions.model.ts b/src/app/core/config/models/normalized-config-submission-definitions.model.ts
new file mode 100644
index 0000000000..3887c566c1
--- /dev/null
+++ b/src/app/core/config/models/normalized-config-submission-definitions.model.ts
@@ -0,0 +1,25 @@
+import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
+import { SubmissionSectionModel } from './config-submission-section.model';
+import { PaginatedList } from '../../data/paginated-list';
+import { NormalizedConfigObject } from './normalized-config.model';
+import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
+
+/**
+ * Normalized class for the configuration describing the submission
+ */
+@inheritSerialization(NormalizedConfigObject)
+export class NormalizedSubmissionDefinitionsModel extends NormalizedConfigObject {
+
+ /**
+ * A boolean representing if this submission definition is the default or not
+ */
+ @autoserialize
+ isDefault: boolean;
+
+ /**
+ * A list of SubmissionSectionModel that are present in this submission definition
+ */
+ @autoserializeAs(SubmissionSectionModel)
+ sections: PaginatedList;
+
+}
diff --git a/src/app/core/config/models/normalized-config-submission-forms.model.ts b/src/app/core/config/models/normalized-config-submission-forms.model.ts
new file mode 100644
index 0000000000..a957e8c7fa
--- /dev/null
+++ b/src/app/core/config/models/normalized-config-submission-forms.model.ts
@@ -0,0 +1,16 @@
+import { autoserialize, inheritSerialization } from 'cerialize';
+import { NormalizedConfigObject } from './normalized-config.model';
+import { FormRowModel, SubmissionFormsModel } from './config-submission-forms.model';
+
+/**
+ * Normalized class for the configuration describing the submission form
+ */
+@inheritSerialization(NormalizedConfigObject)
+export class NormalizedSubmissionFormsModel extends NormalizedConfigObject {
+
+ /**
+ * An array of [FormRowModel] that are present in this form
+ */
+ @autoserialize
+ rows: FormRowModel[];
+}
diff --git a/src/app/core/config/models/normalized-config-submission-section.model.ts b/src/app/core/config/models/normalized-config-submission-section.model.ts
new file mode 100644
index 0000000000..c876acf607
--- /dev/null
+++ b/src/app/core/config/models/normalized-config-submission-section.model.ts
@@ -0,0 +1,37 @@
+import { autoserialize, inheritSerialization } from 'cerialize';
+import { SectionsType } from '../../../submission/sections/sections-type';
+import { NormalizedConfigObject } from './normalized-config.model';
+import { SubmissionFormsModel } from './config-submission-forms.model';
+import { SubmissionSectionVisibility } from './config-submission-section.model';
+
+/**
+ * Normalized class for the configuration describing the submission section
+ */
+@inheritSerialization(NormalizedConfigObject)
+export class NormalizedSubmissionSectionModel extends NormalizedConfigObject {
+
+ /**
+ * The header for this section
+ */
+ @autoserialize
+ header: string;
+
+ /**
+ * A boolean representing if this submission section is the mandatory or not
+ */
+ @autoserialize
+ mandatory: boolean;
+
+ /**
+ * A string representing the kind of section object
+ */
+ @autoserialize
+ sectionType: SectionsType;
+
+ /**
+ * The [SubmissionSectionVisibility] object for this section
+ */
+ @autoserialize
+ visibility: SubmissionSectionVisibility
+
+}
diff --git a/src/app/core/config/models/normalized-config-submission-uploads.model.ts b/src/app/core/config/models/normalized-config-submission-uploads.model.ts
new file mode 100644
index 0000000000..e49171d6a7
--- /dev/null
+++ b/src/app/core/config/models/normalized-config-submission-uploads.model.ts
@@ -0,0 +1,31 @@
+import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
+import { AccessConditionOption } from './config-access-condition-option.model';
+import { SubmissionFormsModel } from './config-submission-forms.model';
+import { NormalizedConfigObject } from './normalized-config.model';
+import { SubmissionUploadsModel } from './config-submission-uploads.model';
+
+/**
+ * Normalized class for the configuration describing the submission upload section
+ */
+@inheritSerialization(NormalizedConfigObject)
+export class NormalizedSubmissionUploadsModel extends NormalizedConfigObject {
+
+ /**
+ * A list of available bitstream access conditions
+ */
+ @autoserialize
+ accessConditionOptions: AccessConditionOption[];
+
+ /**
+ * An object representing the configuration describing the bistream metadata form
+ */
+ @autoserializeAs(SubmissionFormsModel)
+ metadata: SubmissionFormsModel;
+
+ @autoserialize
+ required: boolean;
+
+ @autoserialize
+ maxSize: number;
+
+}
diff --git a/src/app/core/config/models/normalized-config.model.ts b/src/app/core/config/models/normalized-config.model.ts
new file mode 100644
index 0000000000..0b75158588
--- /dev/null
+++ b/src/app/core/config/models/normalized-config.model.ts
@@ -0,0 +1,38 @@
+import { autoserialize, inheritSerialization } from 'cerialize';
+import { NormalizedObject } from '../../cache/models/normalized-object.model';
+import { CacheableObject } from '../../cache/object-cache.reducer';
+import { ResourceType } from '../../shared/resource-type';
+
+/**
+ * Normalized abstract class for a configuration object
+ */
+@inheritSerialization(NormalizedObject)
+export abstract class NormalizedConfigObject implements CacheableObject {
+
+ /**
+ * The name for this configuration
+ */
+ @autoserialize
+ public name: string;
+
+ /**
+ * A string representing the kind of config object
+ */
+ @autoserialize
+ public type: ResourceType;
+
+ /**
+ * The links to all related resources returned by the rest api.
+ */
+ @autoserialize
+ public _links: {
+ [name: string]: string
+ };
+
+ /**
+ * The link to the rest endpoint where this config object can be found
+ */
+ @autoserialize
+ self: string;
+
+}
diff --git a/src/app/core/config/submission-uploads-config.service.ts b/src/app/core/config/submission-uploads-config.service.ts
new file mode 100644
index 0000000000..2e092fa4f3
--- /dev/null
+++ b/src/app/core/config/submission-uploads-config.service.ts
@@ -0,0 +1,21 @@
+import { Injectable } from '@angular/core';
+import { ConfigService } from './config.service';
+import { RequestService } from '../data/request.service';
+import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { ObjectCacheService } from '../cache/object-cache.service';
+
+/**
+ * Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process.
+ */
+@Injectable()
+export class SubmissionUploadsConfigService extends ConfigService {
+ protected linkPath = 'submissionuploads';
+ protected browseEndpoint = '';
+
+ constructor(
+ protected objectCache: ObjectCacheService,
+ protected requestService: RequestService,
+ protected halService: HALEndpointService) {
+ super();
+ }
+}
diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts
index a23516aa45..bb25c49a7a 100644
--- a/src/app/core/core.effects.ts
+++ b/src/app/core/core.effects.ts
@@ -3,6 +3,7 @@ import { ObjectCacheEffects } from './cache/object-cache.effects';
import { UUIDIndexEffects } from './index/index.effects';
import { RequestEffects } from './data/request.effects';
import { AuthEffects } from './auth/auth.effects';
+import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects';
import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
@@ -11,6 +12,7 @@ export const coreEffects = [
ObjectCacheEffects,
UUIDIndexEffects,
AuthEffects,
+ JsonPatchOperationsEffects,
ServerSyncBufferEffects,
ObjectUpdatesEffects
];
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index b8b13e5616..20ae0401cc 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -1,8 +1,8 @@
import {
+ ModuleWithProviders,
NgModule,
Optional,
- SkipSelf,
- ModuleWithProviders
+ SkipSelf
} from '@angular/core';
import { CommonModule } from '@angular/common';
@@ -24,7 +24,9 @@ import { DSOResponseParsingService } from './data/dso-response-parsing.service';
import { SearchResponseParsingService } from './data/search-response-parsing.service';
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
import { FormBuilderService } from '../shared/form/builder/form-builder.service';
+import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service';
import { FormService } from '../shared/form/form.service';
+import { GroupEpersonService } from './eperson/group-eperson.service';
import { HostWindowService } from '../shared/host-window.service';
import { ItemDataService } from './data/item-data.service';
import { MetadataService } from './metadata/metadata.service';
@@ -37,13 +39,17 @@ import { ServerResponseService } from '../shared/services/server-response.servic
import { NativeWindowFactory, NativeWindowService } from '../shared/services/window.service';
import { BrowseService } from './browse/browse.service';
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
-import { ConfigResponseParsingService } from './data/config-response-parsing.service';
+import { ConfigResponseParsingService } from './config/config-response-parsing.service';
import { RouteService } from '../shared/services/route.service';
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
+import { SubmissionResponseParsingService } from './submission/submission-response-parsing.service';
+import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service';
+import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder';
import { AuthorityService } from './integration/authority.service';
import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service';
+import { WorkspaceitemDataService } from './submission/workspaceitem-data.service';
import { UUIDService } from './shared/uuid.service';
import { AuthenticatedGuard } from './auth/authenticated.guard';
import { AuthRequestService } from './auth/auth-request.service';
@@ -58,14 +64,18 @@ import { RegistryService } from './registry/registry.service';
import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service';
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
+import { WorkflowitemDataService } from './submission/workflowitem-data.service';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { UploaderService } from '../shared/uploader/uploader.service';
+import { FileService } from './shared/file.service';
+import { SubmissionRestService } from './submission/submission-rest.service';
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service';
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
import { MenuService } from '../shared/menu/menu.service';
+import { SubmissionJsonPatchOperationsService } from './submission/submission-json-patch-operations.service';
import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service';
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
@@ -99,7 +109,10 @@ const PROVIDERS = [
DynamicFormService,
DynamicFormValidationService,
FormBuilderService,
+ SectionFormOperationsService,
FormService,
+ EpersonResponseParsingService,
+ GroupEpersonService,
HALEndpointService,
HostWindowService,
ItemDataService,
@@ -128,12 +141,21 @@ const PROVIDERS = [
RouteService,
SubmissionDefinitionsConfigService,
SubmissionFormsConfigService,
+ SubmissionRestService,
SubmissionSectionsConfigService,
+ SubmissionResponseParsingService,
+ SubmissionJsonPatchOperationsService,
+ JsonPatchOperationsBuilder,
AuthorityService,
IntegrationResponseParsingService,
MetadataschemaParsingService,
UploaderService,
UUIDService,
+ NotificationsService,
+ WorkspaceitemDataService,
+ WorkflowitemDataService,
+ UploaderService,
+ FileService,
DSpaceObjectDataService,
DSOChangeAnalyzer,
DefaultChangeAnalyzer,
diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts
index e0ddb4a9de..c93b4bf44b 100644
--- a/src/app/core/core.reducers.ts
+++ b/src/app/core/core.reducers.ts
@@ -4,9 +4,10 @@ import {
} from '@ngrx/store';
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
-import { indexReducer, IndexState } from './index/index.reducer';
+import { indexReducer, MetaIndexState } from './index/index.reducer';
import { requestReducer, RequestState } from './data/request.reducer';
import { authReducer, AuthState } from './auth/auth.reducer';
+import { jsonPatchOperationsReducer, JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer';
import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-sync-buffer.reducer';
import {
objectUpdatesReducer,
@@ -18,8 +19,9 @@ export interface CoreState {
'cache/syncbuffer': ServerSyncBufferState,
'cache/object-updates': ObjectUpdatesState
'data/request': RequestState,
- 'index': IndexState,
+ 'index': MetaIndexState,
'auth': AuthState,
+ 'json/patch': JsonPatchOperationsState
}
export const coreReducers: ActionReducerMap = {
@@ -29,6 +31,5 @@ export const coreReducers: ActionReducerMap = {
'data/request': requestReducer,
'index': indexReducer,
'auth': authReducer,
+ 'json/patch': jsonPatchOperationsReducer
};
-
-export const coreSelector = createFeatureSelector('core');
diff --git a/src/app/core/core.selectors.ts b/src/app/core/core.selectors.ts
new file mode 100644
index 0000000000..60365be7c2
--- /dev/null
+++ b/src/app/core/core.selectors.ts
@@ -0,0 +1,7 @@
+import { createFeatureSelector } from '@ngrx/store';
+import { CoreState } from './core.reducers';
+
+/**
+ * Base selector to select the core state from the store
+ */
+export const coreSelector = createFeatureSelector('core');
diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts
index f7f904f790..71564883f2 100644
--- a/src/app/core/data/base-response-parsing.service.ts
+++ b/src/app/core/data/base-response-parsing.service.ts
@@ -37,11 +37,11 @@ export abstract class BaseResponseParsingService {
if (isNotEmpty(parsedObj)) {
if (isRestPaginatedList(data._embedded[property])) {
object[property] = parsedObj;
- object[property].page = parsedObj.page.map((obj) => obj.self);
+ object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj));
} else if (isRestDataObject(data._embedded[property])) {
- object[property] = parsedObj.self;
+ object[property] = this.retrieveObjectOrUrl(parsedObj);
} else if (Array.isArray(parsedObj)) {
- object[property] = parsedObj.map((obj) => obj.self)
+ object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj))
}
}
});
@@ -55,8 +55,7 @@ export abstract class BaseResponseParsingService {
.filter((property) => data.hasOwnProperty(property))
.filter((property) => hasValue(data[property]))
.forEach((property) => {
- const obj = this.process(data[property], requestUUID);
- result[property] = obj;
+ result[property] = this.process(data[property], requestUUID);
});
return result;
@@ -93,8 +92,7 @@ export abstract class BaseResponseParsingService {
if (hasValue(normObjConstructor)) {
const serializer = new DSpaceRESTv2Serializer(normObjConstructor);
- const res = serializer.deserialize(obj);
- return res;
+ return serializer.deserialize(obj);
} else {
// TODO: move check to Validator?
// throw new Error(`The server returned an object with an unknown a known type: ${type}`);
@@ -142,6 +140,10 @@ export abstract class BaseResponseParsingService {
return obj[keys[0]];
}
+ protected retrieveObjectOrUrl(obj: any): any {
+ return this.toCache ? obj.self : obj;
+ }
+
// TODO Remove when https://jira.duraspace.org/browse/DS-4006 is fixed
// See https://github.com/DSpace/dspace-angular/issues/292
private fixBadEPersonRestResponse(obj: any): any {
diff --git a/src/app/core/data/browse-entries-response-parsing.service.spec.ts b/src/app/core/data/browse-entries-response-parsing.service.spec.ts
index ee706d202c..ef9a833765 100644
--- a/src/app/core/data/browse-entries-response-parsing.service.spec.ts
+++ b/src/app/core/data/browse-entries-response-parsing.service.spec.ts
@@ -101,15 +101,17 @@ describe('BrowseEntriesResponseParsingService', () => {
number: 0
}
},
- statusCode: '200'
+ statusCode: 200,
+ statusText: 'OK'
} as DSpaceRESTV2Response;
const invalidResponseNotAList = {
- statusCode: '200'
+ statusCode: 200,
+ statusText: 'OK'
} as DSpaceRESTV2Response;
const invalidResponseStatusCode = {
- payload: {}, statusCode: '500'
+ payload: {}, statusCode: 500, statusText: 'Internal Server Error'
} as DSpaceRESTV2Response;
it('should return a GenericSuccessResponse if data contains a valid browse entries response', () => {
diff --git a/src/app/core/data/browse-entries-response-parsing.service.ts b/src/app/core/data/browse-entries-response-parsing.service.ts
index d61df1f611..4690d738ed 100644
--- a/src/app/core/data/browse-entries-response-parsing.service.ts
+++ b/src/app/core/data/browse-entries-response-parsing.service.ts
@@ -36,12 +36,12 @@ export class BrowseEntriesResponseParsingService extends BaseResponseParsingServ
const serializer = new DSpaceRESTv2Serializer(BrowseEntry);
browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
}
- return new GenericSuccessResponse(browseEntries, data.statusCode, this.processPageInfo(data.payload));
+ return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from browse endpoint'),
- { statusText: data.statusCode }
+ { statusCode: data.statusCode, statusText: data.statusText }
)
);
}
diff --git a/src/app/core/data/browse-items-response-parsing-service.spec.ts b/src/app/core/data/browse-items-response-parsing-service.spec.ts
index f512a9af26..50b3be5de7 100644
--- a/src/app/core/data/browse-items-response-parsing-service.spec.ts
+++ b/src/app/core/data/browse-items-response-parsing-service.spec.ts
@@ -108,7 +108,8 @@ describe('BrowseItemsResponseParsingService', () => {
number: 0
}
},
- statusCode: '200'
+ statusCode: 200,
+ statusText: 'OK'
} as DSpaceRESTV2Response;
const invalidResponseNotAList = {
@@ -145,11 +146,12 @@ describe('BrowseItemsResponseParsingService', () => {
}
}
},
- statusCode: '200'
+ statusCode: 200,
+ statusText: 'OK'
} as DSpaceRESTV2Response;
const invalidResponseStatusCode = {
- payload: {}, statusCode: '500'
+ payload: {}, statusCode: 500, statusText: 'Internal Server Error'
} as DSpaceRESTV2Response;
it('should return a GenericSuccessResponse if data contains a valid browse items response', () => {
diff --git a/src/app/core/data/browse-items-response-parsing-service.ts b/src/app/core/data/browse-items-response-parsing-service.ts
index b1feb2ab7f..fb950f6c68 100644
--- a/src/app/core/data/browse-items-response-parsing-service.ts
+++ b/src/app/core/data/browse-items-response-parsing-service.ts
@@ -1,19 +1,15 @@
import { Inject, Injectable } from '@angular/core';
+
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { ObjectCacheService } from '../cache/object-cache.service';
-import {
- ErrorResponse,
- GenericSuccessResponse,
- RestResponse
-} from '../cache/response.models';
+import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
-import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
@@ -45,14 +41,14 @@ export class BrowseItemsResponseParsingService extends BaseResponseParsingServic
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
const serializer = new DSpaceRESTv2Serializer(NormalizedDSpaceObject);
const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
- return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload));
+ return new GenericSuccessResponse(items, data.statusCode, data.statusText, this.processPageInfo(data.payload));
} else if (hasValue(data.payload) && hasValue(data.payload.page)) {
- return new GenericSuccessResponse([], data.statusCode, this.processPageInfo(data.payload));
+ return new GenericSuccessResponse([], data.statusCode, data.statusText, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from browse endpoint'),
- { statusText: data.statusCode }
+ { statusCode: data.statusCode, statusText: data.statusText }
)
);
}
diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts
index bedf5f03a7..c1b0566e0b 100644
--- a/src/app/core/data/browse-response-parsing.service.spec.ts
+++ b/src/app/core/data/browse-response-parsing.service.spec.ts
@@ -55,7 +55,7 @@ describe('BrowseResponseParsingService', () => {
},
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
- }, statusCode: '200'
+ }, statusCode: 200, statusText: 'OK'
} as DSpaceRESTV2Response;
invalidResponse1 = {
@@ -78,21 +78,21 @@ describe('BrowseResponseParsingService', () => {
},
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
- }, statusCode: '200'
+ }, statusCode: 200, statusText: 'OK'
} as DSpaceRESTV2Response;
invalidResponse2 = {
payload: {
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
- }, statusCode: '200'
+ }, statusCode: 200, statusText: 'OK'
} as DSpaceRESTV2Response;
invalidResponse3 = {
payload: {
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
- }, statusCode: '500'
+ }, statusCode: 500, statusText: 'Internal Server Error'
} as DSpaceRESTV2Response;
definitions = [
diff --git a/src/app/core/data/browse-response-parsing.service.ts b/src/app/core/data/browse-response-parsing.service.ts
index 523fffd565..3c67b2b3eb 100644
--- a/src/app/core/data/browse-response-parsing.service.ts
+++ b/src/app/core/data/browse-response-parsing.service.ts
@@ -15,12 +15,12 @@ export class BrowseResponseParsingService implements ResponseParsingService {
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
const serializer = new DSpaceRESTv2Serializer(BrowseDefinition);
const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
- return new GenericSuccessResponse(browseDefinitions, data.statusCode);
+ return new GenericSuccessResponse(browseDefinitions, data.statusCode, data.statusText);
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from browse endpoint'),
- { statusText: data.statusCode }
+ { statusCode: data.statusCode, statusText: data.statusText }
)
);
}
diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts
index d9b722fb46..3d03b9397d 100644
--- a/src/app/core/data/collection-data.service.ts
+++ b/src/app/core/data/collection-data.service.ts
@@ -17,6 +17,7 @@ import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
@Injectable()
export class CollectionDataService extends ComColDataService {
protected linkPath = 'collections';
+ protected forceBypassCache = false;
constructor(
protected requestService: RequestService,
diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts
index d93600a06a..7f628fc5b9 100644
--- a/src/app/core/data/comcol-data.service.spec.ts
+++ b/src/app/core/data/comcol-data.service.spec.ts
@@ -28,6 +28,7 @@ class NormalizedTestObject extends NormalizedObject- {
}
class TestService extends ComColDataService
{
+ protected forceBypassCache = false;
constructor(
protected requestService: RequestService,
diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts
index 662b82d6ea..75ef58b06b 100644
--- a/src/app/core/data/community-data.service.ts
+++ b/src/app/core/data/community-data.service.ts
@@ -25,6 +25,7 @@ export class CommunityDataService extends ComColDataService {
protected linkPath = 'communities';
protected topLinkPath = 'communities/search/top';
protected cds = this;
+ protected forceBypassCache = false;
constructor(
protected requestService: RequestService,
diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts
index 51474f6740..4a244db24f 100644
--- a/src/app/core/data/data.service.spec.ts
+++ b/src/app/core/data/data.service.spec.ts
@@ -25,6 +25,8 @@ class NormalizedTestObject extends NormalizedObject- {
}
class TestService extends DataService
{
+ protected forceBypassCache = false;
+
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts
index 72af52c4c8..fc4da69a5c 100644
--- a/src/app/core/data/data.service.ts
+++ b/src/app/core/data/data.service.ts
@@ -1,15 +1,9 @@
-import {
- distinctUntilChanged,
- filter,
- find,
- first,
- map,
- mergeMap,
- switchMap,
- take
-} from 'rxjs/operators';
+import { HttpClient } from '@angular/common/http';
+
import { Observable } from 'rxjs';
+import { distinctUntilChanged, filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators';
import { Store } from '@ngrx/store';
+
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { CoreState } from '../core.reducers';
@@ -26,12 +20,13 @@ import {
GetRequest
} from './request.models';
import { RequestService } from './request.service';
+import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { NormalizedObject } from '../cache/models/normalized-object.model';
+import { SearchParam } from '../cache/models/search-param.model';
import { Operation } from 'fast-json-patch';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DSpaceObject } from '../shared/dspace-object.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
-import { HttpClient } from '@angular/common/http';
import { configureRequest, getResponseFromEntry } from '../shared/operators';
import { ErrorResponse, RestResponse } from '../cache/response.models';
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
@@ -50,6 +45,7 @@ export abstract class DataService {
protected abstract store: Store;
protected abstract linkPath: string;
protected abstract halService: HALEndpointService;
+ protected abstract forceBypassCache = false;
protected abstract objectCache: ObjectCacheService;
protected abstract notificationsService: NotificationsService;
protected abstract http: HttpClient;
@@ -57,11 +53,57 @@ export abstract class DataService {
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable
+ /**
+ * Create the HREF with given options object
+ *
+ * @param options The [[FindAllOptions]] object
+ * @param linkPath The link path for the object
+ * @return {Observable}
+ * Return an observable that emits created HREF
+ */
protected getFindAllHref(options: FindAllOptions = {}, linkPath?: string): Observable {
let result: Observable;
const args = [];
- result = this.getBrowseEndpoint(options, linkPath);
+ result = this.getBrowseEndpoint(options, linkPath).pipe(distinctUntilChanged());
+
+ return this.buildHrefFromFindOptions(result, args, options);
+ }
+
+ /**
+ * Create the HREF for a specific object's search method with given options object
+ *
+ * @param searchMethod The search method for the object
+ * @param options The [[FindAllOptions]] object
+ * @return {Observable}
+ * Return an observable that emits created HREF
+ */
+ protected getSearchByHref(searchMethod: string, options: FindAllOptions = {}): Observable {
+ let result: Observable;
+ const args = [];
+
+ result = this.getSearchEndpoint(searchMethod);
+
+ if (hasValue(options.searchParams)) {
+ options.searchParams.forEach((param: SearchParam) => {
+ args.push(`${param.fieldName}=${param.fieldValue}`);
+ })
+ }
+
+ return this.buildHrefFromFindOptions(result, args, options);
+ }
+
+ /**
+ * Turn an options object into a query string and combine it with the given HREF
+ *
+ * @param href$ The HREF to which the query string should be appended
+ * @param args Array with additional params to combine with query string
+ * @param options The [[FindAllOptions]] object
+ * @return {Observable}
+ * Return an observable that emits created HREF
+ */
+ protected buildHrefFromFindOptions(href$: Observable, args: string[], options: FindAllOptions): Observable {
+
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
args.push(`page=${options.currentPage - 1}`);
@@ -76,9 +118,9 @@ export abstract class DataService {
args.push(`startsWith=${options.startsWith}`);
}
if (isNotEmpty(args)) {
- return result.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()));
+ return href$.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()));
} else {
- return result;
+ return href$;
}
}
@@ -86,11 +128,10 @@ export abstract class DataService {
const hrefObs = this.getFindAllHref(options);
hrefObs.pipe(
- filter((href: string) => hasValue(href)),
- take(1))
+ first((href: string) => hasValue(href)))
.subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
- this.requestService.configure(request);
+ this.requestService.configure(request, this.forceBypassCache);
});
return this.rdbService.buildList(hrefObs) as Observable>>;
@@ -113,17 +154,50 @@ export abstract class DataService {
find((href: string) => hasValue(href)))
.subscribe((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id);
- this.requestService.configure(request);
+ this.requestService.configure(request, this.forceBypassCache);
});
return this.rdbService.buildSingle(hrefObs);
}
- findByHref(href: string): Observable> {
- this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href));
+ findByHref(href: string, options?: HttpOptions): Observable> {
+ this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href, null, options), this.forceBypassCache);
return this.rdbService.buildSingle(href);
}
+ /**
+ * Return object search endpoint by given search method
+ *
+ * @param searchMethod The search method for the object
+ */
+ protected getSearchEndpoint(searchMethod: string): Observable {
+ return this.halService.getEndpoint(`${this.linkPath}/search`).pipe(
+ filter((href: string) => isNotEmpty(href)),
+ map((href: string) => `${href}/${searchMethod}`));
+ }
+
+ /**
+ * Make a new FindAllRequest with given search method
+ *
+ * @param searchMethod The search method for the object
+ * @param options The [[FindAllOptions]] object
+ * @return {Observable>}
+ * Return an observable that emits response from the server
+ */
+ protected searchBy(searchMethod: string, options: FindAllOptions = {}): Observable>> {
+
+ const hrefObs = this.getSearchByHref(searchMethod, options);
+
+ hrefObs.pipe(
+ first((href: string) => hasValue(href)))
+ .subscribe((href: string) => {
+ const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
+ this.requestService.configure(request, true);
+ });
+
+ return this.rdbService.buildList(hrefObs) as Observable>>;
+ }
+
/**
* Add a new patch to the object cache to a specified object
* @param {string} href The selflink of the object that will be patched
diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts
index 3cb0b1e8ff..eb95cdae8a 100644
--- a/src/app/core/data/dso-response-parsing.service.ts
+++ b/src/app/core/data/dso-response-parsing.service.ts
@@ -39,7 +39,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
let objectList = processRequestDTO;
if (hasNoValue(processRequestDTO)) {
- return new DSOSuccessResponse([], data.statusCode, undefined)
+ return new DSOSuccessResponse([], data.statusCode, data.statusText, undefined)
}
if (hasValue(processRequestDTO.page)) {
objectList = processRequestDTO.page;
@@ -47,7 +47,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
objectList = [processRequestDTO];
}
const selfLinks = objectList.map((no) => no.self);
- return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload))
+ return new DSOSuccessResponse(selfLinks, data.statusCode, data.statusText, this.processPageInfo(data.payload))
}
}
diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts
index 7047db6065..a0bba214ae 100644
--- a/src/app/core/data/dspace-object-data.service.spec.ts
+++ b/src/app/core/data/dspace-object-data.service.spec.ts
@@ -72,7 +72,7 @@ describe('DSpaceObjectDataService', () => {
scheduler.schedule(() => service.findById(testObject.uuid));
scheduler.flush();
- expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid));
+ expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid), false);
});
it('should return a RemoteData for the object with the given ID', () => {
diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts
index bb02afbcd1..4f0653f416 100644
--- a/src/app/core/data/dspace-object-data.service.ts
+++ b/src/app/core/data/dspace-object-data.service.ts
@@ -18,6 +18,7 @@ import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
/* tslint:disable:max-classes-per-file */
class DataServiceImpl extends DataService {
protected linkPath = 'dso';
+ protected forceBypassCache = false;
constructor(
protected requestService: RequestService,
diff --git a/src/app/core/data/endpoint-map-response-parsing.service.ts b/src/app/core/data/endpoint-map-response-parsing.service.ts
index a145477953..080c665ccf 100644
--- a/src/app/core/data/endpoint-map-response-parsing.service.ts
+++ b/src/app/core/data/endpoint-map-response-parsing.service.ts
@@ -20,12 +20,12 @@ export class EndpointMapResponseParsingService implements ResponseParsingService
for (const link of Object.keys(links)) {
links[link] = links[link].href;
}
- return new EndpointMapSuccessResponse(links, data.statusCode);
+ return new EndpointMapSuccessResponse(links, data.statusCode, data.statusText);
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from root endpoint'),
- { statusText: data.statusCode }
+ { statusCode: data.statusCode, statusText: data.statusText }
)
);
}
diff --git a/src/app/core/data/facet-config-response-parsing.service.ts b/src/app/core/data/facet-config-response-parsing.service.ts
index 02b12dfa10..e65e317642 100644
--- a/src/app/core/data/facet-config-response-parsing.service.ts
+++ b/src/app/core/data/facet-config-response-parsing.service.ts
@@ -27,6 +27,6 @@ export class FacetConfigResponseParsingService extends BaseResponseParsingServic
const config = data.payload._embedded.facets;
const serializer = new DSpaceRESTv2Serializer(SearchFilterConfig);
const facetConfig = serializer.deserializeArray(config);
- return new FacetConfigSuccessResponse(facetConfig, data.statusCode);
+ return new FacetConfigSuccessResponse(facetConfig, data.statusCode, data.statusText);
}
}
diff --git a/src/app/core/data/facet-value-map-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts
index 2f580ee952..e03c1a78df 100644
--- a/src/app/core/data/facet-value-map-response-parsing.service.ts
+++ b/src/app/core/data/facet-value-map-response-parsing.service.ts
@@ -35,10 +35,10 @@ export class FacetValueMapResponseParsingService extends BaseResponseParsingServ
payload._embedded.facets.map((facet) => {
const values = facet._embedded.values.map((value) => {value.search = value._links.search.href; return value;});
const facetValues = serializer.deserializeArray(values);
- const valuesResponse = new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload));
+ const valuesResponse = new FacetValueSuccessResponse(facetValues, data.statusCode, data.statusText, this.processPageInfo(data.payload));
facetMap[facet.name] = valuesResponse;
});
- return new FacetValueMapSuccessResponse(facetMap, data.statusCode);
+ return new FacetValueMapSuccessResponse(facetMap, data.statusCode, data.statusText);
}
}
diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts
index 54f36a0564..e7665ebed2 100644
--- a/src/app/core/data/facet-value-response-parsing.service.ts
+++ b/src/app/core/data/facet-value-response-parsing.service.ts
@@ -26,6 +26,6 @@ export class FacetValueResponseParsingService extends BaseResponseParsingService
// const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;});
const facetValues = serializer.deserializeArray(payload._embedded.values);
- return new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload));
+ return new FacetValueSuccessResponse(facetValues, data.statusCode, data.statusText, this.processPageInfo(data.payload));
}
}
diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts
index 45f7ae3069..166a915b16 100644
--- a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts
+++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts
@@ -30,6 +30,6 @@ export class FilteredDiscoveryPageResponseParsingService extends BaseResponsePar
*/
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const query = data.payload['discovery-query'];
- return new FilteredDiscoveryQueryResponse(query, data.statusCode);
+ return new FilteredDiscoveryQueryResponse(query, data.statusCode, data.statusText);
}
}
diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts
index 02c70791b5..3553a63af4 100644
--- a/src/app/core/data/item-data.service.spec.ts
+++ b/src/app/core/data/item-data.service.spec.ts
@@ -30,7 +30,7 @@ describe('ItemDataService', () => {
},
getByHref(requestHref: string) {
const responseCacheEntry = new RequestEntry();
- responseCacheEntry.response = new RestResponse(true, '200');
+ responseCacheEntry.response = new RestResponse(true, 200, 'OK');
return observableOf(responseCacheEntry);
}
} as RequestService;
@@ -133,7 +133,7 @@ describe('ItemDataService', () => {
});
it('should setWithDrawn', () => {
- const expected = new RestResponse(true, '200');
+ const expected = new RestResponse(true, 200, 'OK');
const result = service.setWithDrawn(scopeID, true);
result.subscribe((v) => expect(v).toEqual(expected));
@@ -155,7 +155,7 @@ describe('ItemDataService', () => {
});
it('should setDiscoverable', () => {
- const expected = new RestResponse(true, '200');
+ const expected = new RestResponse(true, 200, 'OK');
const result = service.setDiscoverable(scopeID, false);
result.subscribe((v) => expect(v).toEqual(expected));
diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts
index a2f6a1cc14..f6adbb23c2 100644
--- a/src/app/core/data/item-data.service.ts
+++ b/src/app/core/data/item-data.service.ts
@@ -24,6 +24,7 @@ import { RequestEntry } from './request.reducer';
@Injectable()
export class ItemDataService extends DataService- {
protected linkPath = 'items';
+ protected forceBypassCache = false;
constructor(
protected requestService: RequestService,
diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts
index 05879d6fbb..1d2bf3b221 100644
--- a/src/app/core/data/metadata-schema-data.service.ts
+++ b/src/app/core/data/metadata-schema-data.service.ts
@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
-import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { CoreState } from '../core.reducers';
@@ -22,12 +21,12 @@ import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
@Injectable()
export class MetadataSchemaDataService extends DataService
{
protected linkPath = 'metadataschemas';
+ protected forceBypassCache = false;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store,
- private bs: BrowseService,
protected halService: HALEndpointService,
protected objectCache: ObjectCacheService,
protected comparator: DefaultChangeAnalyzer,
diff --git a/src/app/core/data/metadatafield-parsing.service.ts b/src/app/core/data/metadatafield-parsing.service.ts
index 86a3c8a925..f9582c394d 100644
--- a/src/app/core/data/metadatafield-parsing.service.ts
+++ b/src/app/core/data/metadatafield-parsing.service.ts
@@ -16,7 +16,7 @@ export class MetadatafieldParsingService implements ResponseParsingService {
const payload = data.payload;
const deserialized = new DSpaceRESTv2Serializer(MetadataField).deserialize(payload);
- return new MetadatafieldSuccessResponse(deserialized, data.statusCode);
+ return new MetadatafieldSuccessResponse(deserialized, data.statusCode, data.statusText);
}
}
diff --git a/src/app/core/data/metadataschema-parsing.service.ts b/src/app/core/data/metadataschema-parsing.service.ts
index 78a5257456..f76d6ed2e3 100644
--- a/src/app/core/data/metadataschema-parsing.service.ts
+++ b/src/app/core/data/metadataschema-parsing.service.ts
@@ -13,7 +13,7 @@ export class MetadataschemaParsingService implements ResponseParsingService {
const payload = data.payload;
const deserialized = new DSpaceRESTv2Serializer(MetadataSchema).deserialize(payload);
- return new MetadataschemaSuccessResponse(deserialized, data.statusCode);
+ return new MetadataschemaSuccessResponse(deserialized, data.statusCode, data.statusText);
}
}
diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts
index 85e17b5b2f..a13fb9487b 100644
--- a/src/app/core/data/object-updates/object-updates.service.ts
+++ b/src/app/core/data/object-updates/object-updates.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
-import { coreSelector, CoreState } from '../../core.reducers';
+import { CoreState } from '../../core.reducers';
+import { coreSelector } from '../../core.selectors';
import {
FieldState,
FieldUpdates,
diff --git a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts b/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts
index 2ee3bbf75e..899fee4d1e 100644
--- a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts
+++ b/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts
@@ -19,7 +19,7 @@ export class RegistryBitstreamformatsResponseParsingService implements ResponseP
payload.bitstreamformats = bitstreamformats;
const deserialized = new DSpaceRESTv2Serializer(RegistryBitstreamformatsResponse).deserialize(payload);
- return new RegistryBitstreamformatsSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload.page));
+ return new RegistryBitstreamformatsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload.page));
}
}
diff --git a/src/app/core/data/registry-metadatafields-response-parsing.service.ts b/src/app/core/data/registry-metadatafields-response-parsing.service.ts
index 93fd67b702..a4bed3240e 100644
--- a/src/app/core/data/registry-metadatafields-response-parsing.service.ts
+++ b/src/app/core/data/registry-metadatafields-response-parsing.service.ts
@@ -31,7 +31,7 @@ export class RegistryMetadatafieldsResponseParsingService implements ResponsePar
payload.metadatafields = metadatafields;
const deserialized = new DSpaceRESTv2Serializer(RegistryMetadatafieldsResponse).deserialize(payload);
- return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload));
+ return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload));
}
}
diff --git a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts b/src/app/core/data/registry-metadataschemas-response-parsing.service.ts
index 05a61f6b4f..d19b334131 100644
--- a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts
+++ b/src/app/core/data/registry-metadataschemas-response-parsing.service.ts
@@ -23,7 +23,7 @@ export class RegistryMetadataschemasResponseParsingService implements ResponsePa
payload.metadataschemas = metadataschemas;
const deserialized = new DSpaceRESTv2Serializer(RegistryMetadataschemasResponse).deserialize(payload);
- return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload));
+ return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload));
}
}
diff --git a/src/app/core/data/remote-data-error.ts b/src/app/core/data/remote-data-error.ts
index a2ff27a073..9291bc5447 100644
--- a/src/app/core/data/remote-data-error.ts
+++ b/src/app/core/data/remote-data-error.ts
@@ -1,6 +1,7 @@
export class RemoteDataError {
constructor(
- public statusCode: string,
+ public statusCode: number,
+ public statusText: string,
public message: string
) {
}
diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts
index 951dbacff6..d2cdd45a0a 100644
--- a/src/app/core/data/request.models.ts
+++ b/src/app/core/data/request.models.ts
@@ -5,11 +5,14 @@ import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
import { BrowseResponseParsingService } from './browse-response-parsing.service';
-import { ConfigResponseParsingService } from './config-response-parsing.service';
+import { ConfigResponseParsingService } from '../config/config-response-parsing.service';
import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
+import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service';
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
import { RestRequestMethod } from './rest-request-method';
+import { SearchParam } from '../cache/models/search-param.model';
+import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service';
import { MetadataschemaParsingService } from './metadataschema-parsing.service';
@@ -132,6 +135,7 @@ export class FindAllOptions {
elementsPerPage?: number;
currentPage?: number;
sort?: SortOptions;
+ searchParams?: SearchParam[];
startsWith?: string;
}
@@ -182,8 +186,8 @@ export class BrowseItemsRequest extends GetRequest {
}
export class ConfigRequest extends GetRequest {
- constructor(uuid: string, href: string) {
- super(uuid, href);
+ constructor(uuid: string, href: string, public options?: HttpOptions) {
+ super(uuid, href, null, options);
}
getResponseParser(): GenericConstructor {
@@ -273,6 +277,77 @@ export class UpdateMetadataFieldRequest extends PutRequest {
}
}
+/**
+ * Class representing a submission HTTP GET request object
+ */
+export class SubmissionRequest extends GetRequest {
+ constructor(uuid: string, href: string) {
+ super(uuid, href);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return SubmissionResponseParsingService;
+ }
+}
+
+/**
+ * Class representing a submission HTTP DELETE request object
+ */
+export class SubmissionDeleteRequest extends DeleteRequest {
+ constructor(public uuid: string,
+ public href: string) {
+ super(uuid, href);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return SubmissionResponseParsingService;
+ }
+}
+
+/**
+ * Class representing a submission HTTP PATCH request object
+ */
+export class SubmissionPatchRequest extends PatchRequest {
+ constructor(public uuid: string,
+ public href: string,
+ public body?: any) {
+ super(uuid, href, body);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return SubmissionResponseParsingService;
+ }
+}
+
+/**
+ * Class representing a submission HTTP POST request object
+ */
+export class SubmissionPostRequest extends PostRequest {
+ constructor(public uuid: string,
+ public href: string,
+ public body?: any,
+ public options?: HttpOptions) {
+ super(uuid, href, body, options);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return SubmissionResponseParsingService;
+ }
+}
+
+/**
+ * Class representing an eperson HTTP GET request object
+ */
+export class EpersonRequest extends GetRequest {
+ constructor(uuid: string, href: string) {
+ super(uuid, href);
+ }
+
+ getResponseParser(): GenericConstructor {
+ return EpersonResponseParsingService;
+ }
+}
+
export class CreateRequest extends PostRequest {
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
super(uuid, href, body, options);
@@ -297,6 +372,7 @@ export class DeleteByIDRequest extends DeleteRequest {
}
export class RequestError extends Error {
+ statusCode: number;
statusText: string;
}
/* tslint:enable:max-classes-per-file */
diff --git a/src/app/core/data/request.reducer.spec.ts b/src/app/core/data/request.reducer.spec.ts
index 5c35c0a398..65a4ddba17 100644
--- a/src/app/core/data/request.reducer.spec.ts
+++ b/src/app/core/data/request.reducer.spec.ts
@@ -9,7 +9,7 @@ import {
import { GetRequest } from './request.models';
import { RestResponse } from '../cache/response.models';
-const response = new RestResponse(true, 'OK');
+const response = new RestResponse(true, 200, 'OK');
class NullAction extends RequestCompleteAction {
type = null;
payload = null;
@@ -89,8 +89,8 @@ describe('requestReducer', () => {
expect(newState[id1].requestPending).toEqual(state[id1].requestPending);
expect(newState[id1].responsePending).toEqual(false);
expect(newState[id1].completed).toEqual(true);
- expect(newState[id1].response.isSuccessful).toEqual(response.isSuccessful)
- expect(newState[id1].response.statusCode).toEqual(response.statusCode)
+ expect(newState[id1].response.isSuccessful).toEqual(response.isSuccessful);
+ expect(newState[id1].response.statusCode).toEqual(response.statusCode);
expect(newState[id1].response.timeAdded).toBeTruthy()
});
diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts
index b3d9436d30..e2bc04040f 100644
--- a/src/app/core/data/request.service.spec.ts
+++ b/src/app/core/data/request.service.spec.ts
@@ -1,13 +1,14 @@
+import * as ngrx from '@ngrx/store';
+import { ActionsSubject, Store } from '@ngrx/store';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
-import { of as observableOf, EMPTY } from 'rxjs';
+import { BehaviorSubject, EMPTY, of as observableOf } from 'rxjs';
+
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers';
import { UUIDService } from '../shared/uuid.service';
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
-import * as ngrx from '@ngrx/store';
-import { ActionsSubject, Store } from '@ngrx/store';
import {
DeleteRequest,
GetRequest,
@@ -20,7 +21,6 @@ import {
} from './request.models';
import { RequestService } from './request.service';
import { TestScheduler } from 'rxjs/testing';
-import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
describe('RequestService', () => {
let scheduler: TestScheduler;
@@ -172,9 +172,6 @@ describe('RequestService', () => {
it('should return an Observable of undefined', () => {
const result = service.getByUUID(testUUID);
- // const expected = cold('b', {
- // b: undefined
- // });
scheduler.expectObservable(result).toBe('b', { b: undefined });
});
@@ -292,29 +289,8 @@ describe('RequestService', () => {
service.configure(testPatchRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest);
});
-
- it('shouldn\'t track it on it\'s way to the store', () => {
- spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore');
-
- serviceAsAny.dispatchRequest(testPostRequest);
- expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
-
- serviceAsAny.dispatchRequest(testPutRequest);
- expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
-
- serviceAsAny.dispatchRequest(testDeleteRequest);
- expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
-
- serviceAsAny.dispatchRequest(testOptionsRequest);
- expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
-
- serviceAsAny.dispatchRequest(testHeadRequest);
- expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
-
- serviceAsAny.dispatchRequest(testPatchRequest);
- expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
- });
});
+
});
describe('isCachedOrPending', () => {
@@ -385,6 +361,30 @@ describe('RequestService', () => {
serviceAsAny.dispatchRequest(request);
expect(store.dispatch).toHaveBeenCalledWith(new RequestExecuteAction(request.uuid));
});
+
+ describe('when it\'s not a GET request', () => {
+ it('shouldn\'t track it', () => {
+ spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore');
+
+ serviceAsAny.dispatchRequest(testPostRequest);
+ expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
+
+ serviceAsAny.dispatchRequest(testPutRequest);
+ expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
+
+ serviceAsAny.dispatchRequest(testDeleteRequest);
+ expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
+
+ serviceAsAny.dispatchRequest(testOptionsRequest);
+ expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
+
+ serviceAsAny.dispatchRequest(testHeadRequest);
+ expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
+
+ serviceAsAny.dispatchRequest(testPatchRequest);
+ expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
+ });
+ });
});
describe('trackRequestsOnTheirWayToTheStore', () => {
diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts
index f129f8becd..fd463047f1 100644
--- a/src/app/core/data/request.service.ts
+++ b/src/app/core/data/request.service.ts
@@ -1,27 +1,78 @@
-import { Observable, race as observableRace } from 'rxjs';
-import { filter, mergeMap, take } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
+import { Observable, race as observableRace } from 'rxjs';
+import { filter, mergeMap, take } from 'rxjs/operators';
+
+import { AppState } from '../../app.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service';
-import { coreSelector, CoreState } from '../core.reducers';
-import { IndexName, IndexState } from '../index/index.reducer';
-import { pathSelector } from '../shared/selectors';
-import { UUIDService } from '../shared/uuid.service';
+import { CoreState } from '../core.reducers';
+import { IndexName, IndexState, MetaIndexState } from '../index/index.reducer';
import {
- RequestConfigureAction,
- RequestExecuteAction,
- RequestRemoveAction
-} from './request.actions';
-import { EndpointMapRequest, GetRequest, RestRequest } from './request.models';
-
-import { RequestEntry } from './request.reducer';
+ originalRequestUUIDFromRequestUUIDSelector,
+ requestIndexSelector,
+ uuidFromHrefSelector
+} from '../index/index.selectors';
+import { UUIDService } from '../shared/uuid.service';
+import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions';
+import { GetRequest, RestRequest } from './request.models';
+import { RequestEntry, RequestState } from './request.reducer';
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
import { RestRequestMethod } from './rest-request-method';
import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions';
+import { coreSelector } from '../core.selectors';
+/**
+ * The base selector function to select the request state in the store
+ */
+const requestCacheSelector = createSelector(
+ coreSelector,
+ (state: CoreState) => state['data/request']
+);
+
+/**
+ * Selector function to select a request entry by uuid from the cache
+ * @param uuid The uuid of the request
+ */
+const entryFromUUIDSelector = (uuid: string): MemoizedSelector => createSelector(
+ requestCacheSelector,
+ (state: RequestState) => {
+ return hasValue(state) ? state[uuid] : undefined;
+ }
+);
+
+/**
+ * Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
+ * contains a given substring
+ * @param selector MemoizedSelector to start from
+ * @param name The name of the index substate we're fetching request UUIDs from
+ * @param href Substring that the request's href should contain
+ */
+const uuidsFromHrefSubstringSelector =
+ (selector: MemoizedSelector, href: string): MemoizedSelector => createSelector(
+ selector,
+ (state: IndexState) => getUuidsFromHrefSubstring(state, href)
+ );
+
+/**
+ * Fetch a list of request UUIDs from a given index substate of which the request href contains a given substring
+ * @param state The IndexState
+ * @param href Substring that the request's href should contain
+ */
+const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => {
+ let result = [];
+ if (isNotEmpty(state)) {
+ result = Object.values(state)
+ .filter((value: string) => value.startsWith(href));
+ }
+ return result;
+};
+
+/**
+ * A service to interact with the request state in the store
+ */
@Injectable()
export class RequestService {
private requestsOnTheirWayToTheStore: string[] = [];
@@ -29,51 +80,7 @@ export class RequestService {
constructor(private objectCache: ObjectCacheService,
private uuidService: UUIDService,
private store: Store,
- private indexStore: Store) {
- }
-
- private entryFromUUIDSelector(uuid: string): MemoizedSelector {
- return pathSelector(coreSelector, 'data/request', uuid);
- }
-
- private uuidFromHrefSelector(href: string): MemoizedSelector {
- return pathSelector(coreSelector, 'index', IndexName.REQUEST, href);
- }
-
- private originalUUIDFromUUIDSelector(uuid: string): MemoizedSelector {
- return pathSelector(coreSelector, 'index', IndexName.UUID_MAPPING, uuid);
- }
-
- /**
- * Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
- * contains a given substring
- * @param selector MemoizedSelector to start from
- * @param name The name of the index substate we're fetching request UUIDs from
- * @param href Substring that the request's href should contain
- */
- private uuidsFromHrefSubstringSelector(selector: MemoizedSelector, name: string, href: string): MemoizedSelector {
- return createSelector(selector, (state: IndexState) => this.getUuidsFromHrefSubstring(state, name, href));
- }
-
- /**
- * Fetch a list of request UUIDs from a given index substate of which the request href contains a given substring
- * @param state The IndexState
- * @param name The name of the index substate we're fetching request UUIDs from
- * @param href Substring that the request's href should contain
- */
- private getUuidsFromHrefSubstring(state: IndexState, name: string, href: string): string[] {
- let result = [];
- if (isNotEmpty(state)) {
- const subState = state[name];
- if (isNotEmpty(subState)) {
- for (const value in subState) {
- if (value.indexOf(href) > -1) {
- result = [...result, subState[value]];
- }
- }
- }
- }
- return result;
+ private indexStore: Store) {
}
generateRequestId(): string {
@@ -104,11 +111,11 @@ export class RequestService {
*/
getByUUID(uuid: string): Observable {
return observableRace(
- this.store.pipe(select(this.entryFromUUIDSelector(uuid))),
+ this.store.pipe(select(entryFromUUIDSelector(uuid))),
this.store.pipe(
- select(this.originalUUIDFromUUIDSelector(uuid)),
+ select(originalRequestUUIDFromRequestUUIDSelector(uuid)),
mergeMap((originalUUID) => {
- return this.store.pipe(select(this.entryFromUUIDSelector(originalUUID)))
+ return this.store.pipe(select(entryFromUUIDSelector(originalUUID)))
},
))
);
@@ -119,7 +126,7 @@ export class RequestService {
*/
getByHref(href: string): Observable {
return this.store.pipe(
- select(this.uuidFromHrefSelector(href)),
+ select(uuidFromHrefSelector(href)),
mergeMap((uuid: string) => this.getByUUID(uuid))
);
}
@@ -130,7 +137,7 @@ export class RequestService {
* @param {RestRequest} request The request to send out
* @param {boolean} forceBypassCache When true, a new request is always dispatched
*/
- // TODO to review "overrideRequest" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
+ // TODO to review "forceBypassCache" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
configure(request: RestRequest, forceBypassCache: boolean = false): void {
const isGetRequest = request.method === RestRequestMethod.GET;
if (!isGetRequest || !this.isCachedOrPending(request) || forceBypassCache) {
@@ -156,7 +163,7 @@ export class RequestService {
*/
removeByHrefSubstring(href: string) {
this.store.pipe(
- select(this.uuidsFromHrefSubstringSelector(pathSelector(coreSelector, 'index'), IndexName.REQUEST, href)),
+ select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
take(1)
).subscribe((uuids: string[]) => {
for (const uuid of uuids) {
diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts
index 8e3171d05e..0ca793c5ae 100644
--- a/src/app/core/data/search-response-parsing.service.ts
+++ b/src/app/core/data/search-response-parsing.service.ts
@@ -38,7 +38,8 @@ export class SearchResponseParsingService implements ResponseParsingService {
.map((dso) => Object.assign({}, dso, { _embedded: undefined }))
.map((dso) => this.dsoParser.parse(request, {
payload: dso,
- statusCode: data.statusCode
+ statusCode: data.statusCode,
+ statusText: data.statusText
}))
.map((obj) => obj.resourceSelfLinks)
.reduce((combined, thisElement) => [...combined, ...thisElement], []);
@@ -55,6 +56,6 @@ export class SearchResponseParsingService implements ResponseParsingService {
}));
payload.objects = objects;
const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload);
- return new SearchSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(payload));
+ return new SearchSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(payload));
}
}
diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts
index 17fb389707..d09d398d7c 100644
--- a/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts
+++ b/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts
@@ -8,5 +8,6 @@ export interface DSpaceRESTV2Response {
page?: any;
},
headers?: HttpHeaders,
- statusCode: string
+ statusCode: number,
+ statusText: string
}
diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts
index 26bd1ba5de..18b9090844 100644
--- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts
+++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts
@@ -8,7 +8,11 @@ describe('DSpaceRESTv2Service', () => {
let dSpaceRESTv2Service: DSpaceRESTv2Service;
let httpMock: HttpTestingController;
const url = 'http://www.dspace.org/';
- const mockError = new ErrorEvent('test error');
+ const mockError: any = {
+ statusCode: 0,
+ statusText: 'Unknown Error',
+ message: 'Http failure response for http://www.dspace.org/: 0 '
+ };
beforeEach(() => {
TestBed.configureTestingModule({
@@ -31,25 +35,26 @@ describe('DSpaceRESTv2Service', () => {
const mockPayload = {
page: 1
};
- const mockStatusCode = 'GREAT';
+ const mockStatusCode = 200;
+ const mockStatusText = 'GREAT';
dSpaceRESTv2Service.get(url).subscribe((response) => {
expect(response).toBeTruthy();
expect(response.statusCode).toEqual(mockStatusCode);
+ expect(response.statusText).toEqual(mockStatusText);
expect(response.payload.page).toEqual(mockPayload.page);
});
const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET');
- req.flush(mockPayload, { statusText: mockStatusCode});
+ req.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText});
});
});
it('should throw an error', () => {
dSpaceRESTv2Service.get(url).subscribe(() => undefined, (err) => {
- expect(err.error).toBe(mockError);
+ expect(err).toEqual(mockError);
});
-
const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET');
req.error(mockError);
diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts
index 6bf5eb0818..a2a9f2530c 100644
--- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts
+++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts
@@ -39,10 +39,10 @@ export class DSpaceRESTv2Service {
*/
get(absoluteURL: string): Observable {
return this.http.get(absoluteURL, { observe: 'response' }).pipe(
- map((res: HttpResponse) => ({ payload: res.body, statusCode: res.statusText })),
+ map((res: HttpResponse) => ({ payload: res.body, statusCode: res.status, statusText: res.statusText })),
catchError((err) => {
console.log('Error: ', err);
- return observableThrowError(err);
+ return observableThrowError({statusCode: err.status, statusText: err.statusText, message: err.message});
}));
}
@@ -72,10 +72,10 @@ export class DSpaceRESTv2Service {
requestOptions.responseType = options.responseType;
}
return this.http.request(method, url, requestOptions).pipe(
- map((res) => ({ payload: res.body, headers: res.headers, statusCode: res.statusText })),
+ map((res) => ({ payload: res.body, headers: res.headers, statusCode: res.status, statusText: res.statusText })),
catchError((err) => {
console.log('Error: ', err);
- return observableThrowError(err);
+ return observableThrowError({statusCode: err.status, statusText: err.statusText, message: err.message});
}));
}
diff --git a/src/app/core/eperson/eperson-response-parsing.service.ts b/src/app/core/eperson/eperson-response-parsing.service.ts
new file mode 100644
index 0000000000..6c591b0b99
--- /dev/null
+++ b/src/app/core/eperson/eperson-response-parsing.service.ts
@@ -0,0 +1,46 @@
+import { Inject, Injectable } from '@angular/core';
+
+import { RestRequest } from '../data/request.models';
+import { ResponseParsingService } from '../data/parsing.service';
+import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
+import { EpersonSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models';
+import { isNotEmpty } from '../../shared/empty.util';
+import { BaseResponseParsingService } from '../data/base-response-parsing.service';
+import { GLOBAL_CONFIG } from '../../../config';
+import { GlobalConfig } from '../../../config/global-config.interface';
+import { ObjectCacheService } from '../cache/object-cache.service';
+import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
+import { ResourceType } from '../shared/resource-type';
+import { DSpaceObject } from '../shared/dspace-object.model';
+
+/**
+ * Provides method to parse response from eperson endpoint.
+ */
+@Injectable()
+export class EpersonResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
+
+ protected objectFactory = NormalizedObjectFactory;
+ protected toCache = false;
+
+ constructor(
+ @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
+ protected objectCache: ObjectCacheService,
+ ) {
+ super();
+ }
+
+ parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
+ if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
+ const epersonDefinition = this.process(data.payload, request.href);
+ return new EpersonSuccessResponse(epersonDefinition[Object.keys(epersonDefinition)[0]], data.statusCode, data.statusText, this.processPageInfo(data.payload));
+ } else {
+ return new ErrorResponse(
+ Object.assign(
+ new Error('Unexpected response from EPerson endpoint'),
+ {statusCode: data.statusCode, statusText: data.statusText}
+ )
+ );
+ }
+ }
+
+}
diff --git a/src/app/core/eperson/eperson.service.ts b/src/app/core/eperson/eperson.service.ts
new file mode 100644
index 0000000000..70ecf3f59e
--- /dev/null
+++ b/src/app/core/eperson/eperson.service.ts
@@ -0,0 +1,14 @@
+import { Observable } from 'rxjs';
+import { FindAllOptions } from '../data/request.models';
+import { DataService } from '../data/data.service';
+import { CacheableObject } from '../cache/object-cache.reducer';
+
+/**
+ * An abstract class that provides methods to make HTTP request to eperson endpoint.
+ */
+export abstract class EpersonService extends DataService {
+
+ public getBrowseEndpoint(options: FindAllOptions): Observable {
+ return this.halService.getEndpoint(this.linkPath);
+ }
+}
diff --git a/src/app/core/eperson/group-eperson.service.ts b/src/app/core/eperson/group-eperson.service.ts
new file mode 100644
index 0000000000..07a1bb6aba
--- /dev/null
+++ b/src/app/core/eperson/group-eperson.service.ts
@@ -0,0 +1,66 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+import { Store } from '@ngrx/store';
+import { Observable } from 'rxjs';
+import { filter, map, take } from 'rxjs/operators';
+
+import { EpersonService } from './eperson.service';
+import { RequestService } from '../data/request.service';
+import { FindAllOptions } from '../data/request.models';
+import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { Group } from './models/group.model';
+import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
+import { CoreState } from '../core.reducers';
+import { ObjectCacheService } from '../cache/object-cache.service';
+import { SearchParam } from '../cache/models/search-param.model';
+import { RemoteData } from '../data/remote-data';
+import { PaginatedList } from '../data/paginated-list';
+import { NotificationsService } from '../../shared/notifications/notifications.service';
+import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
+import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
+
+/**
+ * Provides methods to retrieve eperson group resources.
+ */
+@Injectable()
+export class GroupEpersonService extends EpersonService {
+ protected linkPath = 'groups';
+ protected browseEndpoint = '';
+ protected forceBypassCache = false;
+
+ constructor(
+ protected comparator: DSOChangeAnalyzer,
+ protected dataBuildService: NormalizedObjectBuildService,
+ protected http: HttpClient,
+ protected notificationsService: NotificationsService,
+ protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
+ protected store: Store,
+ protected objectCache: ObjectCacheService,
+ protected halService: HALEndpointService
+ ) {
+ super();
+ }
+
+ /**
+ * Check if the current user is member of to the indicated group
+ *
+ * @param groupName
+ * the group name
+ * @return boolean
+ * true if user is member of the indicated group, false otherwise
+ */
+ isMemberOf(groupName: string): Observable {
+ const searchHref = 'isMemberOf';
+ const options = new FindAllOptions();
+ options.searchParams = [new SearchParam('groupName', groupName)];
+
+ return this.searchBy(searchHref, options).pipe(
+ filter((groups: RemoteData>) => !groups.isResponsePending),
+ take(1),
+ map((groups: RemoteData>) => groups.payload.totalElements > 0)
+ );
+ }
+
+}
diff --git a/src/app/core/eperson/models/eperson.model.ts b/src/app/core/eperson/models/eperson.model.ts
index 7d2138b633..32286929ee 100644
--- a/src/app/core/eperson/models/eperson.model.ts
+++ b/src/app/core/eperson/models/eperson.model.ts
@@ -1,22 +1,50 @@
+import { Observable } from 'rxjs';
+
import { DSpaceObject } from '../../shared/dspace-object.model';
import { Group } from './group.model';
+import { RemoteData } from '../../data/remote-data';
+import { PaginatedList } from '../../data/paginated-list';
export class EPerson extends DSpaceObject {
+ /**
+ * A string representing the unique handle of this Collection
+ */
public handle: string;
- public groups: Group[];
+ /**
+ * List of Groups that this EPerson belong to
+ */
+ public groups: Observable>>;
+ /**
+ * A string representing the netid of this EPerson
+ */
public netid: string;
+ /**
+ * A string representing the last active date for this EPerson
+ */
public lastActive: string;
+ /**
+ * A boolean representing if this EPerson can log in
+ */
public canLogIn: boolean;
+ /**
+ * The EPerson email address
+ */
public email: string;
+ /**
+ * A boolean representing if this EPerson require certificate
+ */
public requireCertificate: boolean;
+ /**
+ * A boolean representing if this EPerson registered itself
+ */
public selfRegistered: boolean;
/** Getter to retrieve the EPerson's full name as a string */
diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts
index cd41ce9e25..91ce5d90f3 100644
--- a/src/app/core/eperson/models/group.model.ts
+++ b/src/app/core/eperson/models/group.model.ts
@@ -1,8 +1,28 @@
+import { Observable } from 'rxjs';
+
import { DSpaceObject } from '../../shared/dspace-object.model';
+import { PaginatedList } from '../../data/paginated-list';
+import { RemoteData } from '../../data/remote-data';
export class Group extends DSpaceObject {
+ /**
+ * List of Groups that this Group belong to
+ */
+ public groups: Observable>>;
+
+ /**
+ * A string representing the unique handle of this Group
+ */
public handle: string;
+ /**
+ * A string representing the name of this Group
+ */
+ public name: string;
+
+ /**
+ * A string representing the name of this Group is permanent
+ */
public permanent: boolean;
}
diff --git a/src/app/core/eperson/models/normalized-eperson.model.ts b/src/app/core/eperson/models/normalized-eperson.model.ts
index bcd7e49871..ad4b20ee80 100644
--- a/src/app/core/eperson/models/normalized-eperson.model.ts
+++ b/src/app/core/eperson/models/normalized-eperson.model.ts
@@ -1,4 +1,5 @@
-import { autoserialize, inheritSerialization } from 'cerialize';
+import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
+
import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
@@ -10,28 +11,52 @@ import { ResourceType } from '../../shared/resource-type';
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedEPerson extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
+ /**
+ * A string representing the unique handle of this EPerson
+ */
@autoserialize
public handle: string;
- @autoserialize
+ /**
+ * List of Groups that this EPerson belong to
+ */
+ @deserialize
@relationship(ResourceType.Group, true)
groups: string[];
+ /**
+ * A string representing the netid of this EPerson
+ */
@autoserialize
public netid: string;
+ /**
+ * A string representing the last active date for this EPerson
+ */
@autoserialize
public lastActive: string;
+ /**
+ * A boolean representing if this EPerson can log in
+ */
@autoserialize
public canLogIn: boolean;
+ /**
+ * The EPerson email address
+ */
@autoserialize
public email: string;
+ /**
+ * A boolean representing if this EPerson require certificate
+ */
@autoserialize
public requireCertificate: boolean;
+ /**
+ * A boolean representing if this EPerson registered itself
+ */
@autoserialize
public selfRegistered: boolean;
}
diff --git a/src/app/core/eperson/models/normalized-group.model.ts b/src/app/core/eperson/models/normalized-group.model.ts
index d576f399ff..f86bec8628 100644
--- a/src/app/core/eperson/models/normalized-group.model.ts
+++ b/src/app/core/eperson/models/normalized-group.model.ts
@@ -1,17 +1,38 @@
-import { autoserialize, inheritSerialization } from 'cerialize';
+import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
+
import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
-import { mapsTo } from '../../cache/builders/build-decorators';
+import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { Group } from './group.model';
+import { ResourceType } from '../../shared/resource-type';
@mapsTo(Group)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedGroup extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
+ /**
+ * List of Groups that this Group belong to
+ */
+ @deserialize
+ @relationship(ResourceType.Group, true)
+ groups: string[];
+
+ /**
+ * A string representing the unique handle of this Group
+ */
@autoserialize
public handle: string;
+ /**
+ * A string representing the name of this Group
+ */
+ @autoserialize
+ public name: string;
+
+ /**
+ * A string representing the name of this Group is permanent
+ */
@autoserialize
public permanent: boolean;
}
diff --git a/src/app/core/index/index.reducer.spec.ts b/src/app/core/index/index.reducer.spec.ts
index d1403ac5bf..ef46c760c6 100644
--- a/src/app/core/index/index.reducer.spec.ts
+++ b/src/app/core/index/index.reducer.spec.ts
@@ -1,6 +1,6 @@
import * as deepFreeze from 'deep-freeze';
-import { IndexName, indexReducer, IndexState } from './index.reducer';
+import { IndexName, indexReducer, MetaIndexState } from './index.reducer';
import { AddToIndexAction, RemoveFromIndexBySubstringAction, RemoveFromIndexByValueAction } from './index.actions';
class NullAction extends AddToIndexAction {
@@ -17,7 +17,7 @@ describe('requestReducer', () => {
const key2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb';
const val1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8';
const val2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb';
- const testState: IndexState = {
+ const testState: MetaIndexState = {
[IndexName.OBJECT]: {
[key1]: val1
},[IndexName.REQUEST]: {
diff --git a/src/app/core/index/index.reducer.ts b/src/app/core/index/index.reducer.ts
index 3597c786d8..b4cd8aa84b 100644
--- a/src/app/core/index/index.reducer.ts
+++ b/src/app/core/index/index.reducer.ts
@@ -1,26 +1,57 @@
import {
+ AddToIndexAction,
IndexAction,
IndexActionTypes,
- AddToIndexAction,
- RemoveFromIndexByValueAction, RemoveFromIndexBySubstringAction
+ RemoveFromIndexBySubstringAction,
+ RemoveFromIndexByValueAction
} from './index.actions';
+/**
+ * An enum containing all index names
+ */
export enum IndexName {
+ // Contains all objects in the object cache indexed by UUID
OBJECT = 'object/uuid-to-self-link',
+
+ // contains all requests in the request cache indexed by UUID
REQUEST = 'get-request/href-to-uuid',
+
+ /**
+ * Contains the UUIDs of requests that were sent to the server and
+ * have their responses cached, indexed by the UUIDs of requests that
+ * weren't sent because the response they requested was already cached
+ */
UUID_MAPPING = 'get-request/configured-to-cache-uuid'
}
-export type IndexState = {
- [name in IndexName]: {
- [key: string]: string
- }
+/**
+ * The state of a single index
+ */
+export interface IndexState {
+ [key: string]: string
+}
+
+/**
+ * The state that contains all indices
+ */
+export type MetaIndexState = {
+ [name in IndexName]: IndexState
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
-const initialState: IndexState = Object.create(null);
+const initialState: MetaIndexState = Object.create(null);
-export function indexReducer(state = initialState, action: IndexAction): IndexState {
+/**
+ * The Index Reducer
+ *
+ * @param state
+ * the current state
+ * @param action
+ * the action to perform on the state
+ * @return MetaIndexState
+ * the new state
+ */
+export function indexReducer(state = initialState, action: IndexAction): MetaIndexState {
switch (action.type) {
case IndexActionTypes.ADD: {
@@ -41,7 +72,17 @@ export function indexReducer(state = initialState, action: IndexAction): IndexSt
}
}
-function addToIndex(state: IndexState, action: AddToIndexAction): IndexState {
+/**
+ * Add an entry to a given index
+ *
+ * @param state
+ * The MetaIndexState that contains all indices
+ * @param action
+ * The AddToIndexAction containing the value to add, and the index to add it to
+ * @return MetaIndexState
+ * the new state
+ */
+function addToIndex(state: MetaIndexState, action: AddToIndexAction): MetaIndexState {
const subState = state[action.payload.name];
const newSubState = Object.assign({}, subState, {
[action.payload.key]: action.payload.value
@@ -52,7 +93,17 @@ function addToIndex(state: IndexState, action: AddToIndexAction): IndexState {
return obs;
}
-function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
+/**
+ * Remove a entries that contain a given value from a given index
+ *
+ * @param state
+ * The MetaIndexState that contains all indices
+ * @param action
+ * The RemoveFromIndexByValueAction containing the value to remove, and the index to remove it from
+ * @return MetaIndexState
+ * the new state
+ */
+function removeFromIndexByValue(state: MetaIndexState, action: RemoveFromIndexByValueAction): MetaIndexState {
const subState = state[action.payload.name];
const newSubState = Object.create(null);
for (const value in subState) {
@@ -66,11 +117,16 @@ function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValu
}
/**
- * Remove values from the IndexState's substate that contain a given substring
- * @param state The IndexState to remove values from
- * @param action The RemoveFromIndexByValueAction containing the necessary information to remove the values
+ * Remove entries that contain a given substring from a given index
+ *
+ * @param state
+ * The MetaIndexState that contains all indices
+ * @param action
+ * The RemoveFromIndexByValueAction the substring to remove, and the index to remove it from
+ * @return MetaIndexState
+ * the new state
*/
-function removeFromIndexBySubstring(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
+function removeFromIndexBySubstring(state: MetaIndexState, action: RemoveFromIndexByValueAction): MetaIndexState {
const subState = state[action.payload.name];
const newSubState = Object.create(null);
for (const value in subState) {
diff --git a/src/app/core/index/index.selectors.ts b/src/app/core/index/index.selectors.ts
new file mode 100644
index 0000000000..3c7b331a92
--- /dev/null
+++ b/src/app/core/index/index.selectors.ts
@@ -0,0 +1,94 @@
+import { createSelector, MemoizedSelector } from '@ngrx/store';
+import { AppState } from '../../app.reducer';
+import { hasValue } from '../../shared/empty.util';
+import { CoreState } from '../core.reducers';
+import { coreSelector } from '../core.selectors';
+import { IndexName, IndexState, MetaIndexState } from './index.reducer';
+
+/**
+ * Return the MetaIndexState based on the CoreSate
+ *
+ * @returns
+ * a MemoizedSelector to select the MetaIndexState
+ */
+export const metaIndexSelector: MemoizedSelector = createSelector(
+ coreSelector,
+ (state: CoreState) => state.index
+);
+
+/**
+ * Return the object index based on the MetaIndexState
+ * It contains all objects in the object cache indexed by UUID
+ *
+ * @returns
+ * a MemoizedSelector to select the object index
+ */
+export const objectIndexSelector: MemoizedSelector = createSelector(
+ metaIndexSelector,
+ (state: MetaIndexState) => state[IndexName.OBJECT]
+);
+
+/**
+ * Return the request index based on the MetaIndexState
+ *
+ * @returns
+ * a MemoizedSelector to select the request index
+ */
+export const requestIndexSelector: MemoizedSelector = createSelector(
+ metaIndexSelector,
+ (state: MetaIndexState) => state[IndexName.REQUEST]
+);
+
+/**
+ * Return the request UUID mapping index based on the MetaIndexState
+ *
+ * @returns
+ * a MemoizedSelector to select the request UUID mapping
+ */
+export const requestUUIDIndexSelector: MemoizedSelector = createSelector(
+ metaIndexSelector,
+ (state: MetaIndexState) => state[IndexName.UUID_MAPPING]
+);
+
+/**
+ * Return the self link of an object in the object-cache based on its UUID
+ *
+ * @param uuid
+ * the UUID for which you want to find the matching self link
+ * @returns
+ * a MemoizedSelector to select the self link
+ */
+export const selfLinkFromUuidSelector =
+ (uuid: string): MemoizedSelector => createSelector(
+ objectIndexSelector,
+ (state: IndexState) => hasValue(state) ? state[uuid] : undefined
+ );
+
+/**
+ * Return the UUID of a GET request based on its href
+ *
+ * @param href
+ * the href of the GET request
+ * @returns
+ * a MemoizedSelector to select the UUID
+ */
+export const uuidFromHrefSelector =
+ (href: string): MemoizedSelector => createSelector(
+ requestIndexSelector,
+ (state: IndexState) => hasValue(state) ? state[href] : undefined
+ );
+
+/**
+ * Return the UUID of a cached request based on the UUID of a request
+ * that wasn't sent because the response was already cached
+ *
+ * @param uuid
+ * The UUID of the new request
+ * @returns
+ * a MemoizedSelector to select the UUID of the cached request
+ */
+export const originalRequestUUIDFromRequestUUIDSelector =
+ (uuid: string): MemoizedSelector => createSelector(
+ requestUUIDIndexSelector,
+ (state: IndexState) => hasValue(state) ? state[uuid] : undefined
+ );
diff --git a/src/app/core/integration/authority.service.ts b/src/app/core/integration/authority.service.ts
index a5fa3a8d09..f0a1759be6 100644
--- a/src/app/core/integration/authority.service.ts
+++ b/src/app/core/integration/authority.service.ts
@@ -3,15 +3,19 @@ import { Injectable } from '@angular/core';
import { RequestService } from '../data/request.service';
import { IntegrationService } from './integration.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@Injectable()
export class AuthorityService extends IntegrationService {
protected linkPath = 'authorities';
- protected browseEndpoint = 'entries';
+ protected entriesEndpoint = 'entries';
+ protected entryValueEndpoint = 'entryValues';
constructor(
protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
protected halService: HALEndpointService) {
super();
}
+
}
diff --git a/src/app/core/integration/integration-object-factory.ts b/src/app/core/integration/integration-object-factory.ts
index 4f69dbd6fe..f66a070fdf 100644
--- a/src/app/core/integration/integration-object-factory.ts
+++ b/src/app/core/integration/integration-object-factory.ts
@@ -1,13 +1,13 @@
import { GenericConstructor } from '../shared/generic-constructor';
import { IntegrationType } from './intergration-type';
-import { AuthorityValueModel } from './models/authority-value.model';
import { IntegrationModel } from './models/integration.model';
+import { NormalizedAuthorityValue } from './models/normalized-authority-value.model';
export class IntegrationObjectFactory {
public static getConstructor(type): GenericConstructor {
switch (type) {
case IntegrationType.Authority: {
- return AuthorityValueModel;
+ return NormalizedAuthorityValue;
}
default: {
return undefined;
diff --git a/src/app/core/integration/integration-response-parsing.service.spec.ts b/src/app/core/integration/integration-response-parsing.service.spec.ts
index baa4343724..4187606265 100644
--- a/src/app/core/integration/integration-response-parsing.service.spec.ts
+++ b/src/app/core/integration/integration-response-parsing.service.spec.ts
@@ -7,7 +7,7 @@ import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { IntegrationResponseParsingService } from './integration-response-parsing.service';
import { IntegrationRequest } from '../data/request.models';
-import { AuthorityValueModel } from './models/authority-value.model';
+import { AuthorityValue } from './models/authority.value';
import { PageInfo } from '../shared/page-info.model';
import { PaginatedList } from '../data/paginated-list';
@@ -35,35 +35,35 @@ describe('IntegrationResponseParsingService', () => {
function initVars() {
pageInfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1, self: 'https://rest.api/integration/authorities/type/entries'});
definitions = new PaginatedList(pageInfo,[
- Object.assign({}, new AuthorityValueModel(), {
+ Object.assign(new AuthorityValue(), {
type: 'authority',
display: 'One',
id: 'One',
otherInformation: undefined,
value: 'One'
}),
- Object.assign({}, new AuthorityValueModel(), {
+ Object.assign(new AuthorityValue(), {
type: 'authority',
display: 'Two',
id: 'Two',
otherInformation: undefined,
value: 'Two'
}),
- Object.assign({}, new AuthorityValueModel(), {
+ Object.assign(new AuthorityValue(), {
type: 'authority',
display: 'Three',
id: 'Three',
otherInformation: undefined,
value: 'Three'
}),
- Object.assign({}, new AuthorityValueModel(), {
+ Object.assign(new AuthorityValue(), {
type: 'authority',
display: 'Four',
id: 'Four',
otherInformation: undefined,
value: 'Four'
}),
- Object.assign({}, new AuthorityValueModel(), {
+ Object.assign(new AuthorityValue(), {
type: 'authority',
display: 'Five',
id: 'Five',
@@ -125,12 +125,14 @@ describe('IntegrationResponseParsingService', () => {
self: { href: 'https://rest.api/integration/authorities/type/entries' }
}
},
- statusCode: '200'
+ statusCode: 200,
+ statusText: 'OK'
};
invalidResponse1 = {
payload: {},
- statusCode: '200'
+ statusCode: 400,
+ statusText: 'Bad Request'
};
invalidResponse2 = {
@@ -183,7 +185,8 @@ describe('IntegrationResponseParsingService', () => {
},
_links: {}
},
- statusCode: '200'
+ statusCode: 500,
+ statusText: 'Internal Server Error'
};
}
beforeEach(() => {
diff --git a/src/app/core/integration/integration-response-parsing.service.ts b/src/app/core/integration/integration-response-parsing.service.ts
index ef278c93de..2d3693cf3d 100644
--- a/src/app/core/integration/integration-response-parsing.service.ts
+++ b/src/app/core/integration/integration-response-parsing.service.ts
@@ -16,12 +16,14 @@ import { GlobalConfig } from '../../../config/global-config.interface';
import { ObjectCacheService } from '../cache/object-cache.service';
import { IntegrationModel } from './models/integration.model';
import { IntegrationType } from './intergration-type';
+import { AuthorityValue } from './models/authority.value';
+import { PaginatedList } from '../data/paginated-list';
@Injectable()
export class IntegrationResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = IntegrationObjectFactory;
- protected toCache = false;
+ protected toCache = true;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
@@ -33,15 +35,26 @@ export class IntegrationResponseParsingService extends BaseResponseParsingServic
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
const dataDefinition = this.process(data.payload, request.uuid);
- return new IntegrationSuccessResponse(dataDefinition, data.statusCode, this.processPageInfo(data.payload.page));
+ return new IntegrationSuccessResponse(this.processResponse(dataDefinition), data.statusCode, data.statusText, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from Integration endpoint'),
- {statusText: data.statusCode}
+ {statusCode: data.statusCode, statusText: data.statusText}
)
);
}
}
+ protected processResponse(data: PaginatedList): any {
+ const returnList = Array.of();
+ data.page.forEach((item, index) => {
+ if (item.type === IntegrationType.Authority) {
+ data.page[index] = Object.assign(new AuthorityValue(), item);
+ }
+ });
+
+ return data;
+ }
+
}
diff --git a/src/app/core/integration/integration.service.spec.ts b/src/app/core/integration/integration.service.spec.ts
index 152d7ab165..02fff950ed 100644
--- a/src/app/core/integration/integration.service.spec.ts
+++ b/src/app/core/integration/integration.service.spec.ts
@@ -8,16 +8,21 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { IntegrationService } from './integration.service';
import { IntegrationSearchOptions } from './models/integration-options.model';
+import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
+import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
const LINK_NAME = 'authorities';
-const BROWSE = 'entries';
+const ENTRIES = 'entries';
+const ENTRY_VALUE = 'entryValue';
class TestService extends IntegrationService {
protected linkPath = LINK_NAME;
- protected browseEndpoint = BROWSE;
+ protected entriesEndpoint = ENTRIES;
+ protected entryValueEndpoint = ENTRY_VALUE;
constructor(
protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
protected halService: HALEndpointService) {
super();
}
@@ -27,28 +32,33 @@ describe('IntegrationService', () => {
let scheduler: TestScheduler;
let service: TestService;
let requestService: RequestService;
+ let rdbService: RemoteDataBuildService;
let halService: any;
let findOptions: IntegrationSearchOptions;
const name = 'type';
const metadata = 'dc.type';
const query = '';
+ const value = 'test';
const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
const integrationEndpoint = 'https://rest.api/integration';
const serviceEndpoint = `${integrationEndpoint}/${LINK_NAME}`;
const entriesEndpoint = `${serviceEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`;
+ const entryValueEndpoint = `${serviceEndpoint}/${name}/entryValue/${value}?metadata=${metadata}`;
findOptions = new IntegrationSearchOptions(uuid, name, metadata);
function initTestService(): TestService {
return new TestService(
requestService,
+ rdbService,
halService
);
}
beforeEach(() => {
requestService = getMockRequestService();
+ rdbService = getMockRemoteDataBuildService();
scheduler = getTestScheduler();
halService = new HALEndpointServiceStub(integrationEndpoint);
findOptions = new IntegrationSearchOptions(uuid, name, metadata, query);
@@ -67,4 +77,20 @@ describe('IntegrationService', () => {
});
});
+ describe('getEntryByValue', () => {
+
+ it('should configure a new IntegrationRequest', () => {
+ findOptions = new IntegrationSearchOptions(
+ null,
+ name,
+ metadata,
+ value);
+
+ const expected = new IntegrationRequest(requestService.generateRequestId(), entryValueEndpoint);
+ scheduler.schedule(() => service.getEntryByValue(findOptions).subscribe());
+ scheduler.flush();
+
+ expect(requestService.configure).toHaveBeenCalledWith(expected);
+ });
+ });
});
diff --git a/src/app/core/integration/integration.service.ts b/src/app/core/integration/integration.service.ts
index 2ace710dc7..5826f4646d 100644
--- a/src/app/core/integration/integration.service.ts
+++ b/src/app/core/integration/integration.service.ts
@@ -7,23 +7,25 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { IntegrationData } from './integration-data';
import { IntegrationSearchOptions } from './models/integration-options.model';
-import { RequestEntry } from '../data/request.reducer';
import { getResponseFromEntry } from '../shared/operators';
export abstract class IntegrationService {
protected request: IntegrationRequest;
protected abstract requestService: RequestService;
protected abstract linkPath: string;
- protected abstract browseEndpoint: string;
+ protected abstract entriesEndpoint: string;
+ protected abstract entryValueEndpoint: string;
protected abstract halService: HALEndpointService;
protected getData(request: GetRequest): Observable {
return this.requestService.getByHref(request.href).pipe(
getResponseFromEntry(),
- mergeMap((response) => {
+ mergeMap((response: IntegrationSuccessResponse) => {
if (response.isSuccessful && isNotEmpty(response)) {
- const dataResponse = response as IntegrationSuccessResponse;
- return observableOf(new IntegrationData(dataResponse.pageInfo, dataResponse.dataDefinition));
+ return observableOf(new IntegrationData(
+ response.pageInfo,
+ (response.dataDefinition) ? response.dataDefinition.page : []
+ ));
} else if (!response.isSuccessful) {
return observableThrowError(new Error(`Couldn't retrieve the integration data`));
}
@@ -32,12 +34,12 @@ export abstract class IntegrationService {
);
}
- protected getIntegrationHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string {
+ protected getEntriesHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string {
let result;
const args = [];
if (hasValue(options.name)) {
- result = `${endpoint}/${options.name}/${this.browseEndpoint}`;
+ result = `${endpoint}/${options.name}/${this.entriesEndpoint}`;
} else {
result = endpoint;
}
@@ -73,9 +75,41 @@ export abstract class IntegrationService {
return result;
}
+ protected getEntryValueHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string {
+ let result;
+ const args = [];
+
+ if (hasValue(options.name) && hasValue(options.query)) {
+ result = `${endpoint}/${options.name}/${this.entryValueEndpoint}/${options.query}`;
+ } else {
+ result = endpoint;
+ }
+
+ if (hasValue(options.metadata)) {
+ args.push(`metadata=${options.metadata}`);
+ }
+
+ if (isNotEmpty(args)) {
+ result = `${result}?${args.join('&')}`;
+ }
+
+ return result;
+ }
+
public getEntriesByName(options: IntegrationSearchOptions): Observable {
return this.halService.getEndpoint(this.linkPath).pipe(
- map((endpoint: string) => this.getIntegrationHref(endpoint, options)),
+ map((endpoint: string) => this.getEntriesHref(endpoint, options)),
+ filter((href: string) => isNotEmpty(href)),
+ distinctUntilChanged(),
+ map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL)),
+ tap((request: GetRequest) => this.requestService.configure(request)),
+ mergeMap((request: GetRequest) => this.getData(request)),
+ distinctUntilChanged());
+ }
+
+ public getEntryByValue(options: IntegrationSearchOptions): Observable {
+ return this.halService.getEndpoint(this.linkPath).pipe(
+ map((endpoint: string) => this.getEntryValueHref(endpoint, options)),
filter((href: string) => isNotEmpty(href)),
distinctUntilChanged(),
map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL)),
diff --git a/src/app/core/integration/models/authority-value.model.ts b/src/app/core/integration/models/authority-value.model.ts
deleted file mode 100644
index e2ef9ce9db..0000000000
--- a/src/app/core/integration/models/authority-value.model.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { IntegrationModel } from './integration.model';
-import { autoserialize } from 'cerialize';
-
-export class AuthorityValueModel extends IntegrationModel {
-
- @autoserialize
- id: string;
-
- @autoserialize
- display: string;
-
- @autoserialize
- value: string;
-
- @autoserialize
- otherInformation: any;
-
- @autoserialize
- language: string;
-}
diff --git a/src/app/core/integration/models/authority.value.ts b/src/app/core/integration/models/authority.value.ts
new file mode 100644
index 0000000000..31cb0a5787
--- /dev/null
+++ b/src/app/core/integration/models/authority.value.ts
@@ -0,0 +1,72 @@
+import { IntegrationModel } from './integration.model';
+import { isNotEmpty } from '../../../shared/empty.util';
+import { PLACEHOLDER_PARENT_METADATA } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
+import { OtherInformation } from '../../../shared/form/builder/models/form-field-metadata-value.model';
+import { MetadataValueInterface } from '../../shared/metadata.models';
+
+/**
+ * Class representing an authority object
+ */
+export class AuthorityValue extends IntegrationModel implements MetadataValueInterface {
+
+ /**
+ * The identifier of this authority
+ */
+ id: string;
+
+ /**
+ * The display value of this authority
+ */
+ display: string;
+
+ /**
+ * The value of this authority
+ */
+ value: string;
+
+ /**
+ * An object containing additional information related to this authority
+ */
+ otherInformation: OtherInformation;
+
+ /**
+ * The language code of this authority value
+ */
+ language: string;
+
+ /**
+ * This method checks if authority has an identifier value
+ *
+ * @return boolean
+ */
+ hasAuthority(): boolean {
+ return isNotEmpty(this.id);
+ }
+
+ /**
+ * This method checks if authority has a value
+ *
+ * @return boolean
+ */
+ hasValue(): boolean {
+ return isNotEmpty(this.value);
+ }
+
+ /**
+ * This method checks if authority has related information object
+ *
+ * @return boolean
+ */
+ hasOtherInformation(): boolean {
+ return isNotEmpty(this.otherInformation);
+ }
+
+ /**
+ * This method checks if authority has a placeholder as value
+ *
+ * @return boolean
+ */
+ hasPlaceholder(): boolean {
+ return this.hasValue() && this.value === PLACEHOLDER_PARENT_METADATA;
+ }
+}
diff --git a/src/app/core/integration/models/confidence-type.ts b/src/app/core/integration/models/confidence-type.ts
new file mode 100644
index 0000000000..3630d02970
--- /dev/null
+++ b/src/app/core/integration/models/confidence-type.ts
@@ -0,0 +1,44 @@
+export enum ConfidenceType {
+ /**
+ * This authority value has been confirmed as accurate by an
+ * interactive user or authoritative policy
+ */
+ CF_ACCEPTED = 600,
+
+ /**
+ * Value is singular and valid but has not been seen and accepted
+ * by a human, so its provenance is uncertain.
+ */
+ CF_UNCERTAIN = 500,
+
+ /**
+ * There are multiple matching authority values of equal validity.
+ */
+ CF_AMBIGUOUS = 400,
+
+ /**
+ * There are no matching answers from the authority.
+ */
+ CF_NOTFOUND = 300,
+
+ /**
+ * The authority encountered an internal failure - this preserves a
+ * record in the metadata of why there is no value.
+ */
+ CF_FAILED = 200,
+
+ /**
+ * The authority recommends this submission be rejected.
+ */
+ CF_REJECTED = 100,
+
+ /**
+ * No reasonable confidence value is available
+ */
+ CF_NOVALUE = 0,
+
+ /**
+ * Value has not been set (DB default).
+ */
+ CF_UNSET = -1
+}
diff --git a/src/app/core/integration/models/integration.model.ts b/src/app/core/integration/models/integration.model.ts
index d3383ab94a..3158abc7eb 100644
--- a/src/app/core/integration/models/integration.model.ts
+++ b/src/app/core/integration/models/integration.model.ts
@@ -1,12 +1,20 @@
import { autoserialize } from 'cerialize';
+import { CacheableObject } from '../../cache/object-cache.reducer';
-export abstract class IntegrationModel {
+export abstract class IntegrationModel implements CacheableObject {
@autoserialize
- public type: string;
+ self: string;
+
+ @autoserialize
+ uuid: string;
+
+ @autoserialize
+ public type: any;
@autoserialize
public _links: {
[name: string]: string
}
+
}
diff --git a/src/app/core/integration/models/normalized-authority-value.model.ts b/src/app/core/integration/models/normalized-authority-value.model.ts
new file mode 100644
index 0000000000..5ebb61281d
--- /dev/null
+++ b/src/app/core/integration/models/normalized-authority-value.model.ts
@@ -0,0 +1,28 @@
+import { autoserialize, inheritSerialization } from 'cerialize';
+import { IntegrationModel } from './integration.model';
+import { mapsTo } from '../../cache/builders/build-decorators';
+import { AuthorityValue } from './authority.value';
+
+/**
+ * Normalized model class for an Authority Value
+ */
+@mapsTo(AuthorityValue)
+@inheritSerialization(IntegrationModel)
+export class NormalizedAuthorityValue extends IntegrationModel {
+
+ @autoserialize
+ id: string;
+
+ @autoserialize
+ display: string;
+
+ @autoserialize
+ value: string;
+
+ @autoserialize
+ otherInformation: any;
+
+ @autoserialize
+ language: string;
+
+}
diff --git a/src/app/core/json-patch/builder/json-patch-operation-path-combiner.ts b/src/app/core/json-patch/builder/json-patch-operation-path-combiner.ts
new file mode 100644
index 0000000000..d29bf993cc
--- /dev/null
+++ b/src/app/core/json-patch/builder/json-patch-operation-path-combiner.ts
@@ -0,0 +1,57 @@
+import { isNotUndefined } from '../../../shared/empty.util';
+import { URLCombiner } from '../../url-combiner/url-combiner';
+
+/**
+ * Interface used to represent a JSON-PATCH path member
+ * in JsonPatchOperationsState
+ */
+export interface JsonPatchOperationPathObject {
+ rootElement: string;
+ subRootElement: string;
+ path: string;
+}
+
+/**
+ * Combines a variable number of strings representing parts
+ * of a JSON-PATCH path
+ */
+export class JsonPatchOperationPathCombiner extends URLCombiner {
+ private _rootElement: string;
+ private _subRootElement: string;
+
+ constructor(rootElement, ...subRootElements: string[]) {
+ super(rootElement, ...subRootElements);
+ this._rootElement = rootElement;
+ this._subRootElement = subRootElements.join('/');
+ }
+
+ get rootElement(): string {
+ return this._rootElement;
+ }
+
+ get subRootElement(): string {
+ return this._subRootElement;
+ }
+
+ /**
+ * Combines the parts of this JsonPatchOperationPathCombiner in to a JSON-PATCH path member
+ *
+ * e.g. new JsonPatchOperationPathCombiner('sections', 'basic').getPath(['dc.title', '0'])
+ * returns: {rootElement: 'sections', subRootElement: 'basic', path: '/sections/basic/dc.title/0'}
+ *
+ * @return {JsonPatchOperationPathObject}
+ * The combined path object
+ */
+ public getPath(fragment?: string|string[]): JsonPatchOperationPathObject {
+ if (isNotUndefined(fragment) && Array.isArray(fragment)) {
+ fragment = fragment.join('/');
+ }
+
+ let path = '/' + this.toString();
+ if (isNotUndefined(fragment)) {
+ path += '/' + fragment;
+ }
+
+ return {rootElement: this._rootElement, subRootElement: this._subRootElement, path: path};
+ }
+}
diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts
new file mode 100644
index 0000000000..c45183b4ef
--- /dev/null
+++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts
@@ -0,0 +1,138 @@
+import { Store } from '@ngrx/store';
+import { CoreState } from '../../core.reducers';
+import {
+ NewPatchAddOperationAction,
+ NewPatchRemoveOperationAction,
+ NewPatchReplaceOperationAction
+} from '../json-patch-operations.actions';
+import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner';
+import { Injectable } from '@angular/core';
+import { isEmpty, isNotEmpty } from '../../../shared/empty.util';
+import { dateToISOFormat } from '../../../shared/date.util';
+import { AuthorityValue } from '../../integration/models/authority.value';
+import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
+import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model';
+
+/**
+ * Provides methods to dispatch JsonPatch Operations Actions
+ */
+@Injectable()
+export class JsonPatchOperationsBuilder {
+
+ constructor(private store: Store) {
+ }
+
+ /**
+ * Dispatches a new NewPatchAddOperationAction
+ *
+ * @param path
+ * a JsonPatchOperationPathObject representing path
+ * @param value
+ * The value to update the referenced path
+ * @param first
+ * A boolean representing if the value to be added is the first of an array
+ * @param plain
+ * A boolean representing if the value to be added is a plain text value
+ */
+ add(path: JsonPatchOperationPathObject, value, first = false, plain = false) {
+ this.store.dispatch(
+ new NewPatchAddOperationAction(
+ path.rootElement,
+ path.subRootElement,
+ path.path, this.prepareValue(value, plain, first)));
+ }
+
+ /**
+ * Dispatches a new NewPatchReplaceOperationAction
+ *
+ * @param path
+ * a JsonPatchOperationPathObject representing path
+ * @param value
+ * the value to update the referenced path
+ * @param plain
+ * a boolean representing if the value to be added is a plain text value
+ */
+ replace(path: JsonPatchOperationPathObject, value, plain = false) {
+ this.store.dispatch(
+ new NewPatchReplaceOperationAction(
+ path.rootElement,
+ path.subRootElement,
+ path.path,
+ this.prepareValue(value, plain, false)));
+ }
+
+ /**
+ * Dispatches a new NewPatchRemoveOperationAction
+ *
+ * @param path
+ * a JsonPatchOperationPathObject representing path
+ */
+ remove(path: JsonPatchOperationPathObject) {
+ this.store.dispatch(
+ new NewPatchRemoveOperationAction(
+ path.rootElement,
+ path.subRootElement,
+ path.path));
+ }
+
+ protected prepareValue(value: any, plain: boolean, first: boolean) {
+ let operationValue: any = null;
+ if (isNotEmpty(value)) {
+ if (plain) {
+ operationValue = value;
+ } else {
+ if (Array.isArray(value)) {
+ operationValue = [];
+ value.forEach((entry) => {
+ if ((typeof entry === 'object')) {
+ operationValue.push(this.prepareObjectValue(entry));
+ } else {
+ operationValue.push(new FormFieldMetadataValueObject(entry));
+ }
+ });
+ } else if (typeof value === 'object') {
+ operationValue = this.prepareObjectValue(value);
+ } else {
+ operationValue = new FormFieldMetadataValueObject(value);
+ }
+ }
+ }
+ return (first && !Array.isArray(operationValue)) ? [operationValue] : operationValue;
+ }
+
+ protected prepareObjectValue(value: any) {
+ let operationValue = Object.create({});
+ if (isEmpty(value) || value instanceof FormFieldMetadataValueObject) {
+ operationValue = value;
+ } else if (value instanceof Date) {
+ operationValue = new FormFieldMetadataValueObject(dateToISOFormat(value));
+ } else if (value instanceof AuthorityValue) {
+ operationValue = this.prepareAuthorityValue(value);
+ } else if (value instanceof FormFieldLanguageValueObject) {
+ operationValue = new FormFieldMetadataValueObject(value.value, value.language);
+ } else if (value.hasOwnProperty('value')) {
+ operationValue = new FormFieldMetadataValueObject(value.value);
+ } else {
+ Object.keys(value)
+ .forEach((key) => {
+ if (typeof value[key] === 'object') {
+ operationValue[key] = this.prepareObjectValue(value[key]);
+ } else {
+ operationValue[key] = value[key];
+ }
+ });
+ }
+ return operationValue;
+ }
+
+ protected prepareAuthorityValue(value: any) {
+ let operationValue: any = null;
+ if (isNotEmpty(value.id)) {
+ operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.id);
+ } else {
+ operationValue = new FormFieldMetadataValueObject(value.value, value.language);
+ }
+ return operationValue;
+ }
+
+}
diff --git a/src/app/core/json-patch/json-patch-operations.actions.ts b/src/app/core/json-patch/json-patch-operations.actions.ts
new file mode 100644
index 0000000000..cb3e3b0d38
--- /dev/null
+++ b/src/app/core/json-patch/json-patch-operations.actions.ts
@@ -0,0 +1,279 @@
+import { Action } from '@ngrx/store';
+
+import { type } from '../../shared/ngrx/type';
+
+/**
+ * For each action type in an action group, make a simple
+ * enum object for all of this group's action types.
+ *
+ * The 'type' utility function coerces strings into string
+ * literal types and runs a simple check to guarantee all
+ * action types in the application are unique.
+ */
+export const JsonPatchOperationsActionTypes = {
+ NEW_JSON_PATCH_ADD_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_ADD_OPERATION'),
+ NEW_JSON_PATCH_COPY_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_COPY_OPERATION'),
+ NEW_JSON_PATCH_MOVE_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_MOVE_OPERATION'),
+ NEW_JSON_PATCH_REMOVE_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_REMOVE_OPERATION'),
+ NEW_JSON_PATCH_REPLACE_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_REPLACE_OPERATION'),
+ COMMIT_JSON_PATCH_OPERATIONS: type('dspace/core/patch/COMMIT_JSON_PATCH_OPERATIONS'),
+ ROLLBACK_JSON_PATCH_OPERATIONS: type('dspace/core/patch/ROLLBACK_JSON_PATCH_OPERATIONS'),
+ FLUSH_JSON_PATCH_OPERATIONS: type('dspace/core/patch/FLUSH_JSON_PATCH_OPERATIONS'),
+ START_TRANSACTION_JSON_PATCH_OPERATIONS: type('dspace/core/patch/START_TRANSACTION_JSON_PATCH_OPERATIONS'),
+};
+
+/* tslint:disable:max-classes-per-file */
+
+/**
+ * An ngrx action to commit the current transaction
+ */
+export class CommitPatchOperationsAction implements Action {
+ type = JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS;
+ payload: {
+ resourceType: string;
+ resourceId: string;
+ };
+
+ /**
+ * Create a new CommitPatchOperationsAction
+ *
+ * @param resourceType
+ * the resource's type
+ * @param resourceId
+ * the resource's ID
+ */
+ constructor(resourceType: string, resourceId: string) {
+ this.payload = { resourceType, resourceId };
+ }
+}
+
+/**
+ * An ngrx action to rollback the current transaction
+ */
+export class RollbacktPatchOperationsAction implements Action {
+ type = JsonPatchOperationsActionTypes.ROLLBACK_JSON_PATCH_OPERATIONS;
+ payload: {
+ resourceType: string;
+ resourceId: string;
+ };
+
+ /**
+ * Create a new CommitPatchOperationsAction
+ *
+ * @param resourceType
+ * the resource's type
+ * @param resourceId
+ * the resource's ID
+ */
+ constructor(resourceType: string, resourceId: string) {
+ this.payload = { resourceType, resourceId };
+ }
+}
+
+/**
+ * An ngrx action to initiate a transaction block
+ */
+export class StartTransactionPatchOperationsAction implements Action {
+ type = JsonPatchOperationsActionTypes.START_TRANSACTION_JSON_PATCH_OPERATIONS;
+ payload: {
+ resourceType: string;
+ resourceId: string;
+ startTime: number;
+ };
+
+ /**
+ * Create a new CommitPatchOperationsAction
+ *
+ * @param resourceType
+ * the resource's type
+ * @param resourceId
+ * the resource's ID
+ * @param startTime
+ * the start timestamp
+ */
+ constructor(resourceType: string, resourceId: string, startTime: number) {
+ this.payload = { resourceType, resourceId, startTime };
+ }
+}
+
+/**
+ * An ngrx action to flush list of the JSON Patch operations
+ */
+export class FlushPatchOperationsAction implements Action {
+ type = JsonPatchOperationsActionTypes.FLUSH_JSON_PATCH_OPERATIONS;
+ payload: {
+ resourceType: string;
+ resourceId: string;
+ };
+
+ /**
+ * Create a new FlushPatchOperationsAction
+ *
+ * @param resourceType
+ * the resource's type
+ * @param resourceId
+ * the resource's ID
+ */
+ constructor(resourceType: string, resourceId: string) {
+ this.payload = { resourceType, resourceId };
+ }
+}
+
+/**
+ * An ngrx action to Add new HTTP/PATCH ADD operations to state
+ */
+export class NewPatchAddOperationAction implements Action {
+ type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION;
+ payload: {
+ resourceType: string;
+ resourceId: string;
+ path: string;
+ value: any
+ };
+
+ /**
+ * Create a new NewPatchAddOperationAction
+ *
+ * @param resourceType
+ * the resource's type where to add operation
+ * @param resourceId
+ * the resource's ID
+ * @param path
+ * the path of the operation
+ * @param value
+ * the operation's payload
+ */
+ constructor(resourceType: string, resourceId: string, path: string, value: any) {
+ this.payload = { resourceType, resourceId, path, value };
+ }
+}
+
+/**
+ * An ngrx action to add new JSON Patch COPY operation to state
+ */
+export class NewPatchCopyOperationAction implements Action {
+ type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_COPY_OPERATION;
+ payload: {
+ resourceType: string;
+ resourceId: string;
+ from: string;
+ path: string;
+ };
+
+ /**
+ * Create a new NewPatchCopyOperationAction
+ *
+ * @param resourceType
+ * the resource's type
+ * @param resourceId
+ * the resource's ID
+ * @param from
+ * the path to copy the value from
+ * @param path
+ * the path where to copy the value
+ */
+ constructor(resourceType: string, resourceId: string, from: string, path: string) {
+ this.payload = { resourceType, resourceId, from, path };
+ }
+}
+
+/**
+ * An ngrx action to Add new JSON Patch MOVE operation to state
+ */
+export class NewPatchMoveOperationAction implements Action {
+ type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION;
+ payload: {
+ resourceType: string;
+ resourceId: string;
+ from: string;
+ path: string;
+ };
+
+ /**
+ * Create a new NewPatchMoveOperationAction
+ *
+ * @param resourceType
+ * the resource's type
+ * @param resourceId
+ * the resource's ID
+ * @param from
+ * the path to move the value from
+ * @param path
+ * the path where to move the value
+ */
+ constructor(resourceType: string, resourceId: string, from: string, path: string) {
+ this.payload = { resourceType, resourceId, from, path };
+ }
+}
+
+/**
+ * An ngrx action to Add new JSON Patch REMOVE operation to state
+ */
+export class NewPatchRemoveOperationAction implements Action {
+ type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION;
+ payload: {
+ resourceType: string;
+ resourceId: string;
+ path: string;
+ };
+
+ /**
+ * Create a new NewPatchRemoveOperationAction
+ *
+ * @param resourceType
+ * the resource's type
+ * @param resourceId
+ * the resource's ID
+ * @param path
+ * the path of the operation
+ */
+ constructor(resourceType: string, resourceId: string, path: string) {
+ this.payload = { resourceType, resourceId, path };
+ }
+}
+
+/**
+ * An ngrx action to add new JSON Patch REPLACE operation to state
+ */
+export class NewPatchReplaceOperationAction implements Action {
+ type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION;
+ payload: {
+ resourceType: string;
+ resourceId: string;
+ path: string;
+ value: any
+ };
+
+ /**
+ * Create a new NewPatchReplaceOperationAction
+ *
+ * @param resourceType
+ * the resource's type
+ * @param resourceId
+ * the resource's ID
+ * @param path
+ * the path of the operation
+ * @param value
+ * the operation's payload
+ */
+ constructor(resourceType: string, resourceId: string, path: string, value: any) {
+ this.payload = { resourceType, resourceId, path, value };
+ }
+}
+
+/* tslint:enable:max-classes-per-file */
+
+/**
+ * Export a type alias of all actions in this action group
+ * so that reducers can easily compose action types
+ */
+export type PatchOperationsActions
+ = CommitPatchOperationsAction
+ | FlushPatchOperationsAction
+ | NewPatchAddOperationAction
+ | NewPatchCopyOperationAction
+ | NewPatchMoveOperationAction
+ | NewPatchRemoveOperationAction
+ | NewPatchReplaceOperationAction
+ | RollbacktPatchOperationsAction
+ | StartTransactionPatchOperationsAction
diff --git a/src/app/core/json-patch/json-patch-operations.effects.spec.ts b/src/app/core/json-patch/json-patch-operations.effects.spec.ts
new file mode 100644
index 0000000000..c0fa12cbf3
--- /dev/null
+++ b/src/app/core/json-patch/json-patch-operations.effects.spec.ts
@@ -0,0 +1,54 @@
+import { TestBed } from '@angular/core/testing';
+
+import { cold, hot } from 'jasmine-marbles';
+import { provideMockActions } from '@ngrx/effects/testing';
+import { Store } from '@ngrx/store';
+import { Observable, of as observableOf } from 'rxjs';
+
+import { JsonPatchOperationsEffects } from './json-patch-operations.effects';
+import { JsonPatchOperationsState } from './json-patch-operations.reducer';
+
+import { FlushPatchOperationsAction, JsonPatchOperationsActionTypes } from './json-patch-operations.actions';
+
+describe('JsonPatchOperationsEffects test suite', () => {
+ let jsonPatchOperationsEffects: JsonPatchOperationsEffects;
+ let actions: Observable;
+ const store: Store = jasmine.createSpyObj('store', {
+ /* tslint:disable:no-empty */
+ dispatch: {},
+ /* tslint:enable:no-empty */
+ select: observableOf(true)
+ });
+ const testJsonPatchResourceType = 'testResourceType';
+ const testJsonPatchResourceId = 'testResourceId';
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ JsonPatchOperationsEffects,
+ {provide: Store, useValue: store},
+ provideMockActions(() => actions),
+ // other providers
+ ],
+ });
+
+ jsonPatchOperationsEffects = TestBed.get(JsonPatchOperationsEffects);
+ });
+
+ describe('commit$', () => {
+ it('should return a FLUSH_JSON_PATCH_OPERATIONS action in response to a COMMIT_JSON_PATCH_OPERATIONS action', () => {
+ actions = hot('--a-', {
+ a: {
+ type: JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS,
+ payload: {resourceType: testJsonPatchResourceType, resourceId: testJsonPatchResourceId}
+ }
+ });
+
+ const expected = cold('--b-', {
+ b: new FlushPatchOperationsAction(testJsonPatchResourceType, testJsonPatchResourceId)
+ });
+
+ expect(jsonPatchOperationsEffects.commit$).toBeObservable(expected);
+ });
+ });
+});
diff --git a/src/app/core/json-patch/json-patch-operations.effects.ts b/src/app/core/json-patch/json-patch-operations.effects.ts
new file mode 100644
index 0000000000..3304db5b9e
--- /dev/null
+++ b/src/app/core/json-patch/json-patch-operations.effects.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+
+import { map } from 'rxjs/operators';
+import { Effect, Actions, ofType } from '@ngrx/effects';
+
+import {
+ CommitPatchOperationsAction, FlushPatchOperationsAction,
+ JsonPatchOperationsActionTypes
+} from './json-patch-operations.actions';
+
+/**
+ * Provides effect methods for jsonPatch Operations actions
+ */
+@Injectable()
+export class JsonPatchOperationsEffects {
+
+ /**
+ * Dispatches a FlushPatchOperationsAction for every dispatched CommitPatchOperationsAction
+ */
+ @Effect() commit$ = this.actions$.pipe(
+ ofType(JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS),
+ map((action: CommitPatchOperationsAction) => {
+ return new FlushPatchOperationsAction(action.payload.resourceType, action.payload.resourceId);
+ }));
+
+ constructor(private actions$: Actions) {}
+
+}
diff --git a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts
new file mode 100644
index 0000000000..c6b21ce037
--- /dev/null
+++ b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts
@@ -0,0 +1,326 @@
+import * as deepFreeze from 'deep-freeze';
+
+import {
+ CommitPatchOperationsAction,
+ FlushPatchOperationsAction,
+ NewPatchAddOperationAction,
+ NewPatchRemoveOperationAction,
+ RollbacktPatchOperationsAction,
+ StartTransactionPatchOperationsAction
+} from './json-patch-operations.actions';
+import {
+ JsonPatchOperationsEntry,
+ jsonPatchOperationsReducer,
+ JsonPatchOperationsResourceEntry,
+ JsonPatchOperationsState
+} from './json-patch-operations.reducer';
+
+class NullAction extends NewPatchAddOperationAction {
+ resourceType: string;
+ resourceId: string;
+ path: string;
+ value: any;
+
+ constructor() {
+ super(null, null, null, null);
+ this.type = null;
+ }
+}
+
+describe('jsonPatchOperationsReducer test suite', () => {
+ const testJsonPatchResourceType = 'testResourceType';
+ const testJsonPatchResourceId = 'testResourceId';
+ const testJsonPatchResourceAnotherId = 'testResourceAnotherId';
+ const testJsonPatchResourcePath = '/testResourceType/testResourceId/testField';
+ const testJsonPatchResourceValue = ['test'];
+ const patchOpBody = [{
+ op: 'add',
+ path: '/testResourceType/testResourceId/testField',
+ value: ['test']
+ }];
+ const timestampBeforeStart = 1545994811991;
+ const timestampAfterStart = 1545994837492;
+ const startTimestamp = 1545994827492;
+ const testState: JsonPatchOperationsState = {
+ testResourceType: {
+ children: {
+ testResourceId: {
+ body: [
+ {
+ operation: {
+ op: 'add',
+ path: '/testResourceType/testResourceId/testField',
+ value: ['test']
+ },
+ timeAdded: timestampBeforeStart
+ },
+ ]
+ } as JsonPatchOperationsEntry
+ },
+ transactionStartTime: null,
+ commitPending: false
+ } as JsonPatchOperationsResourceEntry
+ };
+
+ let initState: JsonPatchOperationsState;
+
+ const anotherTestState: JsonPatchOperationsState = {
+ testResourceType: {
+ children: {
+ testResourceId: {
+ body: [
+ {
+ operation: {
+ op: 'add',
+ path: '/testResourceType/testResourceId/testField',
+ value: ['test']
+ },
+ timeAdded: timestampBeforeStart
+ },
+ {
+ operation: {
+ op: 'remove',
+ path: '/testResourceType/testResourceId/testField'
+ },
+ timeAdded: timestampBeforeStart
+ },
+ ]
+ } as JsonPatchOperationsEntry
+ },
+ transactionStartTime: null,
+ commitPending: false
+ } as JsonPatchOperationsResourceEntry
+ };
+ deepFreeze(testState);
+
+ beforeEach(() => {
+ spyOn(Date.prototype, 'getTime').and.callFake(() => {
+ return timestampBeforeStart;
+ });
+ });
+
+ it('should start with an empty state', () => {
+ const action = new NullAction();
+ const initialState = jsonPatchOperationsReducer(undefined, action);
+
+ expect(initialState).toEqual(Object.create(null));
+ });
+
+ it('should return the current state when no valid actions have been made', () => {
+ const action = new NullAction();
+ const newState = jsonPatchOperationsReducer(testState, action);
+
+ expect(newState).toEqual(testState);
+ });
+
+ describe('When a new patch operation actions have been dispatched', () => {
+
+ it('should return the properly state when it is empty', () => {
+ const action = new NewPatchAddOperationAction(
+ testJsonPatchResourceType,
+ testJsonPatchResourceId,
+ testJsonPatchResourcePath,
+ testJsonPatchResourceValue);
+ const newState = jsonPatchOperationsReducer(undefined, action);
+
+ expect(newState).toEqual(testState);
+ });
+
+ it('should return the properly state when it is not empty', () => {
+ const action = new NewPatchRemoveOperationAction(
+ testJsonPatchResourceType,
+ testJsonPatchResourceId,
+ testJsonPatchResourcePath);
+ const newState = jsonPatchOperationsReducer(testState, action);
+
+ expect(newState).toEqual(anotherTestState);
+ });
+ });
+
+ describe('When StartTransactionPatchOperationsAction has been dispatched', () => {
+ it('should set \'transactionStartTime\' and \'commitPending\' to true', () => {
+ const action = new StartTransactionPatchOperationsAction(
+ testJsonPatchResourceType,
+ testJsonPatchResourceId,
+ startTimestamp);
+ const newState = jsonPatchOperationsReducer(testState, action);
+
+ expect(newState[testJsonPatchResourceType].transactionStartTime).toEqual(startTimestamp);
+ expect(newState[testJsonPatchResourceType].commitPending).toBeTruthy();
+ });
+ });
+
+ describe('When CommitPatchOperationsAction has been dispatched', () => {
+ it('should set \'commitPending\' to false ', () => {
+ const action = new CommitPatchOperationsAction(
+ testJsonPatchResourceType,
+ testJsonPatchResourceId);
+ initState = Object.assign({}, testState, {
+ [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
+ transactionStartTime: startTimestamp,
+ commitPending: true
+ })
+ });
+ const newState = jsonPatchOperationsReducer(initState, action);
+
+ expect(newState[testJsonPatchResourceType].transactionStartTime).toEqual(startTimestamp);
+ expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
+ });
+ });
+
+ describe('When RollbacktPatchOperationsAction has been dispatched', () => {
+ it('should set \'transactionStartTime\' to null and \'commitPending\' to false ', () => {
+ const action = new RollbacktPatchOperationsAction(
+ testJsonPatchResourceType,
+ testJsonPatchResourceId);
+ initState = Object.assign({}, testState, {
+ [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
+ transactionStartTime: startTimestamp,
+ commitPending: true
+ })
+ });
+ const newState = jsonPatchOperationsReducer(initState, action);
+
+ expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull();
+ expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
+ });
+ });
+
+ describe('When FlushPatchOperationsAction has been dispatched', () => {
+
+ it('should flush only committed operations', () => {
+ const action = new FlushPatchOperationsAction(
+ testJsonPatchResourceType,
+ testJsonPatchResourceId);
+ initState = Object.assign({}, testState, {
+ [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
+ children: {
+ testResourceId: {
+ body: [
+ {
+ operation: {
+ op: 'add',
+ path: '/testResourceType/testResourceId/testField',
+ value: ['test']
+ },
+ timeAdded: timestampBeforeStart
+ },
+ {
+ operation: {
+ op: 'remove',
+ path: '/testResourceType/testResourceId/testField'
+ },
+ timeAdded: timestampAfterStart
+ },
+ ]
+ } as JsonPatchOperationsEntry
+ },
+ transactionStartTime: startTimestamp,
+ commitPending: false
+ })
+ });
+ const newState = jsonPatchOperationsReducer(initState, action);
+ const expectedBody: any = [
+ {
+ operation: {
+ op: 'remove',
+ path: '/testResourceType/testResourceId/testField'
+ },
+ timeAdded: timestampAfterStart
+ },
+ ];
+ expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull();
+ expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
+ expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual(expectedBody);
+ });
+
+ beforeEach(() => {
+ initState = Object.assign({}, testState, {
+ [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
+ children: {
+ testResourceId: {
+ body: [
+ {
+ operation: {
+ op: 'add',
+ path: '/testResourceType/testResourceId/testField',
+ value: ['test']
+ },
+ timeAdded: timestampBeforeStart
+ },
+ {
+ operation: {
+ op: 'remove',
+ path: '/testResourceType/testResourceId/testField'
+ },
+ timeAdded: timestampBeforeStart
+ },
+ ]
+ } as JsonPatchOperationsEntry,
+ testResourceAnotherId: {
+ body: [
+ {
+ operation: {
+ op: 'add',
+ path: '/testResourceType/testResourceAnotherId/testField',
+ value: ['test']
+ },
+ timeAdded: timestampBeforeStart
+ },
+ {
+ operation: {
+ op: 'remove',
+ path: '/testResourceType/testResourceAnotherId/testField'
+ },
+ timeAdded: timestampBeforeStart
+ },
+ ]
+ } as JsonPatchOperationsEntry
+ },
+ transactionStartTime: startTimestamp,
+ commitPending: false
+ })
+ });
+ });
+
+ it('should flush committed operations for specified resource id', () => {
+ const action = new FlushPatchOperationsAction(
+ testJsonPatchResourceType,
+ testJsonPatchResourceId);
+ const newState = jsonPatchOperationsReducer(initState, action);
+ const expectedBody: any = [
+ {
+ operation: {
+ op: 'add',
+ path: '/testResourceType/testResourceAnotherId/testField',
+ value: ['test']
+ },
+ timeAdded: timestampBeforeStart
+ },
+ {
+ operation: {
+ op: 'remove',
+ path: '/testResourceType/testResourceAnotherId/testField'
+ },
+ timeAdded: timestampBeforeStart
+ },
+ ];
+ expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull();
+ expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
+ expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual([]);
+ expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceAnotherId].body).toEqual(expectedBody);
+ });
+
+ it('should flush operation list', () => {
+ const action = new FlushPatchOperationsAction(testJsonPatchResourceType, undefined);
+ const newState = jsonPatchOperationsReducer(initState, action);
+
+ expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull();
+ expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy();
+ expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual([]);
+ expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceAnotherId].body).toEqual([]);
+ });
+
+ });
+
+});
diff --git a/src/app/core/json-patch/json-patch-operations.reducer.ts b/src/app/core/json-patch/json-patch-operations.reducer.ts
new file mode 100644
index 0000000000..906d5e0331
--- /dev/null
+++ b/src/app/core/json-patch/json-patch-operations.reducer.ts
@@ -0,0 +1,322 @@
+import { hasValue, isNotEmpty, isNotUndefined, isNull } from '../../shared/empty.util';
+
+import {
+ FlushPatchOperationsAction,
+ PatchOperationsActions,
+ JsonPatchOperationsActionTypes,
+ NewPatchAddOperationAction,
+ NewPatchCopyOperationAction,
+ NewPatchMoveOperationAction,
+ NewPatchRemoveOperationAction,
+ NewPatchReplaceOperationAction,
+ CommitPatchOperationsAction,
+ StartTransactionPatchOperationsAction,
+ RollbacktPatchOperationsAction
+} from './json-patch-operations.actions';
+import { JsonPatchOperationModel, JsonPatchOperationType } from './json-patch.model';
+
+/**
+ * An interface to represent JSON-PATCH Operation objects to execute
+ */
+export interface JsonPatchOperationObject {
+ operation: JsonPatchOperationModel;
+ timeAdded: number;
+}
+
+/**
+ * An interface to represent the body containing a list of JsonPatchOperationObject
+ */
+export interface JsonPatchOperationsEntry {
+ body: JsonPatchOperationObject[];
+}
+
+/**
+ * Interface used to represent a JSON-PATCH path member
+ * in JsonPatchOperationsState
+ */
+export interface JsonPatchOperationsResourceEntry {
+ children: { [resourceId: string]: JsonPatchOperationsEntry };
+ transactionStartTime: number;
+ commitPending: boolean;
+}
+
+/**
+ * The JSON patch operations State
+ *
+ * Consists of a map with a namespace as key,
+ * and an array of JsonPatchOperationModel as values
+ */
+export interface JsonPatchOperationsState {
+ [resourceType: string]: JsonPatchOperationsResourceEntry;
+}
+
+const initialState: JsonPatchOperationsState = Object.create(null);
+
+/**
+ * The JSON-PATCH operations Reducer
+ *
+ * @param state
+ * the current state
+ * @param action
+ * the action to perform on the state
+ * @return JsonPatchOperationsState
+ * the new state
+ */
+export function jsonPatchOperationsReducer(state = initialState, action: PatchOperationsActions): JsonPatchOperationsState {
+ switch (action.type) {
+
+ case JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS: {
+ return commitOperations(state, action as CommitPatchOperationsAction);
+ }
+
+ case JsonPatchOperationsActionTypes.FLUSH_JSON_PATCH_OPERATIONS: {
+ return flushOperation(state, action as FlushPatchOperationsAction);
+ }
+
+ case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION: {
+ return newOperation(state, action as NewPatchAddOperationAction);
+ }
+
+ case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_COPY_OPERATION: {
+ return newOperation(state, action as NewPatchCopyOperationAction);
+ }
+
+ case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION: {
+ return newOperation(state, action as NewPatchMoveOperationAction);
+ }
+
+ case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION: {
+ return newOperation(state, action as NewPatchRemoveOperationAction);
+ }
+
+ case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION: {
+ return newOperation(state, action as NewPatchReplaceOperationAction);
+ }
+
+ case JsonPatchOperationsActionTypes.ROLLBACK_JSON_PATCH_OPERATIONS: {
+ return rollbackOperations(state, action as RollbacktPatchOperationsAction);
+ }
+
+ case JsonPatchOperationsActionTypes.START_TRANSACTION_JSON_PATCH_OPERATIONS: {
+ return startTransactionPatchOperations(state, action as StartTransactionPatchOperationsAction);
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
+
+/**
+ * Set the transaction start time.
+ *
+ * @param state
+ * the current state
+ * @param action
+ * an StartTransactionPatchOperationsAction
+ * @return JsonPatchOperationsState
+ * the new state.
+ */
+function startTransactionPatchOperations(state: JsonPatchOperationsState, action: StartTransactionPatchOperationsAction): JsonPatchOperationsState {
+ if (hasValue(state[ action.payload.resourceType ])
+ && isNull(state[ action.payload.resourceType ].transactionStartTime)) {
+ return Object.assign({}, state, {
+ [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
+ transactionStartTime: action.payload.startTime,
+ commitPending: true
+ })
+ });
+ } else {
+ return state;
+ }
+}
+
+/**
+ * Set commit pending state.
+ *
+ * @param state
+ * the current state
+ * @param action
+ * an CommitPatchOperationsAction
+ * @return JsonPatchOperationsState
+ * the new state, with the section new validity status.
+ */
+function commitOperations(state: JsonPatchOperationsState, action: CommitPatchOperationsAction): JsonPatchOperationsState {
+ if (hasValue(state[ action.payload.resourceType ])
+ && state[ action.payload.resourceType ].commitPending) {
+ return Object.assign({}, state, {
+ [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
+ commitPending: false
+ })
+ });
+ } else {
+ return state;
+ }
+}
+
+/**
+ * Set commit pending state.
+ *
+ * @param state
+ * the current state
+ * @param action
+ * an RollbacktPatchOperationsAction
+ * @return JsonPatchOperationsState
+ * the new state.
+ */
+function rollbackOperations(state: JsonPatchOperationsState, action: RollbacktPatchOperationsAction): JsonPatchOperationsState {
+ if (hasValue(state[ action.payload.resourceType ])
+ && state[ action.payload.resourceType ].commitPending) {
+ return Object.assign({}, state, {
+ [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
+ transactionStartTime: null,
+ commitPending: false
+ })
+ });
+ } else {
+ return state;
+ }
+}
+
+/**
+ * Add new JSON patch operation list.
+ *
+ * @param state
+ * the current state
+ * @param action
+ * an NewPatchAddOperationAction
+ * @return JsonPatchOperationsState
+ * the new state, with the section new validity status.
+ */
+function newOperation(state: JsonPatchOperationsState, action): JsonPatchOperationsState {
+ const newState = Object.assign({}, state);
+ const body: any[] = hasValidBody(newState, action.payload.resourceType, action.payload.resourceId)
+ ? newState[ action.payload.resourceType ].children[ action.payload.resourceId ].body : Array.of();
+ const newBody = addOperationToList(
+ body,
+ action.type,
+ action.payload.path,
+ hasValue(action.payload.value) ? action.payload.value : null);
+
+ if (hasValue(newState[ action.payload.resourceType ])
+ && hasValue(newState[ action.payload.resourceType ].children)) {
+ return Object.assign({}, state, {
+ [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
+ children: Object.assign({}, state[ action.payload.resourceType ].children, {
+ [action.payload.resourceId]: {
+ body: newBody,
+ }
+ }),
+ commitPending: isNotUndefined(state[ action.payload.resourceType ].commitPending) ? state[ action.payload.resourceType ].commitPending : false
+ })
+ });
+ } else {
+ return Object.assign({}, state, {
+ [action.payload.resourceType]: Object.assign({}, {
+ children: {
+ [action.payload.resourceId]: {
+ body: newBody,
+ }
+ },
+ transactionStartTime: null,
+ commitPending: false
+ })
+ });
+ }
+}
+
+/**
+ * Check if state has a valid body.
+ *
+ * @param state
+ * the current state
+ * @param resourceType
+ * an resource type
+ * @param resourceId
+ * an resource ID
+ * @return boolean
+ */
+function hasValidBody(state: JsonPatchOperationsState, resourceType: any, resourceId: any): boolean {
+ return (hasValue(state[ resourceType ])
+ && hasValue(state[ resourceType ].children)
+ && hasValue(state[ resourceType ].children[ resourceId ])
+ && isNotEmpty(state[ resourceType ].children[ resourceId ].body))
+}
+
+/**
+ * Set the section validity.
+ *
+ * @param state
+ * the current state
+ * @param action
+ * an FlushPatchOperationsAction
+ * @return SubmissionObjectState
+ * the new state, with the section new validity status.
+ */
+function flushOperation(state: JsonPatchOperationsState, action: FlushPatchOperationsAction): JsonPatchOperationsState {
+ if (hasValue(state[ action.payload.resourceType ])) {
+ let newChildren;
+ if (isNotUndefined(action.payload.resourceId)) {
+ // flush only specified child's operations
+ if (hasValue(state[ action.payload.resourceType ].children)
+ && hasValue(state[ action.payload.resourceType ].children[ action.payload.resourceId ])) {
+ newChildren = Object.assign({}, state[ action.payload.resourceType ].children, {
+ [action.payload.resourceId]: {
+ body: state[ action.payload.resourceType ].children[ action.payload.resourceId ].body
+ .filter((entry) => entry.timeAdded > state[ action.payload.resourceType ].transactionStartTime)
+ }
+ });
+ } else {
+ newChildren = state[ action.payload.resourceType ].children;
+ }
+ } else {
+ // flush all children's operations
+ newChildren = state[ action.payload.resourceType ].children;
+ Object.keys(newChildren)
+ .forEach((resourceId) => {
+ newChildren = Object.assign({}, newChildren, {
+ [resourceId]: {
+ body: newChildren[ resourceId ].body
+ .filter((entry) => entry.timeAdded > state[ action.payload.resourceType ].transactionStartTime)
+ }
+ });
+ })
+ }
+ return Object.assign({}, state, {
+ [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
+ children: newChildren,
+ transactionStartTime: null,
+ })
+ });
+ } else {
+ return state;
+ }
+}
+
+function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?) {
+ const newBody = Array.from(body);
+ switch (actionType) {
+ case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION:
+ newBody.push(makeOperationEntry({
+ op: JsonPatchOperationType.add,
+ path: targetPath,
+ value: value
+ }));
+ break;
+ case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION:
+ newBody.push(makeOperationEntry({
+ op: JsonPatchOperationType.replace,
+ path: targetPath,
+ value: value
+ }));
+ break;
+ case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION:
+ newBody.push(makeOperationEntry({ op: JsonPatchOperationType.remove, path: targetPath }));
+ break;
+ }
+ return newBody;
+}
+
+function makeOperationEntry(operation) {
+ return { operation: operation, timeAdded: new Date().getTime() };
+}
diff --git a/src/app/core/json-patch/json-patch-operations.service.spec.ts b/src/app/core/json-patch/json-patch-operations.service.spec.ts
new file mode 100644
index 0000000000..4ecc215dc7
--- /dev/null
+++ b/src/app/core/json-patch/json-patch-operations.service.spec.ts
@@ -0,0 +1,253 @@
+import { async, TestBed } from '@angular/core/testing';
+
+import { getTestScheduler } from 'jasmine-marbles';
+import { TestScheduler } from 'rxjs/testing';
+import { of as observableOf } from 'rxjs';
+import { Store, StoreModule } from '@ngrx/store';
+
+import { getMockRequestService } from '../../shared/mocks/mock-request.service';
+import { RequestService } from '../data/request.service';
+import { SubmissionPatchRequest } from '../data/request.models';
+import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
+import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
+import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
+import { JsonPatchOperationsService } from './json-patch-operations.service';
+import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model';
+import { CoreState } from '../core.reducers';
+import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
+import {
+ CommitPatchOperationsAction,
+ RollbacktPatchOperationsAction,
+ StartTransactionPatchOperationsAction
+} from './json-patch-operations.actions';
+import { MockStore } from '../../shared/testing/mock-store';
+import { RequestEntry } from '../data/request.reducer';
+import { catchError } from 'rxjs/operators';
+
+class TestService extends JsonPatchOperationsService {
+ protected linkPath = '';
+ protected patchRequestConstructor = SubmissionPatchRequest;
+
+ constructor(
+ protected requestService: RequestService,
+ protected store: Store,
+ protected halService: HALEndpointService) {
+
+ super();
+ }
+}
+
+describe('JsonPatchOperationsService test suite', () => {
+ let scheduler: TestScheduler;
+ let service: TestService;
+ let requestService: RequestService;
+ let rdbService: RemoteDataBuildService;
+ let halService: any;
+ let store: any;
+
+ const timestamp = 1545994811991;
+ const timestampResponse = 1545994811992;
+ const mockState = {
+ 'json/patch': {
+ testResourceType: {
+ children: {
+ testResourceId: {
+ body: [
+ {
+ operation: {
+ op: 'add',
+ path: '/testResourceType/testResourceId/testField',
+ value: ['test']
+ },
+ timeAdded: timestamp
+ },
+ ]
+ } as JsonPatchOperationsEntry
+ },
+ transactionStartTime: null,
+ commitPending: false
+ } as JsonPatchOperationsResourceEntry
+ }
+ };
+ const resourceEndpointURL = 'https://rest.api/endpoint';
+ const resourceEndpoint = 'resource';
+ const resourceScope = '260';
+ const resourceHref = resourceEndpointURL + '/' + resourceEndpoint + '/' + resourceScope;
+
+ const testJsonPatchResourceType = 'testResourceType';
+ const testJsonPatchResourceId = 'testResourceId';
+ const patchOpBody = [{
+ op: 'add',
+ path: '/testResourceType/testResourceId/testField',
+ value: ['test']
+ }];
+
+ const getRequestEntry$ = (successful: boolean) => {
+ return observableOf({
+ response: { isSuccessful: successful, timeAdded: timestampResponse } as any
+ } as RequestEntry)
+ };
+
+ function initTestService(): TestService {
+ return new TestService(
+ requestService,
+ store,
+ halService
+ );
+
+ }
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ StoreModule.forRoot({}),
+ ],
+ providers: [
+ { provide: Store, useClass: MockStore }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ store = TestBed.get(Store);
+ requestService = getMockRequestService(getRequestEntry$(true));
+ rdbService = getMockRemoteDataBuildService();
+ scheduler = getTestScheduler();
+ halService = new HALEndpointServiceStub(resourceEndpointURL);
+ service = initTestService();
+
+ spyOn(store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType]));
+ spyOn(store, 'dispatch').and.callThrough();
+ spyOn(Date.prototype, 'getTime').and.callFake(() => {
+ return timestamp;
+ });
+ });
+
+ describe('jsonPatchByResourceType', () => {
+
+ it('should call submitJsonPatchOperations method', () => {
+ spyOn((service as any), 'submitJsonPatchOperations').and.callThrough();
+
+ scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpointURL, resourceScope, testJsonPatchResourceType).subscribe());
+ scheduler.flush();
+
+ expect((service as any).submitJsonPatchOperations).toHaveBeenCalled();
+ });
+
+ it('should configure a new SubmissionPatchRequest', () => {
+ const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, patchOpBody);
+ scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe());
+ scheduler.flush();
+
+ expect(requestService.configure).toHaveBeenCalledWith(expected);
+ });
+
+ it('should dispatch a new StartTransactionPatchOperationsAction', () => {
+ const expectedAction = new StartTransactionPatchOperationsAction(testJsonPatchResourceType, undefined, timestamp);
+ scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe());
+ scheduler.flush();
+
+ expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
+ });
+
+ describe('when request is successful', () => {
+ it('should dispatch a new CommitPatchOperationsAction', () => {
+ const expectedAction = new CommitPatchOperationsAction(testJsonPatchResourceType, undefined);
+ scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe());
+ scheduler.flush();
+
+ expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
+ });
+ });
+
+ describe('when request is not successful', () => {
+ beforeEach(() => {
+ store = TestBed.get(Store);
+ requestService = getMockRequestService(getRequestEntry$(false));
+ rdbService = getMockRemoteDataBuildService();
+ scheduler = getTestScheduler();
+ halService = new HALEndpointServiceStub(resourceEndpointURL);
+ service = initTestService();
+
+ store.select.and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType]));
+ store.dispatch.and.callThrough();
+ });
+
+ it('should dispatch a new RollbacktPatchOperationsAction', () => {
+
+ const expectedAction = new RollbacktPatchOperationsAction(testJsonPatchResourceType, undefined);
+ scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType)
+ .pipe(catchError(() => observableOf({})))
+ .subscribe());
+ scheduler.flush();
+
+ expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
+ });
+ });
+ });
+
+ describe('jsonPatchByResourceID', () => {
+
+ it('should call submitJsonPatchOperations method', () => {
+ spyOn((service as any), 'submitJsonPatchOperations').and.callThrough();
+
+ scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpointURL, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe());
+ scheduler.flush();
+
+ expect((service as any).submitJsonPatchOperations).toHaveBeenCalled();
+ });
+
+ it('should configure a new SubmissionPatchRequest', () => {
+ const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, patchOpBody);
+ scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe());
+ scheduler.flush();
+
+ expect(requestService.configure).toHaveBeenCalledWith(expected);
+ });
+
+ it('should dispatch a new StartTransactionPatchOperationsAction', () => {
+ const expectedAction = new StartTransactionPatchOperationsAction(testJsonPatchResourceType, testJsonPatchResourceId, timestamp);
+ scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe());
+ scheduler.flush();
+
+ expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
+ });
+
+ describe('when request is successful', () => {
+ it('should dispatch a new CommitPatchOperationsAction', () => {
+ const expectedAction = new CommitPatchOperationsAction(testJsonPatchResourceType, testJsonPatchResourceId);
+ scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe());
+ scheduler.flush();
+
+ expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
+ });
+ });
+
+ describe('when request is not successful', () => {
+ beforeEach(() => {
+ store = TestBed.get(Store);
+ requestService = getMockRequestService(getRequestEntry$(false));
+ rdbService = getMockRemoteDataBuildService();
+ scheduler = getTestScheduler();
+ halService = new HALEndpointServiceStub(resourceEndpointURL);
+ service = initTestService();
+
+ store.select.and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType]));
+ store.dispatch.and.callThrough();
+ });
+
+ it('should dispatch a new RollbacktPatchOperationsAction', () => {
+
+ const expectedAction = new RollbacktPatchOperationsAction(testJsonPatchResourceType, testJsonPatchResourceId);
+ scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId)
+ .pipe(catchError(() => observableOf({})))
+ .subscribe());
+ scheduler.flush();
+
+ expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
+ });
+ });
+ });
+
+});
diff --git a/src/app/core/json-patch/json-patch-operations.service.ts b/src/app/core/json-patch/json-patch-operations.service.ts
new file mode 100644
index 0000000000..90eaf87a0e
--- /dev/null
+++ b/src/app/core/json-patch/json-patch-operations.service.ts
@@ -0,0 +1,170 @@
+import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
+import { distinctUntilChanged, filter, find, flatMap, map, partition, take, tap } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { hasValue, isEmpty, isNotEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util';
+import { ErrorResponse, PostPatchSuccessResponse, RestResponse } from '../cache/response.models';
+import { PatchRequest } from '../data/request.models';
+import { RequestService } from '../data/request.service';
+import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { CoreState } from '../core.reducers';
+import { jsonPatchOperationsByResourceType } from './selectors';
+import { JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
+import {
+ CommitPatchOperationsAction,
+ RollbacktPatchOperationsAction,
+ StartTransactionPatchOperationsAction
+} from './json-patch-operations.actions';
+import { JsonPatchOperationModel } from './json-patch.model';
+import { getResponseFromEntry } from '../shared/operators';
+import { ObjectCacheEntry } from '../cache/object-cache.reducer';
+
+/**
+ * An abstract class that provides methods to make JSON Patch requests.
+ */
+export abstract class JsonPatchOperationsService {
+
+ protected abstract requestService: RequestService;
+ protected abstract store: Store;
+ protected abstract linkPath: string;
+ protected abstract halService: HALEndpointService;
+ protected abstract patchRequestConstructor: any;
+
+ /**
+ * Submit a new JSON Patch request with all operations stored in the state that are ready to be dispatched
+ *
+ * @param hrefObs
+ * Observable of request href
+ * @param resourceType
+ * The resource type value
+ * @param resourceId
+ * The resource id value
+ * @return Observable
+ * observable of response
+ */
+ protected submitJsonPatchOperations(hrefObs: Observable, resourceType: string, resourceId?: string): Observable {
+ const requestId = this.requestService.generateRequestId();
+ let startTransactionTime = null;
+ const [patchRequest$, emptyRequest$] = partition((request: PatchRequestDefinition) => isNotEmpty(request.body))(hrefObs.pipe(
+ flatMap((endpointURL: string) => {
+ return this.store.select(jsonPatchOperationsByResourceType(resourceType)).pipe(
+ take(1),
+ filter((operationsList: JsonPatchOperationsResourceEntry) => isUndefined(operationsList) || !(operationsList.commitPending)),
+ tap(() => startTransactionTime = new Date().getTime()),
+ map((operationsList: JsonPatchOperationsResourceEntry) => {
+ const body: JsonPatchOperationModel[] = [];
+ if (isNotEmpty(operationsList)) {
+ if (isNotEmpty(resourceId)) {
+ if (isNotUndefined(operationsList.children[resourceId]) && isNotEmpty(operationsList.children[resourceId].body)) {
+ operationsList.children[resourceId].body.forEach((entry) => {
+ body.push(entry.operation);
+ });
+ }
+ } else {
+ Object.keys(operationsList.children)
+ .filter((key) => operationsList.children.hasOwnProperty(key))
+ .filter((key) => hasValue(operationsList.children[key]))
+ .filter((key) => hasValue(operationsList.children[key].body))
+ .forEach((key) => {
+ operationsList.children[key].body.forEach((entry) => {
+ body.push(entry.operation);
+ });
+ })
+ }
+ }
+ return this.getRequestInstance(requestId, endpointURL, body);
+ }));
+ })));
+
+ return observableMerge(
+ emptyRequest$.pipe(
+ filter((request: PatchRequestDefinition) => isEmpty(request.body)),
+ tap(() => startTransactionTime = null),
+ map(() => null)),
+ patchRequest$.pipe(
+ filter((request: PatchRequestDefinition) => isNotEmpty(request.body)),
+ tap(() => this.store.dispatch(new StartTransactionPatchOperationsAction(resourceType, resourceId, startTransactionTime))),
+ tap((request: PatchRequestDefinition) => this.requestService.configure(request)),
+ flatMap(() => {
+ const [successResponse$, errorResponse$] = partition((response: RestResponse) => response.isSuccessful)(this.requestService.getByUUID(requestId).pipe(
+ getResponseFromEntry(),
+ find((entry: ObjectCacheEntry) => startTransactionTime < entry.timeAdded),
+ map((entry: ObjectCacheEntry) => entry),
+ ));
+ return observableMerge(
+ errorResponse$.pipe(
+ tap(() => this.store.dispatch(new RollbacktPatchOperationsAction(resourceType, resourceId))),
+ flatMap((error: ErrorResponse) => observableThrowError(error))),
+ successResponse$.pipe(
+ filter((response: PostPatchSuccessResponse) => isNotEmpty(response)),
+ tap(() => this.store.dispatch(new CommitPatchOperationsAction(resourceType, resourceId))),
+ map((response: PostPatchSuccessResponse) => response.dataDefinition),
+ distinctUntilChanged()));
+ }))
+ );
+ }
+
+ /**
+ * Return an instance for RestRequest class
+ *
+ * @param uuid
+ * The request uuid
+ * @param href
+ * The request href
+ * @param body
+ * The request body
+ * @return Object
+ * instance of PatchRequestDefinition
+ */
+ protected getRequestInstance(uuid: string, href: string, body?: any): PatchRequestDefinition {
+ return new this.patchRequestConstructor(uuid, href, body);
+ }
+
+ protected getEndpointByIDHref(endpoint, resourceID): string {
+ return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`;
+ }
+
+ /**
+ * Make a new JSON Patch request with all operations related to the specified resource type
+ *
+ * @param linkPath
+ * The link path of the request
+ * @param scopeId
+ * The scope id
+ * @param resourceType
+ * The resource type value
+ * @return Observable
+ * observable of response
+ */
+ public jsonPatchByResourceType(linkPath: string, scopeId: string, resourceType: string): Observable {
+ const href$ = this.halService.getEndpoint(linkPath).pipe(
+ filter((href: string) => isNotEmpty(href)),
+ distinctUntilChanged(),
+ map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)));
+
+ return this.submitJsonPatchOperations(href$, resourceType);
+ }
+
+ /**
+ * Make a new JSON Patch request with all operations related to the specified resource id
+ *
+ * @param linkPath
+ * The link path of the request
+ * @param scopeId
+ * The scope id
+ * @param resourceType
+ * The resource type value
+ * @param resourceId
+ * The resource id value
+ * @return Observable
+ * observable of response
+ */
+ public jsonPatchByResourceID(linkPath: string, scopeId: string, resourceType: string, resourceId: string): Observable {
+ const hrefObs = this.halService.getEndpoint(linkPath).pipe(
+ filter((href: string) => isNotEmpty(href)),
+ distinctUntilChanged(),
+ map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)));
+
+ return this.submitJsonPatchOperations(hrefObs, resourceType, resourceId);
+ }
+}
diff --git a/src/app/core/json-patch/json-patch.model.ts b/src/app/core/json-patch/json-patch.model.ts
new file mode 100644
index 0000000000..f855333fab
--- /dev/null
+++ b/src/app/core/json-patch/json-patch.model.ts
@@ -0,0 +1,20 @@
+/**
+ * Represents all JSON Patch operations type.
+ */
+export enum JsonPatchOperationType {
+ test = 'test',
+ remove = 'remove',
+ add = 'add',
+ replace = 'replace',
+ move = 'move',
+ copy = 'copy',
+}
+
+/**
+ * Represents a JSON Patch operations.
+ */
+export class JsonPatchOperationModel {
+ op: JsonPatchOperationType;
+ path: string;
+ value: any;
+}
diff --git a/src/app/core/json-patch/selectors.ts b/src/app/core/json-patch/selectors.ts
new file mode 100644
index 0000000000..1ccde294de
--- /dev/null
+++ b/src/app/core/json-patch/selectors.ts
@@ -0,0 +1,32 @@
+import { MemoizedSelector } from '@ngrx/store';
+import { CoreState } from '../core.reducers';
+import { coreSelector } from '../core.selectors';
+import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
+import { keySelector, subStateSelector } from '../../submission/selectors';
+
+/**
+ * Return MemoizedSelector to select all jsonPatchOperations for a specified resource type, stored in the state
+ *
+ * @param resourceType
+ * the resource type
+ * @return MemoizedSelector
+ * MemoizedSelector
+ */
+export function jsonPatchOperationsByResourceType(resourceType: string): MemoizedSelector {
+ return keySelector(coreSelector,'json/patch', resourceType);
+}
+
+/**
+ * Return MemoizedSelector to select all jsonPatchOperations for a specified resource id, stored in the state
+ *
+ * @param resourceType
+ * the resource type
+ * @param resourceId
+ * the resourceId type
+ * @return MemoizedSelector
+ * MemoizedSelector
+ */
+export function jsonPatchOperationsByResourceId(resourceType: string, resourceId: string): MemoizedSelector {
+ const resourceTypeSelector = jsonPatchOperationsByResourceType(resourceType);
+ return subStateSelector(resourceTypeSelector, resourceId);
+}
diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts
index bd3532b840..cfb5a0751d 100644
--- a/src/app/core/metadata/metadata.service.spec.ts
+++ b/src/app/core/metadata/metadata.service.spec.ts
@@ -116,7 +116,7 @@ describe('MetadataService', () => {
{ provide: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
- { provide: HALEndpointService, useValue: {}},
+ { provide: HALEndpointService, useValue: {} },
{ provide: AuthService, useValue: {} },
{ provide: NotificationsService, useValue: {} },
{ provide: HttpClient, useValue: {} },
@@ -180,7 +180,7 @@ describe('MetadataService', () => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(MockItem));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick();
- expect(tagStore.size).toBeGreaterThan(0)
+ expect(tagStore.size).toBeGreaterThan(0);
router.navigate(['/other']);
tick();
expect(tagStore.size).toEqual(2);
@@ -213,13 +213,13 @@ describe('MetadataService', () => {
undefined,
MockItem
));
- }
+ };
const mockType = (mockItem: Item, type: string): Item => {
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
typedMockItem.metadata['dc.type'] = [ { value: type } ] as MetadataValue[];
return typedMockItem;
- }
+ };
const mockPublisher = (mockItem: Item): Item => {
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts
index 64de242eaa..8274ceef60 100644
--- a/src/app/core/registry/registry.service.spec.ts
+++ b/src/app/core/registry/registry.service.spec.ts
@@ -5,7 +5,7 @@ import { RequestService } from '../data/request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
-import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs';
+import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { RequestEntry } from '../data/request.reducer';
import { RemoteData } from '../data/remote-data';
import { PageInfo } from '../shared/page-info.model';
@@ -14,26 +14,29 @@ import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import {
RegistryBitstreamformatsSuccessResponse,
RegistryMetadatafieldsSuccessResponse,
- RegistryMetadataschemasSuccessResponse, RestResponse
+ RegistryMetadataschemasSuccessResponse,
+ RestResponse
} from '../cache/response.models';
import { Component } from '@angular/core';
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
import { map } from 'rxjs/operators';
-import { Store } from '@ngrx/store';
-import { AppState } from '../../app.reducer';
+import { Store, StoreModule } from '@ngrx/store';
import { MockStore } from '../../shared/testing/mock-store';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
import { TranslateModule } from '@ngx-translate/core';
import {
MetadataRegistryCancelFieldAction,
- MetadataRegistryCancelSchemaAction, MetadataRegistryDeselectAllFieldAction,
- MetadataRegistryDeselectAllSchemaAction, MetadataRegistryDeselectFieldAction,
+ MetadataRegistryCancelSchemaAction,
+ MetadataRegistryDeselectAllFieldAction,
+ MetadataRegistryDeselectAllSchemaAction,
+ MetadataRegistryDeselectFieldAction,
MetadataRegistryDeselectSchemaAction,
MetadataRegistryEditFieldAction,
- MetadataRegistryEditSchemaAction, MetadataRegistrySelectFieldAction,
+ MetadataRegistryEditSchemaAction,
+ MetadataRegistrySelectFieldAction,
MetadataRegistrySelectSchemaAction
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
import { MetadataSchema } from '../metadata/metadataschema.model';
@@ -45,6 +48,7 @@ class DummyComponent {
describe('RegistryService', () => {
let registryService: RegistryService;
+ let mockStore;
const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'registry-service-spec-pagination',
pageSize: 20
@@ -98,40 +102,6 @@ describe('RegistryService', () => {
schema: mockSchemasList[1]
}
];
- const mockFormatsList = [
- {
- shortDescription: 'Unknown',
- description: 'Unknown data format',
- mimetype: 'application/octet-stream',
- supportLevel: 0,
- internal: false,
- extensions: null
- },
- {
- shortDescription: 'License',
- description: 'Item-specific license agreed upon to submission',
- mimetype: 'text/plain; charset=utf-8',
- supportLevel: 1,
- internal: true,
- extensions: null
- },
- {
- shortDescription: 'CC License',
- description: 'Item-specific Creative Commons license agreed upon to submission',
- mimetype: 'text/html; charset=utf-8',
- supportLevel: 2,
- internal: true,
- extensions: null
- },
- {
- shortDescription: 'Adobe PDF',
- description: 'Adobe Portable Document Format',
- mimetype: 'application/pdf',
- supportLevel: 0,
- internal: false,
- extensions: null
- }
- ];
const pageInfo = new PageInfo();
pageInfo.elementsPerPage = 20;
@@ -158,11 +128,9 @@ describe('RegistryService', () => {
}
};
- const mockStore = new MockStore(Object.create(null));
-
beforeEach(() => {
TestBed.configureTestingModule({
- imports: [CommonModule, TranslateModule.forRoot()],
+ imports: [CommonModule, StoreModule.forRoot({}), TranslateModule.forRoot()],
declarations: [
DummyComponent
],
@@ -170,13 +138,13 @@ describe('RegistryService', () => {
{ provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: rdbStub },
{ provide: HALEndpointService, useValue: halServiceStub },
- { provide: Store, useValue: mockStore },
+ { provide: Store, useClass: MockStore },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
RegistryService
]
});
registryService = TestBed.get(RegistryService);
-
+ mockStore = TestBed.get(Store);
spyOn((registryService as any).halService, 'getEndpoint').and.returnValue(observableOf(endpoint));
});
@@ -185,7 +153,7 @@ describe('RegistryService', () => {
metadataschemas: mockSchemasList,
page: pageInfo
});
- const response = new RegistryMetadataschemasSuccessResponse(queryResponse, '200', pageInfo);
+ const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => {
@@ -214,7 +182,7 @@ describe('RegistryService', () => {
metadataschemas: mockSchemasList,
page: pageInfo
});
- const response = new RegistryMetadataschemasSuccessResponse(queryResponse, '200', pageInfo);
+ const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => {
@@ -243,7 +211,7 @@ describe('RegistryService', () => {
metadatafields: mockFieldsList,
page: pageInfo
});
- const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, '200', pageInfo);
+ const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, 200, 'OK', pageInfo);
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => {
@@ -272,7 +240,7 @@ describe('RegistryService', () => {
bitstreamformats: mockFieldsList,
page: pageInfo
});
- const response = new RegistryBitstreamformatsSuccessResponse(queryResponse, '200', pageInfo);
+ const response = new RegistryBitstreamformatsSuccessResponse(queryResponse, 200, 'OK', pageInfo);
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => {
diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts
index c630c9dd57..0471d1fbbb 100644
--- a/src/app/core/shared/collection.model.ts
+++ b/src/app/core/shared/collection.model.ts
@@ -3,6 +3,9 @@ import { Bitstream } from './bitstream.model';
import { Item } from './item.model';
import { RemoteData } from '../data/remote-data';
import { Observable } from 'rxjs';
+import { License } from './license.model';
+import { ResourcePolicy } from './resource-policy.model';
+import { PaginatedList } from '../data/paginated-list';
export class Collection extends DSpaceObject {
@@ -39,7 +42,7 @@ export class Collection extends DSpaceObject {
* The license of this Collection
* Corresponds to the metadata field dc.rights.license
*/
- get license(): string {
+ get dcLicense(): string {
return this.firstMetadataValue('dc.rights.license');
}
@@ -51,11 +54,21 @@ export class Collection extends DSpaceObject {
return this.firstMetadataValue('dc.description.tableofcontents');
}
+ /**
+ * The deposit license of this Collection
+ */
+ license: Observable>;
+
/**
* The Bitstream that represents the logo of this Collection
*/
logo: Observable>;
+ /**
+ * The default access conditions of this Collection
+ */
+ defaultAccessConditions: Observable>>;
+
/**
* An array of Collections that are direct parents of this Collection
*/
diff --git a/src/app/core/shared/config/config-authority.model.ts b/src/app/core/shared/config/config-authority.model.ts
deleted file mode 100644
index bbb8605bcc..0000000000
--- a/src/app/core/shared/config/config-authority.model.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
-import { ConfigObject } from './config.model';
-import { SubmissionSectionModel } from './config-submission-section.model';
-
-@inheritSerialization(ConfigObject)
-export class ConfigAuthorityModel extends ConfigObject {
-
- @autoserialize
- id: string;
-
- @autoserialize
- display: string;
-
- @autoserialize
- value: string;
-
- @autoserialize
- otherInformation: any;
-
- @autoserialize
- language: string;
-
-}
diff --git a/src/app/core/shared/config/config-object-factory.ts b/src/app/core/shared/config/config-object-factory.ts
deleted file mode 100644
index 4cb5016983..0000000000
--- a/src/app/core/shared/config/config-object-factory.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-
-import { GenericConstructor } from '../../shared/generic-constructor';
-
-import { SubmissionSectionModel } from './config-submission-section.model';
-import { SubmissionFormsModel } from './config-submission-forms.model';
-import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
-import { ConfigType } from './config-type';
-import { ConfigObject } from './config.model';
-import { ConfigAuthorityModel } from './config-authority.model';
-
-export class ConfigObjectFactory {
- public static getConstructor(type): GenericConstructor {
- switch (type) {
- case ConfigType.SubmissionDefinition:
- case ConfigType.SubmissionDefinitions: {
- return SubmissionDefinitionsModel
- }
- case ConfigType.SubmissionForm:
- case ConfigType.SubmissionForms: {
- return SubmissionFormsModel
- }
- case ConfigType.SubmissionSection:
- case ConfigType.SubmissionSections: {
- return SubmissionSectionModel
- }
- case ConfigType.Authority: {
- return ConfigAuthorityModel
- }
- default: {
- return undefined;
- }
- }
- }
-}
diff --git a/src/app/core/shared/config/config-submission-section.model.ts b/src/app/core/shared/config/config-submission-section.model.ts
deleted file mode 100644
index 0eb9daaeab..0000000000
--- a/src/app/core/shared/config/config-submission-section.model.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
-import { ConfigObject } from './config.model';
-
-@inheritSerialization(ConfigObject)
-export class SubmissionSectionModel extends ConfigObject {
-
- @autoserialize
- header: string;
-
- @autoserialize
- mandatory: boolean;
-
- @autoserialize
- sectionType: string;
-
- @autoserialize
- visibility: {
- main: any,
- other: any
- }
-
-}
diff --git a/src/app/core/shared/config/config.model.ts b/src/app/core/shared/config/config.model.ts
deleted file mode 100644
index 8d86f317e1..0000000000
--- a/src/app/core/shared/config/config.model.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { autoserialize, autoserializeAs } from 'cerialize';
-
-export abstract class ConfigObject {
-
- @autoserialize
- public name: string;
-
- @autoserialize
- public type: string;
-
- @autoserialize
- public _links: {
- [name: string]: string
- }
-
- /**
- * The link to the rest endpoint where this config object can be found
- */
- @autoserialize
- self: string;
-}
diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts
index 085988d745..063398b339 100644
--- a/src/app/core/shared/dspace-object.model.ts
+++ b/src/app/core/shared/dspace-object.model.ts
@@ -1,15 +1,12 @@
-import {
- MetadataMap,
- MetadataValue,
- MetadataValueFilter,
- MetadatumViewModel
-} from './metadata.models';
+import { Observable } from 'rxjs';
+
+import { MetadataMap, MetadataValue, MetadataValueFilter, MetadatumViewModel } from './metadata.models';
import { Metadata } from './metadata.utils';
+import { isUndefined } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer';
import { RemoteData } from '../data/remote-data';
import { ResourceType } from './resource-type';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
-import { Observable } from 'rxjs';
import { hasNoValue } from '../../shared/empty.util';
/**
@@ -17,6 +14,8 @@ import { hasNoValue } from '../../shared/empty.util';
*/
export class DSpaceObject implements CacheableObject, ListableObject {
+ private _name: string;
+
self: string;
/**
@@ -38,7 +37,14 @@ export class DSpaceObject implements CacheableObject, ListableObject {
* The name for this DSpaceObject
*/
get name(): string {
- return this.firstMetadataValue('dc.title');
+ return (isUndefined(this._name)) ? this.firstMetadataValue('dc.title') : this._name;
+ }
+
+ /**
+ * The name for this DSpaceObject
+ */
+ set name(name) {
+ this._name = name;
}
/**
diff --git a/src/app/core/shared/file.service.ts b/src/app/core/shared/file.service.ts
new file mode 100644
index 0000000000..7e89a4e5dd
--- /dev/null
+++ b/src/app/core/shared/file.service.ts
@@ -0,0 +1,46 @@
+import { 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';
+
+/**
+ * Provides utility methods to save files on the client-side.
+ */
+@Injectable()
+export class FileService {
+ constructor(
+ private restService: DSpaceRESTv2Service
+ ) { }
+
+ /**
+ * Makes a HTTP Get request to download a file
+ *
+ * @param url
+ * file url
+ */
+ downloadFile(url: string) {
+ const headers = new HttpHeaders();
+ const options: HttpOptions = Object.create({headers, responseType: 'blob'});
+ return this.restService.request(RestRequestMethod.GET, url, null, options)
+ .subscribe((data) => {
+ saveAs(data.payload as Blob, this.getFileNameFromResponseContentDisposition(data));
+ });
+ }
+
+ /**
+ * Derives file name from the http response
+ * by looking inside content-disposition
+ * @param res
+ * http DSpaceRESTV2Response
+ */
+ getFileNameFromResponseContentDisposition(res: DSpaceRESTV2Response) {
+ // NOTE: to be able to retrieve 'Content-Disposition' header,
+ // you need to set 'Access-Control-Expose-Headers': 'Content-Disposition' ON SERVER SIDE
+ const contentDisposition = res.headers.get('content-disposition') || '';
+ const matches = /filename="([^;]+)"/ig.exec(contentDisposition) || [];
+ return (matches[1] || 'untitled').trim().replace(/\.[^/.]+$/, '');
+ };
+}
diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts
index 7dadfafdd9..645b50d5db 100644
--- a/src/app/core/shared/item.model.ts
+++ b/src/app/core/shared/item.model.ts
@@ -1,5 +1,5 @@
+import { map, startWith, filter, take } from 'rxjs/operators';
import { Observable } from 'rxjs';
-import { filter, map, startWith, tap } from 'rxjs/operators';
import { DSpaceObject } from './dspace-object.model';
import { Collection } from './collection.model';
@@ -95,14 +95,16 @@ export class Item extends DSpaceObject {
*/
getBitstreamsByBundleName(bundleName: string): Observable {
return this.bitstreams.pipe(
+ filter((rd: RemoteData>) => !rd.isResponsePending),
map((rd: RemoteData>) => rd.payload.page),
filter((bitstreams: Bitstream[]) => hasValue(bitstreams)),
+ take(1),
startWith([]),
map((bitstreams) => {
return bitstreams
.filter((bitstream) => hasValue(bitstream))
.filter((bitstream) => bitstream.bundleName === bundleName)
- }),);
+ }));
}
}
diff --git a/src/app/core/shared/license.model.ts b/src/app/core/shared/license.model.ts
new file mode 100644
index 0000000000..a04422242a
--- /dev/null
+++ b/src/app/core/shared/license.model.ts
@@ -0,0 +1,14 @@
+import { DSpaceObject } from './dspace-object.model';
+
+export class License extends DSpaceObject {
+
+ /**
+ * Is the license custom?
+ */
+ custom: boolean;
+
+ /**
+ * The text of the license
+ */
+ text: string;
+}
diff --git a/src/app/core/shared/metadata.models.ts b/src/app/core/shared/metadata.models.ts
index c843b0ec74..9c7e30dcb4 100644
--- a/src/app/core/shared/metadata.models.ts
+++ b/src/app/core/shared/metadata.models.ts
@@ -5,14 +5,28 @@ import { hasValue } from '../../shared/empty.util';
const VIRTUAL_METADATA_PREFIX = 'virtual::';
+/** A single metadata value and its properties. */
+export interface MetadataValueInterface {
+
+ /** The language. */
+ language: string;
+
+ /** The string value. */
+ value: string;
+}
+
/** A map of metadata keys to an ordered list of MetadataValue objects. */
-export class MetadataMap {
+export interface MetadataMapInterface {
+ [key: string]: MetadataValueInterface[];
+}
+
+/** A map of metadata keys to an ordered list of MetadataValue objects. */
+export class MetadataMap implements MetadataMapInterface {
[key: string]: MetadataValue[];
}
/** A single metadata value and its properties. */
-
-export class MetadataValue {
+export class MetadataValue implements MetadataValueInterface {
/** The uuid. */
uuid: string = uuidv4();
diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts
index 330cbdb32b..62a1957e22 100644
--- a/src/app/core/shared/metadata.utils.ts
+++ b/src/app/core/shared/metadata.utils.ts
@@ -1,6 +1,6 @@
import { isEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util';
import {
- MetadataMap,
+ MetadataMapInterface,
MetadataValue,
MetadataValueFilter,
MetadatumViewModel
@@ -25,23 +25,23 @@ export class Metadata {
/**
* Gets all matching metadata in the map(s).
*
- * @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). When multiple maps are given, they will be
+ * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). When multiple maps are given, they will be
* checked in order, and only values from the first with at least one match will be returned.
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {MetadataValue[]} the matching values or an empty array.
*/
- public static all(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
+ public static all(mapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): MetadataValue[] {
- const mdMaps: MetadataMap[] = mapOrMaps instanceof Array ? mapOrMaps : [mapOrMaps];
+ const mdMaps: MetadataMapInterface[] = mapOrMaps instanceof Array ? mapOrMaps : [mapOrMaps];
const matches: MetadataValue[] = [];
for (const mdMap of mdMaps) {
for (const mdKey of Metadata.resolveKeys(mdMap, keyOrKeys)) {
const candidates = mdMap[mdKey];
if (candidates) {
for (const candidate of candidates) {
- if (Metadata.valueMatches(candidate, filter)) {
- matches.push(candidate);
+ if (Metadata.valueMatches(candidate as MetadataValue, filter)) {
+ matches.push(candidate as MetadataValue);
}
}
}
@@ -56,13 +56,13 @@ export class Metadata {
/**
* Like [[Metadata.all]], but only returns string values.
*
- * @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). When multiple maps are given, they will be
+ * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). When multiple maps are given, they will be
* checked in order, and only values from the first with at least one match will be returned.
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {string[]} the matching string values or an empty array.
*/
- public static allValues(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
+ public static allValues(mapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): string[] {
return Metadata.all(mapOrMaps, keyOrKeys, filter).map((mdValue) => mdValue.value);
}
@@ -70,17 +70,17 @@ export class Metadata {
/**
* Gets the first matching MetadataValue object in the map(s), or `undefined`.
*
- * @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
+ * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s).
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {MetadataValue} the first matching value, or `undefined`.
*/
- public static first(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
+ public static first(mdMapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): MetadataValue {
- const mdMaps: MetadataMap[] = mdMapOrMaps instanceof Array ? mdMapOrMaps : [mdMapOrMaps];
+ const mdMaps: MetadataMapInterface[] = mdMapOrMaps instanceof Array ? mdMapOrMaps : [mdMapOrMaps];
for (const mdMap of mdMaps) {
for (const key of Metadata.resolveKeys(mdMap, keyOrKeys)) {
- const values: MetadataValue[] = mdMap[key];
+ const values: MetadataValue[] = mdMap[key] as MetadataValue[];
if (values) {
return values.find((value: MetadataValue) => Metadata.valueMatches(value, filter));
}
@@ -91,12 +91,12 @@ export class Metadata {
/**
* Like [[Metadata.first]], but only returns a string value, or `undefined`.
*
- * @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
+ * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s).
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {string} the first matching string value, or `undefined`.
*/
- public static firstValue(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
+ public static firstValue(mdMapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): string {
const value = Metadata.first(mdMapOrMaps, keyOrKeys, filter);
return isUndefined(value) ? undefined : value.value;
@@ -105,12 +105,12 @@ export class Metadata {
/**
* Checks for a matching metadata value in the given map(s).
*
- * @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
+ * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s).
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {boolean} whether a match is found.
*/
- public static has(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
+ public static has(mdMapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): boolean {
return isNotUndefined(Metadata.first(mdMapOrMaps, keyOrKeys, filter));
}
@@ -146,10 +146,10 @@ export class Metadata {
/**
* Gets the list of keys in the map limited by, and in the order given by `keyOrKeys`.
*
- * @param {MetadataMap} mdMap The source map.
+ * @param {MetadataMapInterface} mdMap The source map.
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
*/
- private static resolveKeys(mdMap: MetadataMap = {}, keyOrKeys: string | string[]): string[] {
+ private static resolveKeys(mdMap: MetadataMapInterface = {}, keyOrKeys: string | string[]): string[] {
const inputKeys: string[] = keyOrKeys instanceof Array ? keyOrKeys : [keyOrKeys];
const outputKeys: string[] = [];
for (const inputKey of inputKeys) {
@@ -168,12 +168,12 @@ export class Metadata {
}
/**
- * Creates an array of MetadatumViewModels from an existing MetadataMap.
+ * Creates an array of MetadatumViewModels from an existing MetadataMapInterface.
*
- * @param {MetadataMap} mdMap The source map.
+ * @param {MetadataMapInterface} mdMap The source map.
* @returns {MetadatumViewModel[]} List of metadata view models based on the source map.
*/
- public static toViewModelList(mdMap: MetadataMap): MetadatumViewModel[] {
+ public static toViewModelList(mdMap: MetadataMapInterface): MetadatumViewModel[] {
let metadatumList: MetadatumViewModel[] = [];
Object.keys(mdMap)
.sort()
@@ -193,13 +193,13 @@ export class Metadata {
}
/**
- * Creates an MetadataMap from an existing array of MetadatumViewModels.
+ * Creates an MetadataMapInterface from an existing array of MetadatumViewModels.
*
* @param {MetadatumViewModel[]} viewModelList The source list.
- * @returns {MetadataMap} Map with metadata values based on the source list.
+ * @returns {MetadataMapInterface} Map with metadata values based on the source list.
*/
- public static toMetadataMap(viewModelList: MetadatumViewModel[]): MetadataMap {
- const metadataMap: MetadataMap = {};
+ public static toMetadataMap(viewModelList: MetadatumViewModel[]): MetadataMapInterface {
+ const metadataMap: MetadataMapInterface = {};
const groupedList = groupBy(viewModelList, (viewModel) => viewModel.key);
Object.keys(groupedList)
.sort()
diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts
index 5086976f8b..2eb47507b2 100644
--- a/src/app/core/shared/operators.spec.ts
+++ b/src/app/core/shared/operators.spec.ts
@@ -142,7 +142,7 @@ describe('Core Module - RxJS Operators', () => {
scheduler.schedule(() => source.pipe(configureRequest(requestService)).subscribe());
scheduler.flush();
- expect(requestService.configure).toHaveBeenCalledWith(testRequest);
+ expect(requestService.configure).toHaveBeenCalledWith(testRequest, undefined);
});
});
diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts
index a2c421255e..ce9740a0fc 100644
--- a/src/app/core/shared/operators.ts
+++ b/src/app/core/shared/operators.ts
@@ -50,9 +50,9 @@ export const getResourceLinksFromResponse = () =>
map((response: DSOSuccessResponse) => response.resourceSelfLinks),
);
-export const configureRequest = (requestService: RequestService) =>
+export const configureRequest = (requestService: RequestService, forceBypassCache?: boolean) =>
(source: Observable): Observable =>
- source.pipe(tap((request: RestRequest) => requestService.configure(request)));
+ source.pipe(tap((request: RestRequest) => requestService.configure(request, forceBypassCache)));
export const getRemoteDataPayload = () =>
(source: Observable>): Observable =>
diff --git a/src/app/core/shared/resource-policy.model.ts b/src/app/core/shared/resource-policy.model.ts
index cccbea1e89..ee3d5293f5 100644
--- a/src/app/core/shared/resource-policy.model.ts
+++ b/src/app/core/shared/resource-policy.model.ts
@@ -18,9 +18,9 @@ export class ResourcePolicy implements CacheableObject {
name: string;
/**
- * The Group this Resource Policy applies to
+ * The uuid of the Group this Resource Policy applies to
*/
- group: Group;
+ groupUUID: string;
/**
* The link to the rest endpoint where this Resource Policy can be found
diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts
index c876d02a56..d5afa4105f 100644
--- a/src/app/core/shared/resource-type.ts
+++ b/src/app/core/shared/resource-type.ts
@@ -14,4 +14,13 @@ export enum ResourceType {
Relationship = 'relationship',
RelationshipType = 'relationshiptype',
ItemType = 'entitytype',
+ License = 'license',
+ Workflowitem = 'workflowitem',
+ Workspaceitem = 'workspaceitem',
+ SubmissionDefinitions = 'submissiondefinitions',
+ SubmissionDefinition = 'submissiondefinition',
+ SubmissionForm = 'submissionform',
+ SubmissionForms = 'submissionforms',
+ SubmissionSections = 'submissionsections',
+ SubmissionSection = 'submissionsection',
}
diff --git a/src/app/core/shared/selectors.ts b/src/app/core/shared/selectors.ts
deleted file mode 100644
index 7bd35d39c1..0000000000
--- a/src/app/core/shared/selectors.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { createSelector, MemoizedSelector } from '@ngrx/store';
-import { hasNoValue, isEmpty } from '../../shared/empty.util';
-
-export function pathSelector(selector: MemoizedSelector, ...path: string[]): MemoizedSelector {
- return createSelector(selector, (state: any) => getSubState(state, path));
-}
-
-function getSubState(state: any, path: string[]) {
- const current = path[0];
- const remainingPath = path.slice(1);
- const subState = state[current];
- if (hasNoValue(subState) || isEmpty(remainingPath)) {
- return subState;
- } else {
- return getSubState(subState, remainingPath);
- }
-}
diff --git a/src/app/core/shared/submit-data-response-definition.model.ts b/src/app/core/shared/submit-data-response-definition.model.ts
new file mode 100644
index 0000000000..beb2b320cf
--- /dev/null
+++ b/src/app/core/shared/submit-data-response-definition.model.ts
@@ -0,0 +1,8 @@
+import { ConfigObject } from '../config/models/config.model';
+import { SubmissionObject } from '../submission/models/submission-object.model';
+
+/**
+ * Defines a type for submission request responses.
+ */
+export type SubmitDataResponseDefinitionObject
+ = Array;
diff --git a/src/app/core/submission/models/normalized-submission-object.model.ts b/src/app/core/submission/models/normalized-submission-object.model.ts
new file mode 100644
index 0000000000..8091781760
--- /dev/null
+++ b/src/app/core/submission/models/normalized-submission-object.model.ts
@@ -0,0 +1,37 @@
+import { autoserialize, inheritSerialization } from 'cerialize';
+
+import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
+import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model';
+import { SubmissionObjectError } from './submission-object.model';
+import { DSpaceObject } from '../../shared/dspace-object.model';
+
+/**
+ * An abstract model class for a NormalizedSubmissionObject.
+ */
+@inheritSerialization(NormalizedDSpaceObject)
+export class NormalizedSubmissionObject extends NormalizedDSpaceObject {
+
+ /**
+ * The workspaceitem/workflowitem identifier
+ */
+ @autoserialize
+ id: string;
+
+ /**
+ * The workspaceitem/workflowitem last modified date
+ */
+ @autoserialize
+ lastModified: Date;
+
+ /**
+ * The workspaceitem/workflowitem last sections data
+ */
+ @autoserialize
+ sections: WorkspaceitemSectionsObject;
+
+ /**
+ * The workspaceitem/workflowitem last sections errors
+ */
+ @autoserialize
+ errors: SubmissionObjectError[];
+}
diff --git a/src/app/core/submission/models/normalized-workflowitem.model.ts b/src/app/core/submission/models/normalized-workflowitem.model.ts
new file mode 100644
index 0000000000..a3fa8992a2
--- /dev/null
+++ b/src/app/core/submission/models/normalized-workflowitem.model.ts
@@ -0,0 +1,43 @@
+import { autoserialize, inheritSerialization } from 'cerialize';
+
+import { mapsTo, relationship } from '../../cache/builders/build-decorators';
+import { Workflowitem } from './workflowitem.model';
+import { NormalizedSubmissionObject } from './normalized-submission-object.model';
+import { ResourceType } from '../../shared/resource-type';
+
+/**
+ * An model class for a NormalizedWorkflowItem.
+ */
+@mapsTo(Workflowitem)
+@inheritSerialization(NormalizedSubmissionObject)
+export class NormalizedWorkflowItem extends NormalizedSubmissionObject {
+
+ /**
+ * The collection this workflowitem belonging to
+ */
+ @autoserialize
+ @relationship(ResourceType.Collection, false)
+ collection: string;
+
+ /**
+ * The item created with this workflowitem
+ */
+ @autoserialize
+ @relationship(ResourceType.Item, false)
+ item: string;
+
+ /**
+ * The configuration object that define this workflowitem
+ */
+ @autoserialize
+ @relationship(ResourceType.SubmissionDefinition, false)
+ submissionDefinition: string;
+
+ /**
+ * The EPerson who submit this workflowitem
+ */
+ @autoserialize
+ @relationship(ResourceType.EPerson, false)
+ submitter: string;
+
+}
diff --git a/src/app/core/submission/models/normalized-workspaceitem.model.ts b/src/app/core/submission/models/normalized-workspaceitem.model.ts
new file mode 100644
index 0000000000..7c15925c98
--- /dev/null
+++ b/src/app/core/submission/models/normalized-workspaceitem.model.ts
@@ -0,0 +1,45 @@
+import { autoserialize, inheritSerialization } from 'cerialize';
+
+import { Workspaceitem } from './workspaceitem.model';
+import { NormalizedSubmissionObject } from './normalized-submission-object.model';
+import { mapsTo, relationship } from '../../cache/builders/build-decorators';
+import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
+import { ResourceType } from '../../shared/resource-type';
+import { Workflowitem } from './workflowitem.model';
+
+/**
+ * An model class for a NormalizedWorkspaceItem.
+ */
+@mapsTo(Workspaceitem)
+@inheritSerialization(NormalizedDSpaceObject)
+@inheritSerialization(NormalizedSubmissionObject)
+export class NormalizedWorkspaceItem extends NormalizedSubmissionObject {
+
+ /**
+ * The collection this workspaceitem belonging to
+ */
+ @autoserialize
+ @relationship(ResourceType.Collection, false)
+ collection: string;
+
+ /**
+ * The item created with this workspaceitem
+ */
+ @autoserialize
+ @relationship(ResourceType.Item, false)
+ item: string;
+
+ /**
+ * The configuration object that define this workspaceitem
+ */
+ @autoserialize
+ @relationship(ResourceType.SubmissionDefinition, false)
+ submissionDefinition: string;
+
+ /**
+ * The EPerson who submit this workspaceitem
+ */
+ @autoserialize
+ @relationship(ResourceType.EPerson, false)
+ submitter: string;
+}
diff --git a/src/app/core/submission/models/submission-object.model.ts b/src/app/core/submission/models/submission-object.model.ts
new file mode 100644
index 0000000000..6b2d9a03b9
--- /dev/null
+++ b/src/app/core/submission/models/submission-object.model.ts
@@ -0,0 +1,62 @@
+import { Observable } from 'rxjs';
+
+import { CacheableObject } from '../../cache/object-cache.reducer';
+import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
+import { DSpaceObject } from '../../shared/dspace-object.model';
+import { EPerson } from '../../eperson/models/eperson.model';
+import { RemoteData } from '../../data/remote-data';
+import { Collection } from '../../shared/collection.model';
+import { Item } from '../../shared/item.model';
+import { SubmissionDefinitionsModel } from '../../config/models/config-submission-definitions.model';
+import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model';
+
+export interface SubmissionObjectError {
+ message: string,
+ paths: string[],
+}
+
+/**
+ * An abstract model class for a SubmissionObject.
+ */
+export abstract class SubmissionObject extends DSpaceObject implements CacheableObject, ListableObject {
+
+ /**
+ * The workspaceitem/workflowitem identifier
+ */
+ id: string;
+
+ /**
+ * The workspaceitem/workflowitem last modified date
+ */
+ lastModified: Date;
+
+ /**
+ * The collection this submission applies to
+ */
+ collection: Observable> | Collection;
+
+ /**
+ * The submission item
+ */
+ item: Observable> | Item;
+
+ /**
+ * The workspaceitem/workflowitem last sections data
+ */
+ sections: WorkspaceitemSectionsObject;
+
+ /**
+ * The configuration object that define this submission
+ */
+ submissionDefinition: Observable> | SubmissionDefinitionsModel;
+
+ /**
+ * The workspaceitem submitter
+ */
+ submitter: Observable> | EPerson;
+
+ /**
+ * The workspaceitem/workflowitem last sections errors
+ */
+ errors: SubmissionObjectError[];
+}
diff --git a/src/app/core/submission/models/submission-upload-file-access-condition.model.ts b/src/app/core/submission/models/submission-upload-file-access-condition.model.ts
new file mode 100644
index 0000000000..8b89397f24
--- /dev/null
+++ b/src/app/core/submission/models/submission-upload-file-access-condition.model.ts
@@ -0,0 +1,30 @@
+/**
+ * An interface to represent bitstream's access condition.
+ */
+export class SubmissionUploadFileAccessConditionObject {
+
+ /**
+ * The access condition id
+ */
+ id: string;
+
+ /**
+ * The access condition name
+ */
+ name: string;
+
+ /**
+ * The access group UUID defined in this access condition
+ */
+ groupUUID: string;
+
+ /**
+ * Possible start date of the access condition
+ */
+ startDate: string;
+
+ /**
+ * Possible end date of the access condition
+ */
+ endDate: string;
+}
diff --git a/src/app/core/submission/models/workflowitem.model.ts b/src/app/core/submission/models/workflowitem.model.ts
new file mode 100644
index 0000000000..f1a0467f43
--- /dev/null
+++ b/src/app/core/submission/models/workflowitem.model.ts
@@ -0,0 +1,7 @@
+import { Workspaceitem } from './workspaceitem.model';
+
+/**
+ * A model class for a Workflowitem.
+ */
+export class Workflowitem extends Workspaceitem {
+}
diff --git a/src/app/core/submission/models/workspaceitem-section-form.model.ts b/src/app/core/submission/models/workspaceitem-section-form.model.ts
new file mode 100644
index 0000000000..1462a96d81
--- /dev/null
+++ b/src/app/core/submission/models/workspaceitem-section-form.model.ts
@@ -0,0 +1,10 @@
+import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
+import { MetadataMapInterface } from '../../shared/metadata.models';
+
+/**
+ * An interface to represent submission's form section data.
+ * A map of metadata keys to an ordered list of FormFieldMetadataValueObject objects.
+ */
+export interface WorkspaceitemSectionFormObject extends MetadataMapInterface {
+ [metadata: string]: FormFieldMetadataValueObject[];
+}
diff --git a/src/app/core/submission/models/workspaceitem-section-license.model.ts b/src/app/core/submission/models/workspaceitem-section-license.model.ts
new file mode 100644
index 0000000000..26f625871e
--- /dev/null
+++ b/src/app/core/submission/models/workspaceitem-section-license.model.ts
@@ -0,0 +1,20 @@
+
+/**
+ * An interface to represent submission's license section data.
+ */
+export interface WorkspaceitemSectionLicenseObject {
+ /**
+ * The license url
+ */
+ url: string;
+
+ /**
+ * The acceptance date of the license
+ */
+ acceptanceDate: string;
+
+ /**
+ * A boolean representing if license has been granted
+ */
+ granted: boolean;
+}
diff --git a/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts b/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts
new file mode 100644
index 0000000000..177473b7d5
--- /dev/null
+++ b/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts
@@ -0,0 +1,46 @@
+import { SubmissionUploadFileAccessConditionObject } from './submission-upload-file-access-condition.model';
+import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
+
+/**
+ * An interface to represent submission's upload section file entry.
+ */
+export class WorkspaceitemSectionUploadFileObject {
+
+ /**
+ * The file UUID
+ */
+ uuid: string;
+
+ /**
+ * The file metadata
+ */
+ metadata: WorkspaceitemSectionFormObject;
+
+ /**
+ * The file size
+ */
+ sizeBytes: number;
+
+ /**
+ * The file check sum
+ */
+ checkSum: {
+ checkSumAlgorithm: string;
+ value: string;
+ };
+
+ /**
+ * The file url
+ */
+ url: string;
+
+ /**
+ * The file thumbnail url
+ */
+ thumbnail: string;
+
+ /**
+ * The list of file access conditions
+ */
+ accessConditions: SubmissionUploadFileAccessConditionObject[];
+}
diff --git a/src/app/core/submission/models/workspaceitem-section-upload.model.ts b/src/app/core/submission/models/workspaceitem-section-upload.model.ts
new file mode 100644
index 0000000000..f98e0584eb
--- /dev/null
+++ b/src/app/core/submission/models/workspaceitem-section-upload.model.ts
@@ -0,0 +1,12 @@
+import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model';
+
+/**
+ * An interface to represent submission's upload section data.
+ */
+export interface WorkspaceitemSectionUploadObject {
+
+ /**
+ * A list of [[WorkspaceitemSectionUploadFileObject]]
+ */
+ files: WorkspaceitemSectionUploadFileObject[];
+}
diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts
new file mode 100644
index 0000000000..165e69869c
--- /dev/null
+++ b/src/app/core/submission/models/workspaceitem-sections.model.ts
@@ -0,0 +1,20 @@
+import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
+import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
+import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
+
+/**
+ * An interface to represent submission's section object.
+ * A map of section keys to an ordered list of WorkspaceitemSectionDataType objects.
+ */
+export class WorkspaceitemSectionsObject {
+ [name: string]: WorkspaceitemSectionDataType;
+}
+
+/**
+ * Export a type alias of all sections
+ */
+export type WorkspaceitemSectionDataType
+ = WorkspaceitemSectionUploadObject
+ | WorkspaceitemSectionFormObject
+ | WorkspaceitemSectionLicenseObject
+ | string;
diff --git a/src/app/core/submission/models/workspaceitem.model.ts b/src/app/core/submission/models/workspaceitem.model.ts
new file mode 100644
index 0000000000..6548191ba2
--- /dev/null
+++ b/src/app/core/submission/models/workspaceitem.model.ts
@@ -0,0 +1,8 @@
+import { SubmissionObject } from './submission-object.model';
+
+/**
+ * A model class for a Workspaceitem.
+ */
+export class Workspaceitem extends SubmissionObject {
+
+}
diff --git a/src/app/core/submission/submission-json-patch-operations.service.spec.ts b/src/app/core/submission/submission-json-patch-operations.service.spec.ts
new file mode 100644
index 0000000000..39e6cd42fb
--- /dev/null
+++ b/src/app/core/submission/submission-json-patch-operations.service.spec.ts
@@ -0,0 +1,37 @@
+import { Store } from '@ngrx/store';
+
+import { getTestScheduler } from 'jasmine-marbles';
+import { TestScheduler } from 'rxjs/testing';
+
+import { CoreState } from '../core.reducers';
+import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { SubmissionJsonPatchOperationsService } from './submission-json-patch-operations.service';
+import { RequestService } from '../data/request.service';
+import { SubmissionPatchRequest } from '../data/request.models';
+
+describe('SubmissionJsonPatchOperationsService', () => {
+ let scheduler: TestScheduler;
+ let service: SubmissionJsonPatchOperationsService;
+ const requestService = {} as RequestService;
+ const store = {} as Store;
+ const halEndpointService = {} as HALEndpointService;
+
+ function initTestService() {
+ return new SubmissionJsonPatchOperationsService(
+ requestService,
+ store,
+ halEndpointService
+ );
+ }
+
+ beforeEach(() => {
+ scheduler = getTestScheduler();
+ service = initTestService();
+ });
+
+ it('should instantiate SubmissionJsonPatchOperationsService properly', () => {
+ expect(service).toBeDefined();
+ expect((service as any).patchRequestConstructor).toEqual(SubmissionPatchRequest);
+ });
+
+});
diff --git a/src/app/core/submission/submission-json-patch-operations.service.ts b/src/app/core/submission/submission-json-patch-operations.service.ts
new file mode 100644
index 0000000000..d469f2098f
--- /dev/null
+++ b/src/app/core/submission/submission-json-patch-operations.service.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+
+import { Store } from '@ngrx/store';
+
+import { RequestService } from '../data/request.service';
+import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { JsonPatchOperationsService } from '../json-patch/json-patch-operations.service';
+import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model';
+import { SubmissionPatchRequest } from '../data/request.models';
+import { CoreState } from '../core.reducers';
+
+/**
+ * A service that provides methods to make JSON Patch requests.
+ */
+@Injectable()
+export class SubmissionJsonPatchOperationsService extends JsonPatchOperationsService {
+ protected linkPath = '';
+ protected patchRequestConstructor = SubmissionPatchRequest;
+
+ constructor(
+ protected requestService: RequestService,
+ protected store: Store,
+ protected halService: HALEndpointService) {
+
+ super();
+ }
+
+}
diff --git a/src/app/core/submission/submission-resource-type.ts b/src/app/core/submission/submission-resource-type.ts
new file mode 100644
index 0000000000..f5b8e2c423
--- /dev/null
+++ b/src/app/core/submission/submission-resource-type.ts
@@ -0,0 +1,21 @@
+export enum SubmissionResourceType {
+ Bundle = 'bundle',
+ Bitstream = 'bitstream',
+ BitstreamFormat = 'bitstreamformat',
+ Item = 'item',
+ Collection = 'collection',
+ Community = 'community',
+ ResourcePolicy = 'resourcePolicy',
+ License = 'license',
+ EPerson = 'eperson',
+ Group = 'group',
+ WorkspaceItem = 'workspaceitem',
+ WorkflowItem = 'workflowitem',
+ SubmissionDefinitions = 'submissiondefinitions',
+ SubmissionDefinition = 'submissiondefinition',
+ SubmissionForm = 'submissionform',
+ SubmissionForms = 'submissionforms',
+ SubmissionSections = 'submissionsections',
+ SubmissionSection = 'submissionsection',
+ Authority = 'authority'
+}
diff --git a/src/app/core/submission/submission-response-parsing.service.ts b/src/app/core/submission/submission-response-parsing.service.ts
new file mode 100644
index 0000000000..20dfb43cbd
--- /dev/null
+++ b/src/app/core/submission/submission-response-parsing.service.ts
@@ -0,0 +1,167 @@
+import { Inject, Injectable } from '@angular/core';
+
+import { ResponseParsingService } from '../data/parsing.service';
+import { RestRequest } from '../data/request.models';
+import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
+import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response.models';
+import { isEmpty, isNotEmpty, isNotNull } from '../../shared/empty.util';
+import { ConfigObject } from '../config/models/config.model';
+import { BaseResponseParsingService } from '../data/base-response-parsing.service';
+import { GLOBAL_CONFIG } from '../../../config';
+import { GlobalConfig } from '../../../config/global-config.interface';
+import { ObjectCacheService } from '../cache/object-cache.service';
+import { SubmissionResourceType } from './submission-resource-type';
+import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model';
+import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model';
+import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model';
+import { SubmissionObject } from './models/submission-object.model';
+import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
+
+/**
+ * Export a function to check if object has same properties of FormFieldMetadataValueObject
+ *
+ * @param obj
+ */
+export function isServerFormValue(obj: any): boolean {
+ return (typeof obj === 'object'
+ && obj.hasOwnProperty('value')
+ && obj.hasOwnProperty('language')
+ && obj.hasOwnProperty('authority')
+ && obj.hasOwnProperty('confidence')
+ && obj.hasOwnProperty('place'))
+}
+
+/**
+ * Export a function to normalize sections object of the server response
+ *
+ * @param obj
+ */
+export function normalizeSectionData(obj: any) {
+ let result: any = obj;
+ if (isNotNull(obj)) {
+ // If is an Instance of FormFieldMetadataValueObject normalize it
+ if (typeof obj === 'object' && isServerFormValue(obj)) {
+ // If authority property is set normalize as a FormFieldMetadataValueObject object
+ /* NOTE: Data received from server could have authority property equal to null, but into form
+ field's model is required a FormFieldMetadataValueObject object as field value, so instantiate it */
+ result = new FormFieldMetadataValueObject(
+ obj.value,
+ obj.language,
+ obj.authority,
+ (obj.display || obj.value),
+ obj.place,
+ obj.confidence,
+ obj.otherInformation
+ );
+ } else if (Array.isArray(obj)) {
+ result = [];
+ obj.forEach((item, index) => {
+ result[index] = normalizeSectionData(item);
+ });
+ } else if (typeof obj === 'object') {
+ result = Object.create({});
+ Object.keys(obj)
+ .forEach((key) => {
+ result[key] = normalizeSectionData(obj[key]);
+ });
+ }
+ }
+ return result;
+}
+
+/**
+ * Provides methods to parse response for a submission request.
+ */
+@Injectable()
+export class SubmissionResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
+
+ protected objectFactory = NormalizedObjectFactory;
+ protected toCache = false;
+
+ constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
+ protected objectCache: ObjectCacheService) {
+ super();
+ }
+
+ /**
+ * Parses data from the workspaceitems/workflowitems endpoints
+ *
+ * @param {RestRequest} request
+ * @param {DSpaceRESTV2Response} data
+ * @returns {RestResponse}
+ */
+ parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
+ if (isNotEmpty(data.payload)
+ && isNotEmpty(data.payload._links)
+ && (data.statusCode === 201 || data.statusCode === 200)) {
+ const dataDefinition = this.processResponse(data.payload, request.href);
+ return new SubmissionSuccessResponse(dataDefinition, data.statusCode, data.statusText, this.processPageInfo(data.payload));
+ } else if (isEmpty(data.payload) && data.statusCode === 204) {
+ // Response from a DELETE request
+ return new SubmissionSuccessResponse(null, data.statusCode, data.statusText);
+ } else {
+ return new ErrorResponse(
+ Object.assign(
+ new Error('Unexpected response from server'),
+ {statusCode: data.statusCode, statusText: data.statusText}
+ )
+ );
+ }
+ }
+
+ /**
+ * Parses response and normalize it
+ *
+ * @param {DSpaceRESTV2Response} data
+ * @param {string} requestHref
+ * @returns {any[]}
+ */
+ protected processResponse(data: any, requestHref: string): any[] {
+ const dataDefinition = this.process(data, requestHref);
+ const normalizedDefinition = Array.of();
+ const processedList = Array.isArray(dataDefinition) ? dataDefinition : Array.of(dataDefinition);
+
+ processedList.forEach((item) => {
+
+ let normalizedItem = Object.assign({}, item);
+ // In case data is an Instance of NormalizedWorkspaceItem normalize field value of all the section of type form
+ if (item instanceof NormalizedWorkspaceItem
+ || item instanceof NormalizedWorkflowItem) {
+ if (item.sections) {
+ const precessedSection = Object.create({});
+ // Iterate over all workspaceitem's sections
+ Object.keys(item.sections)
+ .forEach((sectionId) => {
+ if (typeof item.sections[sectionId] === 'object' && isNotEmpty(item.sections[sectionId])) {
+ const normalizedSectionData = Object.create({});
+ // Iterate over all sections property
+ Object.keys(item.sections[sectionId])
+ .forEach((metdadataId) => {
+ const entry = item.sections[sectionId][metdadataId];
+ // If entry is not an array, for sure is not a section of type form
+ if (Array.isArray(entry)) {
+ normalizedSectionData[metdadataId] = [];
+ entry.forEach((valueItem) => {
+ // Parse value and normalize it
+ const normValue = normalizeSectionData(valueItem);
+ if (isNotEmpty(normValue)) {
+ normalizedSectionData[metdadataId].push(normValue);
+ }
+ });
+ } else {
+ normalizedSectionData[metdadataId] = entry;
+ }
+ });
+ precessedSection[sectionId] = normalizedSectionData;
+ }
+ });
+ normalizedItem = Object.assign({}, item, { sections: precessedSection });
+ }
+ }
+ normalizedDefinition.push(normalizedItem);
+ });
+
+ return normalizedDefinition;
+ }
+
+}
diff --git a/src/app/core/submission/submission-rest.service.spec.ts b/src/app/core/submission/submission-rest.service.spec.ts
new file mode 100644
index 0000000000..6e748c5575
--- /dev/null
+++ b/src/app/core/submission/submission-rest.service.spec.ts
@@ -0,0 +1,88 @@
+import { TestScheduler } from 'rxjs/testing';
+import { getTestScheduler } from 'jasmine-marbles';
+
+import { SubmissionRestService } from './submission-rest.service';
+import { RequestService } from '../data/request.service';
+import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
+import { getMockRequestService } from '../../shared/mocks/mock-request.service';
+import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
+import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
+import {
+ SubmissionDeleteRequest,
+ SubmissionPatchRequest,
+ SubmissionPostRequest,
+ SubmissionRequest
+} from '../data/request.models';
+import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model';
+
+describe('SubmissionRestService test suite', () => {
+ let scheduler: TestScheduler;
+ let service: SubmissionRestService;
+ let requestService: RequestService;
+ let rdbService: RemoteDataBuildService;
+ let halService: any;
+
+ const resourceEndpointURL = 'https://rest.api/endpoint';
+ const resourceEndpoint = 'workspaceitems';
+ const resourceScope = '260';
+ const body = { test: new FormFieldMetadataValueObject('test')};
+ const resourceHref = resourceEndpointURL + '/' + resourceEndpoint + '/' + resourceScope;
+ const timestampResponse = 1545994811992;
+
+ function initTestService() {
+ return new SubmissionRestService(
+ rdbService,
+ requestService,
+ halService
+ );
+ }
+
+ beforeEach(() => {
+ requestService = getMockRequestService();
+ rdbService = getMockRemoteDataBuildService();
+ scheduler = getTestScheduler();
+ halService = new HALEndpointServiceStub(resourceEndpointURL);
+ service = initTestService();
+
+ });
+
+ describe('deleteById', () => {
+ it('should configure a new SubmissionDeleteRequest', () => {
+ const expected = new SubmissionDeleteRequest(requestService.generateRequestId(), resourceHref);
+ scheduler.schedule(() => service.deleteById(resourceScope, resourceEndpoint).subscribe());
+ scheduler.flush();
+
+ expect(requestService.configure).toHaveBeenCalledWith(expected);
+ });
+ });
+
+ describe('getDataById', () => {
+ it('should configure a new SubmissionRequest', () => {
+ const expected = new SubmissionRequest(requestService.generateRequestId(), resourceHref);
+ scheduler.schedule(() => service.getDataById(resourceEndpoint, resourceScope).subscribe());
+ scheduler.flush();
+
+ expect(requestService.configure).toHaveBeenCalledWith(expected, true);
+ });
+ });
+
+ describe('postToEndpoint', () => {
+ it('should configure a new SubmissionPostRequest', () => {
+ const expected = new SubmissionPostRequest(requestService.generateRequestId(), resourceHref, body);
+ scheduler.schedule(() => service.postToEndpoint(resourceEndpoint, body, resourceScope).subscribe());
+ scheduler.flush();
+
+ expect(requestService.configure).toHaveBeenCalledWith(expected);
+ });
+ });
+
+ describe('patchToEndpoint', () => {
+ it('should configure a new SubmissionPatchRequest', () => {
+ const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, body);
+ scheduler.schedule(() => service.patchToEndpoint(resourceEndpoint, body, resourceScope).subscribe());
+ scheduler.flush();
+
+ expect(requestService.configure).toHaveBeenCalledWith(expected);
+ });
+ });
+});
diff --git a/src/app/core/submission/submission-rest.service.ts b/src/app/core/submission/submission-rest.service.ts
new file mode 100644
index 0000000000..e2b8bb01c8
--- /dev/null
+++ b/src/app/core/submission/submission-rest.service.ts
@@ -0,0 +1,167 @@
+import { Injectable } from '@angular/core';
+
+import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
+import { distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/operators';
+
+import { RequestService } from '../data/request.service';
+import { isNotEmpty } from '../../shared/empty.util';
+import {
+ DeleteRequest,
+ PostRequest,
+ RestRequest,
+ SubmissionDeleteRequest,
+ SubmissionPatchRequest,
+ SubmissionPostRequest,
+ SubmissionRequest
+} from '../data/request.models';
+import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model';
+import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
+import { HALEndpointService } from '../shared/hal-endpoint.service';
+import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
+import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response.models';
+import { getResponseFromEntry } from '../shared/operators';
+
+/**
+ * The service handling all submission REST requests
+ */
+@Injectable()
+export class SubmissionRestService {
+ protected linkPath = 'workspaceitems';
+
+ constructor(
+ protected rdbService: RemoteDataBuildService,
+ protected requestService: RequestService,
+ protected halService: HALEndpointService) {
+ }
+
+ /**
+ * Fetch a RestRequest
+ *
+ * @param requestId
+ * The base endpoint for the type of object
+ * @return Observable
+ * server response
+ */
+ protected fetchRequest(requestId: string): Observable {
+ const responses = this.requestService.getByUUID(requestId).pipe(
+ getResponseFromEntry()
+ );
+ const errorResponses = responses.pipe(
+ filter((response: RestResponse) => !response.isSuccessful),
+ mergeMap((error: ErrorResponse) => observableThrowError(error))
+ );
+ const successResponses = responses.pipe(
+ filter((response: RestResponse) => response.isSuccessful),
+ map((response: SubmissionSuccessResponse) => response.dataDefinition as any),
+ distinctUntilChanged()
+ );
+ return observableMerge(errorResponses, successResponses);
+ }
+
+ /**
+ * Create the HREF for a specific submission object based on its identifier
+ *
+ * @param endpoint
+ * The base endpoint for the type of object
+ * @param resourceID
+ * The identifier for the object
+ */
+ protected getEndpointByIDHref(endpoint, resourceID): string {
+ return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`;
+ }
+
+ /**
+ * Delete an existing submission Object on the server
+ *
+ * @param scopeId
+ * The submission Object to be removed
+ * @param linkName
+ * The endpoint link name
+ * @return Observable
+ * server response
+ */
+ public deleteById(scopeId: string, linkName?: string): Observable {
+ const requestId = this.requestService.generateRequestId();
+ return this.halService.getEndpoint(linkName || this.linkPath).pipe(
+ filter((href: string) => isNotEmpty(href)),
+ distinctUntilChanged(),
+ map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)),
+ map((endpointURL: string) => new SubmissionDeleteRequest(requestId, endpointURL)),
+ tap((request: DeleteRequest) => this.requestService.configure(request)),
+ flatMap(() => this.fetchRequest(requestId)),
+ distinctUntilChanged());
+ }
+
+ /**
+ * Return an existing submission Object from the server
+ *
+ * @param linkName
+ * The endpoint link name
+ * @param id
+ * The submission Object to retrieve
+ * @return Observable
+ * server response
+ */
+ public getDataById(linkName: string, id: string): Observable {
+ const requestId = this.requestService.generateRequestId();
+ return this.halService.getEndpoint(linkName).pipe(
+ map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)),
+ filter((href: string) => isNotEmpty(href)),
+ distinctUntilChanged(),
+ map((endpointURL: string) => new SubmissionRequest(requestId, endpointURL)),
+ tap((request: RestRequest) => this.requestService.configure(request, true)),
+ flatMap(() => this.fetchRequest(requestId)),
+ distinctUntilChanged());
+ }
+
+ /**
+ * Make a new post request
+ *
+ * @param linkName
+ * The endpoint link name
+ * @param body
+ * The post request body
+ * @param scopeId
+ * The submission Object id
+ * @param options
+ * The [HttpOptions] object
+ * @return Observable
+ * server response
+ */
+ public postToEndpoint(linkName: string, body: any, scopeId?: string, options?: HttpOptions): Observable {
+ const requestId = this.requestService.generateRequestId();
+ return this.halService.getEndpoint(linkName).pipe(
+ filter((href: string) => isNotEmpty(href)),
+ map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)),
+ distinctUntilChanged(),
+ map((endpointURL: string) => new SubmissionPostRequest(requestId, endpointURL, body, options)),
+ tap((request: PostRequest) => this.requestService.configure(request)),
+ flatMap(() => this.fetchRequest(requestId)),
+ distinctUntilChanged());
+ }
+
+ /**
+ * Make a new patch to a specified object
+ *
+ * @param linkName
+ * The endpoint link name
+ * @param body
+ * The post request body
+ * @param scopeId
+ * The submission Object id
+ * @return Observable