mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'master' into clean-relationships-in-submission
This commit is contained in:
@@ -70,7 +70,8 @@
|
||||
"docs": "typedoc --options typedoc.json ./src/",
|
||||
"coverage": "http-server -c-1 -o -p 9875 ./coverage",
|
||||
"postinstall": "yarn run patch-protractor",
|
||||
"patch-protractor": "ncp node_modules/webdriver-manager node_modules/protractor/node_modules/webdriver-manager"
|
||||
"patch-protractor": "ncp node_modules/webdriver-manager node_modules/protractor/node_modules/webdriver-manager",
|
||||
"sync-i18n": "node ./scripts/sync-i18n-files.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^6.1.4",
|
||||
@@ -175,7 +176,9 @@
|
||||
"angular2-template-loader": "0.6.2",
|
||||
"autoprefixer": "^9.1.3",
|
||||
"caniuse-lite": "^1.0.30000697",
|
||||
"cli-progress": "^3.3.1",
|
||||
"codelyzer": "^4.4.4",
|
||||
"commander": "^3.0.2",
|
||||
"compression-webpack-plugin": "^1.1.6",
|
||||
"copy-webpack-plugin": "^4.4.1",
|
||||
"copyfiles": "^2.1.1",
|
||||
|
3220
resources/i18n/ar.json5
Normal file
3220
resources/i18n/ar.json5
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1610
resources/i18n/es.json5
Normal file
1610
resources/i18n/es.json5
Normal file
File diff suppressed because it is too large
Load Diff
3220
resources/i18n/fi.json5
Normal file
3220
resources/i18n/fi.json5
Normal file
File diff suppressed because it is too large
Load Diff
3220
resources/i18n/fr.json5
Normal file
3220
resources/i18n/fr.json5
Normal file
File diff suppressed because it is too large
Load Diff
3220
resources/i18n/ja.json5
Normal file
3220
resources/i18n/ja.json5
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3220
resources/i18n/pl.json5
Normal file
3220
resources/i18n/pl.json5
Normal file
File diff suppressed because it is too large
Load Diff
3220
resources/i18n/pt.json5
Normal file
3220
resources/i18n/pt.json5
Normal file
File diff suppressed because it is too large
Load Diff
3220
resources/i18n/sw.json5
Normal file
3220
resources/i18n/sw.json5
Normal file
File diff suppressed because it is too large
Load Diff
3220
resources/i18n/tr.json5
Normal file
3220
resources/i18n/tr.json5
Normal file
File diff suppressed because it is too large
Load Diff
342
scripts/sync-i18n-files.js
Executable file
342
scripts/sync-i18n-files.js
Executable file
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env node
|
||||
const commander = require('commander');
|
||||
const fs = require('fs');
|
||||
const JSON5 = require('json5');
|
||||
const _cliProgress = require('cli-progress');
|
||||
const _ = require('lodash');
|
||||
const {projectRoot} = require('../webpack/helpers');
|
||||
|
||||
const program = new commander.Command();
|
||||
program.version('1.0.0', '-v, --version');
|
||||
|
||||
const NEW_MESSAGE_TODO = '// TODO New key - Add a translation';
|
||||
const MESSAGE_CHANGED_TODO = '// TODO Source message changed - Revise the translation';
|
||||
const COMMENTS_CHANGED_TODO = '// TODO Source comments changed - Revise the translation';
|
||||
|
||||
const DEFAULT_SOURCE_FILE_LOCATION = 'resources/i18n/en.json5';
|
||||
const LANGUAGE_FILES_LOCATION = 'resources/i18n';
|
||||
|
||||
parseCliInput();
|
||||
|
||||
/**
|
||||
* Parses the CLI input given by the user
|
||||
* If no parameters are set (standard usage) -> source file is default (set to DEFAULT_SOURCE_FILE_LOCATION) and all
|
||||
* other language files in the LANGUAGE_FILES_LOCATION are synced with this one in-place
|
||||
* (replaced with newly synced file)
|
||||
* If only target-file -t is set -> either -i in-place or -o output-file must be set
|
||||
* Source file can be set with -s if it should be something else than DEFAULT_SOURCE_FILE_LOCATION
|
||||
*
|
||||
* If any of the paths to files/dirs given by user are not valid, an error message is printed and script gets aborted
|
||||
*/
|
||||
function parseCliInput() {
|
||||
program
|
||||
.option('-d, --output-dir <output-dir>', 'output dir when running script on all language files; mutually exclusive with -o')
|
||||
.option('-t, --target-file <target>', 'target file we compare with and where completed output ends up if -o is not configured and -i is')
|
||||
.option('-i, --edit-in-place', 'edit-in-place; store output straight in target file; mutually exclusive with -o')
|
||||
.option('-s, --source-file <source>', 'source file to be parsed for translation', projectRoot(DEFAULT_SOURCE_FILE_LOCATION))
|
||||
.option('-o, --output-file <output>', 'where output of script ends up; mutually exclusive with -i')
|
||||
.usage('([-d <output-dir>] [-s <source-file>]) || (-t <target-file> (-i | -o <output>) [-s <source-file>])')
|
||||
.parse(process.argv);
|
||||
|
||||
if (!program.targetFile) {
|
||||
fs.readdirSync(projectRoot(LANGUAGE_FILES_LOCATION)).forEach(file => {
|
||||
if (!program.sourceFile.toString().endsWith(file)) {
|
||||
const targetFileLocation = projectRoot(LANGUAGE_FILES_LOCATION + "/" + file);
|
||||
console.log('Syncing file at: ' + targetFileLocation + ' with source file at: ' + program.sourceFile);
|
||||
if (program.outputDir) {
|
||||
if (!fs.existsSync(program.outputDir)) {
|
||||
fs.mkdirSync(program.outputDir);
|
||||
}
|
||||
const outputFileLocation = program.outputDir + "/" + file;
|
||||
console.log('Output location: ' + outputFileLocation);
|
||||
syncFileWithSource(targetFileLocation, outputFileLocation);
|
||||
} else {
|
||||
console.log('Replacing in target location');
|
||||
syncFileWithSource(targetFileLocation, targetFileLocation);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (program.targetFile && !checkIfPathToFileIsValid(program.targetFile)) {
|
||||
console.error('Directory path of target file is not valid.');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
if (program.targetFile && checkIfFileExists(program.targetFile) && !(program.editInPlace || program.outputFile)) {
|
||||
console.error('This target file already exists, if you want to overwrite this add option -i, or add an -o output location');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
if (!checkIfFileExists(program.sourceFile)) {
|
||||
console.error('Path of source file is not valid.');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
if (program.outputFile && !checkIfPathToFileIsValid(program.outputFile)) {
|
||||
console.error('Directory path of output file is not valid.');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
syncFileWithSource(program.targetFile, getOutputFileLocationIfExistsElseTargetFileLocation(program.targetFile));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates chunk lists for both the source and the target files (for example en.json5 and nl.json5 respectively)
|
||||
* > Creates output chunks by comparing the source chunk with corresponding target chunk (based on key of translation)
|
||||
* > Writes the output chunks to a new valid lang.json5 file, either replacing the target file (-i in-place)
|
||||
* or sending it to an output file specified by the user
|
||||
* @param pathToTargetFile Valid path to target file to generate target chunks from
|
||||
* @param pathToOutputFile Valid path to output file to write output chunks to
|
||||
*/
|
||||
function syncFileWithSource(pathToTargetFile, pathToOutputFile) {
|
||||
const progressBar = new _cliProgress.SingleBar({}, _cliProgress.Presets.shades_classic);
|
||||
progressBar.start(100, 0);
|
||||
|
||||
const sourceLines = [];
|
||||
const targetLines = [];
|
||||
const existingTargetFile = readFileIfExists(pathToTargetFile);
|
||||
existingTargetFile.toString().split("\n").forEach((function (line) {
|
||||
targetLines.push(line.trim());
|
||||
}));
|
||||
progressBar.update(10);
|
||||
const sourceFile = readFileIfExists(program.sourceFile);
|
||||
sourceFile.toString().split("\n").forEach((function (line) {
|
||||
sourceLines.push(line.trim());
|
||||
}));
|
||||
progressBar.update(20);
|
||||
const sourceChunks = createChunks(sourceLines, progressBar, false);
|
||||
const targetChunks = createChunks(targetLines, progressBar, true);
|
||||
|
||||
const outputChunks = compareChunksAndCreateOutput(sourceChunks, targetChunks, progressBar);
|
||||
|
||||
const file = fs.createWriteStream(pathToOutputFile);
|
||||
file.on('error', function (err) {
|
||||
console.error('Something went wrong writing to output file at: ' + pathToOutputFile + err)
|
||||
});
|
||||
file.on('open', function() {
|
||||
file.write("{\n");
|
||||
outputChunks.forEach(function (chunk) {
|
||||
progressBar.increment();
|
||||
chunk.split("\n").forEach(function (line) {
|
||||
file.write(" " + line + "\n");
|
||||
});
|
||||
});
|
||||
file.write("\n}");
|
||||
file.end();
|
||||
});
|
||||
file.on('finish', function() {
|
||||
const osName = process.platform;
|
||||
if (osName.startsWith("win")) {
|
||||
replaceLineEndingsToCRLF(pathToOutputFile);
|
||||
}
|
||||
});
|
||||
|
||||
progressBar.update(100);
|
||||
progressBar.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* For each of the source chunks:
|
||||
* - Determine if it's a new key-value => Add it to output, with source comments, source key-value commented, a message indicating it's new and the source-key value uncommented
|
||||
* - If it's not new, compare it with the corresponding target chunk and log the differences, see createNewChunkComparingSourceAndTarget
|
||||
* @param sourceChunks All the source chunks, split per key-value pair group
|
||||
* @param targetChunks All the target chunks, split per key-value pair group
|
||||
* @param progressBar The progressbar for the CLI
|
||||
* @return {Array} All the output chunks, split per key-value pair group
|
||||
*/
|
||||
function compareChunksAndCreateOutput(sourceChunks, targetChunks, progressBar) {
|
||||
const outputChunks = [];
|
||||
sourceChunks.map((sourceChunk) => {
|
||||
progressBar.increment();
|
||||
if (sourceChunk.trim().length !== 0) {
|
||||
let newChunk = [];
|
||||
const sourceList = sourceChunk.split("\n");
|
||||
const keyValueSource = sourceList[sourceList.length - 1];
|
||||
const keySource = getSubStringBeforeLastString(keyValueSource, ":");
|
||||
const commentSource = getSubStringBeforeLastString(sourceChunk, keyValueSource);
|
||||
|
||||
const correspondingTargetChunk = targetChunks.find((targetChunk) => {
|
||||
return targetChunk.includes(keySource);
|
||||
});
|
||||
|
||||
// Create new chunk with: the source comments, the commented source key-value, the todos and either the old target key-value pair or if it's a new pair, the source key-value pair
|
||||
newChunk.push(removeWhiteLines(commentSource));
|
||||
newChunk.push("// " + keyValueSource);
|
||||
if (correspondingTargetChunk === undefined) {
|
||||
newChunk.push(NEW_MESSAGE_TODO);
|
||||
newChunk.push(keyValueSource);
|
||||
} else {
|
||||
createNewChunkComparingSourceAndTarget(correspondingTargetChunk, sourceChunk, commentSource, keyValueSource, newChunk);
|
||||
}
|
||||
|
||||
outputChunks.push(newChunk.filter(Boolean).join("\n"));
|
||||
} else {
|
||||
outputChunks.push(sourceChunk);
|
||||
}
|
||||
});
|
||||
return outputChunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a corresponding target chunk is found:
|
||||
* - If old key value is not found in comments > Assumed it is new key
|
||||
* - If the target comments do not contain the source comments (because they have changed since last time) => Add comments changed message
|
||||
* - If the key-value in the target comments is not the same as the source key-value (because it changes since last time) => Add message changed message
|
||||
* - Add the old todos if they haven't been added already
|
||||
* - End with the original target key-value
|
||||
*/
|
||||
function createNewChunkComparingSourceAndTarget(correspondingTargetChunk, sourceChunk, commentSource, keyValueSource, newChunk) {
|
||||
let commentsOfSourceHaveChanged = false;
|
||||
let messageOfSourceHasChanged = false;
|
||||
|
||||
const targetList = correspondingTargetChunk.split("\n");
|
||||
const oldKeyValueInTargetComments = getSubStringWithRegex(correspondingTargetChunk, "\\s*\\/\\/\\s*\".*");
|
||||
const keyValueTarget = targetList[targetList.length - 1];
|
||||
|
||||
if (oldKeyValueInTargetComments != null) {
|
||||
const oldKeyValueUncommented = getSubStringWithRegex(oldKeyValueInTargetComments[0], "\".*")[0];
|
||||
|
||||
if (!(_.isEmpty(correspondingTargetChunk) && _.isEmpty(commentSource)) && !removeWhiteLines(correspondingTargetChunk).includes(removeWhiteLines(commentSource.trim()))) {
|
||||
commentsOfSourceHaveChanged = true;
|
||||
newChunk.push(COMMENTS_CHANGED_TODO);
|
||||
}
|
||||
const parsedOldKey = JSON5.stringify("{" + oldKeyValueUncommented + "}");
|
||||
const parsedSourceKey = JSON5.stringify("{" + keyValueSource + "}");
|
||||
if (!_.isEqual(parsedOldKey, parsedSourceKey)) {
|
||||
messageOfSourceHasChanged = true;
|
||||
newChunk.push(MESSAGE_CHANGED_TODO);
|
||||
}
|
||||
addOldTodosIfNeeded(targetList, newChunk, commentsOfSourceHaveChanged, messageOfSourceHasChanged);
|
||||
}
|
||||
newChunk.push(keyValueTarget);
|
||||
}
|
||||
|
||||
// Adds old todos found in target comments if they've not been added already
|
||||
function addOldTodosIfNeeded(targetList, newChunk, commentsOfSourceHaveChanged, messageOfSourceHasChanged) {
|
||||
targetList.map((targetLine) => {
|
||||
const foundTODO = getSubStringWithRegex(targetLine, "\\s*//\\s*TODO.*");
|
||||
if (foundTODO != null) {
|
||||
const todo = foundTODO[0];
|
||||
if (!((todo.includes(COMMENTS_CHANGED_TODO) && commentsOfSourceHaveChanged)
|
||||
|| (todo.includes(MESSAGE_CHANGED_TODO) && messageOfSourceHasChanged))) {
|
||||
newChunk.push(todo);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates chunks from an array of lines, each chunk contains either an empty line or a grouping of comments with their corresponding key-value pair
|
||||
* @param lines Array of lines, to be grouped into chunks
|
||||
* @param progressBar Progressbar of the CLI
|
||||
* @return {Array} Array of chunks, grouped by key-value and their corresponding comments or an empty line
|
||||
*/
|
||||
function createChunks(lines, progressBar, creatingTarget) {
|
||||
const chunks = [];
|
||||
let nextChunk = [];
|
||||
let onMultiLineComment = false;
|
||||
lines.map((line) => {
|
||||
progressBar.increment();
|
||||
if (line.length === 0) {
|
||||
chunks.push(line);
|
||||
}
|
||||
if (isOneLineCommentLine(line)) {
|
||||
nextChunk.push(line);
|
||||
}
|
||||
if (onMultiLineComment) {
|
||||
nextChunk.push(line);
|
||||
if (isEndOfMultiLineComment(line)) {
|
||||
onMultiLineComment = false;
|
||||
}
|
||||
}
|
||||
if (isStartOfMultiLineComment(line)) {
|
||||
nextChunk.push(line);
|
||||
onMultiLineComment = true;
|
||||
}
|
||||
if (isKeyValuePair(line)) {
|
||||
nextChunk.push(line);
|
||||
const newMessageLineIfExists = nextChunk.find((lineInChunk) => lineInChunk.trim().startsWith(NEW_MESSAGE_TODO));
|
||||
if (newMessageLineIfExists === undefined || !creatingTarget) {
|
||||
chunks.push(nextChunk.join("\n"));
|
||||
}
|
||||
nextChunk = [];
|
||||
}
|
||||
});
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function readFileIfExists(pathToFile) {
|
||||
if (checkIfFileExists(pathToFile)) {
|
||||
try {
|
||||
return fs.readFileSync(pathToFile, 'utf8');
|
||||
} catch (e) {
|
||||
console.error('Error:', e.stack);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isOneLineCommentLine(line) {
|
||||
return (line.startsWith("//"));
|
||||
}
|
||||
|
||||
function isStartOfMultiLineComment(line) {
|
||||
return (line.startsWith("/*"));
|
||||
}
|
||||
|
||||
function isEndOfMultiLineComment(line) {
|
||||
return (line.endsWith("*/"));
|
||||
}
|
||||
|
||||
function isKeyValuePair(line) {
|
||||
return (line.startsWith("\""));
|
||||
}
|
||||
|
||||
|
||||
function getSubStringWithRegex(string, regex) {
|
||||
return string.match(regex);
|
||||
}
|
||||
|
||||
function getSubStringBeforeLastString(string, char) {
|
||||
const lastCharIndex = string.lastIndexOf(char);
|
||||
return string.substr(0, lastCharIndex);
|
||||
}
|
||||
|
||||
|
||||
function getOutputFileLocationIfExistsElseTargetFileLocation(targetLocation) {
|
||||
if (program.outputFile) {
|
||||
return program.outputFile;
|
||||
}
|
||||
return targetLocation;
|
||||
}
|
||||
|
||||
function checkIfPathToFileIsValid(pathToCheck) {
|
||||
if (!pathToCheck.includes("/")) {
|
||||
return true;
|
||||
}
|
||||
return checkIfFileExists(getPathOfDirectory(pathToCheck));
|
||||
}
|
||||
|
||||
function checkIfFileExists(pathToCheck) {
|
||||
return fs.existsSync(pathToCheck);
|
||||
}
|
||||
|
||||
function getPathOfDirectory(pathToCheck) {
|
||||
return getSubStringBeforeLastString(pathToCheck, "/");
|
||||
}
|
||||
|
||||
function removeWhiteLines(string) {
|
||||
return string.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces UNIX \n LF line endings to windows \r\n CRLF line endings.
|
||||
* @param filePath Path to file whose line endings are being converted
|
||||
*/
|
||||
function replaceLineEndingsToCRLF(filePath) {
|
||||
const data = readFileIfExists(filePath);
|
||||
const result = data.replace(/\n/g,"\r\n");
|
||||
fs.writeFileSync(filePath, result, 'utf8');
|
||||
}
|
@@ -11,7 +11,6 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
|
||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
@@ -82,7 +82,8 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
const date = firstItemRD.payload.firstMetadataValue(metadataField);
|
||||
if (hasValue(date)) {
|
||||
const dateObj = new Date(date);
|
||||
lowerLimit = dateObj.getFullYear();
|
||||
// TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC.
|
||||
lowerLimit = dateObj.getUTCFullYear();
|
||||
}
|
||||
}
|
||||
const options = [];
|
||||
|
@@ -23,7 +23,7 @@ import {
|
||||
} from '../../shared/testing/utils';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -50,7 +50,7 @@ describe('ItemPageFieldComponent', () => {
|
||||
|
||||
export function mockItemWithMetadataFieldAndValue(field: string, value: string): Item {
|
||||
const item = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: new MetadataMap()
|
||||
});
|
||||
item.metadata[field] = [{
|
||||
|
@@ -22,7 +22,7 @@ import {
|
||||
} from '../../shared/testing/utils';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: [],
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
|
@@ -17,7 +17,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/ut
|
||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: new MetadataMap(),
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
|
@@ -83,7 +83,7 @@ describe('MetadataRepresentationListComponent', () => {
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should load 2 metadata-representation-loader components', () => {
|
||||
it('should load 2 ds-metadata-representation-loader components', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-metadata-representation-loader'));
|
||||
expect(fields.length).toBe(2);
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
@@ -16,7 +16,7 @@ import { RelationshipService } from '../../../core/data/relationship.service';
|
||||
* This component is used for displaying relations between items
|
||||
* It expects a parent item and relationship type, as well as a label to display on top
|
||||
*/
|
||||
export class RelatedItemsComponent implements OnInit {
|
||||
export class RelatedItemsComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The parent of the list of related items to display
|
||||
*/
|
||||
@@ -39,6 +39,11 @@ export class RelatedItemsComponent implements OnInit {
|
||||
*/
|
||||
@Input() label: string;
|
||||
|
||||
/**
|
||||
* Completely hide the component until there's at least one item visible
|
||||
*/
|
||||
@HostBinding('class.d-none') hidden = true;
|
||||
|
||||
/**
|
||||
* The list of related items
|
||||
*/
|
||||
@@ -60,11 +65,19 @@ export class RelatedItemsComponent implements OnInit {
|
||||
*/
|
||||
showingAll = false;
|
||||
|
||||
/**
|
||||
* Subscription on items used to update the "hidden" property of this component
|
||||
*/
|
||||
itemSub: Subscription;
|
||||
|
||||
constructor(public relationshipService: RelationshipService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options);
|
||||
this.itemSub = this.items$.subscribe((itemsRD: RemoteData<PaginatedList<Item>>) => {
|
||||
this.hidden = !(itemsRD.hasSucceeded && itemsRD.payload && itemsRD.payload.page.length > 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,4 +95,13 @@ export class RelatedItemsComponent implements OnInit {
|
||||
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options);
|
||||
this.showingAll = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the item subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (this.itemSub) {
|
||||
this.itemSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -11,17 +11,17 @@ import { RelationshipService } from '../../../core/data/relationship.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
const parentItem: Item = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: [],
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
const mockItem1: Item = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: [],
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
const mockItem2: Item = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: [],
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
|
43
src/app/+lookup-by-id/lookup-by-id-routing.module.ts
Normal file
43
src/app/+lookup-by-id/lookup-by-id-routing.module.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { LookupGuard } from './lookup-guard';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, UrlSegment } from '@angular/router';
|
||||
import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
matcher: urlMatcher,
|
||||
canActivate: [LookupGuard],
|
||||
component: ObjectNotFoundComponent }
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
LookupGuard
|
||||
]
|
||||
})
|
||||
|
||||
export class LookupRoutingModule {
|
||||
|
||||
}
|
||||
|
||||
export function urlMatcher(url) {
|
||||
// The expected path is :idType/:id
|
||||
const idType = url[0].path;
|
||||
// Allow for handles that are delimited with a forward slash.
|
||||
const id = url
|
||||
.slice(1)
|
||||
.map((us: UrlSegment) => us.path)
|
||||
.join('/');
|
||||
if (isNotEmpty(idType) && isNotEmpty(id)) {
|
||||
return {
|
||||
consumed: url,
|
||||
posParams: {
|
||||
idType: new UrlSegment(idType, {}),
|
||||
id: new UrlSegment(id, {})
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
23
src/app/+lookup-by-id/lookup-by-id.module.ts
Normal file
23
src/app/+lookup-by-id/lookup-by-id.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { LookupRoutingModule } from './lookup-by-id-routing.module';
|
||||
import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component';
|
||||
import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
LookupRoutingModule,
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
ObjectNotFoundComponent
|
||||
],
|
||||
providers: [
|
||||
DsoRedirectDataService
|
||||
]
|
||||
})
|
||||
export class LookupIdModule {
|
||||
|
||||
}
|
50
src/app/+lookup-by-id/lookup-guard.spec.ts
Normal file
50
src/app/+lookup-by-id/lookup-guard.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { LookupGuard } from './lookup-guard';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { IdentifierType } from '../core/data/request.models';
|
||||
|
||||
describe('LookupGuard', () => {
|
||||
let dsoService: any;
|
||||
let guard: any;
|
||||
|
||||
beforeEach(() => {
|
||||
dsoService = {
|
||||
findById: jasmine.createSpy('findById').and.returnValue(observableOf({ hasFailed: false,
|
||||
hasSucceeded: true }))
|
||||
};
|
||||
guard = new LookupGuard(dsoService);
|
||||
});
|
||||
|
||||
it('should call findById with handle params', () => {
|
||||
const scopedRoute = {
|
||||
params: {
|
||||
id: '1234',
|
||||
idType: '123456789'
|
||||
}
|
||||
};
|
||||
guard.canActivate(scopedRoute as any, undefined);
|
||||
expect(dsoService.findById).toHaveBeenCalledWith('123456789/1234', IdentifierType.HANDLE)
|
||||
});
|
||||
|
||||
it('should call findById with handle params', () => {
|
||||
const scopedRoute = {
|
||||
params: {
|
||||
id: '123456789%2F1234',
|
||||
idType: 'handle'
|
||||
}
|
||||
};
|
||||
guard.canActivate(scopedRoute as any, undefined);
|
||||
expect(dsoService.findById).toHaveBeenCalledWith('123456789%2F1234', IdentifierType.HANDLE)
|
||||
});
|
||||
|
||||
it('should call findById with UUID params', () => {
|
||||
const scopedRoute = {
|
||||
params: {
|
||||
id: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
|
||||
idType: 'uuid'
|
||||
}
|
||||
};
|
||||
guard.canActivate(scopedRoute as any, undefined);
|
||||
expect(dsoService.findById).toHaveBeenCalledWith('34cfed7c-f597-49ef-9cbe-ea351f0023c2', IdentifierType.UUID)
|
||||
});
|
||||
|
||||
});
|
53
src/app/+lookup-by-id/lookup-guard.ts
Normal file
53
src/app/+lookup-by-id/lookup-guard.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { IdentifierType } from '../core/data/request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { FindByIDRequest } from '../core/data/request.models';
|
||||
import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service';
|
||||
|
||||
interface LookupParams {
|
||||
type: IdentifierType;
|
||||
id: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LookupGuard implements CanActivate {
|
||||
|
||||
constructor(private dsoService: DsoRedirectDataService) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable<boolean> {
|
||||
const params = this.getLookupParams(route);
|
||||
return this.dsoService.findById(params.id, params.type).pipe(
|
||||
map((response: RemoteData<FindByIDRequest>) => response.hasFailed)
|
||||
);
|
||||
}
|
||||
|
||||
private getLookupParams(route: ActivatedRouteSnapshot): LookupParams {
|
||||
let type;
|
||||
let id;
|
||||
const idType = route.params.idType;
|
||||
|
||||
// If the idType is not recognized, assume a legacy handle request (handle/prefix/id)
|
||||
if (idType !== IdentifierType.HANDLE && idType !== IdentifierType.UUID) {
|
||||
type = IdentifierType.HANDLE;
|
||||
const prefix = route.params.idType;
|
||||
const handleId = route.params.id;
|
||||
id = `${prefix}/${handleId}`;
|
||||
|
||||
} else if (route.params.idType === IdentifierType.HANDLE) {
|
||||
type = IdentifierType.HANDLE;
|
||||
id = route.params.id;
|
||||
|
||||
} else {
|
||||
type = IdentifierType.UUID;
|
||||
id = route.params.id;
|
||||
}
|
||||
return {
|
||||
type: type,
|
||||
id: id
|
||||
};
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
<div class="object-not-found container">
|
||||
<h1>{{"error.identifier" | translate}}</h1>
|
||||
<h2><small><em>{{missingItem}}</em></small></h2>
|
||||
<br />
|
||||
<p class="text-center">
|
||||
<a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a>
|
||||
</p>
|
||||
</div>
|
@@ -0,0 +1,79 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { ObjectNotFoundComponent } from './objectnotfound.component';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
describe('ObjectNotFoundComponent', () => {
|
||||
let comp: ObjectNotFoundComponent;
|
||||
let fixture: ComponentFixture<ObjectNotFoundComponent>;
|
||||
const testUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||
const uuidType = 'uuid';
|
||||
const handlePrefix = '123456789';
|
||||
const handleId = '22';
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({id: testUUID, idType: uuidType})
|
||||
});
|
||||
const activatedRouteStubHandle = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({id: handleId, idType: handlePrefix})
|
||||
});
|
||||
describe('uuid request', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
], providers: [
|
||||
{provide: ActivatedRoute, useValue: activatedRouteStub}
|
||||
],
|
||||
declarations: [ObjectNotFoundComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ObjectNotFoundComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create instance', () => {
|
||||
expect(comp).toBeDefined()
|
||||
});
|
||||
|
||||
it('should have id and idType', () => {
|
||||
expect(comp.id).toEqual(testUUID);
|
||||
expect(comp.idType).toEqual(uuidType);
|
||||
expect(comp.missingItem).toEqual('uuid: ' + testUUID);
|
||||
});
|
||||
});
|
||||
|
||||
describe( 'legacy handle request', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
], providers: [
|
||||
{provide: ActivatedRoute, useValue: activatedRouteStubHandle}
|
||||
],
|
||||
declarations: [ObjectNotFoundComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ObjectNotFoundComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should have handle prefix and id', () => {
|
||||
expect(comp.id).toEqual(handleId);
|
||||
expect(comp.idType).toEqual(handlePrefix);
|
||||
expect(comp.missingItem).toEqual('handle: ' + handlePrefix + '/' + handleId);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,43 @@
|
||||
|
||||
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* This component representing the `PageNotFound` DSpace page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-objnotfound',
|
||||
styleUrls: ['./objectnotfound.component.scss'],
|
||||
templateUrl: './objectnotfound.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Default
|
||||
})
|
||||
export class ObjectNotFoundComponent implements OnInit {
|
||||
|
||||
idType: string;
|
||||
|
||||
id: string;
|
||||
|
||||
missingItem: string;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {AuthService} authservice
|
||||
* @param {ServerResponseService} responseService
|
||||
*/
|
||||
constructor(private route: ActivatedRoute) {
|
||||
route.params.subscribe((params) => {
|
||||
this.idType = params.idType;
|
||||
this.id = params.id;
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.idType.startsWith('handle') || this.idType.startsWith('uuid')) {
|
||||
this.missingItem = this.idType + ': ' + this.id;
|
||||
} else {
|
||||
this.missingItem = 'handle: ' + this.idType + '/' + this.id;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -15,7 +15,6 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from './my-dspace-page.component';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { routeServiceStub } from '../shared/testing/route-service-stub';
|
||||
@@ -49,6 +48,7 @@ describe('MyDSpacePageComponent', () => {
|
||||
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
||||
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||
search: mockResults,
|
||||
getEndpoint: observableOf('discover/search/objects'),
|
||||
getSearchLink: '/mydspace',
|
||||
getScopes: observableOf(['test-scope']),
|
||||
setServiceOptions: {}
|
||||
@@ -75,6 +75,7 @@ describe('MyDSpacePageComponent', () => {
|
||||
scope: scopeParam
|
||||
})
|
||||
};
|
||||
|
||||
const sidebarService = {
|
||||
isCollapsed: observableOf(true),
|
||||
collapse: () => this.isCollapsed = observableOf(true),
|
||||
|
@@ -124,13 +124,13 @@ export class MyDSpacePageComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.configurationList$ = this.searchConfigService.getAvailableConfigurationOptions();
|
||||
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
tap(() => this.resultsRD$.next(null)),
|
||||
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getSucceededRemoteData())))
|
||||
.subscribe((results) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||
);
|
||||
|
@@ -27,6 +27,8 @@ export function getAdminModulePath() {
|
||||
RouterModule.forRoot([
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||
|
@@ -128,7 +128,7 @@ const EXPORTS = [
|
||||
...PROVIDERS
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS,
|
||||
...DECLARATIONS
|
||||
],
|
||||
exports: [
|
||||
...EXPORTS
|
||||
|
@@ -44,7 +44,11 @@ export class AuthRequestService {
|
||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => new AuthPostRequest(this.requestService.generateRequestId(), endpointURL, body, options)),
|
||||
tap((request: PostRequest) => this.requestService.configure(request, true)),
|
||||
map ((request: PostRequest) => {
|
||||
request.responseMsToLive = 10 * 1000;
|
||||
return request;
|
||||
}),
|
||||
tap((request: PostRequest) => this.requestService.configure(request)),
|
||||
mergeMap((request: PostRequest) => this.fetchRequest(request)),
|
||||
distinctUntilChanged());
|
||||
}
|
||||
@@ -55,7 +59,11 @@ export class AuthRequestService {
|
||||
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => new AuthGetRequest(this.requestService.generateRequestId(), endpointURL, options)),
|
||||
tap((request: GetRequest) => this.requestService.configure(request, true)),
|
||||
map ((request: GetRequest) => {
|
||||
request.responseMsToLive = 10 * 1000;
|
||||
return request;
|
||||
}),
|
||||
tap((request: GetRequest) => this.requestService.configure(request)),
|
||||
mergeMap((request: GetRequest) => this.fetchRequest(request)),
|
||||
distinctUntilChanged());
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) {
|
||||
const response = this.process<NormalizedObject<AuthStatus>>(data.payload, request.uuid);
|
||||
const response = this.process<NormalizedObject<AuthStatus>>(data.payload, request);
|
||||
return new AuthStatusResponse(response, data.statusCode, data.statusText);
|
||||
} else {
|
||||
return new AuthStatusResponse(data.payload as NormalizedAuthStatus, data.statusCode, data.statusText);
|
||||
|
@@ -249,30 +249,34 @@ describe('AuthService test', () => {
|
||||
|
||||
it ('should set redirect url to previous page', () => {
|
||||
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
||||
spyOn(routerStub, 'navigateByUrl');
|
||||
authService.redirectAfterLoginSuccess(true);
|
||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith(['/collection/123']);
|
||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/collection/123');
|
||||
});
|
||||
|
||||
it ('should set redirect url to current page', () => {
|
||||
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
||||
spyOn(routerStub, 'navigateByUrl');
|
||||
authService.redirectAfterLoginSuccess(false);
|
||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith(['/home']);
|
||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/home');
|
||||
});
|
||||
|
||||
it ('should redirect to / and not to /login', () => {
|
||||
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['/login', '/login']));
|
||||
spyOn(routerStub, 'navigateByUrl');
|
||||
authService.redirectAfterLoginSuccess(true);
|
||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith(['/']);
|
||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
it ('should redirect to / when no redirect url is found', () => {
|
||||
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['']));
|
||||
spyOn(routerStub, 'navigateByUrl');
|
||||
authService.redirectAfterLoginSuccess(true);
|
||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith(['/']);
|
||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -347,8 +347,7 @@ export class AuthService {
|
||||
if (isNotEmpty(redirectUrl)) {
|
||||
this.clearRedirectUrl();
|
||||
this.router.onSameUrlNavigation = 'reload';
|
||||
const url = decodeURIComponent(redirectUrl);
|
||||
this.navigateToRedirectUrl(url);
|
||||
this.navigateToRedirectUrl(redirectUrl);
|
||||
} else {
|
||||
// If redirectUrl is empty use history.
|
||||
this.routeService.getHistory().pipe(
|
||||
@@ -368,16 +367,17 @@ export class AuthService {
|
||||
|
||||
}
|
||||
|
||||
protected navigateToRedirectUrl(url: string) {
|
||||
protected navigateToRedirectUrl(redirectUrl: string) {
|
||||
const url = decodeURIComponent(redirectUrl);
|
||||
// in case the user navigates directly to /login (via bookmark, etc), or the route history is not found.
|
||||
if (isEmpty(url) || url.startsWith(LOGIN_ROUTE)) {
|
||||
this.router.navigate(['/']);
|
||||
this.router.navigateByUrl('/');
|
||||
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
|
||||
// this._window.nativeWindow.location.href = '/';
|
||||
} else {
|
||||
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
|
||||
// this._window.nativeWindow.location.href = url;
|
||||
this.router.navigate([url]);
|
||||
this.router.navigateByUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -114,7 +114,7 @@ describe('BrowseService', () => {
|
||||
scheduler.schedule(() => service.getBrowseDefinitions().subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
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, undefined);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
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, undefined);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||
@@ -303,7 +303,7 @@ describe('BrowseService', () => {
|
||||
scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
|
||||
|
@@ -11,6 +11,13 @@ import { Bitstream } from '../../shared/bitstream.model';
|
||||
@mapsTo(Bundle)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedBundle extends NormalizedDSpaceObject<Bundle> {
|
||||
|
||||
/**
|
||||
* The bundle's name
|
||||
*/
|
||||
@autoserialize
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The primary bitstream of this Bundle
|
||||
*/
|
||||
|
@@ -3,13 +3,9 @@ import { inheritSerialization, deserialize, autoserialize, autoserializeAs } fro
|
||||
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
||||
import { Item } from '../../shared/item.model';
|
||||
import { mapsTo, relationship } from '../builders/build-decorators';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { NormalizedCollection } from './normalized-collection.model';
|
||||
import { NormalizedBitstream } from './normalized-bitstream.model';
|
||||
import { NormalizedRelationship } from './items/normalized-relationship.model';
|
||||
import { Collection } from '../../shared/collection.model';
|
||||
import { Bitstream } from '../../shared/bitstream.model';
|
||||
import { Relationship } from '../../shared/item-relationships/relationship.model';
|
||||
import { Bundle } from '../../shared/bundle.model';
|
||||
|
||||
/**
|
||||
* Normalized model class for a DSpace Item
|
||||
@@ -66,8 +62,8 @@ export class NormalizedItem extends NormalizedDSpaceObject<Item> {
|
||||
* List of Bitstreams that are owned by this Item
|
||||
*/
|
||||
@deserialize
|
||||
@relationship(Bitstream, true)
|
||||
bitstreams: string[];
|
||||
@relationship(Bundle, true)
|
||||
bundles: string[];
|
||||
|
||||
@autoserialize
|
||||
@relationship(Relationship, true)
|
||||
|
1
src/app/core/cache/object-cache.reducer.ts
vendored
1
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -44,6 +44,7 @@ export abstract class TypedObject {
|
||||
*/
|
||||
export class CacheableObject extends TypedObject {
|
||||
uuid?: string;
|
||||
handle?: string;
|
||||
self: string;
|
||||
// isNew: boolean;
|
||||
// dirtyType: DirtyType;
|
||||
|
5
src/app/core/cache/object-cache.service.ts
vendored
5
src/app/core/cache/object-cache.service.ts
vendored
@@ -4,7 +4,7 @@ import { applyPatch, Operation } from 'fast-json-patch';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
|
||||
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
@@ -80,7 +80,8 @@ export class ObjectCacheService {
|
||||
* @return Observable<NormalizedObject<T>>
|
||||
* An observable of the requested object in normalized form
|
||||
*/
|
||||
getObjectByUUID<T extends CacheableObject>(uuid: string): Observable<NormalizedObject<T>> {
|
||||
getObjectByUUID<T extends CacheableObject>(uuid: string):
|
||||
Observable<NormalizedObject<T>> {
|
||||
return this.store.pipe(
|
||||
select(selfLinkFromUuidSelector(uuid)),
|
||||
mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink)
|
||||
|
@@ -24,7 +24,7 @@ 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)) {
|
||||
const configDefinition = this.process<ConfigObject>(data.payload, request.uuid);
|
||||
const configDefinition = this.process<ConfigObject>(data.payload, request);
|
||||
return new ConfigSuccessResponse(configDefinition, data.statusCode, data.statusText, this.processPageInfo(data.payload));
|
||||
} else {
|
||||
return new ErrorResponse(
|
||||
|
@@ -9,6 +9,7 @@ import { PaginatedList } from './paginated-list';
|
||||
import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service';
|
||||
import { ResourceType } from '../shared/resource-type';
|
||||
import { getMapsToType } from '../cache/builders/build-decorators';
|
||||
import { RestRequest } from './request.models';
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
export abstract class BaseResponseParsingService {
|
||||
@@ -16,14 +17,14 @@ export abstract class BaseResponseParsingService {
|
||||
protected abstract objectCache: ObjectCacheService;
|
||||
protected abstract toCache: boolean;
|
||||
|
||||
protected process<ObjectDomain>(data: any, requestUUID: string): any {
|
||||
protected process<ObjectDomain>(data: any, request: RestRequest): any {
|
||||
if (isNotEmpty(data)) {
|
||||
if (hasNoValue(data) || (typeof data !== 'object')) {
|
||||
return data;
|
||||
} else if (isRestPaginatedList(data)) {
|
||||
return this.processPaginatedList(data, requestUUID);
|
||||
return this.processPaginatedList(data, request);
|
||||
} else if (Array.isArray(data)) {
|
||||
return this.processArray(data, requestUUID);
|
||||
return this.processArray(data, request);
|
||||
} else if (isRestDataObject(data)) {
|
||||
const object = this.deserialize(data);
|
||||
if (isNotEmpty(data._embedded)) {
|
||||
@@ -31,7 +32,7 @@ export abstract class BaseResponseParsingService {
|
||||
.keys(data._embedded)
|
||||
.filter((property) => data._embedded.hasOwnProperty(property))
|
||||
.forEach((property) => {
|
||||
const parsedObj = this.process<ObjectDomain>(data._embedded[property], requestUUID);
|
||||
const parsedObj = this.process<ObjectDomain>(data._embedded[property], request);
|
||||
if (isNotEmpty(parsedObj)) {
|
||||
if (isRestPaginatedList(data._embedded[property])) {
|
||||
object[property] = parsedObj;
|
||||
@@ -44,7 +45,8 @@ export abstract class BaseResponseParsingService {
|
||||
}
|
||||
});
|
||||
}
|
||||
this.cache(object, requestUUID);
|
||||
|
||||
this.cache(object, request);
|
||||
return object;
|
||||
}
|
||||
const result = {};
|
||||
@@ -52,14 +54,14 @@ export abstract class BaseResponseParsingService {
|
||||
.filter((property) => data.hasOwnProperty(property))
|
||||
.filter((property) => hasValue(data[property]))
|
||||
.forEach((property) => {
|
||||
result[property] = this.process(data[property], requestUUID);
|
||||
result[property] = this.process(data[property], request);
|
||||
});
|
||||
return result;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected processPaginatedList<ObjectDomain>(data: any, requestUUID: string): PaginatedList<ObjectDomain> {
|
||||
protected processPaginatedList<ObjectDomain>(data: any, request: RestRequest): PaginatedList<ObjectDomain> {
|
||||
const pageInfo: PageInfo = this.processPageInfo(data);
|
||||
let list = data._embedded;
|
||||
|
||||
@@ -69,14 +71,14 @@ export abstract class BaseResponseParsingService {
|
||||
} else if (!Array.isArray(list)) {
|
||||
list = this.flattenSingleKeyObject(list);
|
||||
}
|
||||
const page: ObjectDomain[] = this.processArray(list, requestUUID);
|
||||
const page: ObjectDomain[] = this.processArray(list, request);
|
||||
return new PaginatedList<ObjectDomain>(pageInfo, page, );
|
||||
}
|
||||
|
||||
protected processArray<ObjectDomain>(data: any, requestUUID: string): ObjectDomain[] {
|
||||
protected processArray<ObjectDomain>(data: any, request: RestRequest): ObjectDomain[] {
|
||||
let array: ObjectDomain[] = [];
|
||||
data.forEach((datum) => {
|
||||
array = [...array, this.process(datum, requestUUID)];
|
||||
array = [...array, this.process(datum, request)];
|
||||
}
|
||||
);
|
||||
return array;
|
||||
@@ -103,17 +105,17 @@ export abstract class BaseResponseParsingService {
|
||||
}
|
||||
}
|
||||
|
||||
protected cache<ObjectDomain>(obj, requestUUID) {
|
||||
protected cache<ObjectDomain>(obj, request: RestRequest) {
|
||||
if (this.toCache) {
|
||||
this.addToObjectCache(obj, requestUUID);
|
||||
this.addToObjectCache(obj, request);
|
||||
}
|
||||
}
|
||||
|
||||
protected addToObjectCache(co: CacheableObject, requestUUID: string): void {
|
||||
protected addToObjectCache(co: CacheableObject, request: RestRequest): void {
|
||||
if (hasNoValue(co) || hasNoValue(co.self)) {
|
||||
throw new Error('The server returned an invalid object');
|
||||
}
|
||||
this.objectCache.add(co, this.EnvConfig.cache.msToLive.default, requestUUID);
|
||||
this.objectCache.add(co, hasValue(request.responseMsToLive) ? request.responseMsToLive : this.EnvConfig.cache.msToLive.default, request.uuid);
|
||||
}
|
||||
|
||||
processPageInfo(payload: any): PageInfo {
|
||||
|
@@ -38,7 +38,6 @@ const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSele
|
||||
export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
||||
|
||||
protected linkPath = 'bitstreamformats';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
46
src/app/core/data/bundle-data.service.ts
Normal file
46
src/app/core/data/bundle-data.service.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DataService } from './data.service';
|
||||
import { Bundle } from '../shared/bundle.model';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { FindAllOptions } from './request.models';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
|
||||
/**
|
||||
* A service responsible for fetching/sending data from/to the REST API on the bundles endpoint
|
||||
*/
|
||||
@Injectable()
|
||||
export class BundleDataService extends DataService<Bundle> {
|
||||
protected linkPath = 'bundles';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected dataBuildService: NormalizedObjectBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<Bundle>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing bundles
|
||||
* @param {FindAllOptions} options
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
getBrowseEndpoint(options: FindAllOptions = {}, linkPath?: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
}
|
@@ -37,7 +37,7 @@ describe('CollectionDataService', () => {
|
||||
});
|
||||
|
||||
it('should configure a GET request', () => {
|
||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest), undefined);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest));
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -31,7 +31,6 @@ import { PaginatedSearchOptions } from '../../shared/search/paginated-search-opt
|
||||
@Injectable()
|
||||
export class CollectionDataService extends ComColDataService<Collection> {
|
||||
protected linkPath = 'collections';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
@@ -28,7 +28,6 @@ class NormalizedTestObject extends NormalizedObject<Item> {
|
||||
}
|
||||
|
||||
class TestService extends ComColDataService<any> {
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
@@ -24,7 +24,6 @@ export class CommunityDataService extends ComColDataService<Community> {
|
||||
protected linkPath = 'communities';
|
||||
protected topLinkPath = 'communities/search/top';
|
||||
protected cds = this;
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
@@ -25,7 +25,6 @@ class NormalizedTestObject extends NormalizedObject<Item> {
|
||||
}
|
||||
|
||||
class TestService extends DataService<any> {
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
@@ -11,14 +11,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { RemoteData } from './remote-data';
|
||||
import {
|
||||
CreateRequest,
|
||||
DeleteByIDRequest,
|
||||
FindAllOptions,
|
||||
FindAllRequest,
|
||||
FindByIDRequest,
|
||||
GetRequest
|
||||
} from './request.models';
|
||||
import { CreateRequest, DeleteByIDRequest, FindAllOptions, FindAllRequest, FindByIDRequest, 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';
|
||||
@@ -45,11 +38,14 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
protected abstract store: Store<CoreState>;
|
||||
protected abstract linkPath: string;
|
||||
protected abstract halService: HALEndpointService;
|
||||
protected abstract forceBypassCache = false;
|
||||
protected abstract objectCache: ObjectCacheService;
|
||||
protected abstract notificationsService: NotificationsService;
|
||||
protected abstract http: HttpClient;
|
||||
protected abstract comparator: ChangeAnalyzer<T>;
|
||||
/**
|
||||
* Allows subclasses to reset the response cache time.
|
||||
*/
|
||||
protected responseMsToLive: number;
|
||||
|
||||
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
|
||||
|
||||
@@ -131,7 +127,10 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
first((href: string) => hasValue(href)))
|
||||
.subscribe((href: string) => {
|
||||
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
|
||||
this.requestService.configure(request, this.forceBypassCache);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.configure(request);
|
||||
});
|
||||
|
||||
return this.rdbService.buildList<T>(hrefObs) as Observable<RemoteData<PaginatedList<T>>>;
|
||||
@@ -147,21 +146,29 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
}
|
||||
|
||||
findById(id: string): Observable<RemoteData<T>> {
|
||||
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
map((endpoint: string) => this.getIDHref(endpoint, id)));
|
||||
map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id))));
|
||||
|
||||
hrefObs.pipe(
|
||||
find((href: string) => hasValue(href)))
|
||||
.subscribe((href: string) => {
|
||||
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id);
|
||||
this.requestService.configure(request, this.forceBypassCache);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.configure(request);
|
||||
});
|
||||
|
||||
return this.rdbService.buildSingle<T>(hrefObs);
|
||||
}
|
||||
|
||||
findByHref(href: string, options?: HttpOptions): Observable<RemoteData<T>> {
|
||||
this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href, null, options), this.forceBypassCache);
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), href, null, options);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.configure(request);
|
||||
return this.rdbService.buildSingle<T>(href);
|
||||
}
|
||||
|
||||
@@ -193,7 +200,9 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
tap((href: string) => {
|
||||
this.requestService.removeByHrefSubstring(href);
|
||||
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
|
||||
this.requestService.configure(request, true);
|
||||
request.responseMsToLive = 10 * 1000;
|
||||
|
||||
this.requestService.configure(request);
|
||||
}
|
||||
),
|
||||
switchMap((href) => this.requestService.getByHref(href)),
|
||||
|
155
src/app/core/data/dso-redirect-data.service.spec.ts
Normal file
155
src/app/core/data/dso-redirect-data.service.spec.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { FindByIDRequest, IdentifierType } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { DsoRedirectDataService } from './dso-redirect-data.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
|
||||
describe('DsoRedirectDataService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: DsoRedirectDataService;
|
||||
let halService: HALEndpointService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let router;
|
||||
let remoteData;
|
||||
const dsoUUID = '9b4f22f4-164a-49db-8817-3316b6ee5746';
|
||||
const dsoHandle = '1234567789/22';
|
||||
const encodedHandle = encodeURIComponent(dsoHandle);
|
||||
const pidLink = 'https://rest.api/rest/api/pid/find{?id}';
|
||||
const requestHandleURL = `https://rest.api/rest/api/pid/find?id=${encodedHandle}`;
|
||||
const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`;
|
||||
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||
const store = {} as Store<CoreState>;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||
const objectCache = {} as ObjectCacheService;
|
||||
let setup;
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', {a: pidLink})
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
configure: true
|
||||
});
|
||||
router = {
|
||||
navigate: jasmine.createSpy('navigate')
|
||||
};
|
||||
|
||||
remoteData = {
|
||||
isSuccessful: true,
|
||||
error: undefined,
|
||||
hasSucceeded: true,
|
||||
isLoading: false,
|
||||
payload: {
|
||||
type: 'item',
|
||||
uuid: '123456789'
|
||||
}
|
||||
};
|
||||
|
||||
setup = () => {
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: cold('a', {
|
||||
a: remoteData
|
||||
})
|
||||
});
|
||||
service = new DsoRedirectDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
dataBuildService,
|
||||
store,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
http,
|
||||
comparator,
|
||||
router
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should call HALEndpointService with the path to the pid endpoint', () => {
|
||||
setup();
|
||||
scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE));
|
||||
scheduler.flush();
|
||||
|
||||
expect(halService.getEndpoint).toHaveBeenCalledWith('pid');
|
||||
});
|
||||
|
||||
it('should call HALEndpointService with the path to the dso endpoint', () => {
|
||||
setup();
|
||||
scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID));
|
||||
scheduler.flush();
|
||||
|
||||
expect(halService.getEndpoint).toHaveBeenCalledWith('dso');
|
||||
});
|
||||
|
||||
it('should call HALEndpointService with the path to the dso endpoint when identifier type not specified', () => {
|
||||
setup();
|
||||
scheduler.schedule(() => service.findById(dsoUUID));
|
||||
scheduler.flush();
|
||||
|
||||
expect(halService.getEndpoint).toHaveBeenCalledWith('dso');
|
||||
});
|
||||
|
||||
it('should configure the proper FindByIDRequest for uuid', () => {
|
||||
setup();
|
||||
scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID));
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestUUIDURL, dsoUUID));
|
||||
});
|
||||
|
||||
it('should configure the proper FindByIDRequest for handle', () => {
|
||||
setup();
|
||||
scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE));
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestHandleURL, dsoHandle));
|
||||
});
|
||||
|
||||
it('should navigate to item route', () => {
|
||||
remoteData.payload.type = 'item';
|
||||
setup();
|
||||
const redir = service.findById(dsoHandle, IdentifierType.HANDLE);
|
||||
// The framework would normally subscribe but do it here so we can test navigation.
|
||||
redir.subscribe();
|
||||
scheduler.schedule(() => redir);
|
||||
scheduler.flush();
|
||||
expect(router.navigate).toHaveBeenCalledWith([remoteData.payload.type + 's/' + remoteData.payload.uuid]);
|
||||
});
|
||||
|
||||
it('should navigate to collections route', () => {
|
||||
remoteData.payload.type = 'collection';
|
||||
setup();
|
||||
const redir = service.findById(dsoHandle, IdentifierType.HANDLE);
|
||||
redir.subscribe();
|
||||
scheduler.schedule(() => redir);
|
||||
scheduler.flush();
|
||||
expect(router.navigate).toHaveBeenCalledWith([remoteData.payload.type + 's/' + remoteData.payload.uuid]);
|
||||
});
|
||||
|
||||
it('should navigate to communities route', () => {
|
||||
remoteData.payload.type = 'community';
|
||||
setup();
|
||||
const redir = service.findById(dsoHandle, IdentifierType.HANDLE);
|
||||
redir.subscribe();
|
||||
scheduler.schedule(() => redir);
|
||||
scheduler.flush();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['communities/' + remoteData.payload.uuid]);
|
||||
});
|
||||
})
|
||||
});
|
90
src/app/core/data/dso-redirect-data.service.ts
Normal file
90
src/app/core/data/dso-redirect-data.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { DataService } from './data.service';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { FindAllOptions, FindByIDRequest, IdentifierType } from './request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { filter, take, tap } from 'rxjs/operators';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { getFinishedRemoteData } from '../shared/operators';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Injectable()
|
||||
export class DsoRedirectDataService extends DataService<any> {
|
||||
|
||||
// Set the default link path to the identifier lookup endpoint.
|
||||
protected linkPath = 'pid';
|
||||
protected forceBypassCache = false;
|
||||
private uuidEndpoint = 'dso';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected dataBuildService: NormalizedObjectBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DSOChangeAnalyzer<any>,
|
||||
private router: Router) {
|
||||
super();
|
||||
}
|
||||
|
||||
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||
return this.halService.getEndpoint(linkPath);
|
||||
}
|
||||
|
||||
setLinkPath(identifierType: IdentifierType) {
|
||||
// The default 'pid' endpoint for identifiers does not support uuid lookups.
|
||||
// For uuid lookups we need to change the linkPath.
|
||||
if (identifierType === IdentifierType.UUID) {
|
||||
this.linkPath = this.uuidEndpoint;
|
||||
}
|
||||
}
|
||||
|
||||
getIDHref(endpoint, resourceID): string {
|
||||
// Supporting both identifier (pid) and uuid (dso) endpoints
|
||||
return endpoint.replace(/\{\?id\}/, `?id=${resourceID}`)
|
||||
.replace(/\{\?uuid\}/, `?uuid=${resourceID}`);
|
||||
}
|
||||
|
||||
findById(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<FindByIDRequest>> {
|
||||
this.setLinkPath(identifierType);
|
||||
return super.findById(id).pipe(
|
||||
getFinishedRemoteData(),
|
||||
take(1),
|
||||
tap((response) => {
|
||||
if (response.hasSucceeded) {
|
||||
const uuid = response.payload.uuid;
|
||||
const newRoute = this.getEndpointFromDSOType(response.payload.type);
|
||||
if (hasValue(uuid) && hasValue(newRoute)) {
|
||||
this.router.navigate([newRoute + '/' + uuid]);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
// Is there an existing method somewhere else that converts dso type to route?
|
||||
getEndpointFromDSOType(dsoType: string): string {
|
||||
// Are there other types to consider?
|
||||
if (dsoType.startsWith('item')) {
|
||||
return 'items'
|
||||
} else if (dsoType.startsWith('community')) {
|
||||
return 'communities';
|
||||
} else if (dsoType.startsWith('collection')) {
|
||||
return 'collections'
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
@@ -30,7 +30,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
|
||||
if (hasValue(data.payload) && hasValue(data.payload.page) && data.payload.page.totalElements === 0) {
|
||||
processRequestDTO = { page: [] };
|
||||
} else {
|
||||
processRequestDTO = this.process<NormalizedObject<DSpaceObject>>(data.payload, request.uuid);
|
||||
processRequestDTO = this.process<NormalizedObject<DSpaceObject>>(data.payload, request);
|
||||
}
|
||||
let objectList = processRequestDTO;
|
||||
|
||||
|
@@ -72,7 +72,7 @@ describe('DSpaceObjectDataService', () => {
|
||||
scheduler.schedule(() => service.findById(testObject.uuid));
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid), false);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid));
|
||||
});
|
||||
|
||||
it('should return a RemoteData<DSpaceObject> for the object with the given ID', () => {
|
||||
|
@@ -18,7 +18,6 @@ import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
class DataServiceImpl extends DataService<DSpaceObject> {
|
||||
protected linkPath = 'dso';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
@@ -41,7 +41,6 @@ import { PaginatedList } from './paginated-list';
|
||||
@Injectable()
|
||||
export class ItemDataService extends DataService<Item> {
|
||||
protected linkPath = 'items';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
@@ -19,7 +19,6 @@ import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
class DataServiceImpl extends DataService<MetadataSchema> {
|
||||
protected linkPath = 'metadataschemas';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
@@ -23,13 +23,6 @@ describe('RelationshipService', () => {
|
||||
const restEndpointURL = 'https://rest.api/';
|
||||
const relationshipsEndpointURL = `${restEndpointURL}/relationships`;
|
||||
const halService: any = new HALEndpointServiceStub(restEndpointURL);
|
||||
const rdbService = getMockRemoteDataBuildService();
|
||||
const objectCache = Object.assign({
|
||||
/* tslint:disable:no-empty */
|
||||
remove: () => {},
|
||||
hasBySelfLinkObservable: () => observableOf(false)
|
||||
/* tslint:enable:no-empty */
|
||||
}) as ObjectCacheService;
|
||||
|
||||
const relationshipType = Object.assign(new RelationshipType(), {
|
||||
id: '1',
|
||||
@@ -74,6 +67,15 @@ describe('RelationshipService', () => {
|
||||
relationship2.rightItem = getRemotedataObservable(item);
|
||||
const relatedItems = [relatedItem1, relatedItem2];
|
||||
|
||||
const buildList$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [relatedItems]));
|
||||
const rdbService = getMockRemoteDataBuildService(undefined, buildList$);
|
||||
const objectCache = Object.assign({
|
||||
/* tslint:disable:no-empty */
|
||||
remove: () => {},
|
||||
hasBySelfLinkObservable: () => observableOf(false)
|
||||
/* tslint:enable:no-empty */
|
||||
}) as ObjectCacheService;
|
||||
|
||||
const itemService = jasmine.createSpyObj('itemService', {
|
||||
findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.find((relatedItem) => relatedItem.id === uuid)),
|
||||
findByHref: createSuccessfulRemoteDataObject$(relatedItems[0])
|
||||
@@ -115,7 +117,7 @@ describe('RelationshipService', () => {
|
||||
|
||||
it('should send a DeleteRequest', () => {
|
||||
const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should clear the related items their cache', () => {
|
||||
@@ -141,6 +143,15 @@ describe('RelationshipService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelatedItemsByLabel', () => {
|
||||
it('should return the related items by label', () => {
|
||||
service.getRelatedItemsByLabel(item, relationshipType.rightwardType).subscribe((result) => {
|
||||
expect(result.payload.page).toEqual(relatedItems);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
|
||||
|
@@ -22,8 +22,15 @@ import { MappedCollectionsReponseParsingService } from './mapped-collections-rep
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
// uuid and handle requests have separate endpoints
|
||||
export enum IdentifierType {
|
||||
UUID ='uuid',
|
||||
HANDLE = 'handle'
|
||||
}
|
||||
|
||||
export abstract class RestRequest {
|
||||
public responseMsToLive = 0;
|
||||
public responseMsToLive = 10 * 1000;
|
||||
public forceBypassCache = false;
|
||||
constructor(
|
||||
public uuid: string,
|
||||
public href: string,
|
||||
@@ -49,7 +56,7 @@ export class GetRequest extends RestRequest {
|
||||
public uuid: string,
|
||||
public href: string,
|
||||
public body?: any,
|
||||
public options?: HttpOptions,
|
||||
public options?: HttpOptions
|
||||
) {
|
||||
super(uuid, href, RestRequestMethod.GET, body, options)
|
||||
}
|
||||
@@ -293,6 +300,7 @@ export class UpdateMetadataFieldRequest extends PutRequest {
|
||||
* Class representing a submission HTTP GET request object
|
||||
*/
|
||||
export class SubmissionRequest extends GetRequest {
|
||||
forceBypassCache = true;
|
||||
constructor(uuid: string, href: string) {
|
||||
super(uuid, href);
|
||||
}
|
||||
@@ -404,7 +412,7 @@ export class TaskDeleteRequest extends DeleteRequest {
|
||||
}
|
||||
|
||||
export class MyDSpaceRequest extends GetRequest {
|
||||
public responseMsToLive = 0;
|
||||
public responseMsToLive = 10 * 1000;
|
||||
}
|
||||
|
||||
export class RequestError extends Error {
|
||||
|
@@ -298,10 +298,11 @@ describe('RequestService', () => {
|
||||
describe('in the ObjectCache', () => {
|
||||
beforeEach(() => {
|
||||
(objectCache.hasBySelfLink as any).and.returnValue(true);
|
||||
(objectCache.hasByUUID as any).and.returnValue(true);
|
||||
spyOn(serviceAsAny, 'hasByHref').and.returnValue(false);
|
||||
});
|
||||
|
||||
it('should return true', () => {
|
||||
it('should return true for GetRequest', () => {
|
||||
const result = serviceAsAny.isCachedOrPending(testGetRequest);
|
||||
const expected = true;
|
||||
|
||||
|
@@ -19,7 +19,7 @@ import {
|
||||
} from '../index/index.selectors';
|
||||
import { UUIDService } from '../shared/uuid.service';
|
||||
import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions';
|
||||
import { GetRequest, RestRequest } from './request.models';
|
||||
import { GetRequest, RestRequest, SubmissionRequest } from './request.models';
|
||||
import { RequestEntry, RequestState } from './request.reducer';
|
||||
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
@@ -145,14 +145,10 @@ export class RequestService {
|
||||
* Configure a certain request
|
||||
* Used to make sure a request is in the cache
|
||||
* @param {RestRequest} request The request to send out
|
||||
* @param {boolean} forceBypassCache When true, a new request is always dispatched
|
||||
*/
|
||||
configure<T extends CacheableObject>(request: RestRequest, forceBypassCache: boolean = false): void {
|
||||
configure<T extends CacheableObject>(request: RestRequest): void {
|
||||
const isGetRequest = request.method === RestRequestMethod.GET;
|
||||
if (forceBypassCache) {
|
||||
this.clearRequestsOnTheirWayToTheStore(request);
|
||||
}
|
||||
if (!isGetRequest || (forceBypassCache && !this.isPending(request)) || !this.isCachedOrPending(request)) {
|
||||
if (!isGetRequest || request.forceBypassCache || !this.isCachedOrPending(request)) {
|
||||
this.dispatchRequest(request);
|
||||
if (isGetRequest) {
|
||||
this.trackRequestsOnTheirWayToTheStore(request);
|
||||
@@ -226,7 +222,6 @@ export class RequestService {
|
||||
const inReqCache = this.hasByHref(request.href);
|
||||
const inObjCache = this.objectCache.hasBySelfLink(request.href);
|
||||
const isCached = inReqCache || inObjCache;
|
||||
|
||||
const isPending = this.isPending(request);
|
||||
return isCached || isPending;
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ describe('ResourcePolicyService', () => {
|
||||
scheduler.schedule(() => service.findByHref(requestURL));
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(new GetRequest(requestUUID, requestURL, null), false);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(new GetRequest(requestUUID, requestURL, null));
|
||||
});
|
||||
|
||||
it('should return a RemoteData<ResourcePolicy> for the object with the given URL', () => {
|
||||
|
@@ -22,7 +22,6 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
class DataServiceImpl extends DataService<ResourcePolicy> {
|
||||
protected linkPath = 'resourcepolicies';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
@@ -28,7 +28,7 @@ export class EpersonResponseParsingService extends BaseResponseParsingService im
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
|
||||
const epersonDefinition = this.process<DSpaceObject>(data.payload, request.href);
|
||||
const epersonDefinition = this.process<DSpaceObject>(data.payload, request);
|
||||
return new EpersonSuccessResponse(epersonDefinition[Object.keys(epersonDefinition)[0]], data.statusCode, data.statusText, this.processPageInfo(data.payload));
|
||||
} else {
|
||||
return new ErrorResponse(
|
||||
|
@@ -27,7 +27,6 @@ import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
export class GroupEpersonService extends EpersonService<Group> {
|
||||
protected linkPath = 'groups';
|
||||
protected browseEndpoint = '';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(
|
||||
protected comparator: DSOChangeAnalyzer<Group>,
|
||||
|
@@ -53,8 +53,9 @@ export const requestUUIDIndexSelector: MemoizedSelector<AppState, IndexState> =
|
||||
/**
|
||||
* Return the self link of an object in the object-cache based on its UUID
|
||||
*
|
||||
* @param uuid
|
||||
* @param id
|
||||
* the UUID for which you want to find the matching self link
|
||||
* @param identifierType the type of index, used to select index from state
|
||||
* @returns
|
||||
* a MemoizedSelector to select the self link
|
||||
*/
|
||||
|
@@ -27,7 +27,7 @@ export class IntegrationResponseParsingService extends BaseResponseParsingServic
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
|
||||
const dataDefinition = this.process<IntegrationModel>(data.payload, request.uuid);
|
||||
const dataDefinition = this.process<IntegrationModel>(data.payload, request);
|
||||
return new IntegrationSuccessResponse(this.processResponse(dataDefinition), data.statusCode, data.statusText, this.processPageInfo(data.payload));
|
||||
} else {
|
||||
return new ErrorResponse(
|
||||
|
@@ -4,10 +4,16 @@ import { Item } from './item.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ResourceType } from './resource-type';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
|
||||
export class Bundle extends DSpaceObject {
|
||||
static type = new ResourceType('bundle');
|
||||
|
||||
/**
|
||||
* The bundle's name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The primary bitstream of this Bundle
|
||||
*/
|
||||
@@ -23,6 +29,9 @@ export class Bundle extends DSpaceObject {
|
||||
*/
|
||||
owner: Observable<RemoteData<Item>>;
|
||||
|
||||
bitstreams: Observable<RemoteData<Bitstream[]>>
|
||||
/**
|
||||
* List of Bitstreams that are part of this Bundle
|
||||
*/
|
||||
bitstreams: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
||||
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import { Item } from './item.model';
|
||||
import { Bitstream } from './bitstream.model';
|
||||
import { isEmpty } from '../../shared/empty.util';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||
import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||
|
||||
describe('Item', () => {
|
||||
|
||||
@@ -18,8 +18,9 @@ describe('Item', () => {
|
||||
const nonExistingBundleName = 'c1e568f7-d14e-496b-bdd7-07026998cc00';
|
||||
let bitstreams;
|
||||
let remoteDataThumbnail;
|
||||
let remoteDataThumbnailList;
|
||||
let remoteDataFiles;
|
||||
let remoteDataAll;
|
||||
let remoteDataBundles;
|
||||
|
||||
beforeEach(() => {
|
||||
const thumbnail = {
|
||||
@@ -33,15 +34,16 @@ describe('Item', () => {
|
||||
}];
|
||||
|
||||
remoteDataThumbnail = createSuccessfulRemoteDataObject$(thumbnail);
|
||||
remoteDataFiles = createSuccessfulRemoteDataObject$(bitstreams);
|
||||
remoteDataAll = createSuccessfulRemoteDataObject$([...bitstreams, thumbnail]);
|
||||
remoteDataThumbnailList = createSuccessfulRemoteDataObject$(createPaginatedList([thumbnail]));
|
||||
remoteDataFiles = createSuccessfulRemoteDataObject$(createPaginatedList(bitstreams));
|
||||
|
||||
// Create Bundles
|
||||
const bundles =
|
||||
[
|
||||
{
|
||||
name: thumbnailBundleName,
|
||||
primaryBitstream: remoteDataThumbnail
|
||||
primaryBitstream: remoteDataThumbnail,
|
||||
bitstreams: remoteDataThumbnailList
|
||||
},
|
||||
|
||||
{
|
||||
@@ -49,7 +51,9 @@ describe('Item', () => {
|
||||
bitstreams: remoteDataFiles
|
||||
}];
|
||||
|
||||
item = Object.assign(new Item(), { bitstreams: remoteDataAll });
|
||||
remoteDataBundles = createSuccessfulRemoteDataObject$(createPaginatedList(bundles));
|
||||
|
||||
item = Object.assign(new Item(), { bundles: remoteDataBundles });
|
||||
});
|
||||
|
||||
it('should return the bitstreams related to this item with the specified bundle name', () => {
|
||||
|
@@ -1,15 +1,16 @@
|
||||
import { map, startWith, filter, take } from 'rxjs/operators';
|
||||
import { map, startWith, filter, switchMap } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
import { Collection } from './collection.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { Bitstream } from './bitstream.model';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { hasValueOperator, isNotEmpty, isEmpty } from '../../shared/empty.util';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { Relationship } from './item-relationships/relationship.model';
|
||||
import { ResourceType } from './resource-type';
|
||||
import { getSucceededRemoteData } from './operators';
|
||||
import { getAllSucceededRemoteData, getSucceededRemoteData } from './operators';
|
||||
import { Bundle } from './bundle.model';
|
||||
import { GenericConstructor } from './generic-constructor';
|
||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||
import { DEFAULT_ENTITY_TYPE } from '../../shared/metadata-representation/metadata-representation.decorator';
|
||||
@@ -59,7 +60,10 @@ export class Item extends DSpaceObject {
|
||||
return this.owningCollection;
|
||||
}
|
||||
|
||||
bitstreams: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
||||
/**
|
||||
* Bitstream bundles within this item
|
||||
*/
|
||||
bundles: Observable<RemoteData<PaginatedList<Bundle>>>;
|
||||
|
||||
relationships: Observable<RemoteData<PaginatedList<Relationship>>>;
|
||||
|
||||
@@ -103,17 +107,15 @@ export class Item extends DSpaceObject {
|
||||
* see https://github.com/DSpace/dspace-angular/issues/332
|
||||
*/
|
||||
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
|
||||
return this.bitstreams.pipe(
|
||||
return this.bundles.pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((rd: RemoteData<PaginatedList<Bundle>>) => rd.payload.page.find((bundle: Bundle) => bundle.name === bundleName)),
|
||||
hasValueOperator(),
|
||||
switchMap((bundle: Bundle) => bundle.bitstreams),
|
||||
getAllSucceededRemoteData(),
|
||||
map((rd: RemoteData<PaginatedList<Bitstream>>) => rd.payload.page),
|
||||
filter((bitstreams: Bitstream[]) => hasValue(bitstreams)),
|
||||
take(1),
|
||||
startWith([]),
|
||||
map((bitstreams) => {
|
||||
return bitstreams
|
||||
.filter((bitstream) => hasValue(bitstream))
|
||||
.filter((bitstream) => bitstream.bundleName === bundleName)
|
||||
}));
|
||||
startWith([])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -148,7 +148,7 @@ describe('Core Module - RxJS Operators', () => {
|
||||
scheduler.schedule(() => source.pipe(configureRequest(requestService)).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(testRequest, undefined);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(testRequest);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -51,9 +51,9 @@ export const getResourceLinksFromResponse = () =>
|
||||
map((response: DSOSuccessResponse) => response.resourceSelfLinks),
|
||||
);
|
||||
|
||||
export const configureRequest = (requestService: RequestService, forceBypassCache?: boolean) =>
|
||||
export const configureRequest = (requestService: RequestService) =>
|
||||
(source: Observable<RestRequest>): Observable<RestRequest> =>
|
||||
source.pipe(tap((request: RestRequest) => requestService.configure(request, forceBypassCache)));
|
||||
source.pipe(tap((request: RestRequest) => requestService.configure(request)));
|
||||
|
||||
export const getRemoteDataPayload = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
|
@@ -88,14 +88,8 @@ export class SearchService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to retrieve a paginated list of search results from the server
|
||||
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
||||
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||
*/
|
||||
search(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
const hrefObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||
getEndpoint(searchOptions?: PaginatedSearchOptions): Observable<string> {
|
||||
return this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||
map((url: string) => {
|
||||
if (hasValue(searchOptions)) {
|
||||
return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
||||
@@ -104,6 +98,17 @@ export class SearchService implements OnDestroy {
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to retrieve a paginated list of search results from the server
|
||||
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
||||
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||
*/
|
||||
search(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
|
||||
const hrefObs = this.getEndpoint(searchOptions);
|
||||
|
||||
const requestObs = hrefObs.pipe(
|
||||
map((url: string) => {
|
||||
|
@@ -91,7 +91,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
|
||||
if (isNotEmpty(data.payload)
|
||||
&& isNotEmpty(data.payload._links)
|
||||
&& this.isSuccessStatus(data.statusCode)) {
|
||||
const dataDefinition = this.processResponse<SubmissionObject | ConfigObject>(data.payload, request.href);
|
||||
const dataDefinition = this.processResponse<SubmissionObject | ConfigObject>(data.payload, request);
|
||||
return new SubmissionSuccessResponse(dataDefinition, data.statusCode, data.statusText, this.processPageInfo(data.payload));
|
||||
} else if (isEmpty(data.payload) && this.isSuccessStatus(data.statusCode)) {
|
||||
return new SubmissionSuccessResponse(null, data.statusCode, data.statusText);
|
||||
@@ -109,11 +109,11 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
|
||||
* Parses response and normalize it
|
||||
*
|
||||
* @param {DSpaceRESTV2Response} data
|
||||
* @param {string} requestHref
|
||||
* @param {RestRequest} request
|
||||
* @returns {any[]}
|
||||
*/
|
||||
protected processResponse<ObjectDomain>(data: any, requestHref: string): any[] {
|
||||
const dataDefinition = this.process<ObjectDomain>(data, requestHref);
|
||||
protected processResponse<ObjectDomain>(data: any, request: RestRequest): any[] {
|
||||
const dataDefinition = this.process<ObjectDomain>(data, request);
|
||||
const normalizedDefinition = Array.of();
|
||||
const processedList = Array.isArray(dataDefinition) ? dataDefinition : Array.of(dataDefinition);
|
||||
|
||||
|
@@ -59,10 +59,13 @@ describe('SubmissionRestService test suite', () => {
|
||||
describe('getDataById', () => {
|
||||
it('should configure a new SubmissionRequest', () => {
|
||||
const expected = new SubmissionRequest(requestService.generateRequestId(), resourceHref);
|
||||
// set cache time to zero
|
||||
expected.responseMsToLive = 0;
|
||||
expected.forceBypassCache = true;
|
||||
scheduler.schedule(() => service.getDataById(resourceEndpoint, resourceScope).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected, true);
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import { distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import {
|
||||
DeleteRequest,
|
||||
DeleteRequest, GetRequest,
|
||||
PostRequest,
|
||||
RestRequest,
|
||||
SubmissionDeleteRequest,
|
||||
@@ -109,7 +109,11 @@ export class SubmissionRestService {
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => new SubmissionRequest(requestId, endpointURL)),
|
||||
tap((request: RestRequest) => this.requestService.configure(request, true)),
|
||||
map ((request: RestRequest) => {
|
||||
request.responseMsToLive = 0;
|
||||
return request;
|
||||
}),
|
||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
||||
flatMap(() => this.fetchRequest(requestId)),
|
||||
distinctUntilChanged());
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
@Injectable()
|
||||
export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
||||
protected linkPath = 'workflowitems';
|
||||
protected forceBypassCache = true;
|
||||
protected responseMsToLive = 10 * 1000;
|
||||
|
||||
constructor(
|
||||
protected comparator: DSOChangeAnalyzer<WorkflowItem>,
|
||||
|
@@ -20,7 +20,7 @@ import { WorkspaceItem } from './models/workspaceitem.model';
|
||||
@Injectable()
|
||||
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
||||
protected linkPath = 'workspaceitems';
|
||||
protected forceBypassCache = true;
|
||||
protected responseMsToLive = 10 * 1000;
|
||||
|
||||
constructor(
|
||||
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
||||
|
@@ -22,16 +22,13 @@ import { ProcessTaskResponse } from './models/process-task-response';
|
||||
@Injectable()
|
||||
export class ClaimedTaskDataService extends TasksService<ClaimedTask> {
|
||||
|
||||
protected responseMsToLive = 10 * 1000;
|
||||
|
||||
/**
|
||||
* The endpoint link name
|
||||
*/
|
||||
protected linkPath = 'claimedtasks';
|
||||
|
||||
/**
|
||||
* When true, a new request is always dispatched
|
||||
*/
|
||||
protected forceBypassCache = true;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
|
@@ -27,10 +27,7 @@ export class PoolTaskDataService extends TasksService<PoolTask> {
|
||||
*/
|
||||
protected linkPath = 'pooltasks';
|
||||
|
||||
/**
|
||||
* When true, a new request is always dispatched
|
||||
*/
|
||||
protected forceBypassCache = true;
|
||||
protected responseMsToLive = 10 * 1000;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
|
@@ -29,7 +29,6 @@ class TestTask extends TaskObject {
|
||||
|
||||
class TestService extends TasksService<TestTask> {
|
||||
protected linkPath = LINK_NAME;
|
||||
protected forceBypassCache = true;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
|
@@ -12,7 +12,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -12,7 +12,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -12,7 +12,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -9,7 +9,7 @@ import { JournalIssueSearchResultGridElementComponent } from './journal-issue-se
|
||||
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithMetadata.hitHighlights = {};
|
||||
mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
@@ -35,7 +35,7 @@ mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithoutMetadata.hitHighlights = {};
|
||||
mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -9,7 +9,7 @@ import { JournalVolumeSearchResultGridElementComponent } from './journal-volume-
|
||||
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithMetadata.hitHighlights = {};
|
||||
mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
@@ -35,7 +35,7 @@ mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithoutMetadata.hitHighlights = {};
|
||||
mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -9,7 +9,7 @@ import { JournalSearchResultGridElementComponent } from './journal-search-result
|
||||
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithMetadata.hitHighlights = {};
|
||||
mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
@@ -41,7 +41,7 @@ mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithoutMetadata.hitHighlights = {};
|
||||
mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -8,7 +8,7 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -8,7 +8,7 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -8,7 +8,7 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -15,7 +15,7 @@ const mockItemWithMetadata: ItemSearchResult = Object.assign(
|
||||
new ItemSearchResult(),
|
||||
{
|
||||
indexableObject: Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
@@ -43,7 +43,7 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
|
||||
new ItemSearchResult(),
|
||||
{
|
||||
indexableObject: Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -15,7 +15,7 @@ const mockItemWithMetadata: ItemSearchResult = Object.assign(
|
||||
new ItemSearchResult(),
|
||||
{
|
||||
indexableObject: Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
@@ -42,7 +42,7 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
|
||||
new ItemSearchResult(),
|
||||
{
|
||||
indexableObject: Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -15,7 +15,7 @@ const mockItemWithMetadata: ItemSearchResult = Object.assign(
|
||||
new ItemSearchResult(),
|
||||
{
|
||||
indexableObject: Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
@@ -37,7 +37,7 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
|
||||
new ItemSearchResult(),
|
||||
{
|
||||
indexableObject: Object.assign(new Item(), {
|
||||
bitstreams: observableOf({}),
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
|
@@ -6,7 +6,7 @@ import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../.
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'publicationissue.issueNumber': [
|
||||
{
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
metadata: {
|
||||
'publicationvolume.volumeNumber': [
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user