Merge branch 'master' into w2p-65572_Add-support-for-bundles

Conflicts:
	src/app/core/shared/item.model.ts
	src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts
	src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts
	src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts
	src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.spec.ts
	src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.spec.ts
	src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.spec.ts
	src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts
	src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts
	src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-list-element.component.spec.ts
	src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.spec.ts
	src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.spec.ts
	src/app/shared/object-grid/item-grid-element/item-grid-element.component.spec.ts
	src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts
	src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.spec.ts
	src/app/shared/testing/utils.ts
This commit is contained in:
Kristof De Langhe
2019-11-21 14:14:39 +01:00
433 changed files with 14771 additions and 4389 deletions

View File

@@ -70,7 +70,8 @@
"docs": "typedoc --options typedoc.json ./src/", "docs": "typedoc --options typedoc.json ./src/",
"coverage": "http-server -c-1 -o -p 9875 ./coverage", "coverage": "http-server -c-1 -o -p 9875 ./coverage",
"postinstall": "yarn run patch-protractor", "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": { "dependencies": {
"@angular/animations": "^6.1.4", "@angular/animations": "^6.1.4",
@@ -174,7 +175,9 @@
"angular2-template-loader": "0.6.2", "angular2-template-loader": "0.6.2",
"autoprefixer": "^9.1.3", "autoprefixer": "^9.1.3",
"caniuse-lite": "^1.0.30000697", "caniuse-lite": "^1.0.30000697",
"cli-progress": "^3.3.1",
"codelyzer": "^4.4.4", "codelyzer": "^4.4.4",
"commander": "^3.0.2",
"compression-webpack-plugin": "^1.1.6", "compression-webpack-plugin": "^1.1.6",
"copy-webpack-plugin": "^4.4.1", "copy-webpack-plugin": "^4.4.1",
"copyfiles": "^2.1.1", "copyfiles": "^2.1.1",

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

342
scripts/sync-i18n-files.js Executable file
View 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');
}

View File

@@ -11,7 +11,6 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
import { MockRouter } from '../../shared/mocks/mock-router'; import { MockRouter } from '../../shared/mocks/mock-router';
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs/internal/observable/of'; 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 { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';

View File

@@ -82,7 +82,8 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
const date = firstItemRD.payload.firstMetadataValue(metadataField); const date = firstItemRD.payload.firstMetadataValue(metadataField);
if (hasValue(date)) { if (hasValue(date)) {
const dateObj = new Date(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 = []; const options = [];

View File

@@ -97,7 +97,7 @@ describe('EditRelationshipListComponent', () => {
relationshipService = jasmine.createSpyObj('relationshipService', relationshipService = jasmine.createSpyObj('relationshipService',
{ {
getRelatedItemsByLabel: observableOf([author1, author2]), getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))),
} }
); );

View File

@@ -4,8 +4,10 @@ import { Observable } from 'rxjs/internal/Observable';
import { FieldUpdate, FieldUpdates } from '../../../../core/data/object-updates/object-updates.reducer'; import { FieldUpdate, FieldUpdates } from '../../../../core/data/object-updates/object-updates.reducer';
import { RelationshipService } from '../../../../core/data/relationship.service'; import { RelationshipService } from '../../../../core/data/relationship.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { hasValue } from '../../../../shared/empty.util'; import { hasValue } from '../../../../shared/empty.util';
import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list';
@Component({ @Component({
selector: 'ds-edit-relationship-list', selector: 'ds-edit-relationship-list',
@@ -63,7 +65,7 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
* Transform the item's relationships of a specific type into related items * Transform the item's relationships of a specific type into related items
* @param label The relationship type's label * @param label The relationship type's label
*/ */
public getRelatedItemsByLabel(label: string): Observable<Item[]> { public getRelatedItemsByLabel(label: string): Observable<RemoteData<PaginatedList<Item>>> {
return this.relationshipService.getRelatedItemsByLabel(this.item, label); return this.relationshipService.getRelatedItemsByLabel(this.item, label);
} }
@@ -73,7 +75,7 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
*/ */
public getUpdatesByLabel(label: string): Observable<FieldUpdates> { public getUpdatesByLabel(label: string): Observable<FieldUpdates> {
return this.getRelatedItemsByLabel(label).pipe( return this.getRelatedItemsByLabel(label).pipe(
switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items)) switchMap((itemsRD) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, itemsRD.payload.page))
) )
} }

View File

@@ -1,6 +1,6 @@
<div class="row" *ngIf="item"> <div class="row" *ngIf="item">
<div class="col-10 relationship"> <div class="col-10 relationship">
<ds-item-type-switcher [object]="item" [viewMode]="viewMode"></ds-item-type-switcher> <ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
</div> </div>
<div class="col-2"> <div class="col-2">
<div class="btn-group relationship-action-buttons"> <div class="btn-group relationship-action-buttons">

View File

@@ -5,7 +5,6 @@ import { ObjectUpdatesService } from '../../../../core/data/object-updates/objec
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { EditRelationshipComponent } from './edit-relationship.component'; import { EditRelationshipComponent } from './edit-relationship.component';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { ResourceType } from '../../../../core/shared/resource-type';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';

View File

@@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
import { ItemViewMode } from '../../../../shared/items/item-type-decorator'; import { ViewMode } from '../../../../core/shared/view-mode.model';
@Component({ @Component({
// tslint:disable-next-line:component-selector // tslint:disable-next-line:component-selector
@@ -31,7 +31,7 @@ export class EditRelationshipComponent implements OnChanges {
/** /**
* The view-mode we're currently on * The view-mode we're currently on
*/ */
viewMode = ItemViewMode.Element; viewMode = ViewMode.ListElement;
constructor(private objectUpdatesService: ObjectUpdatesService) { constructor(private objectUpdatesService: ObjectUpdatesService) {
} }

View File

@@ -23,7 +23,7 @@ import {
} from '../../shared/testing/utils'; } from '../../shared/testing/utils';
const mockItem: Item = Object.assign(new Item(), { const mockItem: Item = Object.assign(new Item(), {
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: { metadata: {
'dc.title': [ 'dc.title': [
{ {

View File

@@ -10,7 +10,7 @@ import { ItemPageFieldComponent } from '../item-page-field.component';
/** /**
* This component can be used to represent metadata on a simple item page. * This component can be used to represent metadata on a simple item page.
* It is the most generic way of displaying metadata values * It is the most generic way of displaying metadata values
* It expects 4 parameters: The item, a seperator, the metadata keys and an i18n key * It expects 4 parameters: The item, a separator, the metadata keys and an i18n key
*/ */
export class GenericItemPageFieldComponent extends ItemPageFieldComponent { export class GenericItemPageFieldComponent extends ItemPageFieldComponent {

View File

@@ -4,12 +4,9 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader'; import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
import { Observable } from 'rxjs';
import { PageInfo } from '../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { ItemPageFieldComponent } from './item-page-field.component'; import { ItemPageFieldComponent } from './item-page-field.component';
import { MetadataValuesComponent } from '../../../field-components/metadata-values/metadata-values.component'; import { MetadataValuesComponent } from '../../../field-components/metadata-values/metadata-values.component';
import { of as observableOf } from 'rxjs';
import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.models'; import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.models';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
@@ -53,7 +50,7 @@ describe('ItemPageFieldComponent', () => {
export function mockItemWithMetadataFieldAndValue(field: string, value: string): Item { export function mockItemWithMetadataFieldAndValue(field: string, value: string): Item {
const item = Object.assign(new Item(), { const item = Object.assign(new Item(), {
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: new MetadataMap() metadata: new MetadataMap()
}); });
item.metadata[field] = [{ item.metadata[field] = [{

View File

@@ -1,7 +1,7 @@
<div class="container" *ngVar="(itemRD$ | async) as itemRD"> <div class="container" *ngVar="(itemRD$ | async) as itemRD">
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut> <div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
<div *ngIf="itemRD?.payload as item"> <div *ngIf="itemRD?.payload as item">
<ds-item-type-switcher [object]="item" [viewMode]="viewMode"></ds-item-type-switcher> <ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
</div> </div>
</div> </div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error> <ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>

View File

@@ -1,21 +1,18 @@
import { map } from 'rxjs/operators';
import { mergeMap, filter, map, take, tap } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Bitstream } from '../../core/shared/bitstream.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { MetadataService } from '../../core/metadata/metadata.service'; import { MetadataService } from '../../core/metadata/metadata.service';
import { fadeInOut } from '../../shared/animations/fade'; import { fadeInOut } from '../../shared/animations/fade';
import { hasValue } from '../../shared/empty.util';
import { redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { redirectToPageNotFoundOn404 } from '../../core/shared/operators';
import { ItemViewMode } from '../../shared/items/item-type-decorator'; import { ViewMode } from '../../core/shared/view-mode.model';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
@@ -44,7 +41,7 @@ export class ItemPageComponent implements OnInit {
/** /**
* The view-mode we're currently on * The view-mode we're currently on
*/ */
viewMode = ItemViewMode.Full; viewMode = ViewMode.StandalonePage;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -53,6 +50,9 @@ export class ItemPageComponent implements OnInit {
private metadataService: MetadataService, private metadataService: MetadataService,
) { } ) { }
/**
* Initialize instance variables
*/
ngOnInit(): void { ngOnInit(): void {
this.itemRD$ = this.route.data.pipe( this.itemRD$ = this.route.data.pipe(
map((data) => data.item as RemoteData<Item>), map((data) => data.item as RemoteData<Item>),

View File

@@ -1,67 +1,72 @@
<h2 class="item-page-title-field"> <h2 class="item-page-title-field">
{{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="item?.allMetadata(['dc.title'])"></ds-metadata-values> {{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2> </h2>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper> <ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async"></ds-thumbnail> <ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-item-page-file-section [item]="item"></ds-item-page-file-section> <ds-item-page-file-section [item]="object"></ds-item-page-file-section>
<ds-item-page-date-field [item]="item"></ds-item-page-date-field> <ds-item-page-date-field [item]="object"></ds-item-page-date-field>
<ds-item-page-author-field *ngIf="!(authors$ | async)" [item]="item"></ds-item-page-author-field> <ds-item-page-author-field [item]="object"></ds-item-page-author-field>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="object"
[fields]="['journal.title']" [fields]="['journal.title']"
[label]="'publication.page.journal-title'"> [label]="'publication.page.journal-title'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="object"
[fields]="['journal.identifier.issn']" [fields]="['journal.identifier.issn']"
[label]="'publication.page.journal-issn'"> [label]="'publication.page.journal-issn'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="object"
[fields]="['journalvolume.identifier.name']" [fields]="['journalvolume.identifier.name']"
[label]="'publication.page.volume-title'"> [label]="'publication.page.volume-title'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="object"
[fields]="['dc.publisher']" [fields]="['dc.publisher']"
[label]="'publication.page.publisher'"> [label]="'publication.page.publisher'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-metadata-representation-list <ds-metadata-representation-list
[label]="'relationships.isAuthorOf' | translate" [parentItem]="object"
[representations]="authors$ | async"> [itemType]="'Person'"
[metadataField]="'dc.contributor.author'"
[label]="'relationships.isAuthorOf' | translate">
</ds-metadata-representation-list> </ds-metadata-representation-list>
<ds-related-items <ds-related-items
[items]="projects$ | async" [parentItem]="object"
[relationType]="'isProjectOfPublication'"
[label]="'relationships.isProjectOf' | translate"> [label]="'relationships.isProjectOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="orgUnits$ | async" [parentItem]="object"
[relationType]="'isOrgUnitOfPublication'"
[label]="'relationships.isOrgUnitOf' | translate"> [label]="'relationships.isOrgUnitOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="journalIssues$ | async" [parentItem]="object"
[relationType]="'isJournalIssueOfPublication'"
[label]="'relationships.isJournalIssueOf' | translate"> [label]="'relationships.isJournalIssueOf' | translate">
</ds-related-items> </ds-related-items>
<ds-item-page-abstract-field [item]="item"></ds-item-page-abstract-field> <ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="object"
[fields]="['dc.description']" [fields]="['dc.description']"
[label]="'publication.page.description'"> [label]="'publication.page.description'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="object"
[fields]="['dc.subject']" [fields]="['dc.subject']"
[separator]="','" [separator]="','"
[label]="'item.page.subject'"> [label]="'item.page.subject'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="object"
[fields]="['dc.identifier.citation']" [fields]="['dc.identifier.citation']"
[label]="'item.page.citation'"> [label]="'item.page.citation'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<ds-item-page-uri-field [item]="item"></ds-item-page-uri-field> <ds-item-page-uri-field [item]="object"></ds-item-page-uri-field>
<ds-item-page-collections [item]="item"></ds-item-page-collections> <ds-item-page-collections [item]="object"></ds-item-page-collections>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']"> <a class="btn btn-outline-primary" [routerLink]="['/items/' + object.id + '/full']">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -3,19 +3,16 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader'; import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { ItemDataService } from '../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../core/data/item-data.service';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service'; import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { PageInfo } from '../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { createRelationshipsObservable } from '../shared/item.component.spec'; import { createRelationshipsObservable } from '../shared/item.component.spec';
import { PublicationComponent } from './publication.component'; import { PublicationComponent } from './publication.component';
import { of as observableOf } from 'rxjs';
import { MetadataMap } from '../../../../core/shared/metadata.models'; import { MetadataMap } from '../../../../core/shared/metadata.models';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
@@ -45,7 +42,6 @@ describe('PublicationComponent', () => {
})], })],
declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe], declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe],
providers: [ providers: [
{provide: ITEM, useValue: mockItem},
{provide: ItemDataService, useValue: {}}, {provide: ItemDataService, useValue: {}},
{provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub}, {provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub},
{provide: TruncatableService, useValue: {}} {provide: TruncatableService, useValue: {}}
@@ -60,6 +56,7 @@ describe('PublicationComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
fixture = TestBed.createComponent(PublicationComponent); fixture = TestBed.createComponent(PublicationComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.object = mockItem;
fixture.detectChanges(); fixture.detectChanges();
})); }));

View File

@@ -1,62 +1,20 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import {
DEFAULT_ITEM_TYPE, ItemViewMode,
rendersItemType
} from '../../../../shared/items/item-type-decorator';
import { ItemComponent } from '../shared/item.component'; import { ItemComponent } from '../shared/item.component';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { getRelatedItemsByTypeLabel } from '../shared/item-relationships-utils'; import { Item } from '../../../../core/shared/item.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
@rendersItemType('Publication', ItemViewMode.Full) /**
@rendersItemType(DEFAULT_ITEM_TYPE, ItemViewMode.Full) * Component that represents a publication Item page
*/
@listableObjectComponent('Publication', ViewMode.StandalonePage)
@listableObjectComponent(Item, ViewMode.StandalonePage)
@Component({ @Component({
selector: 'ds-publication', selector: 'ds-publication',
styleUrls: ['./publication.component.scss'], styleUrls: ['./publication.component.scss'],
templateUrl: './publication.component.html', templateUrl: './publication.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class PublicationComponent extends ItemComponent implements OnInit { export class PublicationComponent extends ItemComponent {
/**
* The authors related to this publication
*/
authors$: Observable<MetadataRepresentation[]>;
/**
* The projects related to this publication
*/
projects$: Observable<Item[]>;
/**
* The organisation units related to this publication
*/
orgUnits$: Observable<Item[]>;
/**
* The journal issues related to this publication
*/
journalIssues$: Observable<Item[]>;
ngOnInit(): void {
super.ngOnInit();
if (this.resolvedRelsAndTypes$) {
this.authors$ = this.buildRepresentations('Person', 'dc.contributor.author');
this.projects$ = this.resolvedRelsAndTypes$.pipe(
getRelatedItemsByTypeLabel(this.item.id, 'isProjectOfPublication')
);
this.orgUnits$ = this.resolvedRelsAndTypes$.pipe(
getRelatedItemsByTypeLabel(this.item.id, 'isOrgUnitOfPublication')
);
this.journalIssues$ = this.resolvedRelsAndTypes$.pipe(
getRelatedItemsByTypeLabel(this.item.id, 'isJournalIssueOfPublication')
);
}
}
} }

View File

@@ -1,18 +1,14 @@
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { MetadataValue } from '../../../../core/shared/metadata.models';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasNoValue, hasValue } from '../../../../shared/empty.util'; import { hasNoValue, hasValue } from '../../../../shared/empty.util';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { distinctUntilChanged, filter, flatMap, map, switchMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, flatMap, map, switchMap } from 'rxjs/operators';
import { of as observableOf, zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs'; import { zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { RelationshipService } from '../../../../core/data/relationship.service'; import { RelationshipService } from '../../../../core/data/relationship.service';
import { PaginatedList } from '../../../../core/data/paginated-list';
/** /**
* Operator for comparing arrays using a mapping function * Operator for comparing arrays using a mapping function
@@ -100,53 +96,37 @@ export const relationsToItems = (thisId: string) =>
); );
/** /**
* Operator for turning a list of relationships and their relationship-types into a list of relevant items by relationship label * Operator for turning a paginated list of relationships into a paginated list of the relevant items
* @param thisId The item's id of which the relations belong to * The result is wrapped in the original RemoteData and PaginatedList
* @param label The label of the relationship-type to filter on * @param {string} thisId The item's id of which the relations belong to
* @param side Filter only on one side of the relationship (for example: child-parent relationships) * @returns {(source: Observable<Relationship[]>) => Observable<Item[]>}
*/ */
export const getRelatedItemsByTypeLabel = (thisId: string, label: string) => export const paginatedRelationsToItems = (thisId: string) =>
(source: Observable<[Relationship[], RelationshipType[]]>): Observable<Item[]> => (source: Observable<RemoteData<PaginatedList<Relationship>>>): Observable<RemoteData<PaginatedList<Item>>> =>
source.pipe( source.pipe(
filterRelationsByTypeLabel(label, thisId), getSucceededRemoteData(),
relationsToItems(thisId) switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => {
); return observableZip(
...relationshipsRD.payload.page.map((rel: Relationship) => observableCombineLatest(rel.leftItem, rel.rightItem))
/** ).pipe(
* Operator for turning a list of relationships into a list of metadatarepresentations given the original metadata map((arr) =>
* @param parentId The id of the parent item arr
* @param itemType The type of relation this list resembles (for creating representations) .filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded)
* @param metadata The list of original Metadatum objects .map(([leftItem, rightItem]) => {
*/ if (leftItem.payload.id === thisId) {
export const relationsToRepresentations = (parentId: string, itemType: string, metadata: MetadataValue[]) => return rightItem.payload;
(source: Observable<Relationship[]>): Observable<MetadataRepresentation[]> => } else if (rightItem.payload.id === thisId) {
source.pipe( return leftItem.payload;
flatMap((rels: Relationship[]) =>
observableZip(
...metadata
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
.map((metadatum: MetadataValue) => {
if (metadatum.isVirtual) {
const matchingRels = rels.filter((rel: Relationship) => ('' + rel.id) === metadatum.virtualValue);
if (matchingRels.length > 0) {
const matchingRel = matchingRels[0];
return observableCombineLatest(matchingRel.leftItem, matchingRel.rightItem).pipe(
map(([leftItem, rightItem]) => {
if (leftItem.payload.id === parentId) {
return rightItem.payload;
} else if (rightItem.payload.id === parentId) {
return leftItem.payload;
}
}),
map((item: Item) => Object.assign(new ItemMetadataRepresentation(metadatum), item))
);
} }
} else { })
return observableOf(Object.assign(new MetadatumRepresentation(itemType), metadatum)); .filter((item: Item) => hasValue(item))
} ),
}) distinctUntilChanged(compareArraysUsingIds()),
map((relatedItems: Item[]) =>
Object.assign(relationshipsRD, { payload: Object.assign(relationshipsRD.payload, { page: relatedItems } )})
)
) )
) })
); );
/** /**

View File

@@ -7,17 +7,14 @@ import { ItemDataService } from '../../../../core/data/item-data.service';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader'; import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { isNotEmpty } from '../../../../shared/empty.util'; import { isNotEmpty } from '../../../../shared/empty.util';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service'; import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { PageInfo } from '../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
import { ItemComponent } from './item.component'; import { ItemComponent } from './item.component';
import { of as observableOf } from 'rxjs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { VarDirective } from '../../../../shared/utils/var.directive'; import { VarDirective } from '../../../../shared/utils/var.directive';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
@@ -56,7 +53,6 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
})], })],
declarations: [component, GenericItemPageFieldComponent, TruncatePipe], declarations: [component, GenericItemPageFieldComponent, TruncatePipe],
providers: [ providers: [
{provide: ITEM, useValue: mockItem},
{provide: ItemDataService, useValue: {}}, {provide: ItemDataService, useValue: {}},
{provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub}, {provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub},
{provide: TruncatableService, useValue: {}} {provide: TruncatableService, useValue: {}}
@@ -71,6 +67,7 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
beforeEach(async(() => { beforeEach(async(() => {
fixture = TestBed.createComponent(component); fixture = TestBed.createComponent(component);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.object = mockItem;
fixture.detectChanges(); fixture.detectChanges();
})); }));
@@ -317,115 +314,4 @@ describe('ItemComponent', () => {
}); });
}); });
describe('when calling buildRepresentations', () => {
let comp: ItemComponent;
let fixture: ComponentFixture<ItemComponent>;
const metadataField = 'dc.contributor.author';
const relatedItem = Object.assign(new Item(), {
id: '2',
metadata: Object.assign(new MetadataMap(), {
'dc.title': [
{
language: 'en_US',
value: 'related item'
}
]
})
});
const mockItem = Object.assign(new Item(), {
id: '1',
uuid: '1',
metadata: new MetadataMap()
});
mockItem.relationships = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [
Object.assign(new Relationship(), {
uuid: '123',
id: '123',
leftItem: createSuccessfulRemoteDataObject$(mockItem),
rightItem: createSuccessfulRemoteDataObject$(relatedItem),
relationshipType: createSuccessfulRemoteDataObject$(new RelationshipType())
})
]));
mockItem.metadata[metadataField] = [
{
value: 'Second value',
place: 1
},
{
value: 'Third value',
place: 2,
authority: 'virtual::123'
},
{
value: 'First value',
place: 0
},
{
value: 'Fourth value',
place: 3,
authority: '123'
}
] as MetadataValue[];
const mockItemDataService = Object.assign({
findById: (id) => {
if (id === relatedItem.id) {
return createSuccessfulRemoteDataObject$(relatedItem)
}
}
}) as ItemDataService;
let representations: Observable<MetadataRepresentation[]>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}), BrowserAnimationsModule],
declarations: [ItemComponent, VarDirective],
providers: [
{provide: ITEM, useValue: mockItem}
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default}
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(ItemComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
representations = comp.buildRepresentations('bogus', metadataField);
}));
it('should contain exactly 4 metadata-representations', () => {
representations.subscribe((reps: MetadataRepresentation[]) => {
expect(reps.length).toEqual(4);
});
});
it('should have all the representations in the correct order', () => {
representations.subscribe((reps: MetadataRepresentation[]) => {
expect(reps[0].getValue()).toEqual('First value');
expect(reps[1].getValue()).toEqual('Second value');
expect(reps[2].getValue()).toEqual('Third value');
expect(reps[3].getValue()).toEqual('Fourth value');
});
});
it('should have created the correct MetadatumRepresentation and ItemMetadataRepresentation objects for the correct Metadata', () => {
representations.subscribe((reps: MetadataRepresentation[]) => {
expect(reps[0] instanceof MetadatumRepresentation).toEqual(true);
expect(reps[1] instanceof MetadatumRepresentation).toEqual(true);
expect(reps[2] instanceof ItemMetadataRepresentation).toEqual(true);
expect(reps[3] instanceof MetadatumRepresentation).toEqual(true);
});
});
})
}); });

View File

@@ -1,15 +1,5 @@
import { Component, Inject, OnInit } from '@angular/core'; import { Component, Inject, Input } from '@angular/core';
import { Observable , zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import { compareArraysUsingIds, relationsToRepresentations } from './item-relationships-utils';
@Component({ @Component({
selector: 'ds-item', selector: 'ds-item',
@@ -18,60 +8,6 @@ import { compareArraysUsingIds, relationsToRepresentations } from './item-relati
/** /**
* A generic component for displaying metadata and relations of an item * A generic component for displaying metadata and relations of an item
*/ */
export class ItemComponent implements OnInit { export class ItemComponent {
/** @Input() object: Item;
* Resolved relationships and types together in one observable
*/
resolvedRelsAndTypes$: Observable<[Relationship[], RelationshipType[]]>;
constructor(
@Inject(ITEM) public item: Item
) {}
ngOnInit(): void {
const relationships$ = this.item.relationships;
if (relationships$) {
const relsCurrentPage$ = relationships$.pipe(
filter((rd: RemoteData<PaginatedList<Relationship>>) => rd.hasSucceeded),
getRemoteDataPayload(),
map((pl: PaginatedList<Relationship>) => pl.page),
distinctUntilChanged(compareArraysUsingIds())
);
const relTypesCurrentPage$ = relsCurrentPage$.pipe(
flatMap((rels: Relationship[]) =>
observableZip(...rels.map((rel: Relationship) => rel.relationshipType)).pipe(
map(([...arr]: Array<RemoteData<RelationshipType>>) => arr.map((d: RemoteData<RelationshipType>) => d.payload))
)
),
distinctUntilChanged(compareArraysUsingIds())
);
this.resolvedRelsAndTypes$ = observableCombineLatest(
relsCurrentPage$,
relTypesCurrentPage$
);
}
}
/**
* Build a list of MetadataRepresentations for the current item. This combines all metadata and relationships of a
* certain type.
* @param itemType The type of item we're building representations of. Used for matching templates.
* @param metadataField The metadata field that resembles the item type.
*/
buildRepresentations(itemType: string, metadataField: string): Observable<MetadataRepresentation[]> {
const metadata = this.item.findMetadataSortedByPlace(metadataField);
const relsCurrentPage$ = this.item.relationships.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((pl: PaginatedList<Relationship>) => pl.page),
distinctUntilChanged(compareArraysUsingIds())
);
return relsCurrentPage$.pipe(
relationsToRepresentations(this.item.id, itemType, metadata)
);
}
} }

View File

@@ -1,5 +1,11 @@
<ds-metadata-field-wrapper *ngIf="representations && representations.length > 0" [label]="label"> <ds-metadata-field-wrapper *ngIf="representations$ && (representations$ | async)?.length > 0" [label]="label">
<ds-item-type-switcher *ngFor="let rep of representations" <ds-metadata-representation-loader *ngFor="let rep of (representations$ | async)"
[object]="rep" [viewMode]="viewMode"> [mdRepresentation]="rep">
</ds-item-type-switcher> </ds-metadata-representation-loader>
<div *ngIf="(representations$ | async)?.length < total" class="mt-2">
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
</div>
<div *ngIf="limit > originalLimit" class="mt-2">
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
</div>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>

View File

@@ -2,23 +2,72 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { MetadataRepresentationListComponent } from './metadata-representation-list.component'; import { MetadataRepresentationListComponent } from './metadata-representation-list.component';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; import { RelationshipService } from '../../../core/data/relationship.service';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { Item } from '../../../core/shared/item.model';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { TranslateModule } from '@ngx-translate/core';
const itemType = 'type'; const itemType = 'Person';
const metadataRepresentation1 = new MetadatumRepresentation(itemType); const metadataField = 'dc.contributor.author';
const metadataRepresentation2 = new ItemMetadataRepresentation(Object.assign({})); const parentItem: Item = Object.assign(new Item(), {
const representations = [metadataRepresentation1, metadataRepresentation2]; id: 'parent-item',
metadata: {
'dc.contributor.author': [
{
language: null,
value: 'Related Author with authority',
authority: 'virtual::related-author',
place: 2
},
{
language: null,
value: 'Author without authority',
place: 1
}
],
'dc.title': [
{
language: null,
value: 'Parent Item'
}
]
}
});
const relatedAuthor: Item = Object.assign(new Item(), {
id: 'related-author',
metadata: {
'dc.title': [
{
language: null,
value: 'Related Author'
}
]
}
});
const relation: Relationship = Object.assign(new Relationship(), {
leftItem: createSuccessfulRemoteDataObject$(parentItem),
rightItem: createSuccessfulRemoteDataObject$(relatedAuthor)
});
let relationshipService: RelationshipService;
describe('MetadataRepresentationListComponent', () => { describe('MetadataRepresentationListComponent', () => {
let comp: MetadataRepresentationListComponent; let comp: MetadataRepresentationListComponent;
let fixture: ComponentFixture<MetadataRepresentationListComponent>; let fixture: ComponentFixture<MetadataRepresentationListComponent>;
relationshipService = jasmine.createSpyObj('relationshipService',
{
findById: createSuccessfulRemoteDataObject$(relation)
}
);
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [TranslateModule.forRoot()],
declarations: [MetadataRepresentationListComponent], declarations: [MetadataRepresentationListComponent],
providers: [], providers: [
{ provide: RelationshipService, useValue: relationshipService }
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(MetadataRepresentationListComponent, { }).overrideComponent(MetadataRepresentationListComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default} set: {changeDetection: ChangeDetectionStrategy.Default}
@@ -28,13 +77,45 @@ describe('MetadataRepresentationListComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
fixture = TestBed.createComponent(MetadataRepresentationListComponent); fixture = TestBed.createComponent(MetadataRepresentationListComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.representations = representations; comp.parentItem = parentItem;
comp.itemType = itemType;
comp.metadataField = metadataField;
fixture.detectChanges(); fixture.detectChanges();
})); }));
it(`should load ${representations.length} item-type-switcher components`, () => { it('should load 2 ds-metadata-representation-loader components', () => {
const fields = fixture.debugElement.queryAll(By.css('ds-item-type-switcher')); const fields = fixture.debugElement.queryAll(By.css('ds-metadata-representation-loader'));
expect(fields.length).toBe(representations.length); expect(fields.length).toBe(2);
});
it('should initialize the original limit', () => {
expect(comp.originalLimit).toEqual(comp.limit);
});
describe('when viewMore is called', () => {
beforeEach(() => {
comp.viewMore();
});
it('should set the limit to a high number in order to retrieve all metadata representations', () => {
expect(comp.limit).toBeGreaterThanOrEqual(999);
});
});
describe('when viewLess is called', () => {
let originalLimit;
beforeEach(() => {
// Store the original value of limit
originalLimit = comp.limit;
// Set limit to a random number
comp.limit = 458;
comp.viewLess();
});
it('should reset the limit to the original value', () => {
expect(comp.limit).toEqual(originalLimit);
});
}); });
}); });

View File

@@ -1,6 +1,16 @@
import { Component, Input } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
import { ItemViewMode } from '../../../shared/items/item-type-decorator'; import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { RelationshipService } from '../../../core/data/relationship.service';
import { Item } from '../../../core/shared/item.model';
import { combineLatest as observableCombineLatest, of as observableOf, zip as observableZip } from 'rxjs';
import { MetadataValue } from '../../../core/shared/metadata.models';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { filter, map, switchMap } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
@Component({ @Component({
selector: 'ds-metadata-representation-list', selector: 'ds-metadata-representation-list',
@@ -8,13 +18,25 @@ import { ItemViewMode } from '../../../shared/items/item-type-decorator';
}) })
/** /**
* This component is used for displaying metadata * This component is used for displaying metadata
* It expects a list of MetadataRepresentation objects and a label to put on top of the list * It expects an item and a metadataField to fetch metadata
* It expects an itemType to resolve the metadata to a an item
* It expects a label to put on top of the list
*/ */
export class MetadataRepresentationListComponent { export class MetadataRepresentationListComponent implements OnInit {
/** /**
* A list of metadata-representations to display * The parent of the list of related items to display
*/ */
@Input() representations: MetadataRepresentation[]; @Input() parentItem: Item;
/**
* The type of item to create a representation of
*/
@Input() itemType: string;
/**
* The metadata field to use for fetching metadata from the item
*/
@Input() metadataField: string;
/** /**
* An i18n label to use as a title for the list * An i18n label to use as a title for the list
@@ -22,8 +44,91 @@ export class MetadataRepresentationListComponent {
@Input() label: string; @Input() label: string;
/** /**
* The view-mode we're currently on * The max amount of representations to display
* @type {ElementViewMode} * Defaults to 10
* The default can optionally be overridden by providing the limit as input to the component
*/ */
viewMode = ItemViewMode.Metadata; @Input() limit = 10;
/**
* A list of metadata-representations to display
*/
representations$: Observable<MetadataRepresentation[]>;
/**
* The originally provided limit
* Used for resetting the limit to the original value when collapsing the list
*/
originalLimit: number;
/**
* The total amount of metadata values available
*/
total: number;
constructor(public relationshipService: RelationshipService) {
}
ngOnInit(): void {
this.originalLimit = this.limit;
this.setRepresentations();
}
/**
* Initialize the metadata representations
*/
setRepresentations() {
const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataField);
this.total = metadata.length;
this.representations$ = this.resolveMetadataRepresentations(metadata);
}
/**
* Resolve a list of metadata values to a list of metadata representations
* @param metadata
*/
resolveMetadataRepresentations(metadata: MetadataValue[]): Observable<MetadataRepresentation[]> {
return observableZip(
...metadata
.slice(0, this.limit)
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
.map((metadatum: MetadataValue) => {
if (metadatum.isVirtual) {
return this.relationshipService.findById(metadatum.virtualValue).pipe(
getSucceededRemoteData(),
switchMap((relRD: RemoteData<Relationship>) =>
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded),
map(([leftItem, rightItem]) => {
if (leftItem.payload.id === this.parentItem.id) {
return rightItem.payload;
} else if (rightItem.payload.id === this.parentItem.id) {
return leftItem.payload;
}
}),
map((item: Item) => Object.assign(new ItemMetadataRepresentation(metadatum), item))
)
));
} else {
return observableOf(Object.assign(new MetadatumRepresentation(this.itemType), metadatum));
}
})
);
}
/**
* Expand the list to display all metadata representations
*/
viewMore() {
this.limit = 9999;
this.setRepresentations();
}
/**
* Collapse the list to display the originally displayed metadata representations
*/
viewLess() {
this.limit = this.originalLimit;
this.setRepresentations();
}
} }

View File

@@ -1,6 +1,12 @@
import { Component, Input } from '@angular/core'; import { Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { ItemViewMode } from '../../../shared/items/item-type-decorator'; import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { RelationshipService } from '../../../core/data/relationship.service';
import { FindAllOptions } from '../../../core/data/request.models';
import { Subscription } from 'rxjs/internal/Subscription';
import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-related-items', selector: 'ds-related-items',
@@ -9,22 +15,94 @@ import { ItemViewMode } from '../../../shared/items/item-type-decorator';
}) })
/** /**
* This component is used for displaying relations between items * This component is used for displaying relations between items
* It expects a list of items to display and a label to put on top * It expects a parent item and relationship type, as well as a label to display on top
*/ */
export class RelatedItemsComponent { export class RelatedItemsComponent implements OnInit, OnDestroy {
/** /**
* A list of items to display * The parent of the list of related items to display
*/ */
@Input() items: Item[]; @Input() parentItem: Item;
/**
* The label of the relationship type to display
* Used in sending a search request to the REST API
*/
@Input() relationType: string;
/**
* Default options to start a search request with
* Optional input, should you wish a different page size (or other options)
*/
@Input() options = Object.assign(new FindAllOptions(), { elementsPerPage: 5 });
/** /**
* An i18n label to use as a title for the list (usually describes the relation) * An i18n label to use as a title for the list (usually describes the relation)
*/ */
@Input() label: string; @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
*/
items$: Observable<RemoteData<PaginatedList<Item>>>;
/**
* Search options for displaying all elements in a list
*/
allOptions = Object.assign(new FindAllOptions(), { elementsPerPage: 9999 });
/** /**
* The view-mode we're currently on * The view-mode we're currently on
* @type {ElementViewMode} * @type {ElementViewMode}
*/ */
viewMode = ItemViewMode.Element; viewMode = ViewMode.ListElement;
/**
* Whether or not the list is currently expanded to show all related items
*/
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);
});
}
/**
* Expand the list to display all related items
*/
viewMore() {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.allOptions);
this.showingAll = true;
}
/**
* Collapse the list to display the originally displayed items
*/
viewLess() {
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();
}
}
} }

View File

@@ -1,5 +1,11 @@
<ds-metadata-field-wrapper *ngIf="items && items.length > 0" [label]="label"> <ds-metadata-field-wrapper *ngIf="(items$ | async)?.payload?.page?.length > 0" [label]="label">
<ds-item-type-switcher *ngFor="let item of items" <ds-listable-object-component-loader *ngFor="let item of (items$ | async)?.payload?.page"
[object]="item" [viewMode]="viewMode"> [object]="item" [viewMode]="viewMode">
</ds-item-type-switcher> </ds-listable-object-component-loader>
<div *ngIf="(items$ | async)?.payload?.page?.length < (items$ | async)?.payload?.totalElements" class="mt-2" id="view-more">
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
</div>
<div *ngIf="showingAll" class="mt-2" id="view-less">
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
</div>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>

View File

@@ -2,35 +2,50 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { RelatedItemsComponent } from './related-items-component'; import { RelatedItemsComponent } from './related-items-component';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { createRelationshipsObservable } from '../item-types/shared/item.component.spec'; import { createRelationshipsObservable } from '../item-types/shared/item.component.spec';
import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { RelationshipService } from '../../../core/data/relationship.service';
import { TranslateModule } from '@ngx-translate/core';
const parentItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: [],
relationships: createRelationshipsObservable()
});
const mockItem1: Item = Object.assign(new Item(), { const mockItem1: Item = Object.assign(new Item(), {
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: [], metadata: [],
relationships: createRelationshipsObservable() relationships: createRelationshipsObservable()
}); });
const mockItem2: Item = Object.assign(new Item(), { const mockItem2: Item = Object.assign(new Item(), {
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: [], metadata: [],
relationships: createRelationshipsObservable() relationships: createRelationshipsObservable()
}); });
const mockItems = [mockItem1, mockItem2]; const mockItems = [mockItem1, mockItem2];
const relationType = 'isItemOfItem';
let relationshipService: RelationshipService;
describe('RelatedItemsComponent', () => { describe('RelatedItemsComponent', () => {
let comp: RelatedItemsComponent; let comp: RelatedItemsComponent;
let fixture: ComponentFixture<RelatedItemsComponent>; let fixture: ComponentFixture<RelatedItemsComponent>;
beforeEach(async(() => { beforeEach(async(() => {
relationshipService = jasmine.createSpyObj('relationshipService',
{
getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockItems)),
}
);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [TranslateModule.forRoot()],
declarations: [RelatedItemsComponent], declarations: [RelatedItemsComponent],
providers: [], providers: [
{ provide: RelationshipService, useValue: relationshipService }
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(RelatedItemsComponent, { }).overrideComponent(RelatedItemsComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default} set: {changeDetection: ChangeDetectionStrategy.Default}
@@ -40,13 +55,42 @@ describe('RelatedItemsComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
fixture = TestBed.createComponent(RelatedItemsComponent); fixture = TestBed.createComponent(RelatedItemsComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.items = mockItems; comp.parentItem = parentItem;
comp.relationType = relationType;
fixture.detectChanges(); fixture.detectChanges();
})); }));
it(`should load ${mockItems.length} item-type-switcher components`, () => { it(`should load ${mockItems.length} item-type-switcher components`, () => {
const fields = fixture.debugElement.queryAll(By.css('ds-item-type-switcher')); const fields = fixture.debugElement.queryAll(By.css('ds-listable-object-component-loader'));
expect(fields.length).toBe(mockItems.length); expect(fields.length).toBe(mockItems.length);
}); });
describe('when viewMore is called', () => {
beforeEach(() => {
comp.viewMore();
});
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.allOptions);
});
it('should set showingAll to true', () => {
expect(comp.showingAll).toEqual(true);
});
});
describe('when viewLess is called', () => {
beforeEach(() => {
comp.viewLess();
});
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.options);
});
it('should set showingAll to false', () => {
expect(comp.showingAll).toEqual(false);
});
});
}); });

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

View 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 {
}

View 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)
});
});

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import { TranslateService } from '@ngx-translate/core';
import { SubmissionState } from '../../submission/submission.reducers'; import { SubmissionState } from '../../submission/submission.reducers';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { MyDSpaceResult } from '../my-dspace-result.model';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
@@ -15,6 +14,7 @@ import { UploaderOptions } from '../../shared/uploader/uploader-options.model';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { NotificationType } from '../../shared/notifications/models/notification-type'; import { NotificationType } from '../../shared/notifications/models/notification-type';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { SearchResult } from '../../+search-page/search-result.model';
/** /**
* This component represents the whole mydspace page header * This component represents the whole mydspace page header
@@ -25,7 +25,10 @@ import { hasValue } from '../../shared/empty.util';
templateUrl: './my-dspace-new-submission.component.html' templateUrl: './my-dspace-new-submission.component.html'
}) })
export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
@Output() uploadEnd = new EventEmitter<Array<MyDSpaceResult<DSpaceObject>>>(); /**
* Output that emits the workspace item when the upload has completed
*/
@Output() uploadEnd = new EventEmitter<Array<SearchResult<DSpaceObject>>>();
/** /**
* The UploaderOptions object * The UploaderOptions object

View File

@@ -39,7 +39,8 @@
</button> </button>
</div> </div>
<ds-my-dspace-results [searchResults]="resultsRD$ | async" <ds-my-dspace-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async"></ds-my-dspace-results> [searchConfig]="searchOptions$ | async"
[context]="context$ | async"></ds-my-dspace-results>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -15,7 +15,6 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; 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 { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from './my-dspace-page.component';
import { RouteService } from '../core/services/route.service'; import { RouteService } from '../core/services/route.service';
import { routeServiceStub } from '../shared/testing/route-service-stub'; import { routeServiceStub } from '../shared/testing/route-service-stub';
@@ -50,6 +49,7 @@ describe('MyDSpacePageComponent', () => {
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']); const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
const searchServiceStub = jasmine.createSpyObj('SearchService', { const searchServiceStub = jasmine.createSpyObj('SearchService', {
search: mockResults, search: mockResults,
getEndpoint: observableOf('discover/search/objects'),
getSearchLink: '/mydspace', getSearchLink: '/mydspace',
getScopes: observableOf(['test-scope']), getScopes: observableOf(['test-scope']),
setServiceOptions: {} setServiceOptions: {}
@@ -76,6 +76,7 @@ describe('MyDSpacePageComponent', () => {
scope: scopeParam scope: scopeParam
}) })
}; };
const sidebarService = { const sidebarService = {
isCollapsed: observableOf(true), isCollapsed: observableOf(true),
collapse: () => this.isCollapsed = observableOf(true), collapse: () => this.isCollapsed = observableOf(true),

View File

@@ -8,7 +8,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { switchMap, tap, } from 'rxjs/operators'; import { map, switchMap, tap, } from 'rxjs/operators';
import { PaginatedList } from '../core/data/paginated-list'; import { PaginatedList } from '../core/data/paginated-list';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
@@ -20,7 +20,6 @@ import { SearchService } from '../+search-page/search-service/search.service';
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service'; import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { getSucceededRemoteData } from '../core/shared/operators'; import { getSucceededRemoteData } from '../core/shared/operators';
import { MyDSpaceResult } from './my-dspace-result.model';
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service'; import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model'; import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model';
import { RoleType } from '../core/roles/role-types'; import { RoleType } from '../core/roles/role-types';
@@ -28,6 +27,8 @@ import { SearchConfigurationService } from '../+search-page/search-service/searc
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service'; import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
import { ViewMode } from '../core/shared/view-mode.model'; import { ViewMode } from '../core/shared/view-mode.model';
import { MyDSpaceRequest } from '../core/data/request.models'; import { MyDSpaceRequest } from '../core/data/request.models';
import { SearchResult } from '../+search-page/search-result.model';
import { Context } from '../core/shared/context.model';
export const MYDSPACE_ROUTE = '/mydspace'; export const MYDSPACE_ROUTE = '/mydspace';
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService'); export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
@@ -63,7 +64,7 @@ export class MyDSpacePageComponent implements OnInit {
/** /**
* The current search results * The current search results
*/ */
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<MyDSpaceResult<DSpaceObject>>>> = new BehaviorSubject(null); resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
/** /**
* The current paginated search options * The current paginated search options
@@ -93,7 +94,12 @@ export class MyDSpacePageComponent implements OnInit {
/** /**
* List of available view mode * List of available view mode
*/ */
viewModeList = [ViewMode.List, ViewMode.Detail]; viewModeList = [ViewMode.ListElement, ViewMode.DetailedListElement];
/**
* The current context of this page: workspace or workflow
*/
context$: Observable<Context>;
constructor(private service: SearchService, constructor(private service: SearchService,
private sidebarService: SearchSidebarService, private sidebarService: SearchSidebarService,
@@ -111,21 +117,35 @@ export class MyDSpacePageComponent implements OnInit {
* *
* Listen to changes in the scope * Listen to changes in the scope
* If something changes, update the list of scopes for the dropdown * If something changes, update the list of scopes for the dropdown
*
* Listen to changes in the configuration
* If something changes, update the current context
*/ */
ngOnInit(): void { ngOnInit(): void {
this.configurationList$ = this.searchConfigService.getAvailableConfigurationOptions(); this.configurationList$ = this.searchConfigService.getAvailableConfigurationOptions();
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
this.sub = this.searchOptions$.pipe( this.sub = this.searchOptions$.pipe(
tap(() => this.resultsRD$.next(null)), tap(() => this.resultsRD$.next(null)),
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getSucceededRemoteData()))) switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getSucceededRemoteData())))
.subscribe((results) => { .subscribe((results) => {
this.resultsRD$.next(results); this.resultsRD$.next(results);
}); });
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe( this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
switchMap((scopeId) => this.service.getScopes(scopeId)) switchMap((scopeId) => this.service.getScopes(scopeId))
); );
this.context$ = this.searchConfigService.getCurrentConfiguration('workspace')
.pipe(
map((configuration: string) => {
if (configuration === 'workspace') {
return Context.Workspace
} else {
return Context.Workflow
}
})
);
} }
/** /**

View File

@@ -7,19 +7,20 @@ import { MyDspacePageRoutingModule } from './my-dspace-page-routing.module';
import { MyDSpacePageComponent } from './my-dspace-page.component'; import { MyDSpacePageComponent } from './my-dspace-page.component';
import { SearchPageModule } from '../+search-page/search-page.module'; import { SearchPageModule } from '../+search-page/search-page.module';
import { MyDSpaceResultsComponent } from './my-dspace-results/my-dspace-results.component'; import { MyDSpaceResultsComponent } from './my-dspace-results/my-dspace-results.component';
import { WorkspaceitemMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workspaceitem-my-dspace-result/workspaceitem-my-dspace-result-list-element.component'; import { WorkspaceItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component';
import { ItemMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/item-my-dspace-result/item-my-dspace-result-list-element.component'; import { ClaimedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component';
import { WorkflowitemMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflowitem-my-dspace-result/workflowitem-my-dspace-result-list-element.component'; import { PoolSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component';
import { ClaimedMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-my-dspace-result/claimed-my-dspace-result-list-element.component';
import { PoolMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/pool-my-dspace-result/pool-my-dspace-result-list-element.component';
import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission/my-dspace-new-submission.component'; import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission/my-dspace-new-submission.component';
import { ItemMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-my-dspace-result/item-my-dspace-result-detail-element.component'; import { ItemSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component';
import { WorkspaceitemMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/workspaceitem-my-dspace-result/workspaceitem-my-dspace-result-detail-element.component'; import { WorkspaceItemSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component';
import { WorkflowitemMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/workflowitem-my-dspace-result/workflowitem-my-dspace-result-detail-element.component'; import { WorkflowItemSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component';
import { ClaimedMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/claimed-my-dspace-result/claimed-my-dspace-result-detail-element.component'; import { ClaimedTaskSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component';
import { PoolMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-my-dspace-result/pool-my-dspace-result-detail-lement.component';
import { MyDSpaceGuard } from './my-dspace.guard'; import { MyDSpaceGuard } from './my-dspace.guard';
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service'; import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
import { SearchResultListElementComponent } from '../shared/object-list/search-result-list-element/search-result-list-element.component';
import { ItemSearchResultListElementSubmissionComponent } from '../shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component';
import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component';
import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -31,33 +32,34 @@ import { MyDSpaceConfigurationService } from './my-dspace-configuration.service'
declarations: [ declarations: [
MyDSpacePageComponent, MyDSpacePageComponent,
MyDSpaceResultsComponent, MyDSpaceResultsComponent,
ItemMyDSpaceResultListElementComponent, WorkspaceItemSearchResultListElementComponent,
WorkspaceitemMyDSpaceResultListElementComponent, WorkflowItemSearchResultListElementComponent,
WorkflowitemMyDSpaceResultListElementComponent, ClaimedSearchResultListElementComponent,
ClaimedMyDSpaceResultListElementComponent, PoolSearchResultListElementComponent,
PoolMyDSpaceResultListElementComponent, ItemSearchResultDetailElementComponent,
ItemMyDSpaceResultDetailElementComponent, WorkspaceItemSearchResultDetailElementComponent,
WorkspaceitemMyDSpaceResultDetailElementComponent, WorkflowItemSearchResultDetailElementComponent,
WorkflowitemMyDSpaceResultDetailElementComponent, ClaimedTaskSearchResultDetailElementComponent,
ClaimedMyDSpaceResultDetailElementComponent, PoolSearchResultDetailElementComponent,
PoolMyDSpaceResultDetailElementComponent, MyDSpaceNewSubmissionComponent,
MyDSpaceNewSubmissionComponent ItemSearchResultListElementSubmissionComponent
], ],
providers: [ providers: [
MyDSpaceGuard, MyDSpaceGuard,
MyDSpaceConfigurationService MyDSpaceConfigurationService
], ],
entryComponents: [ entryComponents: [
ItemMyDSpaceResultListElementComponent, SearchResultListElementComponent,
WorkspaceitemMyDSpaceResultListElementComponent, WorkspaceItemSearchResultListElementComponent,
WorkflowitemMyDSpaceResultListElementComponent, WorkflowItemSearchResultListElementComponent,
ClaimedMyDSpaceResultListElementComponent, ClaimedSearchResultListElementComponent,
PoolMyDSpaceResultListElementComponent, PoolSearchResultListElementComponent,
ItemMyDSpaceResultDetailElementComponent, ItemSearchResultDetailElementComponent,
WorkspaceitemMyDSpaceResultDetailElementComponent, WorkspaceItemSearchResultDetailElementComponent,
WorkflowitemMyDSpaceResultDetailElementComponent, WorkflowItemSearchResultDetailElementComponent,
ClaimedMyDSpaceResultDetailElementComponent, ClaimedTaskSearchResultDetailElementComponent,
PoolMyDSpaceResultDetailElementComponent PoolSearchResultDetailElementComponent,
ItemSearchResultListElementSubmissionComponent
] ]
}) })

View File

@@ -1,19 +0,0 @@
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { MetadataMap } from '../core/shared/metadata.models';
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
/**
* Represents a search result object of a certain (<T>) DSpaceObject
*/
export class MyDSpaceResult<T extends DSpaceObject> implements ListableObject {
/**
* The DSpaceObject that was found
*/
indexableObject: T;
/**
* The metadata that was used to find this item, hithighlighted
*/
hitHighlights: MetadataMap;
}

