mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'master' into w2p-62339_Relationships-link-to-item-via-HAL-links
Conflicts: src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts src/app/+item-page/simple/item-types/shared/item.component.ts
This commit is contained in:
@@ -14,7 +14,7 @@ If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [h
|
|||||||
Quick start
|
Quick start
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**Ensure you're running [Node](https://nodejs.org) >= `v8.0.x`, [npm](https://www.npmjs.com/) >= `v3.x` and [yarn](https://yarnpkg.com) >= `v0.20.x`**
|
**Ensure you're running [Node](https://nodejs.org) >= `v8.0.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# clone the repo
|
# clone the repo
|
||||||
@@ -65,7 +65,7 @@ Requirements
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org), [npm](https://www.npmjs.com/), and [yarn](https://yarnpkg.com)
|
- [Node.js](https://nodejs.org), [npm](https://www.npmjs.com/), and [yarn](https://yarnpkg.com)
|
||||||
- Ensure you're running node >= `v5.x`, npm >= `v3.x` and yarn >= `v0.20.x`
|
- Ensure you're running node >= `v8.x`, npm >= `v5.x` and yarn >= `v1.x`
|
||||||
|
|
||||||
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
||||||
|
|
||||||
|
@@ -10,10 +10,10 @@ module.exports = {
|
|||||||
// The REST API server settings.
|
// The REST API server settings.
|
||||||
rest: {
|
rest: {
|
||||||
ssl: true,
|
ssl: true,
|
||||||
host: 'dspace7-entities.atmire.com',
|
host: 'dspace7.4science.cloud',
|
||||||
port: 443,
|
port: 443,
|
||||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
nameSpace: '/rest/api'
|
nameSpace: '/dspace-spring-rest/api'
|
||||||
},
|
},
|
||||||
// Caching settings
|
// Caching settings
|
||||||
cache: {
|
cache: {
|
||||||
|
10
package.json
10
package.json
@@ -23,9 +23,9 @@
|
|||||||
"prebuild": "yarn run clean:dist",
|
"prebuild": "yarn run clean:dist",
|
||||||
"prebuild:aot": "yarn run prebuild",
|
"prebuild:aot": "yarn run prebuild",
|
||||||
"prebuild:prod": "yarn run prebuild",
|
"prebuild:prod": "yarn run prebuild",
|
||||||
"build": "webpack --progress --mode development",
|
"build": "node ./webpack/run-webpack.js --progress --mode development",
|
||||||
"build:aot": "webpack --env.aot --env.server --mode development && webpack --env.aot --env.client --mode development",
|
"build:aot": "node ./webpack/run-webpack.js --env.aot --env.server --mode development && node ./webpack/run-webpack.js --env.aot --env.client --mode development",
|
||||||
"build:prod": "webpack --env.aot --env.server --mode production && webpack --env.aot --env.client --mode production",
|
"build:prod": "node ./webpack/run-webpack.js --env.aot --env.server --mode production && node ./webpack/run-webpack.js --env.aot --env.client --mode production",
|
||||||
"postbuild:prod": "yarn run rollup",
|
"postbuild:prod": "yarn run rollup",
|
||||||
"rollup": "rollup -c rollup.config.js",
|
"rollup": "rollup -c rollup.config.js",
|
||||||
"prestart": "yarn run build:prod",
|
"prestart": "yarn run build:prod",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"server": "node dist/server.js",
|
"server": "node dist/server.js",
|
||||||
"server:watch": "nodemon dist/server.js",
|
"server:watch": "nodemon dist/server.js",
|
||||||
"server:watch:debug": "nodemon --debug dist/server.js",
|
"server:watch:debug": "nodemon --debug dist/server.js",
|
||||||
"webpack:watch": "webpack -w --mode development",
|
"webpack:watch": "node ./webpack/run-webpack.js -w --mode development",
|
||||||
"watch": "yarn run build && npm-run-all -p webpack:watch server:watch",
|
"watch": "yarn run build && npm-run-all -p webpack:watch server:watch",
|
||||||
"watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug",
|
"watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug",
|
||||||
"predebug": "yarn run build",
|
"predebug": "yarn run build",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"methods": "1.1.2",
|
"methods": "1.1.2",
|
||||||
"moment": "^2.22.1",
|
"moment": "^2.22.1",
|
||||||
"morgan": "1.9.0",
|
"morgan": "^1.9.1",
|
||||||
"ng-mocks": "^6.2.1",
|
"ng-mocks": "^6.2.1",
|
||||||
"ng2-file-upload": "1.2.1",
|
"ng2-file-upload": "1.2.1",
|
||||||
"ng2-nouislider": "^1.7.11",
|
"ng2-nouislider": "^1.7.11",
|
||||||
|
@@ -410,6 +410,7 @@
|
|||||||
},
|
},
|
||||||
"login": "Log In",
|
"login": "Log In",
|
||||||
"logout": "Log Out",
|
"logout": "Log Out",
|
||||||
|
"mydspace": "MyDSpace",
|
||||||
"language": "Language switch",
|
"language": "Language switch",
|
||||||
"search": "Search"
|
"search": "Search"
|
||||||
},
|
},
|
||||||
@@ -446,12 +447,64 @@
|
|||||||
"help": "Select a community to browse its collections."
|
"help": "Select a community to browse its collections."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mydspace": {
|
||||||
|
"title": "MyDSpace",
|
||||||
|
"description": "",
|
||||||
|
"new-submission": "New submission",
|
||||||
|
"results": {
|
||||||
|
"head": "Your submissions",
|
||||||
|
"no-results": "There were no items to show",
|
||||||
|
"no-title": "No title",
|
||||||
|
"no-authors": "No Authors",
|
||||||
|
"no-date": "No Date",
|
||||||
|
"no-abstract": "No Abstract",
|
||||||
|
"no-files": "No Files",
|
||||||
|
"no-uri": "No Uri",
|
||||||
|
"no-collections": "No Collections"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"title": "Messages",
|
||||||
|
"to": "To",
|
||||||
|
"hide-msg": "Hide message",
|
||||||
|
"show-msg": "Show message",
|
||||||
|
"no-messages": "No messages yet.",
|
||||||
|
"no-content": "No content.",
|
||||||
|
"send-btn": "Send",
|
||||||
|
"subject-placeholder": "Subject...",
|
||||||
|
"description-placeholder": "Insert your message here...",
|
||||||
|
"mark-as-read": "Mark as read",
|
||||||
|
"mark-as-unread": "Mark as unread",
|
||||||
|
"submitter-help": "Select this option to send a message to controller.",
|
||||||
|
"controller-help": "Select this option to send a message to item's submitter."
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"workspace": "Your Submissions",
|
||||||
|
"workflow": "All tasks"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"workflow": "Workflow",
|
||||||
|
"validation": "Validation",
|
||||||
|
"waiting-for-controller": "Waiting for controller",
|
||||||
|
"workspace": "Workspace",
|
||||||
|
"archived": "Archived"
|
||||||
|
},
|
||||||
|
"view-btn": "View",
|
||||||
|
"general": {
|
||||||
|
"text-here": "HERE"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"upload-successful": "New workspace item created. Click {{here}} for edit it.",
|
||||||
|
"upload-multiple-successful": "{{qty}} new workspace items created.",
|
||||||
|
"upload-failed": "Error creating new workspace. Please verify the content uploaded before retry."
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "DSpace Angular :: Search",
|
"title": "DSpace Angular :: Search",
|
||||||
"description": "",
|
"description": "",
|
||||||
"form": {
|
"form": {
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"search_dspace": "Search DSpace"
|
"search_dspace": "Search DSpace",
|
||||||
|
"search_mydspace": "Search MyDSpace"
|
||||||
},
|
},
|
||||||
"results": {
|
"results": {
|
||||||
"head": "Search Results",
|
"head": "Search Results",
|
||||||
@@ -471,9 +524,13 @@
|
|||||||
"rpp": "Results per page"
|
"rpp": "Results per page"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"switch-configuration": {
|
||||||
|
"title":"Show"
|
||||||
|
},
|
||||||
"view-switch": {
|
"view-switch": {
|
||||||
"show-list": "Show as list",
|
"show-list": "Show as list",
|
||||||
"show-grid": "Show as grid"
|
"show-grid": "Show as grid",
|
||||||
|
"show-detail": "Show detail"
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"head": "Filters",
|
"head": "Filters",
|
||||||
@@ -484,7 +541,11 @@
|
|||||||
"f.dateIssued.max": "End date",
|
"f.dateIssued.max": "End date",
|
||||||
"f.subject": "Subject",
|
"f.subject": "Subject",
|
||||||
"f.has_content_in_original_bundle": "Has files",
|
"f.has_content_in_original_bundle": "Has files",
|
||||||
"f.entityType": "Item Type"
|
"f.entityType": "Item Type",
|
||||||
|
"f.namedresourcetype": "Status",
|
||||||
|
"f.dateSubmitted": "Date submitted",
|
||||||
|
"f.itemtype": "Type",
|
||||||
|
"f.submitter": "Submitter"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"show-more": "Show more",
|
"show-more": "Show more",
|
||||||
@@ -516,6 +577,26 @@
|
|||||||
"entityType": {
|
"entityType": {
|
||||||
"placeholder": "Item Type",
|
"placeholder": "Item Type",
|
||||||
"head": "Item Type"
|
"head": "Item Type"
|
||||||
|
},
|
||||||
|
"namedresourcetype": {
|
||||||
|
"placeholder": "Status",
|
||||||
|
"head": "Status"
|
||||||
|
},
|
||||||
|
"dateSubmitted": {
|
||||||
|
"placeholder": "Date submitted",
|
||||||
|
"head": "Date submitted"
|
||||||
|
},
|
||||||
|
"itemtype": {
|
||||||
|
"placeholder": "Type",
|
||||||
|
"head": "Type"
|
||||||
|
},
|
||||||
|
"submitter": {
|
||||||
|
"placeholder": "Submitter",
|
||||||
|
"head": "Submitter"
|
||||||
|
},
|
||||||
|
"objectpeople": {
|
||||||
|
"placeholder": "People",
|
||||||
|
"head": "People"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -736,6 +817,7 @@
|
|||||||
"item": "Loading item...",
|
"item": "Loading item...",
|
||||||
"objects": "Loading...",
|
"objects": "Loading...",
|
||||||
"search-results": "Loading search results...",
|
"search-results": "Loading search results...",
|
||||||
|
"mydspace-results": "Loading items...",
|
||||||
"browse-by": "Loading items...",
|
"browse-by": "Loading items...",
|
||||||
"browse-by-page": "Loading page..."
|
"browse-by-page": "Loading page..."
|
||||||
},
|
},
|
||||||
@@ -927,6 +1009,49 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"generic": {
|
||||||
|
"delete": "Delete",
|
||||||
|
"delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.",
|
||||||
|
"edit": "Edit",
|
||||||
|
"edit-help": "Select this option to change the item's metadata.",
|
||||||
|
"view": "View",
|
||||||
|
"view-help": "Select this option to view the item's metadata."
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"generic": {
|
||||||
|
"processing": "Processing...",
|
||||||
|
"success": "Operation successful",
|
||||||
|
"error": "Error occurred during operation...",
|
||||||
|
"submitter": "Submitter"
|
||||||
|
},
|
||||||
|
"claimed": {
|
||||||
|
"approve": "Approve",
|
||||||
|
"approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".",
|
||||||
|
"edit": "Edit",
|
||||||
|
"edit_help": "Select this option to change the item's metadata.",
|
||||||
|
"reject": {
|
||||||
|
"submit": "Reject",
|
||||||
|
"reason": {
|
||||||
|
"submit": "Reject item",
|
||||||
|
"title": "Reason",
|
||||||
|
"info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.",
|
||||||
|
"placeholder": "Describe the reason of reject"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reject_help": "If you have reviewed the item and found it is <strong>not</strong> suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.",
|
||||||
|
"return": "Return to pool",
|
||||||
|
"return_help": "Return the task to the pool so that another user may perform the task."
|
||||||
|
|
||||||
|
},
|
||||||
|
"pool": {
|
||||||
|
"claim": "Claim",
|
||||||
|
"claim_help": "Assign this task to yourself.",
|
||||||
|
"show-detail": "Show detail",
|
||||||
|
"hide-detail": "Hide detail"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uploader": {
|
"uploader": {
|
||||||
|
@@ -6,13 +6,24 @@ import { PaginatedList } from '../../../core/data/paginated-list';
|
|||||||
import { BitstreamFormat } from '../../../core/registry/mock-bitstream-format.model';
|
import { BitstreamFormat } from '../../../core/registry/mock-bitstream-format.model';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders a list of bitstream formats
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-bitstream-formats',
|
selector: 'ds-bitstream-formats',
|
||||||
templateUrl: './bitstream-formats.component.html'
|
templateUrl: './bitstream-formats.component.html'
|
||||||
})
|
})
|
||||||
export class BitstreamFormatsComponent {
|
export class BitstreamFormatsComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A paginated list of bitstream formats to be shown on the page
|
||||||
|
*/
|
||||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current pagination configuration for the page
|
||||||
|
* Currently simply renders all bitstream formats
|
||||||
|
*/
|
||||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
id: 'registry-bitstreamformats-pagination',
|
id: 'registry-bitstreamformats-pagination',
|
||||||
pageSize: 10000
|
pageSize: 10000
|
||||||
@@ -22,11 +33,18 @@ export class BitstreamFormatsComponent {
|
|||||||
this.updateFormats();
|
this.updateFormats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the page is changed, make sure to update the list of bitstreams to match the new page
|
||||||
|
* @param event The page change event
|
||||||
|
*/
|
||||||
onPageChange(event) {
|
onPageChange(event) {
|
||||||
this.config.currentPage = event;
|
this.config.currentPage = event;
|
||||||
this.updateFormats();
|
this.updateFormats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to update the bitstream formats that are shown
|
||||||
|
*/
|
||||||
private updateFormats() {
|
private updateFormats() {
|
||||||
this.bitstreamFormats = this.registryService.getBitstreamFormats(this.config);
|
this.bitstreamFormats = this.registryService.getBitstreamFormats(this.config);
|
||||||
}
|
}
|
||||||
|
@@ -28,7 +28,7 @@ export const MetadataRegistryActionTypes = {
|
|||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
/**
|
/**
|
||||||
* Used to collapse the sidebar
|
* Used to edit a metadata schema in the metadata registry
|
||||||
*/
|
*/
|
||||||
export class MetadataRegistryEditSchemaAction implements Action {
|
export class MetadataRegistryEditSchemaAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.EDIT_SCHEMA;
|
type = MetadataRegistryActionTypes.EDIT_SCHEMA;
|
||||||
@@ -41,12 +41,15 @@ export class MetadataRegistryEditSchemaAction implements Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to expand the sidebar
|
* Used to cancel the editing of a metadata schema in the metadata registry
|
||||||
*/
|
*/
|
||||||
export class MetadataRegistryCancelSchemaAction implements Action {
|
export class MetadataRegistryCancelSchemaAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.CANCEL_EDIT_SCHEMA;
|
type = MetadataRegistryActionTypes.CANCEL_EDIT_SCHEMA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to select a single metadata schema in the metadata registry
|
||||||
|
*/
|
||||||
export class MetadataRegistrySelectSchemaAction implements Action {
|
export class MetadataRegistrySelectSchemaAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.SELECT_SCHEMA;
|
type = MetadataRegistryActionTypes.SELECT_SCHEMA;
|
||||||
|
|
||||||
@@ -57,6 +60,9 @@ export class MetadataRegistrySelectSchemaAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to deselect a single metadata schema in the metadata registry
|
||||||
|
*/
|
||||||
export class MetadataRegistryDeselectSchemaAction implements Action {
|
export class MetadataRegistryDeselectSchemaAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.DESELECT_SCHEMA;
|
type = MetadataRegistryActionTypes.DESELECT_SCHEMA;
|
||||||
|
|
||||||
@@ -67,12 +73,15 @@ export class MetadataRegistryDeselectSchemaAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to deselect all metadata schemas in the metadata registry
|
||||||
|
*/
|
||||||
export class MetadataRegistryDeselectAllSchemaAction implements Action {
|
export class MetadataRegistryDeselectAllSchemaAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.DESELECT_ALL_SCHEMA;
|
type = MetadataRegistryActionTypes.DESELECT_ALL_SCHEMA;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to collapse the sidebar
|
* Used to edit a metadata field in the metadata registry
|
||||||
*/
|
*/
|
||||||
export class MetadataRegistryEditFieldAction implements Action {
|
export class MetadataRegistryEditFieldAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.EDIT_FIELD;
|
type = MetadataRegistryActionTypes.EDIT_FIELD;
|
||||||
@@ -85,12 +94,15 @@ export class MetadataRegistryEditFieldAction implements Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to expand the sidebar
|
* Used to cancel the editing of a metadata field in the metadata registry
|
||||||
*/
|
*/
|
||||||
export class MetadataRegistryCancelFieldAction implements Action {
|
export class MetadataRegistryCancelFieldAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.CANCEL_EDIT_FIELD;
|
type = MetadataRegistryActionTypes.CANCEL_EDIT_FIELD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to select a single metadata field in the metadata registry
|
||||||
|
*/
|
||||||
export class MetadataRegistrySelectFieldAction implements Action {
|
export class MetadataRegistrySelectFieldAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.SELECT_FIELD;
|
type = MetadataRegistryActionTypes.SELECT_FIELD;
|
||||||
|
|
||||||
@@ -101,6 +113,9 @@ export class MetadataRegistrySelectFieldAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to deselect a single metadata field in the metadata registry
|
||||||
|
*/
|
||||||
export class MetadataRegistryDeselectFieldAction implements Action {
|
export class MetadataRegistryDeselectFieldAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.DESELECT_FIELD;
|
type = MetadataRegistryActionTypes.DESELECT_FIELD;
|
||||||
|
|
||||||
@@ -111,6 +126,9 @@ export class MetadataRegistryDeselectFieldAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to deselect all metadata fields in the metadata registry
|
||||||
|
*/
|
||||||
export class MetadataRegistryDeselectAllFieldAction implements Action {
|
export class MetadataRegistryDeselectAllFieldAction implements Action {
|
||||||
type = MetadataRegistryActionTypes.DESELECT_ALL_FIELD;
|
type = MetadataRegistryActionTypes.DESELECT_ALL_FIELD;
|
||||||
}
|
}
|
||||||
@@ -120,6 +138,7 @@ export class MetadataRegistryDeselectAllFieldAction implements Action {
|
|||||||
/**
|
/**
|
||||||
* Export a type alias of all actions in this action group
|
* Export a type alias of all actions in this action group
|
||||||
* so that reducers can easily compose action types
|
* so that reducers can easily compose action types
|
||||||
|
* These are all the actions to perform on the metadata registry state
|
||||||
*/
|
*/
|
||||||
export type MetadataRegistryAction
|
export type MetadataRegistryAction
|
||||||
= MetadataRegistryEditSchemaAction
|
= MetadataRegistryEditSchemaAction
|
||||||
|
@@ -1,35 +0,0 @@
|
|||||||
/**
|
|
||||||
* Makes sure that if the user navigates to another route, the sidebar is collapsed
|
|
||||||
*/
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
|
||||||
import { filter, map, tap } from 'rxjs/operators';
|
|
||||||
import { SearchSidebarCollapseAction } from '../../../+search-page/search-sidebar/search-sidebar.actions';
|
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
|
||||||
import { URLBaser } from '../../../core/url-baser/url-baser';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SearchSidebarEffects {
|
|
||||||
private previousPath: string;
|
|
||||||
@Effect() routeChange$ = this.actions$
|
|
||||||
.pipe(
|
|
||||||
ofType(fromRouter.ROUTER_NAVIGATION),
|
|
||||||
filter((action) => this.previousPath !== this.getBaseUrl(action)),
|
|
||||||
tap((action) => {
|
|
||||||
this.previousPath = this.getBaseUrl(action)
|
|
||||||
}),
|
|
||||||
map(() => new SearchSidebarCollapseAction())
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(private actions$: Actions) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getBaseUrl(action: any): string {
|
|
||||||
/* tslint:disable:no-string-literal */
|
|
||||||
const url: string = action['payload'].routerState.url;
|
|
||||||
return new URLBaser(url).toString();
|
|
||||||
/* tslint:enable:no-string-literal */
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -0,0 +1,183 @@
|
|||||||
|
import {
|
||||||
|
MetadataRegistryCancelFieldAction,
|
||||||
|
MetadataRegistryCancelSchemaAction, MetadataRegistryDeselectAllFieldAction,
|
||||||
|
MetadataRegistryDeselectAllSchemaAction, MetadataRegistryDeselectFieldAction,
|
||||||
|
MetadataRegistryDeselectSchemaAction, MetadataRegistryEditFieldAction,
|
||||||
|
MetadataRegistryEditSchemaAction, MetadataRegistrySelectFieldAction,
|
||||||
|
MetadataRegistrySelectSchemaAction
|
||||||
|
} from './metadata-registry.actions';
|
||||||
|
import { metadataRegistryReducer, MetadataRegistryState } from './metadata-registry.reducers';
|
||||||
|
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||||
|
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||||
|
|
||||||
|
class NullAction extends MetadataRegistryEditSchemaAction {
|
||||||
|
type = null;
|
||||||
|
constructor() {
|
||||||
|
super(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema: MetadataSchema = Object.assign(new MetadataSchema(),
|
||||||
|
{
|
||||||
|
id: 'schema-id',
|
||||||
|
self: 'http://rest.self/schema/dc',
|
||||||
|
prefix: 'dc',
|
||||||
|
namespace: 'http://dublincore.org/documents/dcmi-terms/'
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema2: MetadataSchema = Object.assign(new MetadataSchema(),
|
||||||
|
{
|
||||||
|
id: 'another-schema-id',
|
||||||
|
self: 'http://rest.self/schema/dcterms',
|
||||||
|
prefix: 'dcterms',
|
||||||
|
namespace: 'http://purl.org/dc/terms/'
|
||||||
|
});
|
||||||
|
|
||||||
|
const field: MetadataField = Object.assign(new MetadataField(),
|
||||||
|
{
|
||||||
|
id: 'author-field-id',
|
||||||
|
self: 'http://rest.self/field/author',
|
||||||
|
element: 'contributor',
|
||||||
|
qualifier: 'author',
|
||||||
|
scopeNote: 'Author of an item',
|
||||||
|
schema: schema
|
||||||
|
});
|
||||||
|
|
||||||
|
const field2: MetadataField = Object.assign(new MetadataField(),
|
||||||
|
{
|
||||||
|
id: 'title-field-id',
|
||||||
|
self: 'http://rest.self/field/title',
|
||||||
|
element: 'title',
|
||||||
|
qualifier: null,
|
||||||
|
scopeNote: 'Title of an item',
|
||||||
|
schema: schema
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialState: MetadataRegistryState = {
|
||||||
|
editSchema: null,
|
||||||
|
selectedSchemas: [],
|
||||||
|
editField: null,
|
||||||
|
selectedFields: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const editState: MetadataRegistryState = {
|
||||||
|
editSchema: schema,
|
||||||
|
selectedSchemas: [],
|
||||||
|
editField: field,
|
||||||
|
selectedFields: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectState: MetadataRegistryState = {
|
||||||
|
editSchema: null,
|
||||||
|
selectedSchemas: [schema2],
|
||||||
|
editField: null,
|
||||||
|
selectedFields: [field2]
|
||||||
|
};
|
||||||
|
|
||||||
|
const moreSelectState: MetadataRegistryState = {
|
||||||
|
editSchema: null,
|
||||||
|
selectedSchemas: [schema, schema2],
|
||||||
|
editField: null,
|
||||||
|
selectedFields: [field, field2]
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('metadataRegistryReducer', () => {
|
||||||
|
|
||||||
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
|
const state = initialState;
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with an the initial state', () => {
|
||||||
|
const state = initialState;
|
||||||
|
const action = new NullAction();
|
||||||
|
const initState = metadataRegistryReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(initState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to change the editSchema to a new schema when MetadataRegistryEditSchemaAction is dispatched', () => {
|
||||||
|
const state = editState;
|
||||||
|
const action = new MetadataRegistryEditSchemaAction(schema2);
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.editSchema).toEqual(schema2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to remove the editSchema from the state when MetadataRegistryCancelSchemaAction is dispatched', () => {
|
||||||
|
const state = editState;
|
||||||
|
const action = new MetadataRegistryCancelSchemaAction();
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.editSchema).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to add a given schema to the selectedSchemas when MetadataRegistrySelectSchemaAction is dispatched', () => {
|
||||||
|
const state = selectState;
|
||||||
|
const action = new MetadataRegistrySelectSchemaAction(schema);
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.selectedSchemas).toContain(schema);
|
||||||
|
expect(newState.selectedSchemas).toContain(schema2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to remove a given schema to the selectedSchemas when MetadataRegistryDeselectSchemaAction is dispatched', () => {
|
||||||
|
const state = selectState;
|
||||||
|
const action = new MetadataRegistryDeselectSchemaAction(schema2);
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.selectedSchemas).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to remove a given schema to the selectedSchemas when MetadataRegistryDeselectAllSchemaAction is dispatched', () => {
|
||||||
|
const state = selectState;
|
||||||
|
const action = new MetadataRegistryDeselectAllSchemaAction();
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.selectedSchemas).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to change the editField to a new field when MetadataRegistryEditFieldAction is dispatched', () => {
|
||||||
|
const state = editState;
|
||||||
|
const action = new MetadataRegistryEditFieldAction(field2);
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.editField).toEqual(field2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to remove the editField from the state when MetadataRegistryCancelFieldAction is dispatched', () => {
|
||||||
|
const state = editState;
|
||||||
|
const action = new MetadataRegistryCancelFieldAction();
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.editField).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to add a given field to the selectedFields when MetadataRegistrySelectFieldAction is dispatched', () => {
|
||||||
|
const state = selectState;
|
||||||
|
const action = new MetadataRegistrySelectFieldAction(field);
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.selectedFields).toContain(field);
|
||||||
|
expect(newState.selectedFields).toContain(field2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to remove a given field to the selectedFields when MetadataRegistryDeselectFieldAction is dispatched', () => {
|
||||||
|
const state = selectState;
|
||||||
|
const action = new MetadataRegistryDeselectFieldAction(field2);
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.selectedFields).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the current state to remove a given field to the selectedFields when MetadataRegistryDeselectAllFieldAction is dispatched', () => {
|
||||||
|
const state = selectState;
|
||||||
|
const action = new MetadataRegistryDeselectAllFieldAction();
|
||||||
|
const newState = metadataRegistryReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState.selectedFields).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
@@ -12,8 +12,8 @@ import {
|
|||||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The auth state.
|
* The metadata registry state.
|
||||||
* @interface State
|
* @interface MetadataRegistryState
|
||||||
*/
|
*/
|
||||||
export interface MetadataRegistryState {
|
export interface MetadataRegistryState {
|
||||||
editSchema: MetadataSchema;
|
editSchema: MetadataSchema;
|
||||||
|
@@ -28,6 +28,7 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
getActiveMetadataField: () => observableOf(undefined),
|
||||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
||||||
|
cancelEditMetadataField: () => {},
|
||||||
cancelEditMetadataSchema: () => {},
|
cancelEditMetadataSchema: () => {},
|
||||||
};
|
};
|
||||||
const formBuilderServiceStub = {
|
const formBuilderServiceStub = {
|
||||||
@@ -62,6 +63,11 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
registryService = s;
|
registryService = s;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
component = null;
|
||||||
|
registryService = null
|
||||||
|
})
|
||||||
|
|
||||||
describe('when submitting the form', () => {
|
describe('when submitting the form', () => {
|
||||||
const element = 'fakeElement';
|
const element = 'fakeElement';
|
||||||
const qualifier = 'fakeQualifier';
|
const qualifier = 'fakeQualifier';
|
||||||
|
@@ -2,14 +2,12 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu
|
|||||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||||
import {
|
import {
|
||||||
DynamicFormControlModel,
|
DynamicFormControlModel,
|
||||||
DynamicFormGroupModel,
|
|
||||||
DynamicFormLayout,
|
DynamicFormLayout,
|
||||||
DynamicInputModel
|
DynamicInputModel
|
||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
|
||||||
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
@@ -138,13 +138,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
parentID: 'new',
|
parentID: 'new',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
// model: {
|
||||||
|
// type: MenuItemType.ONCLICK,
|
||||||
|
// text: 'menu.section.new_item',
|
||||||
|
// function: () => {
|
||||||
|
// this.modalService.open(CreateItemParentSelectorComponent);
|
||||||
|
// }
|
||||||
|
// } as OnClickMenuItemModel,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.ONCLICK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.new_item',
|
text: 'menu.section.new_item',
|
||||||
function: () => {
|
link: '/submit'
|
||||||
this.modalService.open(CreateItemParentSelectorComponent);
|
} as LinkMenuItemModel,
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new_item_version',
|
id: 'new_item_version',
|
||||||
@@ -154,7 +159,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.new_item_version',
|
text: 'menu.section.new_item_version',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -230,7 +235,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.import_metadata',
|
text: 'menu.section.import_metadata',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -241,7 +246,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.import_batch',
|
text: 'menu.section.import_batch',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
/* Export */
|
/* Export */
|
||||||
@@ -264,7 +269,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.export_community',
|
text: 'menu.section.export_community',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -275,7 +280,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.export_collection',
|
text: 'menu.section.export_collection',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -286,7 +291,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.export_item',
|
text: 'menu.section.export_item',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
}, {
|
}, {
|
||||||
id: 'export_metadata',
|
id: 'export_metadata',
|
||||||
@@ -296,7 +301,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.export_metadata',
|
text: 'menu.section.export_metadata',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -320,7 +325,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.access_control_people',
|
text: 'menu.section.access_control_people',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -331,7 +336,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.access_control_groups',
|
text: 'menu.section.access_control_groups',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -342,7 +347,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.access_control_authorizations',
|
text: 'menu.section.access_control_authorizations',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -377,7 +382,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.find_withdrawn_items',
|
text: 'menu.section.find_withdrawn_items',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -388,7 +393,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.find_private_items',
|
text: 'menu.section.find_private_items',
|
||||||
link: '/admin/items'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -435,7 +440,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.curation_task',
|
text: 'menu.section.curation_task',
|
||||||
link: '/curation'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
icon: 'filter',
|
icon: 'filter',
|
||||||
index: 7
|
index: 7
|
||||||
@@ -449,7 +454,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.statistics_task',
|
text: 'menu.section.statistics_task',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
icon: 'chart-bar',
|
icon: 'chart-bar',
|
||||||
index: 8
|
index: 8
|
||||||
@@ -463,7 +468,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.control_panel',
|
text: 'menu.section.control_panel',
|
||||||
link: '#'
|
link: ''
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
icon: 'cogs',
|
icon: 'cogs',
|
||||||
index: 9
|
index: 9
|
||||||
|
@@ -18,8 +18,8 @@ import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorat
|
|||||||
templateUrl: './expandable-admin-sidebar-section.component.html',
|
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||||
animations: [rotate, slide, bgColor]
|
animations: [rotate, slide, bgColor]
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@rendersSectionForMenu(MenuID.ADMIN, true)
|
@rendersSectionForMenu(MenuID.ADMIN, true)
|
||||||
export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit {
|
export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
|
@@ -6,7 +6,6 @@ import {
|
|||||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||||
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
|
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
125
src/app/+browse-by/browse-by-guard.spec.ts
Normal file
125
src/app/+browse-by/browse-by-guard.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { BrowseByGuard } from './browse-by-guard';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
describe('BrowseByGuard', () => {
|
||||||
|
describe('canActivate', () => {
|
||||||
|
let guard: BrowseByGuard;
|
||||||
|
let dsoService: any;
|
||||||
|
let translateService: any;
|
||||||
|
|
||||||
|
const name = 'An interesting DSO';
|
||||||
|
const title = 'Author';
|
||||||
|
const field = 'Author';
|
||||||
|
const metadata = 'author';
|
||||||
|
const metadataField = 'dc.contributor';
|
||||||
|
const scope = '1234-65487-12354-1235';
|
||||||
|
const value = 'Filter';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dsoService = {
|
||||||
|
findById: (id: string) => observableOf({ payload: { name: name }, hasSucceeded: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
translateService = {
|
||||||
|
instant: () => field
|
||||||
|
};
|
||||||
|
guard = new BrowseByGuard(dsoService, translateService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true, and sets up the data correctly, with a scope and value', () => {
|
||||||
|
const scopedRoute = {
|
||||||
|
data: {
|
||||||
|
title: field,
|
||||||
|
metadataField,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
queryParams: {
|
||||||
|
scope,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
guard.canActivate(scopedRoute as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) => {
|
||||||
|
const result = {
|
||||||
|
title,
|
||||||
|
metadata,
|
||||||
|
metadataField,
|
||||||
|
collection: name,
|
||||||
|
field,
|
||||||
|
value: '"' + value + '"'
|
||||||
|
};
|
||||||
|
expect(scopedRoute.data).toEqual(result);
|
||||||
|
expect(canActivate).toEqual(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true, and sets up the data correctly, with a scope and without value', () => {
|
||||||
|
const scopedNoValueRoute = {
|
||||||
|
data: {
|
||||||
|
title: field,
|
||||||
|
metadataField,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
queryParams: {
|
||||||
|
scope
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
guard.canActivate(scopedNoValueRoute as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) => {
|
||||||
|
const result = {
|
||||||
|
title,
|
||||||
|
metadata,
|
||||||
|
metadataField,
|
||||||
|
collection: name,
|
||||||
|
field,
|
||||||
|
value: ''
|
||||||
|
};
|
||||||
|
expect(scopedNoValueRoute.data).toEqual(result);
|
||||||
|
expect(canActivate).toEqual(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true, and sets up the data correctly, without a scope and with a value', () => {
|
||||||
|
const route = {
|
||||||
|
data: {
|
||||||
|
title: field,
|
||||||
|
metadataField,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
queryParams: {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
guard.canActivate(route as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) => {
|
||||||
|
const result = {
|
||||||
|
title,
|
||||||
|
metadata,
|
||||||
|
metadataField,
|
||||||
|
collection: '',
|
||||||
|
field,
|
||||||
|
value: '"' + value + '"'
|
||||||
|
};
|
||||||
|
expect(route.data).toEqual(result);
|
||||||
|
expect(canActivate).toEqual(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -2,10 +2,10 @@ import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angul
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
|
||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
import { map } from 'rxjs/operators';
|
||||||
import { map, take } from 'rxjs/operators';
|
|
||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
/**
|
/**
|
||||||
@@ -19,29 +19,23 @@ export class BrowseByGuard implements CanActivate {
|
|||||||
|
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
const title = route.data.title;
|
const title = route.data.title;
|
||||||
const metadata = route.params.metadata || route.queryParams.metadata || route.data.metadata;
|
const metadata = route.params.metadata || route.queryParams.metadata || route.data.metadata;
|
||||||
const metadataField = route.data.metadataField;
|
const metadataField = route.data.metadataField;
|
||||||
const scope = route.queryParams.scope;
|
const scope = route.queryParams.scope;
|
||||||
const value = route.queryParams.value;
|
const value = route.queryParams.value;
|
||||||
|
const metadataTranslated = this.translate.instant('browse.metadata.' + metadata);
|
||||||
const metadataTranslated$ = this.translate.get('browse.metadata.' + metadata).pipe(take(1));
|
|
||||||
|
|
||||||
if (hasValue(scope)) {
|
if (hasValue(scope)) {
|
||||||
const dsoAndMetadata$ = observableCombineLatest(metadataTranslated$, this.dsoService.findById(scope).pipe(getSucceededRemoteData()));
|
const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getSucceededRemoteData());
|
||||||
return dsoAndMetadata$.pipe(
|
return dsoAndMetadata$.pipe(
|
||||||
map(([metadataTranslated, dsoRD]) => {
|
map((dsoRD) => {
|
||||||
const name = dsoRD.payload.name;
|
const name = dsoRD.payload.name;
|
||||||
route.data = this.createData(title, metadata, metadataField, name, metadataTranslated, value);;
|
route.data = this.createData(title, metadata, metadataField, name, metadataTranslated, value);
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return metadataTranslated$.pipe(
|
route.data = this.createData(title, metadata, metadataField, '', metadataTranslated, value);
|
||||||
map((metadataTranslated: string) => {
|
return observableOf(true);
|
||||||
route.data = this.createData(title, metadata, metadataField, '', metadataTranslated, value);
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,58 +1,62 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="collection-page"
|
<div class="collection-page"
|
||||||
*ngVar="(collectionRD$ | async) as collectionRD">
|
*ngVar="(collectionRD$ | async) as collectionRD">
|
||||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="collectionRD?.payload as collection">
|
<div *ngIf="collectionRD?.payload as collection">
|
||||||
<!-- Collection Name -->
|
<!-- Collection Name -->
|
||||||
<ds-comcol-page-header
|
<ds-comcol-page-header
|
||||||
[name]="collection.name">
|
[name]="collection.name">
|
||||||
</ds-comcol-page-header>
|
</ds-comcol-page-header>
|
||||||
<!-- Browse-By Links -->
|
<!-- Browse-By Links -->
|
||||||
<ds-comcol-page-browse-by [id]="collection.id"></ds-comcol-page-browse-by>
|
<ds-comcol-page-browse-by [id]="collection.id"></ds-comcol-page-browse-by>
|
||||||
<!-- Collection logo -->
|
<!-- Collection logo -->
|
||||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||||
[logo]="(logoRD$ | async)?.payload"
|
[logo]="(logoRD$ | async)?.payload"
|
||||||
[alternateText]="'Collection Logo'">
|
[alternateText]="'Collection Logo'">
|
||||||
</ds-comcol-page-logo>
|
</ds-comcol-page-logo>
|
||||||
<!-- Introductionary text -->
|
<!-- Introductionary text -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collection.introductoryText"
|
[content]="collection.introductoryText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- News -->
|
<!-- News -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collection.sidebarText"
|
[content]="collection.sidebarText"
|
||||||
[hasInnerHtml]="true"
|
[hasInnerHtml]="true"
|
||||||
[title]="'community.page.news'">
|
[title]="'community.page.news'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- Copyright -->
|
<!-- Copyright -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collection.copyrightText"
|
[content]="collection.copyrightText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- Licence -->
|
<!-- Licence -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collection.dcLicense"
|
[content]="collection.dcLicense"
|
||||||
[title]="'collection.page.license'">
|
[title]="'collection.page.license'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</div>
|
</div>
|
||||||
|
<br>
|
||||||
|
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
||||||
|
<div *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
||||||
|
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
||||||
|
<ds-viewable-collection
|
||||||
|
[config]="paginationConfig"
|
||||||
|
[sortConfig]="sortConfig"
|
||||||
|
[objects]="itemRD"
|
||||||
|
[hideGear]="true"
|
||||||
|
(paginationChange)="onPaginationChange($event)">
|
||||||
|
</ds-viewable-collection>
|
||||||
|
</div>
|
||||||
|
<ds-error *ngIf="itemRD?.hasFailed"
|
||||||
|
message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||||
|
<ds-loading *ngIf="!itemRD || itemRD.isLoading"
|
||||||
|
message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<ds-error *ngIf="collectionRD?.hasFailed"
|
||||||
|
message="{{'error.collection' | translate}}"></ds-error>
|
||||||
|
<ds-loading *ngIf="collectionRD?.isLoading"
|
||||||
|
message="{{'loading.collection' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="collectionRD?.hasFailed" message="{{'error.collection' | translate}}"></ds-error>
|
|
||||||
<ds-loading *ngIf="collectionRD?.isLoading" message="{{'loading.collection' | translate}}"></ds-loading>
|
|
||||||
<br>
|
|
||||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
|
||||||
<div *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
|
||||||
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
|
||||||
<ds-viewable-collection
|
|
||||||
[config]="paginationConfig"
|
|
||||||
[sortConfig]="sortConfig"
|
|
||||||
[objects]="itemRD"
|
|
||||||
[hideGear]="true"
|
|
||||||
(paginationChange)="onPaginationChange($event)">
|
|
||||||
</ds-viewable-collection>
|
|
||||||
</div>
|
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
|
||||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, of as observableOf, Observable, Subject } from 'rxjs';
|
||||||
|
import { filter, flatMap, map, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { ItemDataService } from '../core/data/item-data.service';
|
|
||||||
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';
|
||||||
|
|
||||||
@@ -11,16 +13,17 @@ import { MetadataService } from '../core/metadata/metadata.service';
|
|||||||
import { Bitstream } from '../core/shared/bitstream.model';
|
import { Bitstream } from '../core/shared/bitstream.model';
|
||||||
|
|
||||||
import { Collection } from '../core/shared/collection.model';
|
import { Collection } from '../core/shared/collection.model';
|
||||||
|
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
|
import {
|
||||||
|
getSucceededRemoteData,
|
||||||
|
redirectToPageNotFoundOn404,
|
||||||
|
toDSpaceObjectListRD
|
||||||
|
} from '../core/shared/operators';
|
||||||
|
|
||||||
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
import { combineLatest, filter, first, flatMap, map } from 'rxjs/operators';
|
|
||||||
import { SearchService } from '../+search-page/search-service/search.service';
|
|
||||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
|
||||||
import { toDSpaceObjectListRD } from '../core/shared/operators';
|
|
||||||
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-collection-page',
|
selector: 'ds-collection-page',
|
||||||
@@ -32,74 +35,70 @@ import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
|||||||
fadeInOut
|
fadeInOut
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageComponent implements OnInit, OnDestroy {
|
export class CollectionPageComponent implements OnInit {
|
||||||
collectionRD$: Observable<RemoteData<Collection>>;
|
collectionRD$: Observable<RemoteData<Collection>>;
|
||||||
itemRD$: Observable<RemoteData<PaginatedList<Item>>>;
|
itemRD$: Observable<RemoteData<PaginatedList<Item>>>;
|
||||||
logoRD$: Observable<RemoteData<Bitstream>>;
|
logoRD$: Observable<RemoteData<Bitstream>>;
|
||||||
paginationConfig: PaginationComponentOptions;
|
paginationConfig: PaginationComponentOptions;
|
||||||
sortConfig: SortOptions;
|
sortConfig: SortOptions;
|
||||||
private subs: Subscription[] = [];
|
private paginationChanges$: Subject<{
|
||||||
private collectionId: string;
|
paginationConfig: PaginationComponentOptions,
|
||||||
|
sortConfig: SortOptions
|
||||||
|
}>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private collectionDataService: CollectionDataService,
|
private collectionDataService: CollectionDataService,
|
||||||
private itemDataService: ItemDataService,
|
private searchService: SearchService,
|
||||||
private metadata: MetadataService,
|
private metadata: MetadataService,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
) {
|
) {
|
||||||
this.paginationConfig = new PaginationComponentOptions();
|
this.paginationConfig = new PaginationComponentOptions();
|
||||||
this.paginationConfig.id = 'collection-page-pagination';
|
this.paginationConfig.id = 'collection-page-pagination';
|
||||||
this.paginationConfig.pageSize = 5;
|
this.paginationConfig.pageSize = 5;
|
||||||
this.paginationConfig.currentPage = 1;
|
this.paginationConfig.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions('dc.date.issued', SortDirection.DESC);
|
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collectionRD$ = this.route.data.pipe(
|
this.collectionRD$ = this.route.data.pipe(
|
||||||
map((data) => data.collection),
|
map((data) => data.collection as RemoteData<Collection>),
|
||||||
first()
|
redirectToPageNotFoundOn404(this.router),
|
||||||
|
take(1)
|
||||||
);
|
);
|
||||||
this.logoRD$ = this.collectionRD$.pipe(
|
this.logoRD$ = this.collectionRD$.pipe(
|
||||||
map((rd: RemoteData<Collection>) => rd.payload),
|
map((rd: RemoteData<Collection>) => rd.payload),
|
||||||
filter((collection: Collection) => hasValue(collection)),
|
filter((collection: Collection) => hasValue(collection)),
|
||||||
flatMap((collection: Collection) => collection.logo)
|
flatMap((collection: Collection) => collection.logo)
|
||||||
);
|
);
|
||||||
this.subs.push(
|
|
||||||
this.route.queryParams.subscribe((params) => {
|
|
||||||
this.metadata.processRemoteData(this.collectionRD$);
|
|
||||||
const page = +params.page || this.paginationConfig.currentPage;
|
|
||||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
|
||||||
const sortDirection = +params.page || this.sortConfig.direction;
|
|
||||||
const pagination = Object.assign({},
|
|
||||||
this.paginationConfig,
|
|
||||||
{ currentPage: page, pageSize: pageSize }
|
|
||||||
);
|
|
||||||
const sort = Object.assign({},
|
|
||||||
this.sortConfig,
|
|
||||||
{ direction: sortDirection, field: this.sortConfig.field }
|
|
||||||
);
|
|
||||||
this.collectionRD$.subscribe((rd: RemoteData<Collection>) => {
|
|
||||||
this.collectionId = rd.payload.id;
|
|
||||||
this.updatePage({
|
|
||||||
pagination: pagination,
|
|
||||||
sort: sort
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePage(searchOptions) {
|
this.paginationChanges$ = new BehaviorSubject({
|
||||||
this.itemRD$ = this.itemDataService.findAll({
|
paginationConfig: this.paginationConfig,
|
||||||
scopeID: this.collectionId,
|
sortConfig: this.sortConfig
|
||||||
currentPage: searchOptions.pagination.currentPage,
|
|
||||||
elementsPerPage: searchOptions.pagination.pageSize,
|
|
||||||
sort: searchOptions.sort
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
this.itemRD$ = this.paginationChanges$.pipe(
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
switchMap((dto) => this.collectionRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
map((rd) => rd.payload.id),
|
||||||
|
switchMap((id: string) => {
|
||||||
|
return this.searchService.search(
|
||||||
|
new PaginatedSearchOptions({
|
||||||
|
scope: id,
|
||||||
|
pagination: dto.paginationConfig,
|
||||||
|
sort: dto.sortConfig,
|
||||||
|
dsoType: DSpaceObjectType.ITEM
|
||||||
|
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>
|
||||||
|
}),
|
||||||
|
startWith(undefined) // Make sure switching pages shows loading component
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
||||||
|
this.metadata.processRemoteData(this.collectionRD$);
|
||||||
|
this.onPaginationChange(params);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isNotEmpty(object: any) {
|
isNotEmpty(object: any) {
|
||||||
@@ -107,15 +106,14 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPaginationChange(event) {
|
onPaginationChange(event) {
|
||||||
this.updatePage({
|
this.paginationConfig.currentPage = +event.page || this.paginationConfig.currentPage;
|
||||||
pagination: {
|
this.paginationConfig.pageSize = +event.pageSize || this.paginationConfig.pageSize;
|
||||||
currentPage: event.page,
|
this.sortConfig.direction = event.sortDirection || this.sortConfig.direction;
|
||||||
pageSize: event.pageSize
|
this.sortConfig.field = event.sortField || this.sortConfig.field;
|
||||||
},
|
|
||||||
sort: {
|
this.paginationChanges$.next({
|
||||||
field: event.sortField,
|
paginationConfig: this.paginationConfig,
|
||||||
direction: event.sortDirection
|
sortConfig: this.sortConfig
|
||||||
}
|
});
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
src/app/+collection-page/collection-page.resolver.spec.ts
Normal file
28
src/app/+collection-page/collection-page.resolver.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { CollectionPageResolver } from './collection-page.resolver';
|
||||||
|
|
||||||
|
describe('CollectionPageResolver', () => {
|
||||||
|
describe('resolve', () => {
|
||||||
|
let resolver: CollectionPageResolver;
|
||||||
|
let collectionService: any;
|
||||||
|
const uuid = '1234-65487-12354-1235';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
collectionService = {
|
||||||
|
findById: (id: string) => observableOf({ payload: { id }, hasSucceeded: true })
|
||||||
|
};
|
||||||
|
resolver = new CollectionPageResolver(collectionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve a collection with the correct id', () => {
|
||||||
|
resolver.resolve({ params: { id: uuid } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(resolved) => {
|
||||||
|
expect(resolved.payload.id).toEqual(uuid);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -4,7 +4,8 @@ import { Collection } from '../core/shared/collection.model';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
import { find } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a resolver that requests a specific collection before the route is activated
|
* This class represents a resolver that requests a specific collection before the route is activated
|
||||||
@@ -18,11 +19,12 @@ export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
|
|||||||
* Method for resolving a collection based on the parameters in the current route
|
* Method for resolving a collection based on the parameters in the current route
|
||||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
* @returns Observable<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route
|
* @returns Observable<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
*/
|
*/
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
|
||||||
return this.collectionService.findById(route.params.id).pipe(
|
return this.collectionService.findById(route.params.id).pipe(
|
||||||
getSucceededRemoteData()
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||||
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { mergeMap, filter, map } from 'rxjs/operators';
|
import { mergeMap, filter, map } from 'rxjs/operators';
|
||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { Subscription, Observable } from 'rxjs';
|
import { Subscription, Observable } from 'rxjs';
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
@@ -13,6 +13,7 @@ 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 { hasValue } from '../shared/empty.util';
|
||||||
|
import { redirectToPageNotFoundOn404 } from '../core/shared/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-community-page',
|
selector: 'ds-community-page',
|
||||||
@@ -21,28 +22,37 @@ import { hasValue } from '../shared/empty.util';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
export class CommunityPageComponent implements OnInit, OnDestroy {
|
/**
|
||||||
|
* This component represents a detail page for a single community
|
||||||
|
*/
|
||||||
|
export class CommunityPageComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The community displayed on this page
|
||||||
|
*/
|
||||||
communityRD$: Observable<RemoteData<Community>>;
|
communityRD$: Observable<RemoteData<Community>>;
|
||||||
logoRD$: Observable<RemoteData<Bitstream>>;
|
|
||||||
private subs: Subscription[] = [];
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logo of this community
|
||||||
|
*/
|
||||||
|
logoRD$: Observable<RemoteData<Bitstream>>;
|
||||||
constructor(
|
constructor(
|
||||||
private communityDataService: CommunityDataService,
|
private communityDataService: CommunityDataService,
|
||||||
private metadata: MetadataService,
|
private metadata: MetadataService,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.communityRD$ = this.route.data.pipe(map((data) => data.community));
|
this.communityRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.community as RemoteData<Community>),
|
||||||
|
redirectToPageNotFoundOn404(this.router)
|
||||||
|
);
|
||||||
this.logoRD$ = this.communityRD$.pipe(
|
this.logoRD$ = this.communityRD$.pipe(
|
||||||
map((rd: RemoteData<Community>) => rd.payload),
|
map((rd: RemoteData<Community>) => rd.payload),
|
||||||
filter((community: Community) => hasValue(community)),
|
filter((community: Community) => hasValue(community)),
|
||||||
mergeMap((community: Community) => community.logo));
|
mergeMap((community: Community) => community.logo));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,10 @@ import { Injectable } from '@angular/core';
|
|||||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
|
||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
|
import { find } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a resolver that requests a specific community before the route is activated
|
* This class represents a resolver that requests a specific community before the route is activated
|
||||||
@@ -18,11 +19,12 @@ export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
|
|||||||
* Method for resolving a community based on the parameters in the current route
|
* Method for resolving a community based on the parameters in the current route
|
||||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
* @returns Observable<<RemoteData<Community>> Emits the found community based on the parameters in the current route
|
* @returns Observable<<RemoteData<Community>> Emits the found community based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
*/
|
*/
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
|
||||||
return this.communityService.findById(route.params.id).pipe(
|
return this.communityService.findById(route.params.id).pipe(
|
||||||
getSucceededRemoteData()
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,16 +3,17 @@
|
|||||||
<div class="d-flex flex-wrap">
|
<div class="d-flex flex-wrap">
|
||||||
<img class="mr-4 dspace-logo" src="assets/images/dspace-logo.svg" alt="" />
|
<img class="mr-4 dspace-logo" src="assets/images/dspace-logo.svg" alt="" />
|
||||||
<div>
|
<div>
|
||||||
<h1 class="display-3">Welcome to DSpace</h1>
|
<h1 class="display-3">Welcome to the DSpace 7 Preview</h1>
|
||||||
<p class="lead">DSpace is an open source software platform that enables organisations to:</p>
|
<p class="lead">DSpace is the world leading open source repository platform that enables organisations to:</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li>capture and describe digital material using a submission workflow module, or a variety of programmatic ingest options
|
<li>easily ingest documents, audio, video, datasets and their corresponding Dublin Core metadata
|
||||||
</li>
|
</li>
|
||||||
<li>distribute an organisation's digital assets over the web through a search and retrieval system
|
<li>open up this content to local and global audiences, thanks to the OAI-PMH interface and Google Scholar optimizations
|
||||||
</li>
|
</li>
|
||||||
<li>preserve digital assets over the long term</li>
|
<li>issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p>Join an international community of <A HREF="https://wiki.duraspace.org/display/DSPACE/DSpace+Positioning" TARGET="_NEW">leading institutions using DSpace</A>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -5,6 +5,10 @@ import { Component } from '@angular/core';
|
|||||||
styleUrls: ['./home-news.component.scss'],
|
styleUrls: ['./home-news.component.scss'],
|
||||||
templateUrl: './home-news.component.html'
|
templateUrl: './home-news.component.html'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render the news section on the home page
|
||||||
|
*/
|
||||||
export class HomeNewsComponent {
|
export class HomeNewsComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ds-home-news></ds-home-news>
|
<ds-home-news></ds-home-news>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ds-search-form></ds-search-form>
|
<ds-search-form [inPlaceSearch]="false"></ds-search-form>
|
||||||
<ds-top-level-community-list></ds-top-level-community-list>
|
<ds-top-level-community-list></ds-top-level-community-list>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
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';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import {filter, map} from 'rxjs/operators';
|
import {filter, map} from 'rxjs/operators';
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { Observable , BehaviorSubject } from 'rxjs';
|
import { Observable , BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
@@ -35,8 +35,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
|||||||
|
|
||||||
metadata$: Observable<MetadataMap>;
|
metadata$: Observable<MetadataMap>;
|
||||||
|
|
||||||
constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) {
|
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, metadataService: MetadataService) {
|
||||||
super(route, items, metadataService);
|
super(route, router, items, metadataService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
||||||
|
@@ -6,9 +6,7 @@ import { GenericItemPageFieldComponent } from './simple/field-components/specifi
|
|||||||
|
|
||||||
import { ItemPageComponent } from './simple/item-page.component';
|
import { ItemPageComponent } from './simple/item-page.component';
|
||||||
import { ItemPageRoutingModule } from './item-page-routing.module';
|
import { ItemPageRoutingModule } from './item-page-routing.module';
|
||||||
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
|
|
||||||
import { MetadataUriValuesComponent } from './field-components/metadata-uri-values/metadata-uri-values.component';
|
import { MetadataUriValuesComponent } from './field-components/metadata-uri-values/metadata-uri-values.component';
|
||||||
import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
|
||||||
import { ItemPageAuthorFieldComponent } from './simple/field-components/specific-field/author/item-page-author-field.component';
|
import { ItemPageAuthorFieldComponent } from './simple/field-components/specific-field/author/item-page-author-field.component';
|
||||||
import { ItemPageDateFieldComponent } from './simple/field-components/specific-field/date/item-page-date-field.component';
|
import { ItemPageDateFieldComponent } from './simple/field-components/specific-field/date/item-page-date-field.component';
|
||||||
import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
||||||
@@ -26,6 +24,8 @@ import { ItemComponent } from './simple/item-types/shared/item.component';
|
|||||||
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
||||||
import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component';
|
import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component';
|
||||||
import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component';
|
import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component';
|
||||||
|
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
|
||||||
|
import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -38,9 +38,7 @@ import { RelatedEntitiesSearchComponent } from './simple/related-entities/relate
|
|||||||
declarations: [
|
declarations: [
|
||||||
ItemPageComponent,
|
ItemPageComponent,
|
||||||
FullItemPageComponent,
|
FullItemPageComponent,
|
||||||
MetadataValuesComponent,
|
|
||||||
MetadataUriValuesComponent,
|
MetadataUriValuesComponent,
|
||||||
MetadataFieldWrapperComponent,
|
|
||||||
ItemPageAuthorFieldComponent,
|
ItemPageAuthorFieldComponent,
|
||||||
ItemPageDateFieldComponent,
|
ItemPageDateFieldComponent,
|
||||||
ItemPageAbstractFieldComponent,
|
ItemPageAbstractFieldComponent,
|
||||||
@@ -63,7 +61,8 @@ import { RelatedEntitiesSearchComponent } from './simple/related-entities/relate
|
|||||||
MetadataFieldWrapperComponent,
|
MetadataFieldWrapperComponent,
|
||||||
GenericItemPageFieldComponent,
|
GenericItemPageFieldComponent,
|
||||||
RelatedEntitiesSearchComponent,
|
RelatedEntitiesSearchComponent,
|
||||||
RelatedItemsComponent
|
RelatedItemsComponent,
|
||||||
|
MetadataRepresentationListComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
PublicationComponent
|
PublicationComponent
|
||||||
|
@@ -2,10 +2,10 @@ import { Injectable } from '@angular/core';
|
|||||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
|
||||||
import { ItemDataService } from '../core/data/item-data.service';
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
import { tap } from 'rxjs/operators';
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { find } from 'rxjs/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a resolver that requests a specific item before the route is activated
|
* This class represents a resolver that requests a specific item before the route is activated
|
||||||
@@ -19,11 +19,13 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
|||||||
* Method for resolving an item based on the parameters in the current route
|
* Method for resolving an item based on the parameters in the current route
|
||||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route
|
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
*/
|
*/
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
||||||
return this.itemService.findById(route.params.id).pipe(
|
return this.itemService.findById(route.params.id)
|
||||||
getSucceededRemoteData()
|
.pipe(
|
||||||
);
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
|||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ItemPageComponent } from './item-page.component';
|
import { ItemPageComponent } from './item-page.component';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
@@ -48,7 +48,8 @@ describe('ItemPageComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{provide: ActivatedRoute, useValue: mockRoute},
|
{provide: ActivatedRoute, useValue: mockRoute},
|
||||||
{provide: ItemDataService, useValue: {}},
|
{provide: ItemDataService, useValue: {}},
|
||||||
{provide: MetadataService, useValue: mockMetadataService}
|
{provide: MetadataService, useValue: mockMetadataService},
|
||||||
|
{provide: Router, useValue: {}}
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { filter, map, mergeMap } 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 } 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';
|
||||||
@@ -13,6 +14,7 @@ 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 { hasValue } from '../../shared/empty.util';
|
||||||
|
import { redirectToPageNotFoundOn404 } from '../../core/shared/operators';
|
||||||
import { ItemViewMode } from '../../shared/items/item-type-decorator';
|
import { ItemViewMode } from '../../shared/items/item-type-decorator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,11 +41,6 @@ export class ItemPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
itemRD$: Observable<RemoteData<Item>>;
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
/**
|
|
||||||
* The item's thumbnail
|
|
||||||
*/
|
|
||||||
thumbnail$: Observable<Bitstream>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The view-mode we're currently on
|
* The view-mode we're currently on
|
||||||
*/
|
*/
|
||||||
@@ -51,16 +48,16 @@ export class ItemPageComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
private items: ItemDataService,
|
private items: ItemDataService,
|
||||||
private metadataService: MetadataService,
|
private metadataService: MetadataService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
|
this.itemRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.item as RemoteData<Item>),
|
||||||
|
redirectToPageNotFoundOn404(this.router)
|
||||||
|
);
|
||||||
this.metadataService.processRemoteData(this.itemRD$);
|
this.metadataService.processRemoteData(this.itemRD$);
|
||||||
this.thumbnail$ = this.itemRD$.pipe(
|
|
||||||
map((rd: RemoteData<Item>) => rd.payload),
|
|
||||||
filter((item: Item) => hasValue(item)),
|
|
||||||
mergeMap((item: Item) => item.getThumbnail()),);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,17 @@
|
|||||||
|
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 { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import { 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, flatMap, map } from 'rxjs/operators';
|
import { distinctUntilChanged, flatMap, map } from 'rxjs/operators';
|
||||||
import { zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
|
import { of as observableOf, 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operator for comparing arrays using a mapping function
|
* Operator for comparing arrays using a mapping function
|
||||||
@@ -75,3 +82,41 @@ export const relationsToItems = (thisId: string) =>
|
|||||||
})),
|
})),
|
||||||
distinctUntilChanged(compareArraysUsingIds()),
|
distinctUntilChanged(compareArraysUsingIds()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a list of relationships into a list of metadatarepresentations given the original metadata
|
||||||
|
* @param parentId The id of the parent item
|
||||||
|
* @param itemType The type of relation this list resembles (for creating representations)
|
||||||
|
* @param metadata The list of original Metadatum objects
|
||||||
|
* @param ids The ItemDataService to use for fetching Items from the Rest API
|
||||||
|
*/
|
||||||
|
export const relationsToRepresentations = (parentId: string, itemType: string, metadata: MetadataValue[], ids: ItemDataService) =>
|
||||||
|
(source: Observable<Relationship[]>): Observable<MetadataRepresentation[]> =>
|
||||||
|
source.pipe(
|
||||||
|
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(), item))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return observableOf(Object.assign(new MetadatumRepresentation(itemType), metadatum));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
export enum MyDSpaceConfigurationValueType {
|
||||||
|
Workspace = 'workspace',
|
||||||
|
Workflow = 'workflow'
|
||||||
|
}
|
259
src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts
Normal file
259
src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||||
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { SearchFilter } from '../+search-page/search-filter.model';
|
||||||
|
import { ActivatedRouteStub } from '../shared/testing/active-router-stub';
|
||||||
|
import { MockRoleService } from '../shared/mocks/mock-role-service';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
|
||||||
|
|
||||||
|
describe('MyDSpaceConfigurationService', () => {
|
||||||
|
let service: MyDSpaceConfigurationService;
|
||||||
|
const value1 = 'random value';
|
||||||
|
const prefixFilter = {
|
||||||
|
'f.namedresourcetype': ['another value'],
|
||||||
|
'f.dateSubmitted.min': ['2013'],
|
||||||
|
'f.dateSubmitted.max': ['2018']
|
||||||
|
};
|
||||||
|
const defaults = new PaginatedSearchOptions({
|
||||||
|
pagination: Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 }),
|
||||||
|
sort: new SortOptions('score', SortDirection.DESC),
|
||||||
|
query: '',
|
||||||
|
scope: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const backendFilters = [new SearchFilter('f.namedresourcetype', ['another value']), new SearchFilter('f.dateSubmitted', ['[2013 TO 2018]'])];
|
||||||
|
|
||||||
|
const spy = jasmine.createSpyObj('RouteService', {
|
||||||
|
getQueryParameterValue: observableOf(value1),
|
||||||
|
getQueryParamsWithPrefix: observableOf(prefixFilter),
|
||||||
|
getRouteParameterValue: observableOf(''),
|
||||||
|
getRouteDataValue: observableOf({})
|
||||||
|
});
|
||||||
|
|
||||||
|
const activatedRoute: any = new ActivatedRouteStub();
|
||||||
|
|
||||||
|
const roleService: any = new MockRoleService();
|
||||||
|
|
||||||
|
const fixedFilterService = jasmine.createSpyObj('SearchFixedFilterService', {
|
||||||
|
getQueryByFilterName: observableOf(''),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new MyDSpaceConfigurationService(roleService, fixedFilterService, spy, activatedRoute);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the scope is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentScope('');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'scope\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('scope');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentConfiguration is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentConfiguration('');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'configuration\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('configuration');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentQuery is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentQuery('');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'query\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('query');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentDSOType is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentDSOType();
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'dsoType\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('dsoType');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentFrontendFilters is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentFrontendFilters();
|
||||||
|
});
|
||||||
|
it('should call getQueryParamsWithPrefix on the routeService with parameter prefix \'f.\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParamsWithPrefix).toHaveBeenCalledWith('f.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentFilters is called', () => {
|
||||||
|
let parsedValues$;
|
||||||
|
beforeEach(() => {
|
||||||
|
parsedValues$ = service.getCurrentFilters();
|
||||||
|
});
|
||||||
|
it('should call getQueryParamsWithPrefix on the routeService with parameter prefix \'f.\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParamsWithPrefix).toHaveBeenCalledWith('f.');
|
||||||
|
parsedValues$.subscribe((values) => {
|
||||||
|
expect(values).toEqual(backendFilters);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentSort is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentSort({} as any);
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'sortDirection\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortDirection');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'sortField\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortField');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentPagination is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentPagination({ currentPage: 1, pageSize: 10 } as any);
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'page\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('page');
|
||||||
|
});
|
||||||
|
it('should call getQueryParameterValue on the routeService with parameter name \'pageSize\'', () => {
|
||||||
|
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('pageSize');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when subscribeToSearchOptions or subscribeToPaginatedSearchOptions is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'getCurrentPagination').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentSort').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentScope').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentConfiguration').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentQuery').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentDSOType').and.callThrough();
|
||||||
|
spyOn(service, 'getCurrentFilters').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when subscribeToSearchOptions is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(service as any).subscribeToSearchOptions(defaults)
|
||||||
|
});
|
||||||
|
it('should call all getters it needs, but not call any others', () => {
|
||||||
|
expect(service.getCurrentPagination).not.toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentSort).not.toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentScope).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentConfiguration).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentQuery).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentDSOType).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentFilters).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when subscribeToPaginatedSearchOptions is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(service as any).subscribeToPaginatedSearchOptions(defaults);
|
||||||
|
});
|
||||||
|
it('should call all getters it needs', () => {
|
||||||
|
expect(service.getCurrentPagination).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentSort).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentScope).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentConfiguration).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentQuery).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentDSOType).toHaveBeenCalled();
|
||||||
|
expect(service.getCurrentFilters).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getAvailableConfigurationTypes is called', () => {
|
||||||
|
|
||||||
|
it('should return properly list when user is submitter', () => {
|
||||||
|
roleService.setSubmitter(true);
|
||||||
|
roleService.setController(false);
|
||||||
|
roleService.setAdmin(false);
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationTypes();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
MyDSpaceConfigurationValueType.Workspace
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return properly list when user is controller', () => {
|
||||||
|
roleService.setSubmitter(false);
|
||||||
|
roleService.setController(true);
|
||||||
|
roleService.setAdmin(false);
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationTypes();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
MyDSpaceConfigurationValueType.Workflow
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return properly list when user is admin', () => {
|
||||||
|
roleService.setSubmitter(false);
|
||||||
|
roleService.setController(false);
|
||||||
|
roleService.setAdmin(true);
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationTypes();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
MyDSpaceConfigurationValueType.Workflow
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return properly list when user is submitter and controller', () => {
|
||||||
|
roleService.setSubmitter(true);
|
||||||
|
roleService.setController(true);
|
||||||
|
roleService.setAdmin(false);
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationTypes();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
MyDSpaceConfigurationValueType.Workspace,
|
||||||
|
MyDSpaceConfigurationValueType.Workflow
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getAvailableConfigurationOptions is called', () => {
|
||||||
|
|
||||||
|
it('should return properly options list', () => {
|
||||||
|
spyOn(service, 'getAvailableConfigurationTypes').and.returnValue(hot('a', {
|
||||||
|
a: [
|
||||||
|
MyDSpaceConfigurationValueType.Workspace,
|
||||||
|
MyDSpaceConfigurationValueType.Workflow
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const list$ = service.getAvailableConfigurationOptions();
|
||||||
|
|
||||||
|
expect(list$).toBeObservable(cold('(b|)', {
|
||||||
|
b: [
|
||||||
|
{
|
||||||
|
value: MyDSpaceConfigurationValueType.Workspace,
|
||||||
|
label: `mydspace.show.${MyDSpaceConfigurationValueType.Workspace}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MyDSpaceConfigurationValueType.Workflow,
|
||||||
|
label: `mydspace.show.${MyDSpaceConfigurationValueType.Workflow}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
120
src/app/+my-dspace-page/my-dspace-configuration.service.ts
Normal file
120
src/app/+my-dspace-page/my-dspace-configuration.service.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { combineLatest, Observable } from 'rxjs';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
|
||||||
|
import { RoleService } from '../core/roles/role.service';
|
||||||
|
import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model';
|
||||||
|
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
|
||||||
|
import { RouteService } from '../shared/services/route.service';
|
||||||
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that performs all actions that have to do with the current mydspace configuration
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class MyDSpaceConfigurationService extends SearchConfigurationService {
|
||||||
|
/**
|
||||||
|
* Default pagination settings
|
||||||
|
*/
|
||||||
|
protected defaultPagination = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'mydspace-page',
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default sort settings
|
||||||
|
*/
|
||||||
|
protected defaultSort = new SortOptions('dc.date.issued', SortDirection.DESC);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration parameter setting
|
||||||
|
*/
|
||||||
|
protected defaultConfiguration = 'workspace';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default scope setting
|
||||||
|
*/
|
||||||
|
protected defaultScope = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default query setting
|
||||||
|
*/
|
||||||
|
protected defaultQuery = '';
|
||||||
|
|
||||||
|
private isAdmin$: Observable<boolean>;
|
||||||
|
private isController$: Observable<boolean>;
|
||||||
|
private isSubmitter$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize class
|
||||||
|
*
|
||||||
|
* @param {roleService} roleService
|
||||||
|
* @param {SearchFixedFilterService} fixedFilterService
|
||||||
|
* @param {RouteService} routeService
|
||||||
|
* @param {ActivatedRoute} route
|
||||||
|
*/
|
||||||
|
constructor(protected roleService: RoleService,
|
||||||
|
protected fixedFilterService: SearchFixedFilterService,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected route: ActivatedRoute) {
|
||||||
|
|
||||||
|
super(routeService, fixedFilterService, route);
|
||||||
|
|
||||||
|
// override parent class initialization
|
||||||
|
this._defaults = null;
|
||||||
|
this.initDefaults();
|
||||||
|
|
||||||
|
this.isSubmitter$ = this.roleService.isSubmitter();
|
||||||
|
this.isController$ = this.roleService.isController();
|
||||||
|
this.isAdmin$ = this.roleService.isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of available configuration depend on the user role
|
||||||
|
*
|
||||||
|
* @return {Observable<MyDSpaceConfigurationValueType[]>}
|
||||||
|
* Emits the available configuration list
|
||||||
|
*/
|
||||||
|
public getAvailableConfigurationTypes(): Observable<MyDSpaceConfigurationValueType[]> {
|
||||||
|
return combineLatest(this.isSubmitter$, this.isController$, this.isAdmin$).pipe(
|
||||||
|
first(),
|
||||||
|
map(([isSubmitter, isController, isAdmin]: [boolean, boolean, boolean]) => {
|
||||||
|
const availableConf: MyDSpaceConfigurationValueType[] = [];
|
||||||
|
if (isSubmitter) {
|
||||||
|
availableConf.push(MyDSpaceConfigurationValueType.Workspace);
|
||||||
|
}
|
||||||
|
if (isController || isAdmin) {
|
||||||
|
availableConf.push(MyDSpaceConfigurationValueType.Workflow);
|
||||||
|
}
|
||||||
|
return availableConf;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the select options for the available configuration list
|
||||||
|
*
|
||||||
|
* @return {Observable<SearchConfigurationOption[]>}
|
||||||
|
* Emits the select options list
|
||||||
|
*/
|
||||||
|
public getAvailableConfigurationOptions(): Observable<SearchConfigurationOption[]> {
|
||||||
|
return this.getAvailableConfigurationTypes().pipe(
|
||||||
|
first(),
|
||||||
|
map((availableConfigurationTypes: MyDSpaceConfigurationValueType[]) => {
|
||||||
|
const configurationOptions: SearchConfigurationOption[] = [];
|
||||||
|
availableConfigurationTypes.forEach((type) => {
|
||||||
|
const value = type;
|
||||||
|
const label = `mydspace.show.${value}`;
|
||||||
|
configurationOptions.push({ value, label });
|
||||||
|
});
|
||||||
|
return configurationOptions;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="parent mb-3">
|
||||||
|
<div class="upload">
|
||||||
|
<ds-uploader *ngIf="uploadFilesOptions.url !== ''"
|
||||||
|
[uploadFilesOptions]="uploadFilesOptions"
|
||||||
|
(onCompleteItem)="onCompleteItem($event)"
|
||||||
|
(onUploadError)="onUploadError($event)"></ds-uploader>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="add">
|
||||||
|
<a class="btn btn-lg btn-primary mt-1 ml-2" [routerLink]="['/submit']" role="button">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> {{'mydspace.new-submission' | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1,11 @@
|
|||||||
|
.parent {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
flex: initial;
|
||||||
|
}
|
@@ -0,0 +1,101 @@
|
|||||||
|
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
|
import { createTestComponent } from '../../shared/testing/utils';
|
||||||
|
import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission.component';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/mock-translate.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { getMockScrollToService } from '../../shared/mocks/mock-scroll-to-service';
|
||||||
|
import { UploaderService } from '../../shared/uploader/uploader.service';
|
||||||
|
|
||||||
|
describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||||
|
|
||||||
|
const translateService: any = getMockTranslateService();
|
||||||
|
const store: Store<AppState> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
pipe: observableOf(true)
|
||||||
|
});
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule,
|
||||||
|
SharedModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
MyDSpaceNewSubmissionComponent,
|
||||||
|
TestComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') },
|
||||||
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: ScrollToService, useValue: getMockScrollToService() },
|
||||||
|
{ provide: Store, useValue: store },
|
||||||
|
{ provide: TranslateService, useValue: translateService },
|
||||||
|
ChangeDetectorRef,
|
||||||
|
MyDSpaceNewSubmissionComponent,
|
||||||
|
UploaderService
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
let testComp: TestComponent;
|
||||||
|
let testFixture: ComponentFixture<TestComponent>;
|
||||||
|
|
||||||
|
// synchronous beforeEach
|
||||||
|
beforeEach(() => {
|
||||||
|
const html = `
|
||||||
|
<ds-my-dspace-new-submission (uploadEnd)="reload($event)"></ds-my-dspace-new-submission>`;
|
||||||
|
|
||||||
|
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||||
|
testComp = testFixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testFixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create MyDSpaceNewSubmissionComponent', inject([MyDSpaceNewSubmissionComponent], (app: MyDSpaceNewSubmissionComponent) => {
|
||||||
|
|
||||||
|
expect(app).toBeDefined();
|
||||||
|
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// declare a test component
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-test-cmp',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
class TestComponent {
|
||||||
|
|
||||||
|
reload = (event) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,118 @@
|
|||||||
|
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SubmissionState } from '../../submission/submission.reducers';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { MyDSpaceResult } from '../my-dspace-result.model';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||||
|
import { UploaderOptions } from '../../shared/uploader/uploader-options.model';
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { NotificationType } from '../../shared/notifications/models/notification-type';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component represents the whole mydspace page header
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-my-dspace-new-submission',
|
||||||
|
styleUrls: ['./my-dspace-new-submission.component.scss'],
|
||||||
|
templateUrl: './my-dspace-new-submission.component.html'
|
||||||
|
})
|
||||||
|
export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
|
||||||
|
@Output() uploadEnd = new EventEmitter<Array<MyDSpaceResult<DSpaceObject>>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UploaderOptions object
|
||||||
|
*/
|
||||||
|
public uploadFilesOptions: UploaderOptions = {
|
||||||
|
url: '',
|
||||||
|
authToken: null,
|
||||||
|
disableMultipart: false,
|
||||||
|
itemAlias: null
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to unsubscribe from
|
||||||
|
*/
|
||||||
|
private sub: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {AuthService} authService
|
||||||
|
* @param {ChangeDetectorRef} changeDetectorRef
|
||||||
|
* @param {HALEndpointService} halService
|
||||||
|
* @param {NotificationsService} notificationsService
|
||||||
|
* @param {Store<SubmissionState>} store
|
||||||
|
* @param {TranslateService} translate
|
||||||
|
*/
|
||||||
|
constructor(private authService: AuthService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private halService: HALEndpointService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private store: Store<SubmissionState>,
|
||||||
|
private translate: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize url and Bearer token
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.sub = this.halService.getEndpoint('workspaceitems').pipe(first()).subscribe((url) => {
|
||||||
|
this.uploadFilesOptions.url = url;
|
||||||
|
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when file upload is completed to notify upload status
|
||||||
|
*/
|
||||||
|
public onCompleteItem(res) {
|
||||||
|
if (res && res._embedded && res._embedded.workspaceitems && res._embedded.workspaceitems.length > 0) {
|
||||||
|
const workspaceitems = res._embedded.workspaceitems;
|
||||||
|
this.uploadEnd.emit(workspaceitems);
|
||||||
|
|
||||||
|
if (workspaceitems.length === 1) {
|
||||||
|
const options = new NotificationOptions();
|
||||||
|
options.timeOut = 0;
|
||||||
|
const link = '/workspaceitems/' + workspaceitems[0].id + '/edit';
|
||||||
|
this.notificationsService.notificationWithAnchor(
|
||||||
|
NotificationType.Success,
|
||||||
|
options,
|
||||||
|
link,
|
||||||
|
'mydspace.general.text-here',
|
||||||
|
'mydspace.upload.upload-successful',
|
||||||
|
'here');
|
||||||
|
} else if (workspaceitems.length > 1) {
|
||||||
|
this.notificationsService.success(null, this.translate.get('mydspace.upload.upload-multiple-successful', {qty: workspaceitems.length}));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called on file upload error
|
||||||
|
*/
|
||||||
|
public onUploadError() {
|
||||||
|
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from the subscription
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.sub)) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
src/app/+my-dspace-page/my-dspace-page-routing.module.ts
Normal file
25
src/app/+my-dspace-page/my-dspace-page-routing.module.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { MyDSpacePageComponent } from './my-dspace-page.component';
|
||||||
|
import { MyDSpaceGuard } from './my-dspace.guard';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: MyDSpacePageComponent,
|
||||||
|
data: { title: 'mydspace.title' },
|
||||||
|
canActivate: [
|
||||||
|
MyDSpaceGuard
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This module defines the default component to load when navigating to the mydspace page path.
|
||||||
|
*/
|
||||||
|
export class MyDspacePageRoutingModule {
|
||||||
|
}
|
48
src/app/+my-dspace-page/my-dspace-page.component.html
Normal file
48
src/app/+my-dspace-page/my-dspace-page.component.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<div class="container">
|
||||||
|
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"
|
||||||
|
(uploadEnd)="reload($event)"></ds-my-dspace-new-submission>
|
||||||
|
<div class="search-page row">
|
||||||
|
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
|
||||||
|
id="search-sidebar"
|
||||||
|
[configurationList]="(configurationList$ | async)"
|
||||||
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
|
[viewModeList]="viewModeList"
|
||||||
|
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||||
|
<div class="col-12 col-md-9">
|
||||||
|
<ds-search-form id="search-form"
|
||||||
|
[query]="(searchOptions$ | async)?.query"
|
||||||
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
|
[currentUrl]="getSearchLink()"
|
||||||
|
[scopes]="(scopeListRD$ | async)"
|
||||||
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
|
</ds-search-form>
|
||||||
|
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
|
<div class="row">
|
||||||
|
<div id="search-body"
|
||||||
|
class="row-offcanvas row-offcanvas-left"
|
||||||
|
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
||||||
|
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
||||||
|
id="search-sidebar-sm"
|
||||||
|
[configurationList]="(configurationList$ | async)"
|
||||||
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
|
(toggleSidebar)="closeSidebar()"
|
||||||
|
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
||||||
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
|
</ds-search-sidebar>
|
||||||
|
<div id="search-content" class="col-12">
|
||||||
|
<div class="d-block d-md-none search-controls clearfix">
|
||||||
|
<ds-view-mode-switch [viewModeList]="viewModeList" [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||||
|
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||||
|
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||||
|
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||||
|
| translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ds-my-dspace-results [searchResults]="resultsRD$ | async"
|
||||||
|
[searchConfig]="searchOptions$ | async"></ds-my-dspace-results>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
1
src/app/+my-dspace-page/my-dspace-page.component.scss
Normal file
1
src/app/+my-dspace-page/my-dspace-page.component.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import '../+search-page/search-page.component.scss';
|
204
src/app/+my-dspace-page/my-dspace-page.component.spec.ts
Normal file
204
src/app/+my-dspace-page/my-dspace-page.component.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from './my-dspace-page.component';
|
||||||
|
import { RouteService } from '../shared/services/route.service';
|
||||||
|
import { routeServiceStub } from '../shared/testing/route-service-stub';
|
||||||
|
import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service-stub';
|
||||||
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
|
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
|
||||||
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
|
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service';
|
||||||
|
import { SearchFilterService } from '../+search-page/search-filters/search-filter/search-filter.service';
|
||||||
|
import { RoleDirective } from '../shared/roles/role.directive';
|
||||||
|
import { RoleService } from '../core/roles/role.service';
|
||||||
|
import { MockRoleService } from '../shared/mocks/mock-role-service';
|
||||||
|
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
|
||||||
|
describe('MyDSpacePageComponent', () => {
|
||||||
|
let comp: MyDSpacePageComponent;
|
||||||
|
let fixture: ComponentFixture<MyDSpacePageComponent>;
|
||||||
|
let searchServiceObject: SearchService;
|
||||||
|
let searchConfigurationServiceObject: SearchConfigurationService;
|
||||||
|
const store: Store<MyDSpacePageComponent> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
select: observableOf(true)
|
||||||
|
});
|
||||||
|
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||||
|
pagination.id = 'mydspace-results-pagination';
|
||||||
|
pagination.currentPage = 1;
|
||||||
|
pagination.pageSize = 10;
|
||||||
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
|
const mockResults = observableOf(new RemoteData(false, false, true, null, ['test', 'data']));
|
||||||
|
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||||
|
search: mockResults,
|
||||||
|
getSearchLink: '/mydspace',
|
||||||
|
getScopes: observableOf(['test-scope']),
|
||||||
|
setServiceOptions: {}
|
||||||
|
});
|
||||||
|
const configurationParam = 'default';
|
||||||
|
const queryParam = 'test query';
|
||||||
|
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||||
|
const paginatedSearchOptions = new PaginatedSearchOptions({
|
||||||
|
configuration: configurationParam,
|
||||||
|
query: queryParam,
|
||||||
|
scope: scopeParam,
|
||||||
|
pagination,
|
||||||
|
sort
|
||||||
|
});
|
||||||
|
const activatedRouteStub = {
|
||||||
|
snapshot: {
|
||||||
|
queryParamMap: new Map([
|
||||||
|
['query', queryParam],
|
||||||
|
['scope', scopeParam]
|
||||||
|
])
|
||||||
|
},
|
||||||
|
queryParams: observableOf({
|
||||||
|
query: queryParam,
|
||||||
|
scope: scopeParam
|
||||||
|
})
|
||||||
|
};
|
||||||
|
const sidebarService = {
|
||||||
|
isCollapsed: observableOf(true),
|
||||||
|
collapse: () => this.isCollapsed = observableOf(true),
|
||||||
|
expand: () => this.isCollapsed = observableOf(false)
|
||||||
|
};
|
||||||
|
const mockFixedFilterService: SearchFixedFilterService = {
|
||||||
|
getQueryByFilterName: (filter: string) => {
|
||||||
|
return observableOf(undefined)
|
||||||
|
}
|
||||||
|
} as SearchFixedFilterService;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule.forRoot()],
|
||||||
|
declarations: [MyDSpacePageComponent, RoleDirective],
|
||||||
|
providers: [
|
||||||
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
|
{
|
||||||
|
provide: CommunityDataService,
|
||||||
|
useValue: jasmine.createSpyObj('communityService', ['findById', 'findAll'])
|
||||||
|
},
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
|
{
|
||||||
|
provide: Store, useValue: store
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService',
|
||||||
|
{
|
||||||
|
isXs: observableOf(true),
|
||||||
|
isSm: observableOf(false),
|
||||||
|
isXsOrSm: observableOf(true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SearchSidebarService,
|
||||||
|
useValue: sidebarService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SearchFilterService,
|
||||||
|
useValue: {}
|
||||||
|
}, {
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useValue: new SearchConfigurationServiceStub()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: RoleService,
|
||||||
|
useValue: new MockRoleService()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SearchFixedFilterService,
|
||||||
|
useValue: mockFixedFilterService
|
||||||
|
}
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(MyDSpacePageComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MyDSpacePageComponent);
|
||||||
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
|
fixture.detectChanges();
|
||||||
|
searchServiceObject = (comp as any).service;
|
||||||
|
searchConfigurationServiceObject = (comp as any).searchConfigService;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
comp = null;
|
||||||
|
searchServiceObject = null;
|
||||||
|
searchConfigurationServiceObject = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the scope and query from the route parameters', () => {
|
||||||
|
|
||||||
|
searchConfigurationServiceObject.paginatedSearchOptions.next(paginatedSearchOptions);
|
||||||
|
expect(comp.searchOptions$).toBeObservable(cold('b', {
|
||||||
|
b: paginatedSearchOptions
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the open sidebar button is clicked in mobile view', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'openSidebar');
|
||||||
|
const openSidebarButton = fixture.debugElement.query(By.css('.open-sidebar'));
|
||||||
|
openSidebarButton.triggerEventHandler('click', null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger the openSidebar function', () => {
|
||||||
|
expect(comp.openSidebar).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when sidebarCollapsed is true in mobile view', () => {
|
||||||
|
let menu: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||||
|
comp.isSidebarCollapsed = () => observableOf(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the sidebar', () => {
|
||||||
|
expect(menu.classList).not.toContain('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when sidebarCollapsed is false in mobile view', () => {
|
||||||
|
let menu: HTMLElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||||
|
comp.isSidebarCollapsed = () => observableOf(false);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the menu', () => {
|
||||||
|
expect(menu.classList).toContain('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
168
src/app/+my-dspace-page/my-dspace-page.component.ts
Normal file
168
src/app/+my-dspace-page/my-dspace-page.component.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
InjectionToken,
|
||||||
|
Input,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||||
|
import { switchMap, tap, } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||||
|
import { pushInOut } from '../shared/animations/push';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
|
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { MyDSpaceResult } from './my-dspace-result.model';
|
||||||
|
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
|
||||||
|
import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model';
|
||||||
|
import { RoleType } from '../core/roles/role-types';
|
||||||
|
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
|
||||||
|
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||||
|
import { ViewMode } from '../core/shared/view-mode.model';
|
||||||
|
import { MyDSpaceRequest } from '../core/data/request.models';
|
||||||
|
|
||||||
|
export const MYDSPACE_ROUTE = '/mydspace';
|
||||||
|
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component represents the whole mydspace page
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-my-dspace-page',
|
||||||
|
styleUrls: ['./my-dspace-page.component.scss'],
|
||||||
|
templateUrl: './my-dspace-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [pushInOut],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useClass: MyDSpaceConfigurationService
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class MyDSpacePageComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the search component should show results on the current page
|
||||||
|
*/
|
||||||
|
@Input() inPlaceSearch = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of available configuration options
|
||||||
|
*/
|
||||||
|
configurationList$: Observable<SearchConfigurationOption[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current search results
|
||||||
|
*/
|
||||||
|
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<MyDSpaceResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current paginated search options
|
||||||
|
*/
|
||||||
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current relevant scopes
|
||||||
|
*/
|
||||||
|
scopeListRD$: Observable<DSpaceObject[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits true if were on a small screen
|
||||||
|
*/
|
||||||
|
isXsOrSm$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to unsubscribe from
|
||||||
|
*/
|
||||||
|
sub: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable for enumeration RoleType
|
||||||
|
*/
|
||||||
|
roleTypeEnum = RoleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of available view mode
|
||||||
|
*/
|
||||||
|
viewModeList = [ViewMode.List, ViewMode.Detail];
|
||||||
|
|
||||||
|
constructor(private service: SearchService,
|
||||||
|
private sidebarService: SearchSidebarService,
|
||||||
|
private windowService: HostWindowService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
|
||||||
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
|
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize available configuration list
|
||||||
|
*
|
||||||
|
* Listening to changes in the paginated search options
|
||||||
|
* If something changes, update the search results
|
||||||
|
*
|
||||||
|
* Listen to changes in the scope
|
||||||
|
* If something changes, update the list of scopes for the dropdown
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.configurationList$ = this.searchConfigService.getAvailableConfigurationOptions();
|
||||||
|
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||||
|
|
||||||
|
this.sub = this.searchOptions$.pipe(
|
||||||
|
tap(() => this.resultsRD$.next(null)),
|
||||||
|
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getSucceededRemoteData())))
|
||||||
|
.subscribe((results) => {
|
||||||
|
this.resultsRD$.next(results);
|
||||||
|
});
|
||||||
|
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||||
|
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the sidebar to a collapsed state
|
||||||
|
*/
|
||||||
|
public closeSidebar(): void {
|
||||||
|
this.sidebarService.collapse()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the sidebar to an expanded state
|
||||||
|
*/
|
||||||
|
public openSidebar(): void {
|
||||||
|
this.sidebarService.expand();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the sidebar is collapsed
|
||||||
|
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||||
|
*/
|
||||||
|
public isSidebarCollapsed(): Observable<boolean> {
|
||||||
|
return this.sidebarService.isCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string} The base path to the search page
|
||||||
|
*/
|
||||||
|
public getSearchLink(): string {
|
||||||
|
return this.service.getSearchLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from the subscription
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.sub)) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
src/app/+my-dspace-page/my-dspace-page.module.ts
Normal file
69
src/app/+my-dspace-page/my-dspace-page.module.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
|
||||||
|
import { MyDspacePageRoutingModule } from './my-dspace-page-routing.module';
|
||||||
|
import { MyDSpacePageComponent } from './my-dspace-page.component';
|
||||||
|
import { SearchPageModule } from '../+search-page/search-page.module';
|
||||||
|
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 { ItemMyDSpaceResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/item-my-dspace-result/item-my-dspace-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 { 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 { ItemMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-my-dspace-result/item-my-dspace-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 { WorkflowitemMyDSpaceResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/workflowitem-my-dspace-result/workflowitem-my-dspace-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 { 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 { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
MyDspacePageRoutingModule,
|
||||||
|
SearchPageModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
MyDSpacePageComponent,
|
||||||
|
MyDSpaceResultsComponent,
|
||||||
|
ItemMyDSpaceResultListElementComponent,
|
||||||
|
WorkspaceitemMyDSpaceResultListElementComponent,
|
||||||
|
WorkflowitemMyDSpaceResultListElementComponent,
|
||||||
|
ClaimedMyDSpaceResultListElementComponent,
|
||||||
|
PoolMyDSpaceResultListElementComponent,
|
||||||
|
ItemMyDSpaceResultDetailElementComponent,
|
||||||
|
WorkspaceitemMyDSpaceResultDetailElementComponent,
|
||||||
|
WorkflowitemMyDSpaceResultDetailElementComponent,
|
||||||
|
ClaimedMyDSpaceResultDetailElementComponent,
|
||||||
|
PoolMyDSpaceResultDetailElementComponent,
|
||||||
|
MyDSpaceNewSubmissionComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
MyDSpaceGuard,
|
||||||
|
MyDSpaceConfigurationService
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
ItemMyDSpaceResultListElementComponent,
|
||||||
|
WorkspaceitemMyDSpaceResultListElementComponent,
|
||||||
|
WorkflowitemMyDSpaceResultListElementComponent,
|
||||||
|
ClaimedMyDSpaceResultListElementComponent,
|
||||||
|
PoolMyDSpaceResultListElementComponent,
|
||||||
|
ItemMyDSpaceResultDetailElementComponent,
|
||||||
|
WorkspaceitemMyDSpaceResultDetailElementComponent,
|
||||||
|
WorkflowitemMyDSpaceResultDetailElementComponent,
|
||||||
|
ClaimedMyDSpaceResultDetailElementComponent,
|
||||||
|
PoolMyDSpaceResultDetailElementComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module handles all components that are necessary for the mydspace page
|
||||||
|
*/
|
||||||
|
export class MyDSpacePageModule {
|
||||||
|
|
||||||
|
}
|
19
src/app/+my-dspace-page/my-dspace-result.model.ts
Normal file
19
src/app/+my-dspace-page/my-dspace-result.model.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0" @fadeIn>
|
||||||
|
<ds-viewable-collection
|
||||||
|
[config]="searchConfig.pagination"
|
||||||
|
[hasBorder]="hasBorder"
|
||||||
|
[sortConfig]="searchConfig.sort"
|
||||||
|
[objects]="searchResults"
|
||||||
|
[hideGear]="true">
|
||||||
|
</ds-viewable-collection>
|
||||||
|
</div>
|
||||||
|
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>
|
||||||
|
<ds-error *ngIf="searchResults?.hasFailed && (!searchResults?.error || searchResults?.error?.statusCode != 400)" message="{{'error.search-results' | translate}}"></ds-error>
|
||||||
|
<h3 *ngIf="searchResults?.payload?.page.length == 0" class="text-center text-muted" ><span>{{'mydspace.results.no-results' | translate}}</span></h3>
|
@@ -0,0 +1,58 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { QueryParamsDirectiveStub } from '../../shared/testing/query-params-directive-stub';
|
||||||
|
import { MyDSpaceResultsComponent } from './my-dspace-results.component';
|
||||||
|
|
||||||
|
describe('MyDSpaceResultsComponent', () => {
|
||||||
|
let comp: MyDSpaceResultsComponent;
|
||||||
|
let fixture: ComponentFixture<MyDSpaceResultsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), NoopAnimationsModule],
|
||||||
|
declarations: [
|
||||||
|
MyDSpaceResultsComponent,
|
||||||
|
QueryParamsDirectiveStub],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MyDSpaceResultsComponent);
|
||||||
|
comp = fixture.componentInstance; // MyDSpaceResultsComponent test instance
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display results when results are not empty', () => {
|
||||||
|
(comp as any).searchResults = { hasSucceeded: true, isLoading: false, payload: { page: { length: 2 } } };
|
||||||
|
(comp as any).searchConfig = {};
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display link when results are not empty', () => {
|
||||||
|
(comp as any).searchResults = { hasSucceeded: true, isLoading: false, payload: { page: { length: 2 } } };
|
||||||
|
(comp as any).searchConfig = {};
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.query(By.css('a'))).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display error message if error is != 400', () => {
|
||||||
|
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 500 } };
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a message if search result is empty', () => {
|
||||||
|
(comp as any).searchResults = { payload: { page: { length: 0 } } };
|
||||||
|
(comp as any).searchConfig = { query: 'foobar' };
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const linkDes = fixture.debugElement.queryAll(By.css('text-muted'));
|
||||||
|
|
||||||
|
expect(linkDes).toBeDefined()
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,51 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
|
import { MyDSpaceResult } from '../my-dspace-result.model';
|
||||||
|
import { SearchOptions } from '../../+search-page/search-options.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
|
import { isEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents all results for mydspace page
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-my-dspace-results',
|
||||||
|
templateUrl: './my-dspace-results.component.html',
|
||||||
|
animations: [
|
||||||
|
fadeIn,
|
||||||
|
fadeInOut
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class MyDSpaceResultsComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual search result objects
|
||||||
|
*/
|
||||||
|
@Input() searchResults: RemoteData<PaginatedList<MyDSpaceResult<DSpaceObject>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current configuration of the search
|
||||||
|
*/
|
||||||
|
@Input() searchConfig: SearchOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current view mode for the search results
|
||||||
|
*/
|
||||||
|
@Input() viewMode: ViewMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if search results entry are separated by a line
|
||||||
|
*/
|
||||||
|
hasBorder = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if mydspace search results are loading
|
||||||
|
*/
|
||||||
|
isLoading() {
|
||||||
|
return !this.searchResults || isEmpty(this.searchResults) || this.searchResults.isLoading;
|
||||||
|
}
|
||||||
|
}
|
57
src/app/+my-dspace-page/my-dspace.guard.ts
Normal file
57
src/app/+my-dspace-page/my-dspace.guard.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, NavigationExtras, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
import { isEmpty } from '../shared/empty.util';
|
||||||
|
import { MYDSPACE_ROUTE } from './my-dspace-page.component';
|
||||||
|
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
|
||||||
|
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unauthorized activating and loading of mydspace configuration
|
||||||
|
* @class MyDSpaceGuard
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class MyDSpaceGuard implements CanActivate {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor(private configurationService: MyDSpaceConfigurationService, private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when configuration is valid
|
||||||
|
* @method canActivate
|
||||||
|
*/
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
return this.configurationService.getAvailableConfigurationTypes().pipe(
|
||||||
|
first(),
|
||||||
|
map((configurationList) => this.validateConfigurationParam(route.queryParamMap.get('configuration'), configurationList)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given configuration is present in the list of those available
|
||||||
|
*
|
||||||
|
* @param configuration
|
||||||
|
* the configuration to validate
|
||||||
|
* @param configurationList
|
||||||
|
* the list of available configuration
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private validateConfigurationParam(configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean {
|
||||||
|
const configurationDefault: string = configurationList[0];
|
||||||
|
if (isEmpty(configuration) || !configurationList.includes(configuration as MyDSpaceConfigurationValueType)) {
|
||||||
|
// If configuration param is empty or is not included in available configurations redirect to a default configuration value
|
||||||
|
const navigationExtras: NavigationExtras = {
|
||||||
|
queryParams: {configuration: configurationDefault}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.router.navigate([MYDSPACE_ROUTE], navigationExtras);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,12 +2,13 @@ import { HostWindowService } from '../shared/host-window.service';
|
|||||||
import { SearchService } from './search-service/search.service';
|
import { SearchService } from './search-service/search.service';
|
||||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core';
|
||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
import { RouteService } from '../shared/services/route.service';
|
import { RouteService } from '../shared/services/route.service';
|
||||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -18,7 +19,13 @@ import { PaginatedSearchOptions } from './paginated-search-options.model';
|
|||||||
styleUrls: ['./search-page.component.scss'],
|
styleUrls: ['./search-page.component.scss'],
|
||||||
templateUrl: './search-page.component.html',
|
templateUrl: './search-page.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [pushInOut]
|
animations: [pushInOut],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useClass: SearchConfigurationService
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class FilteredSearchPageComponent extends SearchPageComponent {
|
export class FilteredSearchPageComponent extends SearchPageComponent {
|
||||||
@@ -32,7 +39,7 @@ export class FilteredSearchPageComponent extends SearchPageComponent {
|
|||||||
constructor(protected service: SearchService,
|
constructor(protected service: SearchService,
|
||||||
protected sidebarService: SearchSidebarService,
|
protected sidebarService: SearchSidebarService,
|
||||||
protected windowService: HostWindowService,
|
protected windowService: HostWindowService,
|
||||||
protected searchConfigService: SearchConfigurationService,
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
protected routeService: RouteService) {
|
protected routeService: RouteService) {
|
||||||
super(service, sidebarService, windowService, searchConfigService, routeService);
|
super(service, sidebarService, windowService, searchConfigService, routeService);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { autoserialize } from 'cerialize';
|
import { autoserialize, autoserializeAs } 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 { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export class NormalizedSearchResult implements ListableObject {
|
|||||||
* The UUID of the DSpaceObject that was found
|
* The UUID of the DSpaceObject that was found
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
dspaceObject: string;
|
indexableObject: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadata that was used to find this item, hithighlighted
|
* The metadata that was used to find this item, hithighlighted
|
||||||
|
@@ -12,7 +12,7 @@ export class PaginatedSearchOptions extends SearchOptions {
|
|||||||
pagination?: PaginationComponentOptions;
|
pagination?: PaginationComponentOptions;
|
||||||
sort?: SortOptions;
|
sort?: SortOptions;
|
||||||
|
|
||||||
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any, pagination?: PaginationComponentOptions, sort?: SortOptions}) {
|
constructor(options: {configuration?: string, scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any, pagination?: PaginationComponentOptions, sort?: SortOptions}) {
|
||||||
super(options);
|
super(options);
|
||||||
this.pagination = options.pagination;
|
this.pagination = options.pagination;
|
||||||
this.sort = options.sort;
|
this.sort = options.sort;
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
<div>
|
||||||
|
<div class="filters py-2">
|
||||||
|
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
|
||||||
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
|
<div [@facetLoad]="animationState">
|
||||||
|
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<div class="clearfix toggle-more-filters">
|
||||||
|
<a class="float-left" *ngIf="!(isLastPage$ | async)"
|
||||||
|
(click)="showMore()">{{"search.filters.filter.show-more"
|
||||||
|
| translate}}</a>
|
||||||
|
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
||||||
|
(click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
||||||
|
| translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-input-suggestions [suggestions]="(filterSearchResults | async)"
|
||||||
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"
|
||||||
|
[action]="getCurrentUrl()"
|
||||||
|
[name]="filterConfig.paramName"
|
||||||
|
[(ngModel)]="filter"
|
||||||
|
(submitSuggestion)="onSubmit($event)"
|
||||||
|
(clickSuggestion)="onSubmit($event)"
|
||||||
|
(findSuggestions)="findSuggestions($event)"
|
||||||
|
ngDefaultControl></ds-input-suggestions>
|
||||||
|
</div>
|
@@ -0,0 +1,23 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
||||||
|
@import '../../../../../styles/mixins.scss';
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
a {
|
||||||
|
color: $body-color;
|
||||||
|
&:hover, &focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
span.badge {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggle-more-filters a {
|
||||||
|
color: $link-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::ng-deep em {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { FilterType } from '../../../search-service/filter-type.model';
|
||||||
|
import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
||||||
|
import { renderFacetFor } from '../search-filter-type-decorator';
|
||||||
|
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-authority-filter',
|
||||||
|
styleUrls: ['./search-authority-filter.component.scss'],
|
||||||
|
templateUrl: './search-authority-filter.component.html',
|
||||||
|
animations: [facetLoad]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents an authority facet for a specific filter configuration
|
||||||
|
*/
|
||||||
|
@renderFacetFor(FilterType.authority)
|
||||||
|
export class SearchAuthorityFilterComponent extends SearchFacetFilterComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
|
||||||
|
* Retrieve facet value from search link
|
||||||
|
*/
|
||||||
|
protected getFacetValue(facet: FacetValue): string {
|
||||||
|
const search = facet.search;
|
||||||
|
const hashes = search.slice(search.indexOf('?') + 1).split('&');
|
||||||
|
const params = {};
|
||||||
|
hashes.map((hash) => {
|
||||||
|
const [key, val] = hash.split('=');
|
||||||
|
params[key] = decodeURIComponent(val)
|
||||||
|
});
|
||||||
|
|
||||||
|
return params[this.filterConfig.paramName];
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="filters py-2">
|
<div class="filters py-2">
|
||||||
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
|
||||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
<div [@facetLoad]="animationState">
|
<div [@facetLoad]="animationState">
|
||||||
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<div class="clearfix toggle-more-filters">
|
||||||
|
@@ -2,15 +2,6 @@
|
|||||||
@import '../../../../../styles/mixins.scss';
|
@import '../../../../../styles/mixins.scss';
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
a {
|
|
||||||
color: $body-color;
|
|
||||||
&:hover, &focus {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
span.badge {
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toggle-more-filters a {
|
.toggle-more-filters a {
|
||||||
color: $link-color;
|
color: $link-color;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
@import '../../../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $body-color;
|
||||||
|
&:hover, &focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
span.badge {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
}
|
@@ -19,10 +19,12 @@ import { By } from '@angular/platform-browser';
|
|||||||
describe('SearchFacetOptionComponent', () => {
|
describe('SearchFacetOptionComponent', () => {
|
||||||
let comp: SearchFacetOptionComponent;
|
let comp: SearchFacetOptionComponent;
|
||||||
let fixture: ComponentFixture<SearchFacetOptionComponent>;
|
let fixture: ComponentFixture<SearchFacetOptionComponent>;
|
||||||
const filterName1 = 'test name';
|
const filterName1 = 'testname';
|
||||||
|
const filterName2 = 'testAuthorityname';
|
||||||
const value1 = 'testvalue1';
|
const value1 = 'testvalue1';
|
||||||
const value2 = 'test2';
|
const value2 = 'test2';
|
||||||
const value3 = 'another value3';
|
const operator = 'authority';
|
||||||
|
|
||||||
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
name: filterName1,
|
name: filterName1,
|
||||||
type: FilterType.range,
|
type: FilterType.range,
|
||||||
@@ -32,14 +34,38 @@ describe('SearchFacetOptionComponent', () => {
|
|||||||
minValue: 200,
|
minValue: 200,
|
||||||
maxValue: 3000,
|
maxValue: 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockAuthorityFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
|
name: filterName2,
|
||||||
|
type: FilterType.authority,
|
||||||
|
hasFacets: false,
|
||||||
|
isOpenByDefault: false,
|
||||||
|
pageSize: 2
|
||||||
|
});
|
||||||
|
|
||||||
const value: FacetValue = {
|
const value: FacetValue = {
|
||||||
value: value2,
|
label: value2,
|
||||||
count: 20,
|
value: value2,
|
||||||
search: ''
|
count: 20,
|
||||||
};
|
search: ``
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedValue: FacetValue = {
|
||||||
|
label: value1,
|
||||||
|
value: value1,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1},${operator}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const authorityValue: FacetValue = {
|
||||||
|
label: value2,
|
||||||
|
value: value2,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}`
|
||||||
|
};
|
||||||
|
|
||||||
const searchLink = '/search';
|
const searchLink = '/search';
|
||||||
const selectedValues = [value1];
|
const selectedValues = [selectedValue];
|
||||||
const selectedValues$ = observableOf(selectedValues);
|
const selectedValues$ = observableOf(selectedValues);
|
||||||
let filterService;
|
let filterService;
|
||||||
let searchService;
|
let searchService;
|
||||||
@@ -90,7 +116,7 @@ describe('SearchFacetOptionComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the updateAddParams method is called wih a value', () => {
|
describe('when the updateAddParams method is called with a value', () => {
|
||||||
it('should update the addQueryParams with the new parameter values', () => {
|
it('should update the addQueryParams with the new parameter values', () => {
|
||||||
comp.addQueryParams = {};
|
comp.addQueryParams = {};
|
||||||
(comp as any).updateAddParams(selectedValues);
|
(comp as any).updateAddParams(selectedValues);
|
||||||
@@ -101,6 +127,21 @@ describe('SearchFacetOptionComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when filter type is authority and the updateAddParams method is called with a value', () => {
|
||||||
|
it('should update the addQueryParams with the new parameter values', () => {
|
||||||
|
comp.filterValue = authorityValue;
|
||||||
|
comp.filterConfig = mockAuthorityFilterConfig;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
comp.addQueryParams = {};
|
||||||
|
(comp as any).updateAddParams(selectedValues);
|
||||||
|
expect(comp.addQueryParams).toEqual({
|
||||||
|
[mockAuthorityFilterConfig.paramName]: [value1, `${value2},${operator}`],
|
||||||
|
page: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when isVisible emits true', () => {
|
describe('when isVisible emits true', () => {
|
||||||
it('the facet option should be visible', () => {
|
it('the facet option should be visible', () => {
|
||||||
comp.isVisible = observableOf(true);
|
comp.isVisible = observableOf(true);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { FacetValue } from '../../../../search-service/facet-value.model';
|
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||||
@@ -8,9 +8,11 @@ import { SearchService } from '../../../../search-service/search.service';
|
|||||||
import { SearchFilterService } from '../../search-filter.service';
|
import { SearchFilterService } from '../../search-filter.service';
|
||||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||||
import { hasValue } from '../../../../../shared/empty.util';
|
import { hasValue } from '../../../../../shared/empty.util';
|
||||||
|
import { FilterType } from '../../../../search-service/filter-type.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-option',
|
selector: 'ds-search-facet-option',
|
||||||
|
styleUrls: ['./search-facet-option.component.scss'],
|
||||||
templateUrl: './search-facet-option.component.html',
|
templateUrl: './search-facet-option.component.html',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -31,7 +33,12 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Emits the active values for this filter
|
* Emits the active values for this filter
|
||||||
*/
|
*/
|
||||||
@Input() selectedValues$: Observable<string[]>;
|
@Input() selectedValues$: Observable<FacetValue[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the search component should show results on the current page
|
||||||
|
*/
|
||||||
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when this option should be visible and false when it should be invisible
|
* Emits true when this option should be visible and false when it should be invisible
|
||||||
@@ -70,13 +77,16 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
|
|||||||
* Checks if a value for this filter is currently active
|
* Checks if a value for this filter is currently active
|
||||||
*/
|
*/
|
||||||
private isChecked(): Observable<boolean> {
|
private isChecked(): Observable<boolean> {
|
||||||
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.filterValue.value);
|
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.getFacetValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||||
*/
|
*/
|
||||||
getSearchLink() {
|
public getSearchLink(): string {
|
||||||
|
if (this.inPlaceSearch) {
|
||||||
|
return './';
|
||||||
|
}
|
||||||
return this.searchService.getSearchLink();
|
return this.searchService.getSearchLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,13 +94,33 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy {
|
|||||||
* Calculates the parameters that should change if a given value for this filter would be added to the active filters
|
* Calculates the parameters that should change if a given value for this filter would be added to the active filters
|
||||||
* @param {string[]} selectedValues The values that are currently selected for this filter
|
* @param {string[]} selectedValues The values that are currently selected for this filter
|
||||||
*/
|
*/
|
||||||
private updateAddParams(selectedValues: string[]): void {
|
private updateAddParams(selectedValues: FacetValue[]): void {
|
||||||
this.addQueryParams = {
|
this.addQueryParams = {
|
||||||
[this.filterConfig.paramName]: [...selectedValues, this.filterValue.value],
|
[this.filterConfig.paramName]: [...selectedValues.map((facetValue: FacetValue) => facetValue.label), this.getFacetValue()],
|
||||||
page: 1
|
page: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
|
||||||
|
* Retrieve facet value related to facet type
|
||||||
|
*/
|
||||||
|
private getFacetValue(): string {
|
||||||
|
if (this.filterConfig.type === FilterType.authority) {
|
||||||
|
const search = this.filterValue.search;
|
||||||
|
const hashes = search.slice(search.indexOf('?') + 1).split('&');
|
||||||
|
const params = {};
|
||||||
|
hashes.map((hash) => {
|
||||||
|
const [key, val] = hash.split('=');
|
||||||
|
params[key] = decodeURIComponent(val)
|
||||||
|
});
|
||||||
|
|
||||||
|
return params[this.filterConfig.paramName];
|
||||||
|
} else {
|
||||||
|
return this.filterValue.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make sure the subscription is unsubscribed from when this component is destroyed
|
* Make sure the subscription is unsubscribed from when this component is destroyed
|
||||||
*/
|
*/
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<a *ngIf="isVisible | async" class="d-flex flex-row"
|
<a *ngIf="isVisible | async" class="d-flex flex-row"
|
||||||
[routerLink]="[getSearchLink()]"
|
[routerLink]="[getSearchLink()]"
|
||||||
[queryParams]="changeQueryParams" queryParamsHandling="merge">
|
[queryParams]="changeQueryParams" queryParamsHandling="merge">
|
||||||
<span class="filter-value px-1">{{filterValue.value}}</span>
|
<span class="filter-value px-1">{{filterValue.label}}</span>
|
||||||
<span class="float-right filter-value-count ml-auto">
|
<span class="float-right filter-value-count ml-auto">
|
||||||
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
|
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
@import '../../../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $link-color;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: $link-hover-color;
|
||||||
|
|
||||||
|
}
|
||||||
|
span.badge {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
}
|
@@ -35,10 +35,11 @@ describe('SearchFacetRangeOptionComponent', () => {
|
|||||||
maxValue: 3000,
|
maxValue: 3000,
|
||||||
});
|
});
|
||||||
const value: FacetValue = {
|
const value: FacetValue = {
|
||||||
value: value2,
|
label: value2,
|
||||||
count: 20,
|
value: value2,
|
||||||
search: ''
|
count: 20,
|
||||||
};
|
search: ''
|
||||||
|
};
|
||||||
|
|
||||||
const searchLink = '/search';
|
const searchLink = '/search';
|
||||||
let filterService;
|
let filterService;
|
||||||
@@ -92,10 +93,11 @@ describe('SearchFacetRangeOptionComponent', () => {
|
|||||||
it('should update the changeQueryParams with the new parameter values', () => {
|
it('should update the changeQueryParams with the new parameter values', () => {
|
||||||
comp.changeQueryParams = {};
|
comp.changeQueryParams = {};
|
||||||
comp.filterValue = {
|
comp.filterValue = {
|
||||||
value: '50-60',
|
label: '50-60',
|
||||||
count: 20,
|
value: '50-60',
|
||||||
search: ''
|
count: 20,
|
||||||
};
|
search: ''
|
||||||
|
};
|
||||||
(comp as any).updateChangeParams();
|
(comp as any).updateChangeParams();
|
||||||
expect(comp.changeQueryParams).toEqual({
|
expect(comp.changeQueryParams).toEqual({
|
||||||
[mockFilterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: ['50'],
|
[mockFilterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: ['50'],
|
||||||
|
@@ -17,6 +17,7 @@ const rangeDelimiter = '-';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-range-option',
|
selector: 'ds-search-facet-range-option',
|
||||||
|
styleUrls: ['./search-facet-range-option.component.scss'],
|
||||||
templateUrl: './search-facet-range-option.component.html',
|
templateUrl: './search-facet-range-option.component.html',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -34,6 +35,11 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Input() filterConfig: SearchFilterConfig;
|
@Input() filterConfig: SearchFilterConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the search component should show results on the current page
|
||||||
|
*/
|
||||||
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when this option should be visible and false when it should be invisible
|
* Emits true when this option should be visible and false when it should be invisible
|
||||||
*/
|
*/
|
||||||
@@ -74,9 +80,12 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||||
*/
|
*/
|
||||||
getSearchLink() {
|
public getSearchLink(): string {
|
||||||
|
if (this.inPlaceSearch) {
|
||||||
|
return './';
|
||||||
|
}
|
||||||
return this.searchService.getSearchLink();
|
return this.searchService.getSearchLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,5 +2,5 @@
|
|||||||
[routerLink]="[getSearchLink()]"
|
[routerLink]="[getSearchLink()]"
|
||||||
[queryParams]="removeQueryParams" queryParamsHandling="merge">
|
[queryParams]="removeQueryParams" queryParamsHandling="merge">
|
||||||
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
||||||
<span class="filter-value pl-1">{{selectedValue}}</span>
|
<span class="filter-value pl-1 text-capitalize">{{selectedValue.label}}</span>
|
||||||
</a>
|
</a>
|
@@ -0,0 +1,11 @@
|
|||||||
|
@import '../../../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $body-color;
|
||||||
|
&:hover, &focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
span.badge {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
}
|
@@ -13,13 +13,18 @@ import { RouterStub } from '../../../../../shared/testing/router-stub';
|
|||||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||||
import { SearchFilterService } from '../../search-filter.service';
|
import { SearchFilterService } from '../../search-filter.service';
|
||||||
import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component';
|
import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component';
|
||||||
|
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||||
|
|
||||||
describe('SearchFacetSelectedOptionComponent', () => {
|
describe('SearchFacetSelectedOptionComponent', () => {
|
||||||
let comp: SearchFacetSelectedOptionComponent;
|
let comp: SearchFacetSelectedOptionComponent;
|
||||||
let fixture: ComponentFixture<SearchFacetSelectedOptionComponent>;
|
let fixture: ComponentFixture<SearchFacetSelectedOptionComponent>;
|
||||||
const filterName1 = 'test name';
|
const filterName1 = 'test name';
|
||||||
|
const filterName2 = 'testAuthorityname';
|
||||||
|
const label1 = 'test value 1';
|
||||||
const value1 = 'testvalue1';
|
const value1 = 'testvalue1';
|
||||||
|
const label2 = 'test 2';
|
||||||
const value2 = 'test2';
|
const value2 = 'test2';
|
||||||
|
const operator = 'authority';
|
||||||
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
name: filterName1,
|
name: filterName1,
|
||||||
type: FilterType.range,
|
type: FilterType.range,
|
||||||
@@ -29,10 +34,55 @@ describe('SearchFacetSelectedOptionComponent', () => {
|
|||||||
minValue: 200,
|
minValue: 200,
|
||||||
maxValue: 3000,
|
maxValue: 3000,
|
||||||
});
|
});
|
||||||
|
const mockAuthorityFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
|
name: filterName2,
|
||||||
|
type: FilterType.authority,
|
||||||
|
hasFacets: false,
|
||||||
|
isOpenByDefault: false,
|
||||||
|
pageSize: 2
|
||||||
|
});
|
||||||
|
|
||||||
const searchLink = '/search';
|
const searchLink = '/search';
|
||||||
const selectedValues = [value1, value2];
|
const selectedValue: FacetValue = {
|
||||||
|
label: value1,
|
||||||
|
value: value1,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName1}=${value1}`
|
||||||
|
};
|
||||||
|
const selectedValue2: FacetValue = {
|
||||||
|
label: value2,
|
||||||
|
value: value2,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName1}=${value2}`
|
||||||
|
};
|
||||||
|
const selectedAuthorityValue: FacetValue = {
|
||||||
|
label: label1,
|
||||||
|
value: value1,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value1},${operator}`
|
||||||
|
};
|
||||||
|
const selectedAuthorityValue2: FacetValue = {
|
||||||
|
label: label2,
|
||||||
|
value: value2,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}`
|
||||||
|
};
|
||||||
|
const selectedValues = [selectedValue, selectedValue2];
|
||||||
|
const selectedAuthorityValues = [selectedAuthorityValue, selectedAuthorityValue2];
|
||||||
|
const facetValue = {
|
||||||
|
label: value2,
|
||||||
|
value: value2,
|
||||||
|
count: 1,
|
||||||
|
search: ''
|
||||||
|
};
|
||||||
|
const authorityValue: FacetValue = {
|
||||||
|
label: label2,
|
||||||
|
value: value2,
|
||||||
|
count: 20,
|
||||||
|
search: `http://test.org/api/discover/search/objects?f.${filterName2}=${value2},${operator}`
|
||||||
|
};
|
||||||
const selectedValues$ = observableOf(selectedValues);
|
const selectedValues$ = observableOf(selectedValues);
|
||||||
|
const selectedAuthorityValues$ = observableOf(selectedAuthorityValues);
|
||||||
let filterService;
|
let filterService;
|
||||||
let searchService;
|
let searchService;
|
||||||
let router;
|
let router;
|
||||||
@@ -76,7 +126,7 @@ describe('SearchFacetSelectedOptionComponent', () => {
|
|||||||
filterService = (comp as any).filterService;
|
filterService = (comp as any).filterService;
|
||||||
searchService = (comp as any).searchService;
|
searchService = (comp as any).searchService;
|
||||||
router = (comp as any).router;
|
router = (comp as any).router;
|
||||||
comp.selectedValue = value2;
|
comp.selectedValue = facetValue;
|
||||||
comp.selectedValues$ = selectedValues$;
|
comp.selectedValues$ = selectedValues$;
|
||||||
comp.filterConfig = mockFilterConfig;
|
comp.filterConfig = mockFilterConfig;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -92,4 +142,20 @@ describe('SearchFacetSelectedOptionComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when filter type is authority and the updateRemoveParams method is called with a value', () => {
|
||||||
|
it('should update the removeQueryParams with the new parameter values', () => {
|
||||||
|
spyOn(filterService, 'getSelectedValuesForFilter').and.returnValue(selectedAuthorityValues);
|
||||||
|
comp.selectedValue = authorityValue;
|
||||||
|
comp.selectedValues$ = selectedAuthorityValues$;
|
||||||
|
comp.filterConfig = mockAuthorityFilterConfig;
|
||||||
|
comp.removeQueryParams = {};
|
||||||
|
fixture.detectChanges();
|
||||||
|
(comp as any).updateRemoveParams(selectedAuthorityValues);
|
||||||
|
expect(comp.removeQueryParams).toEqual({
|
||||||
|
[mockAuthorityFilterConfig.paramName]: [`${value1},${operator}`],
|
||||||
|
page: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -6,9 +6,12 @@ import { SearchService } from '../../../../search-service/search.service';
|
|||||||
import { SearchFilterService } from '../../search-filter.service';
|
import { SearchFilterService } from '../../search-filter.service';
|
||||||
import { hasValue } from '../../../../../shared/empty.util';
|
import { hasValue } from '../../../../../shared/empty.util';
|
||||||
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../search-service/search-configuration.service';
|
||||||
|
import { FacetValue } from '../../../../search-service/facet-value.model';
|
||||||
|
import { FilterType } from '../../../../search-service/filter-type.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-selected-option',
|
selector: 'ds-search-facet-selected-option',
|
||||||
|
styleUrls: ['./search-facet-selected-option.component.scss'],
|
||||||
templateUrl: './search-facet-selected-option.component.html',
|
templateUrl: './search-facet-selected-option.component.html',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -19,7 +22,7 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* The value for this component
|
* The value for this component
|
||||||
*/
|
*/
|
||||||
@Input() selectedValue: string;
|
@Input() selectedValue: FacetValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filter configuration for this facet option
|
* The filter configuration for this facet option
|
||||||
@@ -29,7 +32,12 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Emits the active values for this filter
|
* Emits the active values for this filter
|
||||||
*/
|
*/
|
||||||
@Input() selectedValues$: Observable<string[]>;
|
@Input() selectedValues$: Observable<FacetValue[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the search component should show results on the current page
|
||||||
|
*/
|
||||||
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI parameters when this filter is removed
|
* UI parameters when this filter is removed
|
||||||
@@ -59,9 +67,12 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||||
*/
|
*/
|
||||||
getSearchLink() {
|
public getSearchLink(): string {
|
||||||
|
if (this.inPlaceSearch) {
|
||||||
|
return './';
|
||||||
|
}
|
||||||
return this.searchService.getSearchLink();
|
return this.searchService.getSearchLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,13 +80,35 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy {
|
|||||||
* Calculates the parameters that should change if a given value for this filter would be removed from the active filters
|
* Calculates the parameters that should change if a given value for this filter would be removed from the active filters
|
||||||
* @param {string[]} selectedValues The values that are currently selected for this filter
|
* @param {string[]} selectedValues The values that are currently selected for this filter
|
||||||
*/
|
*/
|
||||||
private updateRemoveParams(selectedValues: string[]): void {
|
private updateRemoveParams(selectedValues: FacetValue[]): void {
|
||||||
this.removeQueryParams = {
|
this.removeQueryParams = {
|
||||||
[this.filterConfig.paramName]: selectedValues.filter((v) => v !== this.selectedValue),
|
[this.filterConfig.paramName]: selectedValues
|
||||||
|
.filter((facetValue: FacetValue) => facetValue.label !== this.selectedValue.label)
|
||||||
|
.map((facetValue: FacetValue) => this.getFacetValue(facetValue)),
|
||||||
page: 1
|
page: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
|
||||||
|
* Retrieve facet value related to facet type
|
||||||
|
*/
|
||||||
|
private getFacetValue(facetValue: FacetValue): string {
|
||||||
|
if (this.filterConfig.type === FilterType.authority) {
|
||||||
|
const search = facetValue.search;
|
||||||
|
const hashes = search.slice(search.indexOf('?') + 1).split('&');
|
||||||
|
const params = {};
|
||||||
|
hashes.map((hash) => {
|
||||||
|
const [key, val] = hash.split('=');
|
||||||
|
params[key] = decodeURIComponent(val)
|
||||||
|
});
|
||||||
|
|
||||||
|
return params[this.filterConfig.paramName];
|
||||||
|
} else {
|
||||||
|
return facetValue.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make sure the subscription is unsubscribed from when this component is destroyed
|
* Make sure the subscription is unsubscribed from when this component is destroyed
|
||||||
*/
|
*/
|
||||||
|
@@ -2,7 +2,7 @@ import { Component, Injector, Input, OnInit } from '@angular/core';
|
|||||||
import { renderFilterType } from '../search-filter-type-decorator';
|
import { renderFilterType } from '../search-filter-type-decorator';
|
||||||
import { FilterType } from '../../../search-service/filter-type.model';
|
import { FilterType } from '../../../search-service/filter-type.model';
|
||||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||||
import { FILTER_CONFIG } from '../search-filter.service';
|
import { FILTER_CONFIG, IN_PLACE_SEARCH } from '../search-filter.service';
|
||||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
import { SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
import { SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
||||||
|
|
||||||
@@ -20,6 +20,11 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() filterConfig: SearchFilterConfig;
|
@Input() filterConfig: SearchFilterConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the search component should show results on the current page
|
||||||
|
*/
|
||||||
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The constructor of the search facet filter that should be rendered, based on the filter config's type
|
* The constructor of the search facet filter that should be rendered, based on the filter config's type
|
||||||
*/
|
*/
|
||||||
@@ -39,7 +44,8 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
|||||||
this.searchFilter = this.getSearchFilter();
|
this.searchFilter = this.getSearchFilter();
|
||||||
this.objectInjector = Injector.create({
|
this.objectInjector = Injector.create({
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] }
|
{ provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] },
|
||||||
|
{ provide: IN_PLACE_SEARCH, useFactory: () => (this.inPlaceSearch), deps: [] }
|
||||||
],
|
],
|
||||||
parent: this.injector
|
parent: this.injector
|
||||||
});
|
});
|
||||||
|
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
|
import { FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService } from '../search-filter.service';
|
||||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||||
import { FilterType } from '../../../search-service/filter-type.model';
|
import { FilterType } from '../../../search-service/filter-type.model';
|
||||||
import { FacetValue } from '../../../search-service/facet-value.model';
|
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||||
@@ -17,7 +17,9 @@ import { Router } from '@angular/router';
|
|||||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
import { SearchFacetFilterComponent } from './search-facet-filter.component';
|
import { SearchFacetFilterComponent } from './search-facet-filter.component';
|
||||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service-stub';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
|
||||||
describe('SearchFacetFilterComponent', () => {
|
describe('SearchFacetFilterComponent', () => {
|
||||||
let comp: SearchFacetFilterComponent;
|
let comp: SearchFacetFilterComponent;
|
||||||
@@ -35,14 +37,17 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
const values: FacetValue[] = [
|
const values: FacetValue[] = [
|
||||||
{
|
{
|
||||||
|
label: value1,
|
||||||
value: value1,
|
value: value1,
|
||||||
count: 52,
|
count: 52,
|
||||||
search: ''
|
search: ''
|
||||||
}, {
|
}, {
|
||||||
|
label: value2,
|
||||||
value: value2,
|
value: value2,
|
||||||
count: 20,
|
count: 20,
|
||||||
search: ''
|
search: ''
|
||||||
}, {
|
}, {
|
||||||
|
label: value3,
|
||||||
value: value3,
|
value: value3,
|
||||||
count: 5,
|
count: 5,
|
||||||
search: ''
|
search: ''
|
||||||
@@ -65,8 +70,9 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
||||||
{ provide: Router, useValue: new RouterStub() },
|
{ provide: Router, useValue: new RouterStub() },
|
||||||
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() },
|
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() },
|
||||||
{ provide: RemoteDataBuildService, useValue: {aggregate: () => observableOf({})} },
|
{ provide: RemoteDataBuildService, useValue: { aggregate: () => observableOf({}) } },
|
||||||
{ provide: SearchConfigurationService, useValue: {searchOptions: observableOf({})} },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
|
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||||
{
|
{
|
||||||
provide: SearchFilterService, useValue: {
|
provide: SearchFilterService, useValue: {
|
||||||
getSelectedValuesForFilter: () => observableOf(selectedValues),
|
getSelectedValuesForFilter: () => observableOf(selectedValues),
|
||||||
@@ -168,13 +174,20 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
const searchUrl = '/search/path';
|
const searchUrl = '/search/path';
|
||||||
const testValue = 'test';
|
const testValue = 'test';
|
||||||
const data = testValue;
|
const data = testValue;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
comp.selectedValues$ = observableOf(selectedValues.map((value) =>
|
||||||
|
Object.assign(new FacetValue(), {
|
||||||
|
label: value,
|
||||||
|
value: value
|
||||||
|
})));
|
||||||
|
fixture.detectChanges();
|
||||||
spyOn(comp, 'getSearchLink').and.returnValue(searchUrl);
|
spyOn(comp, 'getSearchLink').and.returnValue(searchUrl);
|
||||||
comp.onSubmit(data);
|
comp.onSubmit(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call navigate on the router with the right searchlink and parameters', () => {
|
it('should call navigate on the router with the right searchlink and parameters', () => {
|
||||||
expect(router.navigate).toHaveBeenCalledWith([searchUrl], {
|
expect(router.navigate).toHaveBeenCalledWith(searchUrl.split('/'), {
|
||||||
queryParams: { [mockFilterConfig.paramName]: [...selectedValues, testValue] },
|
queryParams: { [mockFilterConfig.paramName]: [...selectedValues, testValue] },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
@@ -188,9 +201,9 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call showFirstPageOnly and empty the filter', () => {
|
it('should call showFirstPageOnly and empty the filter', () => {
|
||||||
expect(comp.animationState).toEqual('loading');
|
expect(comp.animationState).toEqual('loading');
|
||||||
expect((comp as any).collapseNextUpdate).toBeTruthy();
|
expect((comp as any).collapseNextUpdate).toBeTruthy();
|
||||||
expect(comp.filter).toEqual('');
|
expect(comp.filter).toEqual('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ import {
|
|||||||
Subject,
|
Subject,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { switchMap, distinctUntilChanged, map, take } from 'rxjs/operators';
|
import { switchMap, distinctUntilChanged, map, take, flatMap, tap } from 'rxjs/operators';
|
||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -18,11 +18,12 @@ import { EmphasizePipe } from '../../../../shared/utils/emphasize.pipe';
|
|||||||
import { FacetValue } from '../../../search-service/facet-value.model';
|
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||||
import { SearchService } from '../../../search-service/search.service';
|
import { SearchService } from '../../../search-service/search.service';
|
||||||
import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
|
import { FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService } from '../search-filter.service';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||||
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
import { SearchOptions } from '../../../search-options.model';
|
import { SearchOptions } from '../../../search-options.model';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-filter',
|
selector: 'ds-search-facet-filter',
|
||||||
@@ -56,7 +57,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* List of subscriptions to unsubscribe from
|
* List of subscriptions to unsubscribe from
|
||||||
*/
|
*/
|
||||||
private subs: Subscription[] = [];
|
protected subs: Subscription[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the result values for this filter found by the current filter query
|
* Emits the result values for this filter found by the current filter query
|
||||||
@@ -66,8 +67,8 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Emits the active values for this filter
|
* Emits the active values for this filter
|
||||||
*/
|
*/
|
||||||
selectedValues$: Observable<string[]>;
|
selectedValues$: Observable<FacetValue[]>;
|
||||||
private collapseNextUpdate = true;
|
protected collapseNextUpdate = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State of the requested facets used to time the animation
|
* State of the requested facets used to time the animation
|
||||||
@@ -81,9 +82,10 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(protected searchService: SearchService,
|
constructor(protected searchService: SearchService,
|
||||||
protected filterService: SearchFilterService,
|
protected filterService: SearchFilterService,
|
||||||
protected searchConfigService: SearchConfigurationService,
|
|
||||||
protected rdbs: RemoteDataBuildService,
|
protected rdbs: RemoteDataBuildService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
|
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
||||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig) {
|
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +96,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
|
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
|
||||||
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
||||||
|
|
||||||
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig);
|
|
||||||
this.searchOptions$ = this.searchConfigService.searchOptions;
|
this.searchOptions$ = this.searchConfigService.searchOptions;
|
||||||
this.subs.push(this.searchOptions$.subscribe(() => this.updateFilterValueList()));
|
this.subs.push(this.searchOptions$.subscribe(() => this.updateFilterValueList()));
|
||||||
const facetValues = observableCombineLatest(this.searchOptions$, this.currentPage).pipe(
|
const facetValues$ = observableCombineLatest(this.searchOptions$, this.currentPage).pipe(
|
||||||
map(([options, page]) => {
|
map(([options, page]) => {
|
||||||
return { options, page }
|
return { options, page }
|
||||||
}),
|
}),
|
||||||
@@ -115,8 +116,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let filterValues = [];
|
let filterValues = [];
|
||||||
this.subs.push(facetValues.subscribe((facetOutcome) => {
|
this.subs.push(facetValues$.subscribe((facetOutcome) => {
|
||||||
const newValues$ = facetOutcome.values;
|
const newValues$ = facetOutcome.values;
|
||||||
|
|
||||||
if (this.collapseNextUpdate) {
|
if (this.collapseNextUpdate) {
|
||||||
@@ -130,9 +132,24 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
filterValues = [...filterValues, newValues$];
|
filterValues = [...filterValues, newValues$];
|
||||||
|
|
||||||
this.subs.push(this.rdbs.aggregate(filterValues).subscribe((rd: RemoteData<Array<PaginatedList<FacetValue>>>) => {
|
this.subs.push(this.rdbs.aggregate(filterValues).pipe(
|
||||||
|
tap((rd: RemoteData<Array<PaginatedList<FacetValue>>>) => {
|
||||||
|
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig).pipe(
|
||||||
|
map((selectedValues) => {
|
||||||
|
return selectedValues.map((value: string) => {
|
||||||
|
const fValue = [].concat(...rd.payload.map((page) => page.page)).find((facetValue: FacetValue) => facetValue.value === value);
|
||||||
|
if (hasValue(fValue)) {
|
||||||
|
return fValue;
|
||||||
|
}
|
||||||
|
return Object.assign(new FacetValue(), { label: value, value: value });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
).subscribe((rd: RemoteData<Array<PaginatedList<FacetValue>>>) => {
|
||||||
this.animationState = 'ready';
|
this.animationState = 'ready';
|
||||||
this.filterValues$.next(rd);
|
this.filterValues$.next(rd);
|
||||||
|
|
||||||
}));
|
}));
|
||||||
this.subs.push(newValues$.pipe(take(1)).subscribe((rd) => {
|
this.subs.push(newValues$.pipe(take(1)).subscribe((rd) => {
|
||||||
this.isLastPage$.next(hasNoValue(rd.payload.next))
|
this.isLastPage$.next(hasNoValue(rd.payload.next))
|
||||||
@@ -158,12 +175,25 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||||
*/
|
*/
|
||||||
getSearchLink() {
|
public getSearchLink(): string {
|
||||||
|
if (this.inPlaceSearch) {
|
||||||
|
return './';
|
||||||
|
}
|
||||||
return this.searchService.getSearchLink();
|
return this.searchService.getSearchLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string[]} The base path to the search page, or the current page when inPlaceSearch is true, split in separate pieces
|
||||||
|
*/
|
||||||
|
public getSearchLinkParts(): string[] {
|
||||||
|
if (this.inPlaceSearch) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.getSearchLink().split('/');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the next page as well
|
* Show the next page as well
|
||||||
*/
|
*/
|
||||||
@@ -199,9 +229,14 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
onSubmit(data: any) {
|
onSubmit(data: any) {
|
||||||
this.selectedValues$.pipe(take(1)).subscribe((selectedValues) => {
|
this.selectedValues$.pipe(take(1)).subscribe((selectedValues) => {
|
||||||
if (isNotEmpty(data)) {
|
if (isNotEmpty(data)) {
|
||||||
this.router.navigate([this.getSearchLink()], {
|
this.router.navigate(this.getSearchLinkParts(), {
|
||||||
queryParams:
|
queryParams:
|
||||||
{ [this.filterConfig.paramName]: [...selectedValues, data] },
|
{
|
||||||
|
[this.filterConfig.paramName]: [
|
||||||
|
...selectedValues.map((facet) => this.getFacetValue(facet)),
|
||||||
|
data
|
||||||
|
]
|
||||||
|
},
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
this.filter = '';
|
this.filter = '';
|
||||||
@@ -252,7 +287,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
return rd.payload.page.map((facet) => {
|
return rd.payload.page.map((facet) => {
|
||||||
return {
|
return {
|
||||||
displayValue: this.getDisplayValue(facet, data),
|
displayValue: this.getDisplayValue(facet, data),
|
||||||
value: facet.value
|
value: this.getFacetValue(facet)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -264,6 +299,13 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve facet value
|
||||||
|
*/
|
||||||
|
protected getFacetValue(facet: FacetValue): string {
|
||||||
|
return facet.value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value
|
* Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value
|
||||||
* @param {FacetValue} facet The value of the facet as returned by the server
|
* @param {FacetValue} facet The value of the facet as returned by the server
|
||||||
|
@@ -2,6 +2,6 @@
|
|||||||
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fas float-right"
|
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fas float-right"
|
||||||
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
||||||
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
|
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
|
||||||
<ds-search-facet-filter-wrapper [filterConfig]="filter"></ds-search-facet-filter-wrapper>
|
<ds-search-facet-filter-wrapper [filterConfig]="filter" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-filter-wrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -11,6 +11,8 @@ import { SearchFilterComponent } from './search-filter.component';
|
|||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { FilterType } from '../../search-service/filter-type.model';
|
import { FilterType } from '../../search-service/filter-type.model';
|
||||||
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service-stub';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
describe('SearchFilterComponent', () => {
|
describe('SearchFilterComponent', () => {
|
||||||
let comp: SearchFilterComponent;
|
let comp: SearchFilterComponent;
|
||||||
@@ -54,8 +56,6 @@ describe('SearchFilterComponent', () => {
|
|||||||
getFacetValuesFor: (filter) => mockResults
|
getFacetValuesFor: (filter) => mockResults
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchConfigServiceStub = {};
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||||
@@ -66,7 +66,7 @@ describe('SearchFilterComponent', () => {
|
|||||||
provide: SearchFilterService,
|
provide: SearchFilterService,
|
||||||
useValue: mockFilterService
|
useValue: mockFilterService
|
||||||
},
|
},
|
||||||
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchFilterComponent, {
|
}).overrideComponent(SearchFilterComponent, {
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
|
import { Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { filter, first, map, startWith, switchMap, take } from 'rxjs/operators';
|
import { filter, first, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
import { Component, Input, OnInit } from '@angular/core';
|
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { SearchFilterService } from './search-filter.service';
|
import { SearchFilterService } from './search-filter.service';
|
||||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
|
||||||
import { slide } from '../../../shared/animations/slide';
|
import { slide } from '../../../shared/animations/slide';
|
||||||
import { isNotEmpty } from '../../../shared/empty.util';
|
import { isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { SearchService } from '../../search-service/search.service';
|
import { SearchService } from '../../search-service/search.service';
|
||||||
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../search-service/search-configuration.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-filter',
|
selector: 'ds-search-filter',
|
||||||
@@ -24,6 +27,11 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() filter: SearchFilterConfig;
|
@Input() filter: SearchFilterConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the search component should show results on the current page
|
||||||
|
*/
|
||||||
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when the filter is 100% collapsed in the UI
|
* True when the filter is 100% collapsed in the UI
|
||||||
*/
|
*/
|
||||||
@@ -44,7 +52,10 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
active$: Observable<boolean>;
|
active$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private filterService: SearchFilterService, private searchService: SearchService, private searchConfigService: SearchConfigurationService) {
|
constructor(
|
||||||
|
private filterService: SearchFilterService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { SearchFilterAction, SearchFilterActionTypes, SearchFilterInitializeAction } from './search-filter.actions';
|
||||||
SearchFilterAction,
|
|
||||||
SearchFilterActionTypes,
|
|
||||||
SearchFilterInitializeAction
|
|
||||||
} from './search-filter.actions';
|
|
||||||
import { isEmpty, isNotUndefined } from '../../../shared/empty.util';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface that represents the state for a single filters
|
* Interface that represents the state for a single filters
|
||||||
|
@@ -21,10 +21,13 @@ import { SearchOptions } from '../../search-options.model';
|
|||||||
import { PaginatedSearchOptions } from '../../paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../paginated-search-options.model';
|
||||||
import { SearchFixedFilterService } from './search-fixed-filter.service';
|
import { SearchFixedFilterService } from './search-fixed-filter.service';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
|
import * as postcss from 'postcss';
|
||||||
|
import prefix = postcss.vendor.prefix;
|
||||||
// const spy = create();
|
// const spy = create();
|
||||||
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||||
|
|
||||||
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
||||||
|
export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that performs all actions that have to do with search filters and facets
|
* Service that performs all actions that have to do with search filters and facets
|
||||||
@@ -141,7 +144,6 @@ export class SearchFilterService {
|
|||||||
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
|
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
|
||||||
map((params: Params) => [].concat(...Object.values(params))),
|
map((params: Params) => [].concat(...Object.values(params))),
|
||||||
);
|
);
|
||||||
|
|
||||||
return observableCombineLatest(values$, prefixValues$).pipe(
|
return observableCombineLatest(values$, prefixValues$).pipe(
|
||||||
map(([values, prefixValues]) => {
|
map(([values, prefixValues]) => {
|
||||||
if (isNotEmpty(values)) {
|
if (isNotEmpty(values)) {
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="filters py-2">
|
<div class="filters py-2">
|
||||||
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
|
||||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
<div [@facetLoad]="animationState">
|
<div [@facetLoad]="animationState">
|
||||||
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<div class="clearfix toggle-more-filters">
|
||||||
|
@@ -2,15 +2,6 @@
|
|||||||
@import '../../../../../styles/mixins.scss';
|
@import '../../../../../styles/mixins.scss';
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
a {
|
|
||||||
color: $body-color;
|
|
||||||
&:hover, &focus {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
span.badge {
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toggle-more-filters a {
|
.toggle-more-filters a {
|
||||||
color: $link-color;
|
color: $link-color;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@@ -24,7 +24,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
<div [@facetLoad]="animationState">
|
<div [@facetLoad]="animationState">
|
||||||
<ds-search-facet-range-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value"></ds-search-facet-range-option>
|
<ds-search-facet-range-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-range-option>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,17 +3,6 @@
|
|||||||
|
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
a {
|
|
||||||
color: $link-color;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
color: $link-hover-color;
|
|
||||||
|
|
||||||
}
|
|
||||||
span.badge {
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toggle-more-filters a {
|
.toggle-more-filters a {
|
||||||
color: $link-color;
|
color: $link-color;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
|
import { FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService } from '../search-filter.service';
|
||||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||||
import { FilterType } from '../../../search-service/filter-type.model';
|
import { FilterType } from '../../../search-service/filter-type.model';
|
||||||
import { FacetValue } from '../../../search-service/facet-value.model';
|
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||||
@@ -18,7 +18,8 @@ import { PageInfo } from '../../../../core/shared/page-info.model';
|
|||||||
import { SearchRangeFilterComponent } from './search-range-filter.component';
|
import { SearchRangeFilterComponent } from './search-range-filter.component';
|
||||||
import { RouteService } from '../../../../shared/services/route.service';
|
import { RouteService } from '../../../../shared/services/route.service';
|
||||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service-stub';
|
||||||
|
|
||||||
describe('SearchRangeFilterComponent', () => {
|
describe('SearchRangeFilterComponent', () => {
|
||||||
let comp: SearchRangeFilterComponent;
|
let comp: SearchRangeFilterComponent;
|
||||||
@@ -41,14 +42,17 @@ describe('SearchRangeFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
const values: FacetValue[] = [
|
const values: FacetValue[] = [
|
||||||
{
|
{
|
||||||
|
label: value1,
|
||||||
value: value1,
|
value: value1,
|
||||||
count: 52,
|
count: 52,
|
||||||
search: ''
|
search: ''
|
||||||
}, {
|
}, {
|
||||||
|
label: value2,
|
||||||
value: value2,
|
value: value2,
|
||||||
count: 20,
|
count: 20,
|
||||||
search: ''
|
search: ''
|
||||||
}, {
|
}, {
|
||||||
|
label: value3,
|
||||||
value: value3,
|
value: value3,
|
||||||
count: 5,
|
count: 5,
|
||||||
search: ''
|
search: ''
|
||||||
@@ -73,9 +77,8 @@ describe('SearchRangeFilterComponent', () => {
|
|||||||
{ provide: FILTER_CONFIG, useValue: mockFilterConfig },
|
{ provide: FILTER_CONFIG, useValue: mockFilterConfig },
|
||||||
{ provide: RemoteDataBuildService, useValue: {aggregate: () => observableOf({})} },
|
{ provide: RemoteDataBuildService, useValue: {aggregate: () => observableOf({})} },
|
||||||
{ provide: RouteService, useValue: {getQueryParameterValue: () => observableOf({})} },
|
{ provide: RouteService, useValue: {getQueryParameterValue: () => observableOf({})} },
|
||||||
{ provide: SearchConfigurationService, useValue: {
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
searchOptions: observableOf({}) }
|
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: SearchFilterService, useValue: {
|
provide: SearchFilterService, useValue: {
|
||||||
getSelectedValuesForFilter: () => selectedValues,
|
getSelectedValuesForFilter: () => selectedValues,
|
||||||
@@ -116,7 +119,7 @@ describe('SearchRangeFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call navigate on the router with the right searchlink and parameters', () => {
|
it('should call navigate on the router with the right searchlink and parameters', () => {
|
||||||
expect(router.navigate).toHaveBeenCalledWith([searchUrl], {
|
expect(router.navigate).toHaveBeenCalledWith(searchUrl.split('/'), {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
[mockFilterConfig.paramName + minSuffix]: [1900],
|
[mockFilterConfig.paramName + minSuffix]: [1900],
|
||||||
[mockFilterConfig.paramName + maxSuffix]: [1950]
|
[mockFilterConfig.paramName + maxSuffix]: [1950]
|
||||||
|
@@ -10,13 +10,14 @@ import {
|
|||||||
SearchFacetFilterComponent
|
SearchFacetFilterComponent
|
||||||
} from '../search-facet-filter/search-facet-filter.component';
|
} from '../search-facet-filter/search-facet-filter.component';
|
||||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||||
import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
|
import { FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService } from '../search-filter.service';
|
||||||
import { SearchService } from '../../../search-service/search.service';
|
import { SearchService } from '../../../search-service/search.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { RouteService } from '../../../../shared/services/route.service';
|
import { RouteService } from '../../../../shared/services/route.service';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The suffix for a range filters' minimum in the frontend URL
|
* The suffix for a range filters' minimum in the frontend URL
|
||||||
@@ -72,13 +73,14 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
|
|
||||||
constructor(protected searchService: SearchService,
|
constructor(protected searchService: SearchService,
|
||||||
protected filterService: SearchFilterService,
|
protected filterService: SearchFilterService,
|
||||||
protected searchConfigService: SearchConfigurationService,
|
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected rdbs: RemoteDataBuildService,
|
protected rdbs: RemoteDataBuildService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
|
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
||||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
||||||
@Inject(PLATFORM_ID) private platformId: any,
|
@Inject(PLATFORM_ID) private platformId: any,
|
||||||
private route: RouteService) {
|
private route: RouteService) {
|
||||||
super(searchService, filterService, searchConfigService, rdbs, router, filterConfig);
|
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +109,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
onSubmit() {
|
onSubmit() {
|
||||||
const newMin = this.range[0] !== this.min ? [this.range[0]] : null;
|
const newMin = this.range[0] !== this.min ? [this.range[0]] : null;
|
||||||
const newMax = this.range[1] !== this.max ? [this.range[1]] : null;
|
const newMax = this.range[1] !== this.max ? [this.range[1]] : null;
|
||||||
this.router.navigate([this.getSearchLink()], {
|
this.router.navigate(this.getSearchLinkParts(), {
|
||||||
queryParams:
|
queryParams:
|
||||||
{
|
{
|
||||||
[this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: newMin,
|
[this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: newMin,
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="filters py-2">
|
<div class="filters py-2">
|
||||||
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$"></ds-search-facet-selected-option>
|
<ds-search-facet-selected-option *ngFor="let value of (selectedValues$ | async)" [selectedValue]="value" [filterConfig]="filterConfig" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-selected-option>
|
||||||
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
|
||||||
<div [@facetLoad]="animationState">
|
<div [@facetLoad]="animationState">
|
||||||
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$"></ds-search-facet-option>
|
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value" [selectedValues$]="selectedValues$" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-option>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="clearfix toggle-more-filters">
|
<div class="clearfix toggle-more-filters">
|
||||||
|
@@ -2,15 +2,6 @@
|
|||||||
@import '../../../../../styles/mixins.scss';
|
@import '../../../../../styles/mixins.scss';
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
a {
|
|
||||||
color: $body-color;
|
|
||||||
&:hover, &focus {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
span.badge {
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toggle-more-filters a {
|
.toggle-more-filters a {
|
||||||
color: $link-color;
|
color: $link-color;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<h3>{{"search.filters.head" | translate}}</h3>
|
<h3>{{"search.filters.head" | translate}}</h3>
|
||||||
<div *ngIf="(filters | async)?.hasSucceeded">
|
<div *ngIf="(filters | async)?.hasSucceeded">
|
||||||
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
|
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
|
||||||
<ds-search-filter [filter]="filter"></ds-search-filter>
|
<ds-search-filter [filter]="filter" [inPlaceSearch]="inPlaceSearch"></ds-search-filter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button">{{"search.filters.reset" | translate}}</a>
|
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button">{{"search.filters.reset" | translate}}</a>
|
@@ -7,13 +7,15 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { SearchFilterService } from './search-filter/search-filter.service';
|
import { SearchFilterService } from './search-filter/search-filter.service';
|
||||||
import { SearchFiltersComponent } from './search-filters.component';
|
import { SearchFiltersComponent } from './search-filters.component';
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service-stub';
|
||||||
|
|
||||||
describe('SearchFiltersComponent', () => {
|
describe('SearchFiltersComponent', () => {
|
||||||
let comp: SearchFiltersComponent;
|
let comp: SearchFiltersComponent;
|
||||||
let fixture: ComponentFixture<SearchFiltersComponent>;
|
let fixture: ComponentFixture<SearchFiltersComponent>;
|
||||||
let searchService: SearchService;
|
let searchService: SearchService;
|
||||||
|
|
||||||
const searchServiceStub = {
|
const searchServiceStub = {
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
getConfig: () =>
|
getConfig: () =>
|
||||||
@@ -30,17 +32,13 @@ describe('SearchFiltersComponent', () => {
|
|||||||
[]
|
[]
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchConfigServiceStub = jasmine.createSpyObj('SearchConfigurationService', {
|
|
||||||
getCurrentFrontendFilters: observableOf({})
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||||
declarations: [SearchFiltersComponent],
|
declarations: [SearchFiltersComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: searchServiceStub },
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
{ provide: SearchFilterService, useValue: searchFiltersStub },
|
{ provide: SearchFilterService, useValue: searchFiltersStub },
|
||||||
|
|
||||||
],
|
],
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
import { Observable } from 'rxjs';
|
import { Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { map } from 'rxjs/operators';
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
||||||
import { SearchFilterService } from './search-filter/search-filter.service';
|
import { SearchFilterService } from './search-filter/search-filter.service';
|
||||||
import { getSucceededRemoteData } from '../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../core/shared/operators';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-filters',
|
selector: 'ds-search-filters',
|
||||||
@@ -18,7 +20,7 @@ import { getSucceededRemoteData } from '../../core/shared/operators';
|
|||||||
/**
|
/**
|
||||||
* This component represents the part of the search sidebar that contains filters.
|
* This component represents the part of the search sidebar that contains filters.
|
||||||
*/
|
*/
|
||||||
export class SearchFiltersComponent {
|
export class SearchFiltersComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* An observable containing configuration about which filters are shown and how they are shown
|
* An observable containing configuration about which filters are shown and how they are shown
|
||||||
*/
|
*/
|
||||||
@@ -30,24 +32,43 @@ export class SearchFiltersComponent {
|
|||||||
*/
|
*/
|
||||||
clearParams;
|
clearParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the search component should show results on the current page
|
||||||
|
*/
|
||||||
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
* @param {SearchService} searchService
|
* @param {SearchService} searchService
|
||||||
* @param {SearchConfigurationService} searchConfigService
|
* @param {SearchConfigurationService} searchConfigService
|
||||||
* @param {SearchFilterService} filterService
|
* @param {SearchFilterService} filterService
|
||||||
*/
|
*/
|
||||||
constructor(private searchService: SearchService, private searchConfigService: SearchConfigurationService, private filterService: SearchFilterService) {
|
constructor(
|
||||||
this.filters = searchService.getConfig().pipe(getSucceededRemoteData());
|
private searchService: SearchService,
|
||||||
this.clearParams = searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => {
|
private filterService: SearchFilterService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
|
||||||
|
this.filters = this.searchConfigService.searchOptions.pipe(
|
||||||
|
switchMap((options) => this.searchService.getConfig(options.scope, options.configuration).pipe(getSucceededRemoteData()))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.clearParams = this.searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => {
|
||||||
Object.keys(filters).forEach((f) => filters[f] = null);
|
Object.keys(filters).forEach((f) => filters[f] = null);
|
||||||
return filters;
|
return filters;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||||
*/
|
*/
|
||||||
getSearchLink() {
|
public getSearchLink(): string {
|
||||||
|
if (this.inPlaceSearch) {
|
||||||
|
return './';
|
||||||
|
}
|
||||||
return this.searchService.getSearchLink();
|
return this.searchService.getSearchLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,4 +78,5 @@ export class SearchFiltersComponent {
|
|||||||
trackUpdate(index, config: SearchFilterConfig) {
|
trackUpdate(index, config: SearchFilterConfig) {
|
||||||
return config ? config.name : undefined;
|
return config ? config.name : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,11 +2,11 @@
|
|||||||
<div class="labels col-sm-9 offset-sm-3">
|
<div class="labels col-sm-9 offset-sm-3">
|
||||||
<ng-container *ngFor="let key of ((appliedFilters | async) | dsObjectKeys)"><!--Do not remove this to prevent uneven spacing
|
<ng-container *ngFor="let key of ((appliedFilters | async) | dsObjectKeys)"><!--Do not remove this to prevent uneven spacing
|
||||||
--><a *ngFor="let values of (appliedFilters | async)[key]"
|
--><a *ngFor="let values of (appliedFilters | async)[key]"
|
||||||
class="badge badge-primary mr-1 mb-1"
|
class="badge badge-primary mr-1 mb-1 text-capitalize"
|
||||||
[routerLink]="getSearchLink()"
|
[routerLink]="getSearchLink()"
|
||||||
[queryParams]="(getRemoveParams(key, values) | async)" queryParamsHandling="merge">
|
[queryParams]="(getRemoveParams(key, values) | async)" queryParamsHandling="merge">
|
||||||
{{('search.filters.applied.' + key) | translate}}: {{values}}
|
{{('search.filters.applied.' + key) | translate}}: {{normalizeFilterValue(values)}}
|
||||||
<span> ×</span>
|
<span> ×</span>
|
||||||
</a><!--Do not remove this to prevent uneven spacing
|
</a><!--Do not remove this to prevent uneven spacing
|
||||||
--></ng-container>
|
--></ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -9,7 +9,8 @@ import { SearchServiceStub } from '../../shared/testing/search-service-stub';
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { ObjectKeysPipe } from '../../shared/utils/object-keys-pipe';
|
import { ObjectKeysPipe } from '../../shared/utils/object-keys-pipe';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service-stub';
|
||||||
|
|
||||||
describe('SearchLabelsComponent', () => {
|
describe('SearchLabelsComponent', () => {
|
||||||
let comp: SearchLabelsComponent;
|
let comp: SearchLabelsComponent;
|
||||||
@@ -20,8 +21,11 @@ describe('SearchLabelsComponent', () => {
|
|||||||
|
|
||||||
const field1 = 'author';
|
const field1 = 'author';
|
||||||
const field2 = 'subject';
|
const field2 = 'subject';
|
||||||
const value1 = 'TestAuthor';
|
const value1 = 'Test, Author';
|
||||||
|
const normValue1 = 'Test, Author';
|
||||||
const value2 = 'TestSubject';
|
const value2 = 'TestSubject';
|
||||||
|
const value3 = 'Test, Authority,authority';
|
||||||
|
const normValue3 = 'Test, Authority';
|
||||||
const filter1 = [field1, value1];
|
const filter1 = [field1, value1];
|
||||||
const filter2 = [field2, value2];
|
const filter2 = [field2, value2];
|
||||||
const mockFilters = [
|
const mockFilters = [
|
||||||
@@ -35,7 +39,8 @@ describe('SearchLabelsComponent', () => {
|
|||||||
declarations: [SearchLabelsComponent, ObjectKeysPipe],
|
declarations: [SearchLabelsComponent, ObjectKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) },
|
||||||
{ provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} }
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }
|
||||||
|
// { provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchLabelsComponent, {
|
}).overrideComponent(SearchLabelsComponent, {
|
||||||
@@ -65,4 +70,16 @@ describe('SearchLabelsComponent', () => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when normalizeFilterValue is called', () => {
|
||||||
|
it('should return properly filter value', () => {
|
||||||
|
let result: string;
|
||||||
|
|
||||||
|
result = comp.normalizeFilterValue(value1);
|
||||||
|
expect(result).toBe(normValue1);
|
||||||
|
|
||||||
|
result = comp.normalizeFilterValue(value3);
|
||||||
|
expect(result).toBe(normValue3);
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, Inject, Input } from '@angular/core';
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
import { SearchConfigurationService } from '../search-service/search-configuration.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-labels',
|
selector: 'ds-search-labels',
|
||||||
@@ -21,10 +22,17 @@ export class SearchLabelsComponent {
|
|||||||
*/
|
*/
|
||||||
appliedFilters: Observable<Params>;
|
appliedFilters: Observable<Params>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the search component should show results on the current page
|
||||||
|
*/
|
||||||
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the instance variable
|
* Initialize the instance variable
|
||||||
*/
|
*/
|
||||||
constructor(private searchService: SearchService, private searchConfigService: SearchConfigurationService) {
|
constructor(
|
||||||
|
private searchService: SearchService,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) {
|
||||||
this.appliedFilters = this.searchConfigService.getCurrentFrontendFilters();
|
this.appliedFilters = this.searchConfigService.getCurrentFrontendFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +56,25 @@ export class SearchLabelsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||||
*/
|
*/
|
||||||
getSearchLink() {
|
public getSearchLink(): string {
|
||||||
|
if (this.inPlaceSearch) {
|
||||||
|
return './';
|
||||||
|
}
|
||||||
return this.searchService.getSearchLink();
|
return this.searchService.getSearchLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved
|
||||||
|
* Strips authority operator from filter value
|
||||||
|
* e.g. 'test ,authority' => 'test'
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
normalizeFilterValue(value: string) {
|
||||||
|
// const pattern = /,[^,]*$/g;
|
||||||
|
const pattern = /,authority*$/g;
|
||||||
|
return value.replace(pattern, '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import { SetViewMode } from '../shared/view-mode';
|
|||||||
* 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;
|
||||||
view?: SetViewMode = SetViewMode.List;
|
view?: SetViewMode = SetViewMode.List;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
@@ -16,7 +17,8 @@ export class SearchOptions {
|
|||||||
filters?: any;
|
filters?: any;
|
||||||
fixedFilter?: any;
|
fixedFilter?: any;
|
||||||
|
|
||||||
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any}) {
|
constructor(options: {configuration?: string, scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any}) {
|
||||||
|
this.configuration = options.configuration;
|
||||||
this.scope = options.scope;
|
this.scope = options.scope;
|
||||||
this.query = options.query;
|
this.query = options.query;
|
||||||
this.dsoType = options.dsoType;
|
this.dsoType = options.dsoType;
|
||||||
@@ -31,6 +33,9 @@ export class SearchOptions {
|
|||||||
* @returns {string} URL with all search options and passed arguments as query parameters
|
* @returns {string} URL with all search options and passed arguments as query parameters
|
||||||
*/
|
*/
|
||||||
toRestUrl(url: string, args: string[] = []): string {
|
toRestUrl(url: string, args: string[] = []): string {
|
||||||
|
if (isNotEmpty(this.configuration)) {
|
||||||
|
args.push(`configuration=${this.configuration}`);
|
||||||
|
}
|
||||||
if (isNotEmpty(this.fixedFilter)) {
|
if (isNotEmpty(this.fixedFilter)) {
|
||||||
args.push(this.fixedFilter);
|
args.push(this.fixedFilter);
|
||||||
}
|
}
|
||||||
@@ -45,7 +50,10 @@ export class SearchOptions {
|
|||||||
}
|
}
|
||||||
if (isNotEmpty(this.filters)) {
|
if (isNotEmpty(this.filters)) {
|
||||||
this.filters.forEach((filter: SearchFilter) => {
|
this.filters.forEach((filter: SearchFilter) => {
|
||||||
filter.values.forEach((value) => args.push(`${filter.key}=${value},${filter.operator}`));
|
filter.values.forEach((value) => {
|
||||||
|
const filterValue = value.includes(',') ? `${value}` : `${value},${filter.operator}`;
|
||||||
|
args.push(`${filter.key}=${filterValue}`)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
|
@@ -2,15 +2,16 @@
|
|||||||
<div class="search-page row">
|
<div class="search-page row">
|
||||||
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-{{sideBarWidth}} sidebar-md-sticky"
|
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-{{sideBarWidth}} sidebar-md-sticky"
|
||||||
id="search-sidebar"
|
id="search-sidebar"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"></ds-search-sidebar>
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||||
<div class="col-12 col-md-{{12 - sideBarWidth}}">
|
<div class="col-12 col-md-{{12 - sideBarWidth}}">
|
||||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||||
[query]="(searchOptions$ | async)?.query"
|
[query]="(searchOptions$ | async)?.query"
|
||||||
[scope]="(searchOptions$ | async)?.scope"
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
[currentUrl]="getSearchLink()"
|
[currentUrl]="getSearchLink()"
|
||||||
[scopes]="(scopeListRD$ | async)">
|
[scopes]="(scopeListRD$ | async)"
|
||||||
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
<ds-search-labels *ngIf="searchEnabled"></ds-search-labels>
|
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div id="search-body"
|
<div id="search-body"
|
||||||
class="row-offcanvas row-offcanvas-left"
|
class="row-offcanvas row-offcanvas-left"
|
||||||
@@ -23,7 +24,7 @@
|
|||||||
</ds-search-sidebar>
|
</ds-search-sidebar>
|
||||||
<div id="search-content" class="col-12">
|
<div id="search-content" class="col-12">
|
||||||
<div class="d-block d-md-none search-controls clearfix">
|
<div class="d-block d-md-none search-controls clearfix">
|
||||||
<ds-view-mode-switch></ds-view-mode-switch>
|
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||||
|
@@ -20,11 +20,16 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
|||||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||||
import { RouteService } from '../shared/services/route.service';
|
import { RouteService } from '../shared/services/route.service';
|
||||||
|
import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service-stub';
|
||||||
|
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||||
|
import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
|
||||||
let comp: SearchPageComponent;
|
let comp: SearchPageComponent;
|
||||||
let fixture: ComponentFixture<SearchPageComponent>;
|
let fixture: ComponentFixture<SearchPageComponent>;
|
||||||
let searchServiceObject: SearchService;
|
let searchServiceObject: SearchService;
|
||||||
|
let searchConfigurationServiceObject: SearchConfigurationService;
|
||||||
const store: Store<SearchPageComponent> = jasmine.createSpyObj('store', {
|
const store: Store<SearchPageComponent> = jasmine.createSpyObj('store', {
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
dispatch: {},
|
dispatch: {},
|
||||||
@@ -42,17 +47,25 @@ const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
|||||||
getSearchLink: '/search',
|
getSearchLink: '/search',
|
||||||
getScopes: observableOf(['test-scope'])
|
getScopes: observableOf(['test-scope'])
|
||||||
});
|
});
|
||||||
|
const configurationParam = 'default';
|
||||||
const queryParam = 'test query';
|
const queryParam = 'test query';
|
||||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||||
const fixedFilter = 'fixed filter';
|
const fixedFilter = 'fixed filter';
|
||||||
const paginatedSearchOptions = {
|
const paginatedSearchOptions = new PaginatedSearchOptions({
|
||||||
|
configuration: configurationParam,
|
||||||
query: queryParam,
|
query: queryParam,
|
||||||
scope: scopeParam,
|
scope: scopeParam,
|
||||||
fixedFilter: fixedFilter,
|
fixedFilter: fixedFilter,
|
||||||
pagination,
|
pagination,
|
||||||
sort
|
sort
|
||||||
};
|
});
|
||||||
const activatedRouteStub = {
|
const activatedRouteStub = {
|
||||||
|
snapshot: {
|
||||||
|
queryParamMap: new Map([
|
||||||
|
['query', queryParam],
|
||||||
|
['scope', scopeParam]
|
||||||
|
])
|
||||||
|
},
|
||||||
queryParams: observableOf({
|
queryParams: observableOf({
|
||||||
query: queryParam,
|
query: queryParam,
|
||||||
scope: scopeParam
|
scope: scopeParam
|
||||||
@@ -67,8 +80,19 @@ const sidebarService = {
|
|||||||
const routeServiceStub = {
|
const routeServiceStub = {
|
||||||
getRouteParameterValue: () => {
|
getRouteParameterValue: () => {
|
||||||
return observableOf('');
|
return observableOf('');
|
||||||
|
},
|
||||||
|
getQueryParameterValue: () => {
|
||||||
|
return observableOf('')
|
||||||
|
},
|
||||||
|
getQueryParamsWithPrefix: () => {
|
||||||
|
return observableOf('')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const mockFixedFilterService: SearchFixedFilterService = {
|
||||||
|
getQueryByFilterName: (filter: string) => {
|
||||||
|
return observableOf(undefined)
|
||||||
|
}
|
||||||
|
} as SearchFixedFilterService;
|
||||||
|
|
||||||
export function configureSearchComponentTestingModule(compType) {
|
export function configureSearchComponentTestingModule(compType) {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -81,6 +105,7 @@ export function configureSearchComponentTestingModule(compType) {
|
|||||||
useValue: jasmine.createSpyObj('communityService', ['findById', 'findAll'])
|
useValue: jasmine.createSpyObj('communityService', ['findById', 'findAll'])
|
||||||
},
|
},
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
{
|
{
|
||||||
provide: Store, useValue: store
|
provide: Store, useValue: store
|
||||||
},
|
},
|
||||||
@@ -100,6 +125,10 @@ export function configureSearchComponentTestingModule(compType) {
|
|||||||
provide: SearchFilterService,
|
provide: SearchFilterService,
|
||||||
useValue: {}
|
useValue: {}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: SearchFixedFilterService,
|
||||||
|
useValue: mockFixedFilterService
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: SearchConfigurationService,
|
provide: SearchConfigurationService,
|
||||||
useValue: {
|
useValue: {
|
||||||
@@ -114,9 +143,9 @@ export function configureSearchComponentTestingModule(compType) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: RouteService,
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
useValue: routeServiceStub
|
useValue: new SearchConfigurationServiceStub()
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(compType, {
|
}).overrideComponent(compType, {
|
||||||
@@ -125,7 +154,6 @@ export function configureSearchComponentTestingModule(compType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('SearchPageComponent', () => {
|
describe('SearchPageComponent', () => {
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
configureSearchComponentTestingModule(SearchPageComponent);
|
configureSearchComponentTestingModule(SearchPageComponent);
|
||||||
}));
|
}));
|
||||||
@@ -135,25 +163,21 @@ describe('SearchPageComponent', () => {
|
|||||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
searchServiceObject = (comp as any).service;
|
searchServiceObject = (comp as any).service;
|
||||||
|
searchConfigurationServiceObject = (comp as any).searchConfigService;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
comp = null;
|
||||||
|
searchServiceObject = null;
|
||||||
|
searchConfigurationServiceObject = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get the scope and query from the route parameters', () => {
|
it('should get the scope and query from the route parameters', () => {
|
||||||
|
|
||||||
|
searchConfigurationServiceObject.paginatedSearchOptions.next(paginatedSearchOptions);
|
||||||
expect(comp.searchOptions$).toBeObservable(cold('b', {
|
expect(comp.searchOptions$).toBeObservable(cold('b', {
|
||||||
b: paginatedSearchOptions
|
b: paginatedSearchOptions
|
||||||
}));
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the closeSidebar event is emitted clicked in mobile view', () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(comp, 'closeSidebar');
|
|
||||||
const closeSidebarButton = fixture.debugElement.query(By.css('#search-sidebar-sm'));
|
|
||||||
closeSidebarButton.triggerEventHandler('toggleSidebar', null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trigger the closeSidebar function', () => {
|
|
||||||
expect(comp.closeSidebar).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
import { Observable , Subscription , BehaviorSubject } from 'rxjs';
|
import { Observable , Subscription , BehaviorSubject } from 'rxjs';
|
||||||
import { switchMap, } from 'rxjs/operators';
|
import { switchMap, } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
@@ -7,7 +7,6 @@ import { DSpaceObject } from '../core/shared/dspace-object.model';
|
|||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
|
||||||
import { SearchResult } from './search-result.model';
|
import { SearchResult } from './search-result.model';
|
||||||
import { SearchService } from './search-service/search.service';
|
import { SearchService } from './search-service/search.service';
|
||||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
@@ -15,13 +14,28 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
|
|||||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
import { RouteService } from '../shared/services/route.service';
|
import { RouteService } from '../shared/services/route.service';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
|
export const SEARCH_ROUTE = '/search';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders a simple item page.
|
||||||
|
* The route parameter 'id' is used to request the item it represents.
|
||||||
|
* All fields of the item that should be displayed, are defined in its template.
|
||||||
|
*/
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-page',
|
selector: 'ds-search-page',
|
||||||
styleUrls: ['./search-page.component.scss'],
|
styleUrls: ['./search-page.component.scss'],
|
||||||
templateUrl: './search-page.component.html',
|
templateUrl: './search-page.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [pushInOut]
|
animations: [pushInOut],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useClass: SearchConfigurationService
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +69,11 @@ export class SearchPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
sub: Subscription;
|
sub: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the search component should show results on the current page
|
||||||
|
*/
|
||||||
|
@Input() inPlaceSearch = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the search bar should be visible
|
* Whether or not the search bar should be visible
|
||||||
*/
|
*/
|
||||||
@@ -76,7 +95,7 @@ export class SearchPageComponent implements OnInit {
|
|||||||
constructor(protected service: SearchService,
|
constructor(protected service: SearchService,
|
||||||
protected sidebarService: SearchSidebarService,
|
protected sidebarService: SearchSidebarService,
|
||||||
protected windowService: HostWindowService,
|
protected windowService: HostWindowService,
|
||||||
protected searchConfigService: SearchConfigurationService,
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
protected routeService: RouteService) {
|
protected routeService: RouteService) {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
}
|
}
|
||||||
@@ -134,9 +153,12 @@ export class SearchPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||||
*/
|
*/
|
||||||
public getSearchLink(): string {
|
public getSearchLink(): string {
|
||||||
|
if (this.inPlaceSearch) {
|
||||||
|
return './';
|
||||||
|
}
|
||||||
return this.service.getSearchLink();
|
return this.service.getSearchLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,9 +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 { ItemSearchResultListElementComponent } from '../shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component';
|
|
||||||
import { CollectionSearchResultListElementComponent } from '../shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component';
|
|
||||||
import { CommunitySearchResultListElementComponent } from '../shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
|
|
||||||
import { ItemSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.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';
|
||||||
@@ -33,11 +30,39 @@ import { SearchConfigurationService } from './search-service/search-configuratio
|
|||||||
import { SearchFacetOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component';
|
import { SearchFacetOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component';
|
||||||
import { SearchFacetSelectedOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component';
|
import { SearchFacetSelectedOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component';
|
||||||
import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component';
|
import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component';
|
||||||
|
import { SearchSwitchConfigurationComponent } from './search-switch-configuration/search-switch-configuration.component';
|
||||||
|
import { SearchAuthorityFilterComponent } from './search-filters/search-filter/search-authority-filter/search-authority-filter.component';
|
||||||
|
|
||||||
const effects = [
|
const effects = [
|
||||||
SearchSidebarEffects
|
SearchSidebarEffects
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const components = [
|
||||||
|
SearchPageComponent,
|
||||||
|
SearchResultsComponent,
|
||||||
|
SearchSidebarComponent,
|
||||||
|
SearchSettingsComponent,
|
||||||
|
ItemSearchResultGridElementComponent,
|
||||||
|
CollectionSearchResultGridElementComponent,
|
||||||
|
CommunitySearchResultGridElementComponent,
|
||||||
|
SearchFiltersComponent,
|
||||||
|
SearchFilterComponent,
|
||||||
|
SearchFacetFilterComponent,
|
||||||
|
SearchLabelsComponent,
|
||||||
|
SearchFacetFilterComponent,
|
||||||
|
SearchFacetFilterWrapperComponent,
|
||||||
|
SearchRangeFilterComponent,
|
||||||
|
SearchTextFilterComponent,
|
||||||
|
SearchHierarchyFilterComponent,
|
||||||
|
SearchBooleanFilterComponent,
|
||||||
|
SearchFacetOptionComponent,
|
||||||
|
SearchFacetSelectedOptionComponent,
|
||||||
|
SearchFacetRangeOptionComponent,
|
||||||
|
SearchSwitchConfigurationComponent,
|
||||||
|
SearchAuthorityFilterComponent,
|
||||||
|
FilteredSearchPageComponent
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
SearchPageRoutingModule,
|
SearchPageRoutingModule,
|
||||||
@@ -46,29 +71,7 @@ const effects = [
|
|||||||
EffectsModule.forFeature(effects),
|
EffectsModule.forFeature(effects),
|
||||||
CoreModule.forRoot()
|
CoreModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: components,
|
||||||
SearchPageComponent,
|
|
||||||
FilteredSearchPageComponent,
|
|
||||||
SearchResultsComponent,
|
|
||||||
SearchSidebarComponent,
|
|
||||||
SearchSettingsComponent,
|
|
||||||
ItemSearchResultGridElementComponent,
|
|
||||||
CollectionSearchResultGridElementComponent,
|
|
||||||
CommunitySearchResultGridElementComponent,
|
|
||||||
SearchFiltersComponent,
|
|
||||||
SearchFilterComponent,
|
|
||||||
SearchFacetFilterComponent,
|
|
||||||
SearchLabelsComponent,
|
|
||||||
SearchFacetFilterComponent,
|
|
||||||
SearchFacetFilterWrapperComponent,
|
|
||||||
SearchRangeFilterComponent,
|
|
||||||
SearchTextFilterComponent,
|
|
||||||
SearchHierarchyFilterComponent,
|
|
||||||
SearchBooleanFilterComponent,
|
|
||||||
SearchFacetOptionComponent,
|
|
||||||
SearchFacetSelectedOptionComponent,
|
|
||||||
SearchFacetRangeOptionComponent
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
SearchSidebarService,
|
SearchSidebarService,
|
||||||
SearchFilterService,
|
SearchFilterService,
|
||||||
@@ -78,9 +81,6 @@ const effects = [
|
|||||||
SearchConfigurationService
|
SearchConfigurationService
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
ItemSearchResultListElementComponent,
|
|
||||||
CollectionSearchResultListElementComponent,
|
|
||||||
CommunitySearchResultListElementComponent,
|
|
||||||
ItemSearchResultGridElementComponent,
|
ItemSearchResultGridElementComponent,
|
||||||
CollectionSearchResultGridElementComponent,
|
CollectionSearchResultGridElementComponent,
|
||||||
CommunitySearchResultGridElementComponent,
|
CommunitySearchResultGridElementComponent,
|
||||||
@@ -91,11 +91,10 @@ const effects = [
|
|||||||
SearchBooleanFilterComponent,
|
SearchBooleanFilterComponent,
|
||||||
SearchFacetOptionComponent,
|
SearchFacetOptionComponent,
|
||||||
SearchFacetSelectedOptionComponent,
|
SearchFacetSelectedOptionComponent,
|
||||||
SearchFacetRangeOptionComponent
|
SearchFacetRangeOptionComponent,
|
||||||
|
SearchAuthorityFilterComponent
|
||||||
],
|
],
|
||||||
exports: [
|
exports: components
|
||||||
FilteredSearchPageComponent,
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -9,7 +9,7 @@ export class SearchResult<T extends DSpaceObject> implements ListableObject {
|
|||||||
/**
|
/**
|
||||||
* The DSpaceObject that was found
|
* The DSpaceObject that was found
|
||||||
*/
|
*/
|
||||||
dspaceObject: T;
|
indexableObject: T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadata that was used to find this item, hithighlighted
|
* The metadata that was used to find this item, hithighlighted
|
||||||
|
@@ -5,7 +5,13 @@ import { autoserialize, autoserializeAs } from 'cerialize';
|
|||||||
*/
|
*/
|
||||||
export class FacetValue {
|
export class FacetValue {
|
||||||
/**
|
/**
|
||||||
* The display value of the facet value
|
* The display label of the facet value
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the facet value
|
||||||
*/
|
*/
|
||||||
@autoserializeAs(String, 'label')
|
@autoserializeAs(String, 'label')
|
||||||
value: string;
|
value: string;
|
||||||
|
@@ -2,6 +2,11 @@
|
|||||||
* Enumeration containing all possible types for filters
|
* Enumeration containing all possible types for filters
|
||||||
*/
|
*/
|
||||||
export enum FilterType {
|
export enum FilterType {
|
||||||
|
/**
|
||||||
|
* Represents authority facets
|
||||||
|
*/
|
||||||
|
authority = 'authority',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents simple text facets
|
* Represents simple text facets
|
||||||
*/
|
*/
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user