View File

@@ -4,7 +4,8 @@
[hasBorder]="hasBorder" [hasBorder]="hasBorder"
[sortConfig]="searchConfig.sort" [sortConfig]="searchConfig.sort"
[objects]="searchResults" [objects]="searchResults"
[hideGear]="true"> [hideGear]="true"
[context]="context">
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading> <ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>

View File

@@ -1,13 +1,13 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import { MyDSpaceResult } from '../my-dspace-result.model';
import { SearchOptions } from '../../+search-page/search-options.model'; import { SearchOptions } from '../../+search-page/search-options.model';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
import { isEmpty } from '../../shared/empty.util'; import { isEmpty } from '../../shared/empty.util';
import { SearchResult } from '../../+search-page/search-result.model';
import { Context } from '../../core/shared/context.model';
/** /**
* Component that represents all results for mydspace page * Component that represents all results for mydspace page
@@ -25,7 +25,7 @@ export class MyDSpaceResultsComponent {
/** /**
* The actual search result objects * The actual search result objects
*/ */
@Input() searchResults: RemoteData<PaginatedList<MyDSpaceResult<DSpaceObject>>>; @Input() searchResults: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>;
/** /**
* The current configuration of the search * The current configuration of the search
@@ -37,6 +37,10 @@ export class MyDSpaceResultsComponent {
*/ */
@Input() viewMode: ViewMode; @Input() viewMode: ViewMode;
/**
* The current context for the search results
*/
@Input() context: Context;
/** /**
* A boolean representing if search results entry are separated by a line * A boolean representing if search results entry are separated by a line
*/ */

View File

@@ -1,13 +1,12 @@
import { autoserialize, inheritSerialization } from 'cerialize'; import { autoserialize, inheritSerialization } from 'cerialize';
import { MetadataMap } from '../core/shared/metadata.models'; import { MetadataMap } from '../core/shared/metadata.models';
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
import { NormalizedObject } from '../core/cache/models/normalized-object.model'; import { NormalizedObject } from '../core/cache/models/normalized-object.model';
/** /**
* Represents a normalized version of a search result object of a certain DSpaceObject * Represents a normalized version of a search result object of a certain DSpaceObject
*/ */
@inheritSerialization(NormalizedObject) @inheritSerialization(NormalizedObject)
export class NormalizedSearchResult implements ListableObject { export class NormalizedSearchResult {
/** /**
* The UUID of the DSpaceObject that was found * The UUID of the DSpaceObject that was found
*/ */
@@ -19,5 +18,4 @@ export class NormalizedSearchResult implements ListableObject {
*/ */
@autoserialize @autoserialize
hitHighlights: MetadataMap; hitHighlights: MetadataMap;
} }

View File

@@ -71,7 +71,7 @@ export class SearchFiltersComponent implements OnInit {
/** /**
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
*/ */
private getSearchLink(): string { getSearchLink(): string {
if (this.inPlaceSearch) { if (this.inPlaceSearch) {
return './'; return './';
} }

View File

@@ -3,14 +3,14 @@ import { URLCombiner } from '../core/url-combiner/url-combiner';
import 'core-js/library/fn/object/entries'; import 'core-js/library/fn/object/entries';
import { SearchFilter } from './search-filter.model'; import { SearchFilter } from './search-filter.model';
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
import { SetViewMode } from '../shared/view-mode'; import { ViewMode } from '../core/shared/view-mode.model';
/** /**
* This model class represents all parameters needed to request information about a certain search request * This model class represents all parameters needed to request information about a certain search request
*/ */
export class SearchOptions { export class SearchOptions {
configuration?: string; configuration?: string;
view?: SetViewMode = SetViewMode.List; view?: ViewMode = ViewMode.ListElement;
scope?: string; scope?: string;
query?: string; query?: string;
dsoType?: DSpaceObjectType; dsoType?: DSpaceObjectType;

View File

@@ -5,7 +5,6 @@ import { SharedModule } from '../shared/shared.module';
import { SearchPageRoutingModule } from './search-page-routing.module'; import { SearchPageRoutingModule } from './search-page-routing.module';
import { SearchPageComponent } from './search-page.component'; import { SearchPageComponent } from './search-page.component';
import { SearchResultsComponent } from './search-results/search-results.component'; import { SearchResultsComponent } from './search-results/search-results.component';
import { ItemSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component';
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component' import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component'; import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
@@ -44,9 +43,6 @@ const components = [
SearchResultsComponent, SearchResultsComponent,
SearchSidebarComponent, SearchSidebarComponent,
SearchSettingsComponent, SearchSettingsComponent,
ItemSearchResultGridElementComponent,
CollectionSearchResultGridElementComponent,
CommunitySearchResultGridElementComponent,
SearchFiltersComponent, SearchFiltersComponent,
SearchFilterComponent, SearchFilterComponent,
SearchFacetFilterComponent, SearchFacetFilterComponent,
@@ -84,9 +80,6 @@ const components = [
SearchConfigurationService SearchConfigurationService
], ],
entryComponents: [ entryComponents: [
ItemSearchResultGridElementComponent,
CollectionSearchResultGridElementComponent,
CommunitySearchResultGridElementComponent,
SearchFacetFilterComponent, SearchFacetFilterComponent,
SearchRangeFilterComponent, SearchRangeFilterComponent,
SearchTextFilterComponent, SearchTextFilterComponent,

View File

@@ -1,6 +1,7 @@
import { DSpaceObject } from '../core/shared/dspace-object.model'; import { DSpaceObject } from '../core/shared/dspace-object.model';
import { MetadataMap } from '../core/shared/metadata.models'; import { MetadataMap } from '../core/shared/metadata.models';
import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
import { GenericConstructor } from '../core/shared/generic-constructor';
/** /**
* Represents a search result object of a certain (<T>) DSpaceObject * Represents a search result object of a certain (<T>) DSpaceObject
@@ -16,4 +17,10 @@ export class SearchResult<T extends DSpaceObject> implements ListableObject {
*/ */
hitHighlights: MetadataMap; hitHighlights: MetadataMap;
/**
* Method that returns as which type of object this object should be rendered
*/
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
return [this.constructor as GenericConstructor<ListableObject>];
}
} }

View File

@@ -4,6 +4,7 @@
[config]="searchConfig.pagination" [config]="searchConfig.pagination"
[sortConfig]="searchConfig.sort" [sortConfig]="searchConfig.sort"
[objects]="searchResults" [objects]="searchResults"
[linkType]="linkType"
[hideGear]="true"> [hideGear]="true">
</ds-viewable-collection></div> </ds-viewable-collection></div>
<ds-loading *ngIf="hasNoValue(searchResults) || hasNoValue(searchResults.payload) || searchResults.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading> <ds-loading *ngIf="hasNoValue(searchResults) || hasNoValue(searchResults.payload) || searchResults.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>

View File

@@ -2,12 +2,13 @@ import { Component, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import { SetViewMode } from '../../shared/view-mode';
import { SearchOptions } from '../search-options.model'; import { SearchOptions } from '../search-options.model';
import { SearchResult } from '../search-result.model'; import { SearchResult } from '../search-result.model';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
import { SortOptions } from '../../core/cache/models/sort-options.model'; import { SortOptions } from '../../core/cache/models/sort-options.model';
import { ViewMode } from '../../core/shared/view-mode.model';
import { CollectionElementLinkType } from '../../shared/object-collection/collection-element-link.type';
@Component({ @Component({
selector: 'ds-search-results', selector: 'ds-search-results',
@@ -24,6 +25,11 @@ import { SortOptions } from '../../core/cache/models/sort-options.model';
export class SearchResultsComponent { export class SearchResultsComponent {
hasNoValue = hasNoValue; hasNoValue = hasNoValue;
/**
* The link type of the listed search results
*/
@Input() linkType: CollectionElementLinkType;
/** /**
* The actual search result objects * The actual search result objects
*/ */
@@ -42,7 +48,7 @@ export class SearchResultsComponent {
/** /**
* The current view-mode of the list * The current view-mode of the list
*/ */
@Input() viewMode: SetViewMode; @Input() viewMode: ViewMode;
/** /**
* An optional configuration to filter the result on one type * An optional configuration to filter the result on one type

View File

@@ -12,19 +12,12 @@ const searchResultMap = new Map();
* @param {GenericConstructor<ListableObject>} domainConstructor The constructor of the DSpaceObject * @param {GenericConstructor<ListableObject>} domainConstructor The constructor of the DSpaceObject
* @returns Decorator function that performs the actual mapping on initialization of the component * @returns Decorator function that performs the actual mapping on initialization of the component
*/ */
export function searchResultFor(domainConstructor: GenericConstructor<ListableObject>, configuration: string = null) { export function searchResultFor(domainConstructor: GenericConstructor<ListableObject>) {
return function decorator(searchResult: any) { return function decorator(searchResult: any) {
if (!searchResult) { if (!searchResult) {
return; return;
} }
if (isNull(configuration)) { searchResultMap.set(domainConstructor, searchResult);
searchResultMap.set(domainConstructor, searchResult);
} else {
if (!searchResultMap.get(configuration)) {
searchResultMap.set(configuration, new Map());
}
searchResultMap.get(configuration).set(domainConstructor, searchResult);
}
}; };
} }
@@ -33,10 +26,6 @@ export function searchResultFor(domainConstructor: GenericConstructor<ListableOb
* @param {GenericConstructor<ListableObject>} domainConstructor The DSpaceObject's constructor for which the search result component is requested * @param {GenericConstructor<ListableObject>} domainConstructor The DSpaceObject's constructor for which the search result component is requested
* @returns The component's constructor that matches the given DSpaceObject * @returns The component's constructor that matches the given DSpaceObject
*/ */
export function getSearchResultFor(domainConstructor: GenericConstructor<ListableObject>, configuration: string = null) { export function getSearchResultFor(domainConstructor: GenericConstructor<ListableObject>) {
if (isNull(configuration) || configuration === 'default' || hasNoValue(searchResultMap.get(configuration))) {
return searchResultMap.get(domainConstructor); return searchResultMap.get(domainConstructor);
} else {
return searchResultMap.get(configuration).get(domainConstructor);
}
} }

View File

@@ -5,9 +5,6 @@ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { SearchService } from './search.service'; import { SearchService } from './search.service';
import { ItemDataService } from './../../core/data/item-data.service';
import { SetViewMode } from '../../shared/view-mode';
import { GLOBAL_CONFIG } from '../../../config';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { Router, UrlTree } from '@angular/router'; import { Router, UrlTree } from '@angular/router';
import { RequestService } from '../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
@@ -66,7 +63,7 @@ describe('SearchService', () => {
it('should return list view mode', () => { it('should return list view mode', () => {
searchService.getViewMode().subscribe((viewMode) => { searchService.getViewMode().subscribe((viewMode) => {
expect(viewMode).toBe(ViewMode.List); expect(viewMode).toBe(ViewMode.ListElement);
}); });
}); });
}); });
@@ -125,38 +122,38 @@ describe('SearchService', () => {
}); });
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => { it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.List); searchService.setViewMode(ViewMode.ListElement);
expect(router.navigate).toHaveBeenCalledWith(['/search'], { expect(router.navigate).toHaveBeenCalledWith(['/search'], {
queryParams: { view: ViewMode.List, page: 1 }, queryParams: { view: ViewMode.ListElement, page: 1 },
queryParamsHandling: 'merge' queryParamsHandling: 'merge'
}); });
}); });
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => { it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.Grid); searchService.setViewMode(ViewMode.GridElement);
expect(router.navigate).toHaveBeenCalledWith(['/search'], { expect(router.navigate).toHaveBeenCalledWith(['/search'], {
queryParams: { view: ViewMode.Grid, page: 1 }, queryParams: { view: ViewMode.GridElement, page: 1 },
queryParamsHandling: 'merge' queryParamsHandling: 'merge'
}); });
}); });
it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => { it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => {
let viewMode = ViewMode.Grid; let viewMode = ViewMode.GridElement;
spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([ spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([
[ 'view', ViewMode.List ], [ 'view', ViewMode.ListElement ],
]))); ])));
searchService.getViewMode().subscribe((mode) => viewMode = mode); searchService.getViewMode().subscribe((mode) => viewMode = mode);
expect(viewMode).toEqual(ViewMode.List); expect(viewMode).toEqual(ViewMode.ListElement);
}); });
it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => { it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => {
let viewMode = ViewMode.List; let viewMode = ViewMode.ListElement;
spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([ spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([
[ 'view', ViewMode.Grid ], [ 'view', ViewMode.GridElement ],
]))); ])));
searchService.getViewMode().subscribe((mode) => viewMode = mode); searchService.getViewMode().subscribe((mode) => viewMode = mode);
expect(viewMode).toEqual(ViewMode.Grid); expect(viewMode).toEqual(ViewMode.GridElement);
}); });
describe('when search is called', () => { describe('when search is called', () => {

View File

@@ -97,14 +97,8 @@ export class SearchService implements OnDestroy {
} }
} }
/** getEndpoint(searchOptions?: PaginatedSearchOptions): Observable<string> {
* Method to retrieve a paginated list of search results from the server return this.halService.getEndpoint(this.searchLinkPath).pipe(
* @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(
map((url: string) => { map((url: string) => {
if (hasValue(searchOptions)) { if (hasValue(searchOptions)) {
return (searchOptions as PaginatedSearchOptions).toRestUrl(url); return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
@@ -113,6 +107,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( const requestObs = hrefObs.pipe(
map((url: string) => { map((url: string) => {
@@ -160,7 +165,7 @@ export class SearchService implements OnDestroy {
let co = DSpaceObject; let co = DSpaceObject;
if (dsos.payload[index]) { if (dsos.payload[index]) {
const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>; const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>;
co = getSearchResultFor(constructor, searchOptions.configuration); co = getSearchResultFor(constructor);
return Object.assign(new co(), object, { return Object.assign(new co(), object, {
indexableObject: dsos.payload[index] indexableObject: dsos.payload[index]
}); });
@@ -341,7 +346,7 @@ export class SearchService implements OnDestroy {
if (isNotEmpty(params.get('view')) && hasValue(params.get('view'))) { if (isNotEmpty(params.get('view')) && hasValue(params.get('view'))) {
return params.get('view'); return params.get('view');
} else { } else {
return ViewMode.List; return ViewMode.ListElement;
} }
})); }));
} }
@@ -354,7 +359,7 @@ export class SearchService implements OnDestroy {
this.routeService.getQueryParameterValue('pageSize').pipe(first()) this.routeService.getQueryParameterValue('pageSize').pipe(first())
.subscribe((pageSize) => { .subscribe((pageSize) => {
let queryParams = { view: viewMode, page: 1 }; let queryParams = { view: viewMode, page: 1 };
if (viewMode === ViewMode.Detail) { if (viewMode === ViewMode.DetailedListElement) {
queryParams = Object.assign(queryParams, {pageSize: '1'}); queryParams = Object.assign(queryParams, {pageSize: '1'});
} else if (pageSize === '1') { } else if (pageSize === '1') {
queryParams = Object.assign(queryParams, {pageSize: '10'}); queryParams = Object.assign(queryParams, {pageSize: '10'});

View File

@@ -27,6 +27,8 @@ export function getAdminModulePath() {
RouterModule.forRoot([ RouterModule.forRoot([
{ path: '', redirectTo: '/home', pathMatch: 'full' }, { path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' }, { 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: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },

View File

@@ -128,7 +128,7 @@ const EXPORTS = [
...PROVIDERS ...PROVIDERS
], ],
declarations: [ declarations: [
...DECLARATIONS, ...DECLARATIONS
], ],
exports: [ exports: [
...EXPORTS ...EXPORTS

View File

@@ -44,7 +44,11 @@ export class AuthRequestService {
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new AuthPostRequest(this.requestService.generateRequestId(), endpointURL, body, options)), 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)), mergeMap((request: PostRequest) => this.fetchRequest(request)),
distinctUntilChanged()); distinctUntilChanged());
} }
@@ -55,7 +59,11 @@ export class AuthRequestService {
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
distinctUntilChanged(), distinctUntilChanged(),
map((endpointURL: string) => new AuthGetRequest(this.requestService.generateRequestId(), endpointURL, options)), 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)), mergeMap((request: GetRequest) => this.fetchRequest(request)),
distinctUntilChanged()); distinctUntilChanged());
} }

View File

@@ -25,7 +25,7 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) { 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); return new AuthStatusResponse(response, data.statusCode, data.statusText);
} else { } else {
return new AuthStatusResponse(data.payload as NormalizedAuthStatus, data.statusCode, data.statusText); return new AuthStatusResponse(data.payload as NormalizedAuthStatus, data.statusCode, data.statusText);

View File

@@ -249,30 +249,34 @@ describe('AuthService test', () => {
it ('should set redirect url to previous page', () => { it ('should set redirect url to previous page', () => {
spyOn(routeServiceMock, 'getHistory').and.callThrough(); spyOn(routeServiceMock, 'getHistory').and.callThrough();
spyOn(routerStub, 'navigateByUrl');
authService.redirectAfterLoginSuccess(true); authService.redirectAfterLoginSuccess(true);
expect(routeServiceMock.getHistory).toHaveBeenCalled(); expect(routeServiceMock.getHistory).toHaveBeenCalled();
expect(routerStub.navigate).toHaveBeenCalledWith(['/collection/123']); expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/collection/123');
}); });
it ('should set redirect url to current page', () => { it ('should set redirect url to current page', () => {
spyOn(routeServiceMock, 'getHistory').and.callThrough(); spyOn(routeServiceMock, 'getHistory').and.callThrough();
spyOn(routerStub, 'navigateByUrl');
authService.redirectAfterLoginSuccess(false); authService.redirectAfterLoginSuccess(false);
expect(routeServiceMock.getHistory).toHaveBeenCalled(); expect(routeServiceMock.getHistory).toHaveBeenCalled();
expect(routerStub.navigate).toHaveBeenCalledWith(['/home']); expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/home');
}); });
it ('should redirect to / and not to /login', () => { it ('should redirect to / and not to /login', () => {
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['/login', '/login'])); spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['/login', '/login']));
spyOn(routerStub, 'navigateByUrl');
authService.redirectAfterLoginSuccess(true); authService.redirectAfterLoginSuccess(true);
expect(routeServiceMock.getHistory).toHaveBeenCalled(); expect(routeServiceMock.getHistory).toHaveBeenCalled();
expect(routerStub.navigate).toHaveBeenCalledWith(['/']); expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
}); });
it ('should redirect to / when no redirect url is found', () => { it ('should redirect to / when no redirect url is found', () => {
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf([''])); spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['']));
spyOn(routerStub, 'navigateByUrl');
authService.redirectAfterLoginSuccess(true); authService.redirectAfterLoginSuccess(true);
expect(routeServiceMock.getHistory).toHaveBeenCalled(); expect(routeServiceMock.getHistory).toHaveBeenCalled();
expect(routerStub.navigate).toHaveBeenCalledWith(['/']); expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
}); });
}); });
}); });

View File

@@ -347,8 +347,7 @@ export class AuthService {
if (isNotEmpty(redirectUrl)) { if (isNotEmpty(redirectUrl)) {
this.clearRedirectUrl(); this.clearRedirectUrl();
this.router.onSameUrlNavigation = 'reload'; this.router.onSameUrlNavigation = 'reload';
const url = decodeURIComponent(redirectUrl); this.navigateToRedirectUrl(redirectUrl);
this.navigateToRedirectUrl(url);
} else { } else {
// If redirectUrl is empty use history. // If redirectUrl is empty use history.
this.routeService.getHistory().pipe( 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. // 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)) { 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 */ /* 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 = '/'; // this._window.nativeWindow.location.href = '/';
} else { } else {
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */ /* 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._window.nativeWindow.location.href = url;
this.router.navigate([url]); this.router.navigateByUrl(url);
} }
} }

View File

@@ -114,7 +114,7 @@ describe('BrowseService', () => {
scheduler.schedule(() => service.getBrowseDefinitions().subscribe()); scheduler.schedule(() => service.getBrowseDefinitions().subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); expect(requestService.configure).toHaveBeenCalledWith(expected);
}); });
it('should call RemoteDataBuildService to create the RemoteData Observable', () => { 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.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); expect(requestService.configure).toHaveBeenCalledWith(expected);
}); });
it('should call RemoteDataBuildService to create the RemoteData Observable', () => { 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.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); expect(requestService.configure).toHaveBeenCalledWith(expected);
}); });
it('should call RemoteDataBuildService to create the RemoteData Observable', () => { it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
@@ -303,7 +303,7 @@ describe('BrowseService', () => {
scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe()); scheduler.schedule(() => service.getFirstItemFor(browseDefinitions[1].id).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); expect(requestService.configure).toHaveBeenCalledWith(expected);
}); });
it('should call RemoteDataBuildService to create the RemoteData Observable', () => { it('should call RemoteDataBuildService to create the RemoteData Observable', () => {

View File

@@ -44,6 +44,7 @@ export abstract class TypedObject {
*/ */
export class CacheableObject extends TypedObject { export class CacheableObject extends TypedObject {
uuid?: string; uuid?: string;
handle?: string;
self: string; self: string;
// isNew: boolean; // isNew: boolean;
// dirtyType: DirtyType; // dirtyType: DirtyType;

View File

@@ -4,7 +4,7 @@ import { applyPatch, Operation } from 'fast-json-patch';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators'; 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 { CoreState } from '../core.reducers';
import { coreSelector } from '../core.selectors'; import { coreSelector } from '../core.selectors';
import { RestRequestMethod } from '../data/rest-request-method'; import { RestRequestMethod } from '../data/rest-request-method';
@@ -80,7 +80,8 @@ export class ObjectCacheService {
* @return Observable<NormalizedObject<T>> * @return Observable<NormalizedObject<T>>
* An observable of the requested object in normalized form * 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( return this.store.pipe(
select(selfLinkFromUuidSelector(uuid)), select(selfLinkFromUuidSelector(uuid)),
mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink) mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink)

View File

@@ -24,7 +24,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 201 || data.statusCode === 200)) { 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)); return new ConfigSuccessResponse(configDefinition, data.statusCode, data.statusText, this.processPageInfo(data.payload));
} else { } else {
return new ErrorResponse( return new ErrorResponse(

View File

@@ -9,6 +9,7 @@ import { PaginatedList } from './paginated-list';
import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service'; import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service';
import { ResourceType } from '../shared/resource-type'; import { ResourceType } from '../shared/resource-type';
import { getMapsToType } from '../cache/builders/build-decorators'; import { getMapsToType } from '../cache/builders/build-decorators';
import { RestRequest } from './request.models';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
export abstract class BaseResponseParsingService { export abstract class BaseResponseParsingService {
@@ -16,14 +17,14 @@ export abstract class BaseResponseParsingService {
protected abstract objectCache: ObjectCacheService; protected abstract objectCache: ObjectCacheService;
protected abstract toCache: boolean; protected abstract toCache: boolean;
protected process<ObjectDomain>(data: any, requestUUID: string): any { protected process<ObjectDomain>(data: any, request: RestRequest): any {
if (isNotEmpty(data)) { if (isNotEmpty(data)) {
if (hasNoValue(data) || (typeof data !== 'object')) { if (hasNoValue(data) || (typeof data !== 'object')) {
return data; return data;
} else if (isRestPaginatedList(data)) { } else if (isRestPaginatedList(data)) {
return this.processPaginatedList(data, requestUUID); return this.processPaginatedList(data, request);
} else if (Array.isArray(data)) { } else if (Array.isArray(data)) {
return this.processArray(data, requestUUID); return this.processArray(data, request);
} else if (isRestDataObject(data)) { } else if (isRestDataObject(data)) {
const object = this.deserialize(data); const object = this.deserialize(data);
if (isNotEmpty(data._embedded)) { if (isNotEmpty(data._embedded)) {
@@ -31,7 +32,7 @@ export abstract class BaseResponseParsingService {
.keys(data._embedded) .keys(data._embedded)
.filter((property) => data._embedded.hasOwnProperty(property)) .filter((property) => data._embedded.hasOwnProperty(property))
.forEach((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 (isNotEmpty(parsedObj)) {
if (isRestPaginatedList(data._embedded[property])) { if (isRestPaginatedList(data._embedded[property])) {
object[property] = parsedObj; object[property] = parsedObj;
@@ -45,7 +46,7 @@ export abstract class BaseResponseParsingService {
}); });
} }
this.cache(object, requestUUID); this.cache(object, request);
return object; return object;
} }
const result = {}; const result = {};
@@ -53,14 +54,14 @@ export abstract class BaseResponseParsingService {
.filter((property) => data.hasOwnProperty(property)) .filter((property) => data.hasOwnProperty(property))
.filter((property) => hasValue(data[property])) .filter((property) => hasValue(data[property]))
.forEach((property) => { .forEach((property) => {
result[property] = this.process(data[property], requestUUID); result[property] = this.process(data[property], request);
}); });
return result; 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); const pageInfo: PageInfo = this.processPageInfo(data);
let list = data._embedded; let list = data._embedded;
@@ -70,14 +71,14 @@ export abstract class BaseResponseParsingService {
} else if (!Array.isArray(list)) { } else if (!Array.isArray(list)) {
list = this.flattenSingleKeyObject(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, ); 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[] = []; let array: ObjectDomain[] = [];
data.forEach((datum) => { data.forEach((datum) => {
array = [...array, this.process(datum, requestUUID)]; array = [...array, this.process(datum, request)];
} }
); );
return array; return array;
@@ -104,17 +105,17 @@ export abstract class BaseResponseParsingService {
} }
} }
protected cache<ObjectDomain>(obj, requestUUID) { protected cache<ObjectDomain>(obj, request: RestRequest) {
if (this.toCache) { 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)) { if (hasNoValue(co) || hasNoValue(co.self)) {
throw new Error('The server returned an invalid object'); 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 { processPageInfo(payload: any): PageInfo {

View File

@@ -38,7 +38,6 @@ const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSele
export class BitstreamFormatDataService extends DataService<BitstreamFormat> { export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
protected linkPath = 'bitstreamformats'; protected linkPath = 'bitstreamformats';
protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,

View File

@@ -37,7 +37,7 @@ describe('CollectionDataService', () => {
}); });
it('should configure a GET request', () => { it('should configure a GET request', () => {
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest), undefined); expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest));
}); });
}); });

View File

@@ -31,7 +31,6 @@ import { SearchParam } from '../cache/models/search-param.model';
@Injectable() @Injectable()
export class CollectionDataService extends ComColDataService<Collection> { export class CollectionDataService extends ComColDataService<Collection> {
protected linkPath = 'collections'; protected linkPath = 'collections';
protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,

View File

@@ -28,7 +28,6 @@ class NormalizedTestObject extends NormalizedObject<Item> {
} }
class TestService extends ComColDataService<any> { class TestService extends ComColDataService<any> {
protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,

View File

@@ -24,7 +24,6 @@ export class CommunityDataService extends ComColDataService<Community> {
protected linkPath = 'communities'; protected linkPath = 'communities';
protected topLinkPath = 'communities/search/top'; protected topLinkPath = 'communities/search/top';
protected cds = this; protected cds = this;
protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,

View File

@@ -24,7 +24,6 @@ class NormalizedTestObject extends NormalizedObject<Item> {
} }
class TestService extends DataService<any> { class TestService extends DataService<any> {
protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,

View File

@@ -45,11 +45,14 @@ export abstract class DataService<T extends CacheableObject> {
protected abstract store: Store<CoreState>; protected abstract store: Store<CoreState>;
protected abstract linkPath: string; protected abstract linkPath: string;
protected abstract halService: HALEndpointService; protected abstract halService: HALEndpointService;
protected abstract forceBypassCache = false;
protected abstract objectCache: ObjectCacheService; protected abstract objectCache: ObjectCacheService;
protected abstract notificationsService: NotificationsService; protected abstract notificationsService: NotificationsService;
protected abstract http: HttpClient; protected abstract http: HttpClient;
protected abstract comparator: ChangeAnalyzer<T>; 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> public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
@@ -131,7 +134,10 @@ export abstract class DataService<T extends CacheableObject> {
first((href: string) => hasValue(href))) first((href: string) => hasValue(href)))
.subscribe((href: string) => { .subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); 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>>>; return this.rdbService.buildList<T>(hrefObs) as Observable<RemoteData<PaginatedList<T>>>;
@@ -147,21 +153,29 @@ export abstract class DataService<T extends CacheableObject> {
} }
findById(id: string): Observable<RemoteData<T>> { findById(id: string): Observable<RemoteData<T>> {
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( 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( hrefObs.pipe(
find((href: string) => hasValue(href))) find((href: string) => hasValue(href)))
.subscribe((href: string) => { .subscribe((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id); 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); return this.rdbService.buildSingle<T>(hrefObs);
} }
findByHref(href: string, options?: HttpOptions): Observable<RemoteData<T>> { 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); return this.rdbService.buildSingle<T>(href);
} }
@@ -192,7 +206,8 @@ export abstract class DataService<T extends CacheableObject> {
first((href: string) => hasValue(href))) first((href: string) => hasValue(href)))
.subscribe((href: string) => { .subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request, true); request.responseMsToLive = 10 * 1000;
this.requestService.configure(request);
}); });
return this.rdbService.buildList<T>(hrefObs) as Observable<RemoteData<PaginatedList<T>>>; return this.rdbService.buildList<T>(hrefObs) as Observable<RemoteData<PaginatedList<T>>>;

View 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]);
});
})
});

View 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 '';
}
}
}

View File

@@ -30,7 +30,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
if (hasValue(data.payload) && hasValue(data.payload.page) && data.payload.page.totalElements === 0) { if (hasValue(data.payload) && hasValue(data.payload.page) && data.payload.page.totalElements === 0) {
processRequestDTO = { page: [] }; processRequestDTO = { page: [] };
} else { } else {
processRequestDTO = this.process<NormalizedObject<DSpaceObject>>(data.payload, request.uuid); processRequestDTO = this.process<NormalizedObject<DSpaceObject>>(data.payload, request);
} }
let objectList = processRequestDTO; let objectList = processRequestDTO;

View File

@@ -72,7 +72,7 @@ describe('DSpaceObjectDataService', () => {
scheduler.schedule(() => service.findById(testObject.uuid)); scheduler.schedule(() => service.findById(testObject.uuid));
scheduler.flush(); 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', () => { it('should return a RemoteData<DSpaceObject> for the object with the given ID', () => {

View File

@@ -18,7 +18,6 @@ import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
class DataServiceImpl extends DataService<DSpaceObject> { class DataServiceImpl extends DataService<DSpaceObject> {
protected linkPath = 'dso'; protected linkPath = 'dso';
protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,

View File

@@ -41,7 +41,6 @@ import { PaginatedList } from './paginated-list';
@Injectable() @Injectable()
export class ItemDataService extends DataService<Item> { export class ItemDataService extends DataService<Item> {
protected linkPath = 'items'; protected linkPath = 'items';
protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,

View File

@@ -19,7 +19,6 @@ import { MetadataSchema } from '../metadata/metadata-schema.model';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
class DataServiceImpl extends DataService<MetadataSchema> { class DataServiceImpl extends DataService<MetadataSchema> {
protected linkPath = 'metadataschemas'; protected linkPath = 'metadataschemas';
protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,

View File

@@ -14,6 +14,7 @@ import { PageInfo } from '../shared/page-info.model';
import { DeleteRequest } from './request.models'; import { DeleteRequest } from './request.models';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
describe('RelationshipService', () => { describe('RelationshipService', () => {
let service: RelationshipService; let service: RelationshipService;
@@ -22,12 +23,6 @@ describe('RelationshipService', () => {
const restEndpointURL = 'https://rest.api/'; const restEndpointURL = 'https://rest.api/';
const relationshipsEndpointURL = `${restEndpointURL}/relationships`; const relationshipsEndpointURL = `${restEndpointURL}/relationships`;
const halService: any = new HALEndpointServiceStub(restEndpointURL); const halService: any = new HALEndpointServiceStub(restEndpointURL);
const rdbService = getMockRemoteDataBuildService();
const objectCache = Object.assign({
/* tslint:disable:no-empty */
remove: () => {}
/* tslint:enable:no-empty */
}) as ObjectCacheService;
const relationshipType = Object.assign(new RelationshipType(), { const relationshipType = Object.assign(new RelationshipType(), {
id: '1', id: '1',
@@ -72,17 +67,30 @@ describe('RelationshipService', () => {
relationship2.rightItem = getRemotedataObservable(item); relationship2.rightItem = getRemotedataObservable(item);
const relatedItems = [relatedItem1, relatedItem2]; 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: () => {}
/* tslint:enable:no-empty */
}) as ObjectCacheService;
const itemService = jasmine.createSpyObj('itemService', { const itemService = jasmine.createSpyObj('itemService', {
findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.filter((relatedItem) => relatedItem.id === uuid)[0]) findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.filter((relatedItem) => relatedItem.id === uuid)[0])
}); });
function initTestService() { function initTestService() {
return new RelationshipService( return new RelationshipService(
requestService,
halService,
rdbService,
itemService, itemService,
objectCache requestService,
rdbService,
null,
null,
halService,
objectCache,
null,
null,
null
); );
} }
@@ -106,7 +114,7 @@ describe('RelationshipService', () => {
it('should send a DeleteRequest', () => { it('should send a DeleteRequest', () => {
const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid); 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', () => { it('should clear the related items their cache', () => {
@@ -144,7 +152,7 @@ describe('RelationshipService', () => {
describe('getRelatedItemsByLabel', () => { describe('getRelatedItemsByLabel', () => {
it('should return the related items by label', () => { it('should return the related items by label', () => {
service.getRelatedItemsByLabel(item, relationshipType.rightwardType).subscribe((result) => { service.getRelatedItemsByLabel(item, relationshipType.rightwardType).subscribe((result) => {
expect(result).toEqual(relatedItems); expect(result.payload.page).toEqual(relatedItems);
}); });
}); });
}) })

View File

@@ -10,7 +10,7 @@ import {
getRemoteDataPayload, getResponseFromEntry, getRemoteDataPayload, getResponseFromEntry,
getSucceededRemoteData getSucceededRemoteData
} from '../shared/operators'; } from '../shared/operators';
import { DeleteRequest, RestRequest } from './request.models'; import { DeleteRequest, FindAllOptions, RestRequest } from './request.models';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { RestResponse } from '../cache/response.models'; import { RestResponse } from '../cache/response.models';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
@@ -22,23 +22,42 @@ import { zip as observableZip } from 'rxjs';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
import { ItemDataService } from './item-data.service'; import { ItemDataService } from './item-data.service';
import { import {
compareArraysUsingIds, filterRelationsByTypeLabel, compareArraysUsingIds, filterRelationsByTypeLabel, paginatedRelationsToItems,
relationsToItems relationsToItems
} from '../../+item-page/simple/item-types/shared/item-relationships-utils'; } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DataService } from './data.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { SearchParam } from '../cache/models/search-param.model';
/** /**
* The service handling all relationship requests * The service handling all relationship requests
*/ */
@Injectable() @Injectable()
export class RelationshipService { export class RelationshipService extends DataService<Relationship> {
protected linkPath = 'relationships'; protected linkPath = 'relationships';
protected forceBypassCache = false;
constructor(protected requestService: RequestService, constructor(protected itemService: ItemDataService,
protected halService: HALEndpointService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected itemService: ItemDataService, protected dataBuildService: NormalizedObjectBuildService,
protected objectCache: ObjectCacheService) { protected store: Store<CoreState>,
protected halService: HALEndpointService,
protected objectCache: ObjectCacheService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<Relationship>) {
super();
}
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return this.halService.getEndpoint(linkPath);
} }
/** /**
@@ -207,12 +226,31 @@ export class RelationshipService {
* and return the items as an array * and return the items as an array
* @param item * @param item
* @param label * @param label
* @param options
*/ */
getRelatedItemsByLabel(item: Item, label: string): Observable<Item[]> { getRelatedItemsByLabel(item: Item, label: string, options?: FindAllOptions): Observable<RemoteData<PaginatedList<Item>>> {
return this.getItemResolvedRelsAndTypes(item).pipe( return this.getItemRelationshipsByLabel(item, label, options).pipe(paginatedRelationsToItems(item.uuid));
filterRelationsByTypeLabel(label), }
relationsToItems(item.uuid)
); /**
* Resolve a given item's relationships into related items, filtered by a relationship label
* and return the items as an array
* @param item
* @param label
* @param options
*/
getItemRelationshipsByLabel(item: Item, label: string, options?: FindAllOptions): Observable<RemoteData<PaginatedList<Relationship>>> {
let findAllOptions = new FindAllOptions();
if (options) {
findAllOptions = Object.assign(new FindAllOptions(), options);
}
const searchParams = [ new SearchParam('label', label), new SearchParam('dso', item.id) ];
if (findAllOptions.searchParams) {
findAllOptions.searchParams = [...findAllOptions.searchParams, ...searchParams];
} else {
findAllOptions.searchParams = searchParams;
}
return this.searchBy('byLabel', findAllOptions);
} }
/** /**

View File

@@ -22,8 +22,15 @@ import { MappedCollectionsReponseParsingService } from './mapped-collections-rep
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
// uuid and handle requests have separate endpoints
export enum IdentifierType {
UUID ='uuid',
HANDLE = 'handle'
}
export abstract class RestRequest { export abstract class RestRequest {
public responseMsToLive = 0; public responseMsToLive = 10 * 1000;
public forceBypassCache = false;
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
@@ -49,7 +56,7 @@ export class GetRequest extends RestRequest {
public uuid: string, public uuid: string,
public href: string, public href: string,
public body?: any, public body?: any,
public options?: HttpOptions, public options?: HttpOptions
) { ) {
super(uuid, href, RestRequestMethod.GET, body, options) super(uuid, href, RestRequestMethod.GET, body, options)
} }
@@ -293,6 +300,7 @@ export class UpdateMetadataFieldRequest extends PutRequest {
* Class representing a submission HTTP GET request object * Class representing a submission HTTP GET request object
*/ */
export class SubmissionRequest extends GetRequest { export class SubmissionRequest extends GetRequest {
forceBypassCache = true;
constructor(uuid: string, href: string) { constructor(uuid: string, href: string) {
super(uuid, href); super(uuid, href);
} }
@@ -404,7 +412,7 @@ export class TaskDeleteRequest extends DeleteRequest {
} }
export class MyDSpaceRequest extends GetRequest { export class MyDSpaceRequest extends GetRequest {
public responseMsToLive = 0; public responseMsToLive = 10 * 1000;
} }
export class RequestError extends Error { export class RequestError extends Error {

View File

@@ -298,10 +298,11 @@ describe('RequestService', () => {
describe('in the ObjectCache', () => { describe('in the ObjectCache', () => {
beforeEach(() => { beforeEach(() => {
(objectCache.hasBySelfLink as any).and.returnValue(true); (objectCache.hasBySelfLink as any).and.returnValue(true);
(objectCache.hasByUUID as any).and.returnValue(true);
spyOn(serviceAsAny, 'hasByHref').and.returnValue(false); spyOn(serviceAsAny, 'hasByHref').and.returnValue(false);
}); });
it('should return true', () => { it('should return true for GetRequest', () => {
const result = serviceAsAny.isCachedOrPending(testGetRequest); const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = true; const expected = true;

View File

@@ -19,7 +19,7 @@ import {
} from '../index/index.selectors'; } from '../index/index.selectors';
import { UUIDService } from '../shared/uuid.service'; import { UUIDService } from '../shared/uuid.service';
import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions'; 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 { RequestEntry, RequestState } from './request.reducer';
import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
import { RestRequestMethod } from './rest-request-method'; import { RestRequestMethod } from './rest-request-method';
@@ -145,14 +145,10 @@ export class RequestService {
* Configure a certain request * Configure a certain request
* Used to make sure a request is in the cache * Used to make sure a request is in the cache
* @param {RestRequest} request The request to send out * @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; const isGetRequest = request.method === RestRequestMethod.GET;
if (forceBypassCache) { if (!isGetRequest || request.forceBypassCache || !this.isCachedOrPending(request)) {
this.clearRequestsOnTheirWayToTheStore(request);
}
if (!isGetRequest || (forceBypassCache && !this.isPending(request)) || !this.isCachedOrPending(request)) {
this.dispatchRequest(request); this.dispatchRequest(request);
if (isGetRequest) { if (isGetRequest) {
this.trackRequestsOnTheirWayToTheStore(request); this.trackRequestsOnTheirWayToTheStore(request);
@@ -226,7 +222,6 @@ export class RequestService {
const inReqCache = this.hasByHref(request.href); const inReqCache = this.hasByHref(request.href);
const inObjCache = this.objectCache.hasBySelfLink(request.href); const inObjCache = this.objectCache.hasBySelfLink(request.href);
const isCached = inReqCache || inObjCache; const isCached = inReqCache || inObjCache;
const isPending = this.isPending(request); const isPending = this.isPending(request);
return isCached || isPending; return isCached || isPending;
} }

View File

@@ -61,7 +61,7 @@ describe('ResourcePolicyService', () => {
scheduler.schedule(() => service.findByHref(requestURL)); scheduler.schedule(() => service.findByHref(requestURL));
scheduler.flush(); 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', () => { it('should return a RemoteData<ResourcePolicy> for the object with the given URL', () => {

View File

@@ -22,7 +22,6 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
class DataServiceImpl extends DataService<ResourcePolicy> { class DataServiceImpl extends DataService<ResourcePolicy> {
protected linkPath = 'resourcepolicies'; protected linkPath = 'resourcepolicies';
protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,

View File

@@ -28,7 +28,7 @@ export class EpersonResponseParsingService extends BaseResponseParsingService im
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { 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)); return new EpersonSuccessResponse(epersonDefinition[Object.keys(epersonDefinition)[0]], data.statusCode, data.statusText, this.processPageInfo(data.payload));
} else { } else {
return new ErrorResponse( return new ErrorResponse(

View File

@@ -27,7 +27,6 @@ import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
export class GroupEpersonService extends EpersonService<Group> { export class GroupEpersonService extends EpersonService<Group> {
protected linkPath = 'groups'; protected linkPath = 'groups';
protected browseEndpoint = ''; protected browseEndpoint = '';
protected forceBypassCache = false;
constructor( constructor(
protected comparator: DSOChangeAnalyzer<Group>, protected comparator: DSOChangeAnalyzer<Group>,

View File

@@ -1,7 +1,6 @@
import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer'; import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { EPerson } from './eperson.model'; import { EPerson } from './eperson.model';
import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { mapsTo, relationship } from '../../cache/builders/build-decorators';
@@ -9,7 +8,7 @@ import { Group } from './group.model';
@mapsTo(EPerson) @mapsTo(EPerson)
@inheritSerialization(NormalizedDSpaceObject) @inheritSerialization(NormalizedDSpaceObject)
export class NormalizedEPerson extends NormalizedDSpaceObject<EPerson> implements CacheableObject, ListableObject { export class NormalizedEPerson extends NormalizedDSpaceObject<EPerson> implements CacheableObject {
/** /**
* A string representing the unique handle of this EPerson * A string representing the unique handle of this EPerson
*/ */

View File

@@ -1,14 +1,13 @@
import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer'; import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { Group } from './group.model'; import { Group } from './group.model';
@mapsTo(Group) @mapsTo(Group)
@inheritSerialization(NormalizedDSpaceObject) @inheritSerialization(NormalizedDSpaceObject)
export class NormalizedGroup extends NormalizedDSpaceObject<Group> implements CacheableObject, ListableObject { export class NormalizedGroup extends NormalizedDSpaceObject<Group> implements CacheableObject {
/** /**
* List of Groups that this Group belong to * List of Groups that this Group belong to

View File

@@ -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 * 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 * 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 * @returns
* a MemoizedSelector to select the self link * a MemoizedSelector to select the self link
*/ */

View File

@@ -27,7 +27,7 @@ export class IntegrationResponseParsingService extends BaseResponseParsingServic
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { 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)); return new IntegrationSuccessResponse(this.processResponse(dataDefinition), data.statusCode, data.statusText, this.processPageInfo(data.payload));
} else { } else {
return new ErrorResponse( return new ErrorResponse(

View File

@@ -2,6 +2,7 @@ import { ListableObject } from '../../shared/object-collection/shared/listable-o
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { MetadataSchema } from './metadata-schema.model'; import { MetadataSchema } from './metadata-schema.model';
import { ResourceType } from '../shared/resource-type'; import { ResourceType } from '../shared/resource-type';
import { GenericConstructor } from '../shared/generic-constructor';
/** /**
* Class the represents a metadata field * Class the represents a metadata field
@@ -50,4 +51,11 @@ export class MetadataField implements ListableObject {
} }
return key; return key;
} }
/**
* Method that returns as which type of object this object should be rendered
*/
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
return [this.constructor as GenericConstructor<ListableObject>];
}
} }

View File

@@ -1,5 +1,6 @@
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
import { ResourceType } from '../shared/resource-type'; import { ResourceType } from '../shared/resource-type';
import { GenericConstructor } from '../shared/generic-constructor';
/** /**
* Class that represents a metadata schema * Class that represents a metadata schema
@@ -26,4 +27,11 @@ export class MetadataSchema implements ListableObject {
* The namespace of this metadata schema * The namespace of this metadata schema
*/ */
namespace: string; namespace: string;
/**
* Method that returns as which type of object this object should be rendered
*/
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
return [this.constructor as GenericConstructor<ListableObject>];
}
} }

View File

@@ -10,7 +10,7 @@ import { MetadataSchema } from './metadata-schema.model';
*/ */
@mapsTo(MetadataField) @mapsTo(MetadataField)
@inheritSerialization(NormalizedObject) @inheritSerialization(NormalizedObject)
export class NormalizedMetadataField extends NormalizedObject<MetadataField> implements ListableObject { export class NormalizedMetadataField extends NormalizedObject<MetadataField> {
/** /**
* The identifier of this normalized metadata field * The identifier of this normalized metadata field

View File

@@ -9,7 +9,7 @@ import { MetadataSchema } from './metadata-schema.model';
*/ */
@mapsTo(MetadataSchema) @mapsTo(MetadataSchema)
@inheritSerialization(NormalizedObject) @inheritSerialization(NormalizedObject)
export class NormalizedMetadataSchema extends NormalizedObject<MetadataSchema> implements ListableObject { export class NormalizedMetadataSchema extends NormalizedObject<MetadataSchema> {
/** /**
* The unique identifier for this schema * The unique identifier for this schema
*/ */

View File

@@ -55,22 +55,24 @@ describe('RegistryService', () => {
}); });
const mockSchemasList = [ const mockSchemasList = [
{ Object.assign(new MetadataSchema(), {
id: 1, id: 1,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1',
prefix: 'dc', prefix: 'dc',
namespace: 'http://dublincore.org/documents/dcmi-terms/', namespace: 'http://dublincore.org/documents/dcmi-terms/',
type: MetadataSchema.type type: MetadataSchema.type
}, }),
{ Object.assign(new MetadataSchema(), {
id: 2, id: 2,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2',
prefix: 'mock', prefix: 'mock',
namespace: 'http://dspace.org/mockschema', namespace: 'http://dspace.org/mockschema',
type: MetadataSchema.type type: MetadataSchema.type
} })
]; ];
const mockFieldsList = [ const mockFieldsList = [
Object.assign(new MetadataField(),
{ {
id: 1, id: 1,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8',
@@ -79,8 +81,9 @@ describe('RegistryService', () => {
scopeNote: null, scopeNote: null,
schema: mockSchemasList[0], schema: mockSchemasList[0],
type: MetadataField.type type: MetadataField.type
}, }),
{ Object.assign(new MetadataField(),
{
id: 2, id: 2,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9',
element: 'contributor', element: 'contributor',
@@ -88,8 +91,9 @@ describe('RegistryService', () => {
scopeNote: null, scopeNote: null,
schema: mockSchemasList[0], schema: mockSchemasList[0],
type: MetadataField.type type: MetadataField.type
}, }),
{ Object.assign(new MetadataField(),
{
id: 3, id: 3,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10',
element: 'contributor', element: 'contributor',
@@ -97,8 +101,9 @@ describe('RegistryService', () => {
scopeNote: 'test scope note', scopeNote: 'test scope note',
schema: mockSchemasList[1], schema: mockSchemasList[1],
type: MetadataField.type type: MetadataField.type
}, }),
{ Object.assign(new MetadataField(),
{
id: 4, id: 4,
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11',
element: 'contributor', element: 'contributor',
@@ -106,7 +111,7 @@ describe('RegistryService', () => {
scopeNote: null, scopeNote: null,
schema: mockSchemasList[1], schema: mockSchemasList[1],
type: MetadataField.type type: MetadataField.type
} })
]; ];
const pageInfo = new PageInfo(); const pageInfo = new PageInfo();

View File

@@ -400,7 +400,7 @@ export class RegistryService {
distinctUntilChanged() distinctUntilChanged()
); );
const serializedSchema = new DSpaceRESTv2Serializer(getMapsToType(MetadataSchema.type)).serialize(schema as NormalizedMetadataSchema); const serializedSchema = new DSpaceRESTv2Serializer(getMapsToType(MetadataSchema.type)).serialize(schema);
const request$ = endpoint$.pipe( const request$ = endpoint$.pipe(
take(1), take(1),

Some files were not shown because too many files have changed in this diff Show More