Merge branch 'master' into w2p-61142_Relations-add-edit-delete-and-permissions

Conflicts:
	src/app/core/cache/object-cache.service.ts
	src/app/core/core.module.ts
	src/app/core/data/request.service.ts
	src/app/core/shared/item.model.ts
This commit is contained in:
Kristof De Langhe
2019-05-20 13:08:33 +02:00
404 changed files with 10985 additions and 1434 deletions

View File

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

View File

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

View File

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

View File

@@ -97,7 +97,7 @@
"uri": "URI", "uri": "URI",
"files": "Files", "files": "Files",
"collections": "Collections", "collections": "Collections",
"subject": "Keywords", "subject": "Keywords",
"citation": "Citation", "citation": "Citation",
"filesection": { "filesection": {
"download": "Download", "download": "Download",
@@ -306,20 +306,21 @@
}, },
"relationships": { "relationships": {
"isPublicationOf": "Publications", "isPublicationOf": "Publications",
"isProjectOf": "Projects", "isProjectOf": "Research Projects",
"isOrgUnitOf": "Org Units", "isOrgUnitOf": "Organizational Units",
"isAuthorOf": "Authors", "isAuthorOf": "Authors",
"isPersonOf": "Authors", "isPersonOf": "Authors",
"isJournalOf": "Journals", "isJournalOf": "Journals",
"isSingleJournalOf": "Journal", "isSingleJournalOf": "Journal",
"isVolumeOf": "Volumes", "isVolumeOf": "Journal Volumes",
"isSingleVolumeOf": "Volume", "isSingleVolumeOf": "Journal Volume",
"isIssueOf": "Issues", "isIssueOf": "Journal Issues",
"isJournalIssueOf": "Journal Issue", "isJournalIssueOf": "Journal Issue",
"isPublicationOfJournalIssue": "Articles" "isPublicationOfJournalIssue": "Articles"
}, },
"person": { "person": {
"page": { "page": {
"titleprefix": "Person: ",
"jobtitle": "Job Title", "jobtitle": "Job Title",
"lastname": "Last Name", "lastname": "Last Name",
"firstname": "First Name", "firstname": "First Name",
@@ -330,56 +331,85 @@
"link": { "link": {
"full": "Show all metadata" "full": "Show all metadata"
} }
},
"listelement": {
"badge": "Person"
} }
}, },
"project": { "project": {
"page": { "page": {
"titleprefix": "Research Project: ",
"status": "Status", "status": "Status",
"contributor": "Contributors",
"funder": "Funders",
"id": "ID", "id": "ID",
"expectedcompletion": "Expected Completion", "expectedcompletion": "Expected Completion",
"description": "Description", "description": "Description",
"keyword": "Keywords" "keyword": "Keywords"
},
"listelement": {
"badge": "Research Project"
} }
}, },
"orgunit": { "orgunit": {
"page": { "page": {
"titleprefix": "Organizational Unit: ",
"dateestablished": "Date established", "dateestablished": "Date established",
"city": "City", "city": "City",
"country": "Country", "country": "Country",
"id": "ID", "id": "ID",
"description": "Description" "description": "Description"
},
"listelement": {
"badge": "Organizational Unit"
} }
}, },
"journal": { "journal": {
"page": { "page": {
"titleprefix": "Journal: ",
"issn": "ISSN", "issn": "ISSN",
"publisher": "Publisher", "publisher": "Publisher",
"description": "Description", "description": "Description",
"editor": "Editor-in-Chief" "editor": "Editor-in-Chief"
},
"listelement": {
"badge": "Journal"
} }
}, },
"journalvolume": { "journalvolume": {
"page": { "page": {
"titleprefix": "Journal Volume: ",
"volume": "Volume", "volume": "Volume",
"issuedate": "Issue Date", "issuedate": "Issue Date",
"description": "Description" "description": "Description"
},
"listelement": {
"badge": "Journal Volume"
} }
}, },
"journalissue": { "journalissue": {
"page": { "page": {
"titleprefix": "Journal Issue: ",
"number": "Number", "number": "Number",
"issuedate": "Issue Date", "issuedate": "Issue Date",
"description": "Description", "description": "Description",
"keyword": "Keywords", "keyword": "Keywords",
"journal-title": "Journal Title", "journal-title": "Journal Title",
"journal-issn": "Journal ISSN" "journal-issn": "Journal ISSN"
},
"listelement": {
"badge": "Journal Issue"
} }
}, },
"publication": { "publication": {
"page": { "page": {
"titleprefix": "Publication: ",
"journal-title": "Journal Title", "journal-title": "Journal Title",
"journal-issn": "Journal ISSN", "journal-issn": "Journal ISSN",
"volume-title": "Volume Title" "volume-title": "Volume Title"
},
"listelement": {
"badge": "Publication"
} }
}, },
"nav": { "nav": {
@@ -394,6 +424,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"
}, },
@@ -430,6 +461,57 @@
"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": {
"journal": { "journal": {
"title": "DSpace Angular :: Journal Search", "title": "DSpace Angular :: Journal Search",
@@ -453,7 +535,8 @@
"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",
@@ -473,9 +556,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",
@@ -486,7 +573,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",
@@ -518,6 +609,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"
} }
} }
} }
@@ -739,6 +850,7 @@
"items": "Loading items...", "items": "Loading items...",
"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..."
}, },
@@ -930,6 +1042,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": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" width="125.348px" height="161.348px" viewBox="62.326 80.326 125.348 161.348"
enable-background="new 62.326 80.326 125.348 161.348" xml:space="preserve">
<rect x="62.5" y="80.5" fill="#FFFFFF" stroke="#000000" stroke-width="0.3478" stroke-miterlimit="10" width="125" height="161"/>
<path fill="#43515F" d="M135.5,171.5h-21c-2.899,0-5.25,2.352-5.25,5.25v21c0,2.898,2.351,5.25,5.25,5.25h21
c2.898,0,5.25-2.352,5.25-5.25v-21C140.75,173.852,138.398,171.5,135.5,171.5z M104,124.25c0-2.899-2.351-5.25-5.25-5.25h-21
c-2.899,0-5.25,2.351-5.25,5.25v21c0,2.899,2.351,5.25,5.25,5.25h15.704l12.002,21.007c1.822-3.127,5.171-5.257,9.043-5.257h0.046
L104,147.794V140h36.75v-10.5H104V124.25z M172.25,119h-21c-2.898,0-5.25,2.351-5.25,5.25v21c0,2.899,2.352,5.25,5.25,5.25h21
c2.898,0,5.25-2.351,5.25-5.25v-21C177.5,121.351,175.148,119,172.25,119z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" width="125.348px" height="161.348px" viewBox="62.674 80.674 125.348 161.348"
enable-background="new 62.674 80.674 125.348 161.348" xml:space="preserve">
<rect x="62.847" y="80.847" fill="#FFFFFF" stroke="#000000" stroke-width="0.3478" stroke-miterlimit="10" width="125" height="161"/>
<path fill="#43515F" d="M125.347,167.91c16.304,0,29.531-13.228,29.531-29.531c0-16.303-13.228-29.531-29.531-29.531
c-16.303,0-29.531,13.227-29.531,29.531S109.044,167.91,125.347,167.91z M151.597,174.472h-11.301
c-4.552,2.092-9.617,3.281-14.95,3.281c-5.332,0-10.377-1.189-14.95-3.281h-11.3c-14.499,0-26.25,11.751-26.25,26.25v3.281
c0,5.435,4.409,9.844,9.844,9.844h85.312c5.435,0,9.844-4.408,9.844-9.844v-3.281C177.847,186.223,166.096,174.472,151.597,174.472z
"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="125.5px" height="161.5px" viewBox="62.25 80.25 125.5 161.5" enable-background="new 62.25 80.25 125.5 161.5"
xml:space="preserve">
<rect x="62.5" y="80.5" fill="#FFFFFF" stroke="#000000" stroke-width="0.5" stroke-miterlimit="10" width="125" height="161"/>
<g>
<path fill="#43515F" d="M176.007,108.235h-65.078c-0.972,0-1.759,0.788-1.759,1.759v7.035h-7.035c-0.972,0-1.759,0.788-1.759,1.759
v9.246c-5.482,3.937-8.794,10.281-8.794,17.136c0,3.284,0.808,6.52,2.278,9.449c-0.313,0.902-0.52,1.855-0.52,2.863
c0,0.32,0.062,0.625,0.095,0.936l-9.667,9.666c-0.323-0.029-0.65-0.049-0.981-0.049c-5.82,0-10.553,4.733-10.553,10.553
c0,3.115,1.364,5.91,3.517,7.844v25.574c0,0.971,0.787,1.759,1.759,1.759h10.553c0.973,0,1.759-0.788,1.759-1.759v-25.574
c2.153-1.932,3.518-4.727,3.518-7.844c0-1.349-0.264-2.635-0.727-3.82l7.762-7.764v45.002c0,0.971,0.786,1.759,1.759,1.759h65.077
c0.973,0,1.759-0.787,1.759-1.759v-5.275h7.036c0.972,0,1.759-0.789,1.759-1.76v-94.978
C177.765,109.023,176.979,108.235,176.007,108.235z M102.965,130.539c0.044-0.022,0.092-0.037,0.134-0.063l0.193-0.129l3.835,3.834
c-4.083,2.049-6.752,6.234-6.752,10.99c0,0.586,0.048,1.169,0.132,1.749c0.029,0.197,0.083,0.387,0.122,0.583
c0.074,0.376,0.148,0.752,0.257,1.12c0.014,0.051,0.02,0.104,0.035,0.153c-0.103,0.014-0.201,0.049-0.302,0.067
c-0.204,0.035-0.401,0.084-0.6,0.134c-0.3,0.075-0.592,0.167-0.881,0.274c-0.219,0.082-0.437,0.157-0.646,0.253
c-0.256,0.118-0.496,0.261-0.739,0.401c-0.371,0.214-0.718,0.454-1.052,0.719c-0.192,0.153-0.393,0.292-0.57,0.461
c-0.681-1.891-1.031-3.896-1.031-5.914C95.1,139.241,98.084,133.785,102.965,130.539z M106.321,154.306
c0.119,0.157,0.227,0.318,0.329,0.487c0.063,0.107,0.122,0.217,0.177,0.327c0.08,0.156,0.155,0.313,0.218,0.477
c0.116,0.304,0.208,0.611,0.268,0.914c0.06,0.317,0.099,0.639,0.099,0.971c0,0.406-0.058,0.803-0.148,1.191
c-0.028,0.118-0.072,0.227-0.105,0.341c-0.081,0.265-0.176,0.521-0.297,0.77c-0.06,0.122-0.126,0.242-0.197,0.362
c-0.135,0.229-0.291,0.448-0.459,0.653c-0.081,0.099-0.157,0.202-0.243,0.296c-0.266,0.281-0.556,0.544-0.883,0.767
c-0.843,0.566-1.855,0.898-2.944,0.898c-0.397,0-0.781-0.052-1.152-0.136c-0.009,0-0.018-0.007-0.026-0.01
c-0.115-0.027-0.222-0.069-0.333-0.102c-0.132-0.041-0.266-0.073-0.392-0.121c-0.164-0.064-0.318-0.145-0.473-0.225
c-0.103-0.052-0.202-0.105-0.301-0.163c-0.153-0.091-0.305-0.187-0.447-0.292c-0.075-0.056-0.146-0.119-0.218-0.181
c-0.259-0.215-0.496-0.45-0.708-0.709c-0.06-0.072-0.123-0.141-0.177-0.216c-0.105-0.143-0.203-0.294-0.294-0.449
c-0.059-0.098-0.111-0.198-0.164-0.301c-0.079-0.154-0.158-0.31-0.222-0.473c-0.056-0.146-0.097-0.299-0.141-0.45
c-0.026-0.094-0.063-0.181-0.084-0.276c-0.001-0.007-0.005-0.014-0.007-0.021c-0.084-0.369-0.137-0.754-0.137-1.152
c0-0.776,0.178-1.507,0.48-2.172c0.005-0.012,0.016-0.021,0.021-0.033c0.193-0.417,0.442-0.793,0.726-1.138
c0.049-0.058,0.095-0.119,0.146-0.176c0.127-0.142,0.264-0.274,0.404-0.401c0.077-0.069,0.158-0.134,0.241-0.198
c0.141-0.113,0.28-0.226,0.431-0.322c0.214-0.137,0.438-0.261,0.672-0.364c0.118-0.052,0.241-0.091,0.362-0.137
c0.178-0.065,0.359-0.124,0.544-0.169c0.107-0.026,0.214-0.052,0.323-0.072c0.303-0.053,0.608-0.093,0.925-0.093
c0.365,0,0.739,0.044,1.147,0.135h0.001h0.002c0.533,0.118,1.025,0.329,1.481,0.593c0.063,0.037,0.132,0.065,0.191,0.104
c0.179,0.115,0.343,0.248,0.506,0.383c0.078,0.063,0.156,0.122,0.229,0.19c0.149,0.139,0.287,0.29,0.421,0.443
C106.186,154.139,106.256,154.22,106.321,154.306z M110.928,157.357c0.58,0.083,1.168,0.125,1.76,0.125
c4.754,0,8.938-2.667,10.988-6.752l3.863,3.862c-3.22,5.069-8.797,8.166-14.851,8.166c-1.101,0-2.2-0.107-3.289-0.32
c0.009-0.012,0.014-0.026,0.023-0.038c0.007-0.011,0.012-0.024,0.021-0.035c0.06-0.09,0.105-0.19,0.164-0.281
c0.091-0.15,0.179-0.3,0.262-0.455c0.102-0.187,0.198-0.377,0.285-0.572c0.049-0.111,0.095-0.224,0.14-0.337
c0.052-0.131,0.104-0.263,0.15-0.399c0.088-0.255,0.152-0.517,0.216-0.781c0.032-0.13,0.074-0.253,0.099-0.385
c0.014-0.074,0.028-0.146,0.041-0.22c0.081-0.479,0.13-0.962,0.13-1.453C110.929,157.44,110.929,157.398,110.928,157.357z
M86.306,210.247h-7.035v-21.722c0.545,0.194,1.11,0.346,1.692,0.447c0.035,0.006,0.069,0.011,0.102,0.016
c0.563,0.094,1.136,0.153,1.724,0.153c0.587,0,1.161-0.06,1.722-0.153c0.035-0.006,0.069-0.009,0.103-0.016
c0.583-0.102,1.148-0.253,1.693-0.447V210.247z M86.978,184.207c-0.003,0.002-0.007,0.005-0.011,0.006
c-0.287,0.215-0.589,0.398-0.898,0.562c-0.039,0.021-0.078,0.044-0.118,0.063c-0.301,0.151-0.608,0.28-0.924,0.387
c-0.055,0.018-0.109,0.033-0.163,0.051c-1.353,0.421-2.8,0.421-4.153,0c-0.054-0.018-0.109-0.033-0.164-0.051
c-0.315-0.106-0.623-0.234-0.923-0.387c-0.041-0.021-0.079-0.043-0.118-0.063c-0.31-0.163-0.61-0.349-0.898-0.562
c-0.003-0.001-0.007-0.004-0.011-0.006c-1.715-1.283-2.844-3.314-2.844-5.619c0-3.879,3.155-7.034,7.036-7.034
c0.542,0,1.064,0.075,1.573,0.192c0.012,0.003,0.024,0.01,0.037,0.014c0.48,0.111,0.943,0.275,1.382,0.486
c0.004,0.002,0.007,0.004,0.011,0.006c1.319,0.632,2.427,1.678,3.148,3c0.003,0.006,0.009,0.01,0.012,0.015
c0.539,0.995,0.873,2.115,0.873,3.322C89.823,180.893,88.694,182.922,86.978,184.207z M90.738,171.67
c-0.875-1.003-1.932-1.84-3.124-2.456l7.077-7.079c0.009,0.016,0.024,0.027,0.034,0.042c0.695,1.095,1.62,2.018,2.714,2.714
c0.016,0.011,0.027,0.025,0.042,0.033L90.738,171.67z M165.453,210.247h-61.559v-44.148c0.002,0,0.003-0.002,0.005-0.002
c0.031-0.007,0.06-0.021,0.092-0.027c0.506-0.11,0.996-0.265,1.465-0.456c0.167-0.068,0.32-0.16,0.481-0.239
c0.101-0.05,0.201-0.097,0.299-0.147c2.11,0.684,4.275,1.05,6.451,1.05c7.784,0,14.913-4.267,18.602-11.138
c0.367-0.684,0.244-1.526-0.306-2.075l-6.775-6.775c-0.452-0.452-1.111-0.625-1.729-0.447c-0.616,0.178-1.085,0.674-1.228,1.296
c-0.921,4.021-4.442,6.827-8.563,6.827c-0.939,0-1.852-0.187-2.737-0.479c-0.07-0.135-0.161-0.257-0.239-0.389
c-0.144-0.248-0.283-0.495-0.45-0.725c-0.113-0.155-0.245-0.296-0.366-0.443c-0.348-0.424-0.731-0.811-1.154-1.163
c-0.168-0.143-0.331-0.293-0.512-0.422c-0.218-0.157-0.45-0.287-0.683-0.422c-0.218-0.128-0.436-0.251-0.664-0.362
c-0.236-0.113-0.476-0.209-0.721-0.301c-0.099-0.036-0.188-0.088-0.289-0.119c-0.05-0.095-0.102-0.188-0.147-0.287
c-0.063-0.135-0.123-0.272-0.18-0.41c-0.088-0.216-0.165-0.438-0.235-0.659c-0.042-0.134-0.086-0.268-0.122-0.403
c-0.065-0.244-0.113-0.494-0.155-0.744c-0.02-0.118-0.048-0.233-0.063-0.352c-0.049-0.371-0.078-0.742-0.078-1.115
c0-4.121,2.807-7.644,6.825-8.563c0.622-0.143,1.12-0.614,1.296-1.228c0.176-0.614,0.007-1.277-0.446-1.729l-6.775-6.775
c-0.132-0.132-0.283-0.237-0.443-0.318c-0.053-0.026-0.11-0.037-0.166-0.06c-0.097-0.037-0.19-0.084-0.292-0.104v-5.846h7.036
h54.523v89.7H165.453z M174.247,203.212h-5.276v-84.424c0-0.971-0.786-1.759-1.76-1.759h-54.523v-5.277h61.559V203.212
L174.247,203.212z"/>
<path fill="#43515F" d="M144.347,196.177c0-0.97-0.786-1.759-1.759-1.759h-5.277v-7.035c0-0.971-0.785-1.759-1.759-1.759h-5.276
v-12.312c0-0.972-0.786-1.76-1.759-1.76h-7.035c-0.973,0-1.759,0.788-1.759,1.76v8.793h-5.276c-0.973,0-1.759,0.788-1.759,1.76
v17.589h-3.518v3.517h38.695v-3.517h-3.518V196.177L144.347,196.177z M116.206,185.624h3.518v15.829h-3.518V185.624z
M123.241,183.864v-8.794h3.518v12.312v14.07h-3.518V183.864z M130.276,189.141h3.519v7.036v5.276h-3.519V189.141z
M137.312,201.453v-3.518h3.519v3.518H137.312z"/>
<path fill="#43515F" d="M161.844,169.476c0.008-0.064,0.021-0.128,0.029-0.193c0.042-0.411,0.062-0.829,0.062-1.247
c0-6.789-5.523-12.312-12.312-12.312c-6.787,0-12.312,5.523-12.312,12.312c0,0.418,0.021,0.836,0.063,1.247
c0.007,0.065,0.021,0.129,0.028,0.193c0.04,0.349,0.089,0.693,0.158,1.034c0.003,0.016,0.009,0.031,0.012,0.05
c0.481,2.325,1.625,4.464,3.346,6.182c0.009,0.009,0.02,0.011,0.029,0.02c2.227,2.215,5.294,3.587,8.675,3.587
s6.448-1.372,8.675-3.587c0.01-0.009,0.021-0.011,0.03-0.02c1.719-1.719,2.862-3.856,3.346-6.182
c0.003-0.018,0.009-0.034,0.011-0.05C161.754,170.169,161.804,169.824,161.844,169.476z M158.256,169.635
c-0.024,0.137-0.06,0.271-0.09,0.406c-0.097,0.414-0.221,0.817-0.376,1.206c-0.038,0.1-0.072,0.202-0.115,0.301
c-0.204,0.465-0.446,0.911-0.724,1.329l-2.565-2.566c0.017-0.037,0.023-0.08,0.042-0.119c0.094-0.209,0.159-0.43,0.227-0.654
c0.038-0.13,0.092-0.253,0.119-0.387c0.081-0.36,0.127-0.731,0.127-1.115c0-0.332-0.039-0.654-0.099-0.969
c-0.018-0.099-0.046-0.191-0.071-0.288c-0.052-0.216-0.116-0.429-0.196-0.634c-0.038-0.1-0.077-0.197-0.123-0.293
c-0.095-0.206-0.204-0.404-0.323-0.595c-0.046-0.073-0.084-0.149-0.134-0.22c-0.177-0.254-0.37-0.49-0.586-0.709
c-0.036-0.039-0.081-0.069-0.12-0.105c-0.184-0.176-0.38-0.34-0.589-0.488c-0.083-0.057-0.169-0.108-0.255-0.163
c-0.182-0.114-0.369-0.216-0.566-0.307c-0.095-0.045-0.188-0.089-0.287-0.129c-0.058-0.023-0.111-0.055-0.17-0.075v-3.641
c0.001,0,0.003,0,0.005,0c0.032,0.007,0.06,0.021,0.092,0.028c0.507,0.109,0.995,0.264,1.465,0.456
c0.168,0.068,0.32,0.16,0.482,0.239c0.321,0.156,0.64,0.318,0.939,0.51c0.174,0.113,0.334,0.239,0.499,0.365
c0.262,0.197,0.517,0.401,0.757,0.624c0.155,0.146,0.302,0.301,0.448,0.459c0.218,0.236,0.422,0.484,0.612,0.742
c0.126,0.173,0.251,0.347,0.366,0.53c0.177,0.278,0.33,0.571,0.473,0.868c0.09,0.187,0.186,0.369,0.263,0.563
c0.134,0.338,0.235,0.689,0.325,1.047c0.046,0.173,0.105,0.341,0.141,0.521c0.105,0.535,0.17,1.094,0.17,1.665
C158.418,168.582,158.353,169.113,158.256,169.635z M148.381,169.279L148.381,169.279c-0.166-0.164-0.291-0.355-0.379-0.562
c-0.088-0.21-0.137-0.44-0.137-0.682c0-0.969,0.788-1.758,1.759-1.758s1.759,0.789,1.759,1.758c0,0.241-0.049,0.472-0.137,0.682
c-0.088,0.207-0.213,0.398-0.378,0.562l0,0c-0.318,0.318-0.759,0.516-1.243,0.516C149.14,169.795,148.698,169.597,148.381,169.279z
M140.998,166.364c0.035-0.179,0.097-0.347,0.141-0.521c0.092-0.355,0.191-0.707,0.325-1.045c0.077-0.193,0.175-0.377,0.265-0.564
c0.144-0.297,0.297-0.592,0.47-0.869c0.114-0.18,0.239-0.355,0.368-0.529c0.192-0.259,0.395-0.506,0.612-0.742
c0.146-0.156,0.292-0.312,0.448-0.459c0.237-0.223,0.494-0.427,0.757-0.623c0.165-0.123,0.325-0.251,0.499-0.364
c0.299-0.192,0.615-0.354,0.938-0.51c0.162-0.079,0.315-0.171,0.482-0.24c0.469-0.193,0.958-0.346,1.465-0.456
c0.031-0.007,0.06-0.021,0.092-0.028c0.001,0,0.003,0,0.005,0v3.641c-0.06,0.021-0.112,0.053-0.171,0.076
c-0.098,0.038-0.191,0.082-0.286,0.126c-0.198,0.093-0.386,0.193-0.566,0.308c-0.087,0.055-0.173,0.105-0.255,0.164
c-0.209,0.147-0.405,0.311-0.59,0.487c-0.038,0.036-0.082,0.067-0.119,0.105c-0.217,0.218-0.409,0.456-0.586,0.709
c-0.05,0.071-0.088,0.146-0.134,0.22c-0.12,0.191-0.229,0.388-0.323,0.596c-0.044,0.097-0.084,0.193-0.121,0.292
c-0.079,0.206-0.145,0.417-0.198,0.636c-0.022,0.096-0.051,0.189-0.069,0.288c-0.061,0.317-0.099,0.64-0.099,0.972
c0,0.385,0.046,0.756,0.123,1.115c0.028,0.134,0.082,0.259,0.121,0.389c0.067,0.222,0.134,0.443,0.227,0.652
c0.018,0.039,0.023,0.082,0.043,0.119l-2.567,2.566c-0.277-0.418-0.519-0.863-0.722-1.329c-0.043-0.099-0.075-0.2-0.116-0.301
c-0.153-0.389-0.278-0.793-0.377-1.206c-0.031-0.136-0.064-0.27-0.089-0.406c-0.095-0.521-0.16-1.053-0.16-1.6
C140.83,167.464,140.893,166.906,140.998,166.364z M144.781,175.364l2.58-2.582c0.689,0.329,1.449,0.53,2.262,0.53
s1.572-0.2,2.261-0.53l2.58,2.582c-1.39,0.923-3.052,1.466-4.841,1.466C147.835,176.83,146.173,176.286,144.781,175.364z"/>
<path fill="#43515F" d="M156.658,124.064c-2.908,0-5.276,2.367-5.276,5.276c0,0.219,0.039,0.426,0.065,0.639l-8.724,4.362
c-0.949-0.913-2.233-1.483-3.653-1.483c-1.921,0-3.588,1.043-4.512,2.581l-7.875-1.576c-0.369-2.55-2.549-4.521-5.201-4.521
c-2.909,0-5.276,2.367-5.276,5.276s2.367,5.277,5.276,5.277c1.921,0,3.588-1.043,4.511-2.581l7.876,1.576
c0.369,2.55,2.549,4.521,5.201,4.521c2.908,0,5.276-2.367,5.276-5.276c0-0.218-0.038-0.427-0.064-0.64l8.724-4.362
c0.949,0.914,2.233,1.484,3.652,1.484c2.909,0,5.277-2.368,5.277-5.277S159.568,124.064,156.658,124.064z M121.482,136.376
c-0.971,0-1.759-0.79-1.759-1.759c0-0.969,0.788-1.759,1.759-1.759s1.759,0.79,1.759,1.759S122.453,136.376,121.482,136.376z
M139.071,139.894c-0.972,0-1.76-0.79-1.76-1.759c0-0.969,0.788-1.759,1.76-1.759s1.759,0.79,1.759,1.759
C140.83,139.104,140.042,139.894,139.071,139.894z M156.658,131.1c-0.971,0-1.758-0.788-1.758-1.759l0,0
c0-0.969,0.787-1.759,1.758-1.759s1.76,0.79,1.76,1.759S157.63,131.1,156.658,131.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

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

View File

@@ -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 */
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {
/** /**

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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';
@@ -44,9 +42,7 @@ import { RelatedEntitiesSearchComponent } from './simple/related-entities/relate
declarations: [ declarations: [
ItemPageComponent, ItemPageComponent,
FullItemPageComponent, FullItemPageComponent,
MetadataValuesComponent,
MetadataUriValuesComponent, MetadataUriValuesComponent,
MetadataFieldWrapperComponent,
ItemPageAuthorFieldComponent, ItemPageAuthorFieldComponent,
ItemPageDateFieldComponent, ItemPageDateFieldComponent,
ItemPageAbstractFieldComponent, ItemPageAbstractFieldComponent,

View File

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

View File

@@ -1,3 +1,6 @@
<h2 class="item-page-title-field"> <h2 class="item-page-title-field">
<div *ngIf="item.firstMetadataValue('relationship.type') as type">
{{ type.toLowerCase() + '.page.titleprefix' | translate }}
</div>
<ds-metadata-values [mdValues]="item?.allMetadata(fields)"></ds-metadata-values> <ds-metadata-values [mdValues]="item?.allMetadata(fields)"></ds-metadata-values>
</h2> </h2>

View File

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

View File

@@ -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,9 +14,8 @@ 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 * as viewMode from '../../shared/view-mode'; import { redirectToPageNotFoundOn404 } from '../../core/shared/operators';
import { ItemViewMode } from '../../shared/items/item-type-decorator';
export const VIEW_MODE_FULL = 'full';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
@@ -41,28 +41,23 @@ 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
*/ */
viewMode = VIEW_MODE_FULL; viewMode = ItemViewMode.Full;
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()),);
} }
} }

View File

@@ -1,5 +1,5 @@
<h2 class="item-page-title-field"> <h2 class="item-page-title-field">
<ds-metadata-values [mdValues]="item?.allMetadata(['dc.title'])"></ds-metadata-values> {{'journalissue.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="item?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2> </h2>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">

View File

@@ -2,13 +2,13 @@ import { Component, Inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ItemDataService } from '../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../core/data/item-data.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { isNotEmpty } from '../../../../shared/empty.util'; import { isNotEmpty } from '../../../../shared/empty.util';
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; import { ItemComponent } from '../shared/item.component';
import { VIEW_MODE_FULL } from '../../item-page.component'; import { filterRelationsByTypeLabel, relationsToItems } from '../shared/item-relationships-utils';
@rendersItemType('JournalIssue', VIEW_MODE_FULL) @rendersItemType('JournalIssue', ItemViewMode.Full)
@Component({ @Component({
selector: 'ds-journal-issue', selector: 'ds-journal-issue',
styleUrls: ['./journal-issue.component.scss'], styleUrls: ['./journal-issue.component.scss'],

View File

@@ -1,5 +1,5 @@
<h2 class="item-page-title-field"> <h2 class="item-page-title-field">
<ds-metadata-values [mdValues]="item?.allMetadata(['dc.title'])"></ds-metadata-values> {{'journalvolume.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="item?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2> </h2>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">

View File

@@ -2,13 +2,13 @@ import { Component, Inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ItemDataService } from '../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../core/data/item-data.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { isNotEmpty } from '../../../../shared/empty.util'; import { isNotEmpty } from '../../../../shared/empty.util';
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; import { ItemComponent } from '../shared/item.component';
import { VIEW_MODE_FULL } from '../../item-page.component'; import { filterRelationsByTypeLabel, relationsToItems } from '../shared/item-relationships-utils';
@rendersItemType('JournalVolume', VIEW_MODE_FULL) @rendersItemType('JournalVolume', ItemViewMode.Full)
@Component({ @Component({
selector: 'ds-journal-volume', selector: 'ds-journal-volume',
styleUrls: ['./journal-volume.component.scss'], styleUrls: ['./journal-volume.component.scss'],

View File

@@ -1,5 +1,5 @@
<h2 class="item-page-title-field"> <h2 class="item-page-title-field">
<ds-metadata-values [mdValues]="item?.allMetadata(['dc.title'])"></ds-metadata-values> {{'journal.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="item?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2> </h2>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">

View File

@@ -2,13 +2,13 @@ import { Component, Inject } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ItemDataService } from '../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../core/data/item-data.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { isNotEmpty } from '../../../../shared/empty.util'; import { isNotEmpty } from '../../../../shared/empty.util';
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; import { ItemComponent } from '../shared/item.component';
import { VIEW_MODE_FULL } from '../../item-page.component'; import { filterRelationsByTypeLabel, relationsToItems } from '../shared/item-relationships-utils';
@rendersItemType('Journal', VIEW_MODE_FULL) @rendersItemType('Journal', ItemViewMode.Full)
@Component({ @Component({
selector: 'ds-journal', selector: 'ds-journal',
styleUrls: ['./journal.component.scss'], styleUrls: ['./journal.component.scss'],

View File

@@ -1,10 +1,10 @@
<h2 class="item-page-title-field"> <h2 class="item-page-title-field">
<ds-metadata-values [mdValues]="item?.allMetadata(['orgunit.identifier.name'])"></ds-metadata-values> {{'orgunit.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="item?.allMetadata(['orgunit.identifier.name'])"></ds-metadata-values>
</h2> </h2>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper> <ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.jpg'"></ds-thumbnail> <ds-thumbnail [thumbnail]="this.item.getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"
[fields]="['orgunit.identifier.dateestablished']" [fields]="['orgunit.identifier.dateestablished']"

View File

@@ -2,13 +2,13 @@ import { Component, Inject, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ItemDataService } from '../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../core/data/item-data.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { isNotEmpty } from '../../../../shared/empty.util'; import { isNotEmpty } from '../../../../shared/empty.util';
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; import { ItemComponent } from '../shared/item.component';
import { VIEW_MODE_FULL } from '../../item-page.component'; import { filterRelationsByTypeLabel, relationsToItems } from '../shared/item-relationships-utils';
@rendersItemType('OrgUnit', VIEW_MODE_FULL) @rendersItemType('OrgUnit', ItemViewMode.Full)
@Component({ @Component({
selector: 'ds-orgunit', selector: 'ds-orgunit',
styleUrls: ['./orgunit.component.scss'], styleUrls: ['./orgunit.component.scss'],

View File

@@ -1,10 +1,10 @@
<h2 class="item-page-title-field"> <h2 class="item-page-title-field">
<ds-metadata-values [mdValues]="item?.allMetadata(['dc.contributor.author'])"></ds-metadata-values> {{'person.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="item?.allMetadata(['dc.contributor.author'])"></ds-metadata-values>
</h2> </h2>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper> <ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async" [defaultImage]="'assets/images/person-placeholder.png'"></ds-thumbnail> <ds-thumbnail [thumbnail]="this.item.getThumbnail() | async" [defaultImage]="'assets/images/person-placeholder.svg'"></ds-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"
[fields]="['person.identifier.email']" [fields]="['person.identifier.email']"

View File

@@ -2,14 +2,14 @@ import { Component, Inject } from '@angular/core';
import { Observable , of as observableOf } from 'rxjs'; import { Observable , of as observableOf } from 'rxjs';
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 { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service'; import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
import { isNotEmpty } from '../../../../shared/empty.util'; import { isNotEmpty } from '../../../../shared/empty.util';
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; import { ItemComponent } from '../shared/item.component';
import { VIEW_MODE_FULL } from '../../item-page.component'; import { filterRelationsByTypeLabel, relationsToItems } from '../shared/item-relationships-utils';
@rendersItemType('Person', VIEW_MODE_FULL) @rendersItemType('Person', ItemViewMode.Full)
@Component({ @Component({
selector: 'ds-person', selector: 'ds-person',
styleUrls: ['./person.component.scss'], styleUrls: ['./person.component.scss'],

View File

@@ -1,15 +1,23 @@
<h2 class="item-page-title-field"> <h2 class="item-page-title-field">
<ds-metadata-values [mdValues]="item?.allMetadata(['project.identifier.name'])"></ds-metadata-values> {{'project.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="item?.allMetadata(['project.identifier.name'])"></ds-metadata-values>
</h2> </h2>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper> <ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async" [defaultImage]="'assets/images/project-placeholder.png'"></ds-thumbnail> <ds-thumbnail [thumbnail]="this.item.getThumbnail() | async" [defaultImage]="'assets/images/project-placeholder.svg'"></ds-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"
[fields]="['project.identifier.status']" [fields]="['project.identifier.status']"
[label]="'project.page.status'"> [label]="'project.page.status'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<ds-metadata-representation-list
[label]="'project.page.contributor' | translate"
[representations]="contributors$ | async">
</ds-metadata-representation-list>
<ds-generic-item-page-field [item]="item"
[fields]="['project.identifier.funder']"
[label]="'project.page.funder'">
</ds-generic-item-page-field>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"
[fields]="['project.identifier.id']" [fields]="['project.identifier.id']"
[label]="'project.page.id'"> [label]="'project.page.id'">

View File

@@ -2,13 +2,14 @@ import { Component, Inject, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ItemDataService } from '../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../core/data/item-data.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { rendersItemType } from '../../../../shared/items/item-type-decorator'; import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { isNotEmpty } from '../../../../shared/empty.util'; import { isNotEmpty } from '../../../../shared/empty.util';
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; import { ItemComponent } from '../shared/item.component';
import { VIEW_MODE_FULL } from '../../item-page.component'; import { filterRelationsByTypeLabel, relationsToItems } from '../shared/item-relationships-utils';
@rendersItemType('Project', VIEW_MODE_FULL) @rendersItemType('Project', ItemViewMode.Full)
@Component({ @Component({
selector: 'ds-project', selector: 'ds-project',
styleUrls: ['./project.component.scss'], styleUrls: ['./project.component.scss'],
@@ -18,6 +19,11 @@ import { VIEW_MODE_FULL } from '../../item-page.component';
* The component for displaying metadata and relations of an item of the type Project * The component for displaying metadata and relations of an item of the type Project
*/ */
export class ProjectComponent extends ItemComponent implements OnInit { export class ProjectComponent extends ItemComponent implements OnInit {
/**
* The contributors related to this project
*/
contributors$: Observable<MetadataRepresentation[]>;
/** /**
* The people related to this project * The people related to this project
*/ */
@@ -44,6 +50,8 @@ export class ProjectComponent extends ItemComponent implements OnInit {
super.ngOnInit(); super.ngOnInit();
if (isNotEmpty(this.resolvedRelsAndTypes$)) { if (isNotEmpty(this.resolvedRelsAndTypes$)) {
this.contributors$ = this.buildRepresentations('OrgUnit', 'project.contributor.other', this.ids);
this.people$ = this.resolvedRelsAndTypes$.pipe( this.people$ = this.resolvedRelsAndTypes$.pipe(
filterRelationsByTypeLabel('isPersonOfProject'), filterRelationsByTypeLabel('isPersonOfProject'),
relationsToItems(this.item.id, this.ids) relationsToItems(this.item.id, this.ids)

View File

@@ -1,4 +1,6 @@
<ds-item-page-title-field [item]="item"></ds-item-page-title-field> <h2 class="item-page-title-field">
{{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="item?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper> <ds-metadata-field-wrapper>

View File

@@ -3,16 +3,16 @@ import { Observable } from 'rxjs';
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 { import {
DEFAULT_ITEM_TYPE, DEFAULT_ITEM_TYPE, ItemViewMode,
rendersItemType rendersItemType
} from '../../../../shared/items/item-type-decorator'; } from '../../../../shared/items/item-type-decorator';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component'; import { ItemComponent } from '../shared/item.component';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import { VIEW_MODE_FULL } from '../../item-page.component'; import { filterRelationsByTypeLabel, relationsToItems } from '../shared/item-relationships-utils';
@rendersItemType('Publication', VIEW_MODE_FULL) @rendersItemType('Publication', ItemViewMode.Full)
@rendersItemType(DEFAULT_ITEM_TYPE, VIEW_MODE_FULL) @rendersItemType(DEFAULT_ITEM_TYPE, ItemViewMode.Full)
@Component({ @Component({
selector: 'ds-publication', selector: 'ds-publication',
styleUrls: ['./publication.component.scss'], styleUrls: ['./publication.component.scss'],

View File

@@ -0,0 +1,121 @@
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 { Observable } from 'rxjs/internal/Observable';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { distinctUntilChanged, flatMap, map } from 'rxjs/operators';
import { of as observableOf, zip as observableZip } from 'rxjs';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { Item } from '../../../../core/shared/item.model';
import { RemoteData } from '../../../../core/data/remote-data';
/**
* Operator for comparing arrays using a mapping function
* The mapping function should turn the source array into an array of basic types, so that the array can
* be compared using these basic types.
* For example: "(o) => o.id" will compare the two arrays by comparing their content by id.
* @param mapFn Function for mapping the arrays
*/
export const compareArraysUsing = <T>(mapFn: (t: T) => any) =>
(a: T[], b: T[]): boolean => {
if (!Array.isArray(a) || ! Array.isArray(b)) {
return false
}
const aIds = a.map(mapFn);
const bIds = b.map(mapFn);
return aIds.length === bIds.length &&
aIds.every((e) => bIds.includes(e)) &&
bIds.every((e) => aIds.includes(e));
};
/**
* Operator for comparing arrays using the object's ids
*/
export const compareArraysUsingIds = <T extends { id: string }>() =>
compareArraysUsing((t: T) => hasValue(t) ? t.id : undefined);
/**
* Fetch the relationships which match the type label given
* @param {string} label Type label
* @returns {(source: Observable<[Relationship[] , RelationshipType[]]>) => Observable<Relationship[]>}
*/
export const filterRelationsByTypeLabel = (label: string) =>
(source: Observable<[Relationship[], RelationshipType[]]>): Observable<Relationship[]> =>
source.pipe(
map(([relsCurrentPage, relTypesCurrentPage]) =>
relsCurrentPage.filter((rel: Relationship, idx: number) =>
hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === label ||
relTypesCurrentPage[idx].rightLabel === label)
)
),
distinctUntilChanged(compareArraysUsingIds())
);
/**
* Operator for turning a list of relationships into a list of the relevant items
* @param {string} thisId The item's id of which the relations belong to
* @param {ItemDataService} ids The ItemDataService to fetch items from the REST API
* @returns {(source: Observable<Relationship[]>) => Observable<Item[]>}
*/
export const relationsToItems = (thisId: string, ids: ItemDataService) =>
(source: Observable<Relationship[]>): Observable<Item[]> =>
source.pipe(
flatMap((rels: Relationship[]) =>
observableZip(
...rels.map((rel: Relationship) => {
let queryId = rel.leftId;
if (rel.leftId === thisId) {
queryId = rel.rightId;
}
return ids.findById(queryId);
})
)
),
map((arr: Array<RemoteData<Item>>) =>
arr
.filter((d: RemoteData<Item>) => d.hasSucceeded)
.map((d: RemoteData<Item>) => d.payload)),
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];
let queryId = matchingRel.leftId;
if (matchingRel.leftId === parentId) {
queryId = matchingRel.rightId;
}
return ids.findById(queryId).pipe(
getSucceededRemoteData(),
map((d: RemoteData<Item>) => Object.assign(new ItemMetadataRepresentation(), d.payload))
);
}
} else {
return observableOf(Object.assign(new MetadatumRepresentation(itemType), metadatum));
}
})
)
)
);

View File

@@ -16,19 +16,16 @@ import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { PageInfo } from '../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
import { compareArraysUsing, compareArraysUsingIds, ItemComponent } from './item.component'; import { ItemComponent } from './item.component';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ItemPageComponent } from '../../item-page.component';
import { VarDirective } from '../../../../shared/utils/var.directive'; import { VarDirective } from '../../../../shared/utils/var.directive';
import { ActivatedRoute } from '@angular/router';
import { MetadataService } from '../../../../core/metadata/metadata.service';
import { of } from 'rxjs/internal/observable/of';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/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 { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.models'; import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.models';
import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils';
/** /**
* Create a generic test for an item-page-fields component using a mockItem and the type of component * Create a generic test for an item-page-fields component using a mockItem and the type of component

View File

@@ -1,5 +1,5 @@
import { Component, Inject, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { Observable , zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, zip as observableZip } from 'rxjs';
import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators'; import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators';
import { ItemDataService } from '../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../core/data/item-data.service';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
@@ -7,121 +7,10 @@ import { RemoteData } from '../../../../core/data/remote-data';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { of } from 'rxjs/internal/observable/of'; import { compareArraysUsingIds, relationsToRepresentations } from './item-relationships-utils';
import { MetadataValue } from '../../../../core/shared/metadata.models';
/**
* Operator for comparing arrays using a mapping function
* The mapping function should turn the source array into an array of basic types, so that the array can
* be compared using these basic types.
* For example: "(o) => o.id" will compare the two arrays by comparing their content by id.
* @param mapFn Function for mapping the arrays
*/
export const compareArraysUsing = <T>(mapFn: (t: T) => any) =>
(a: T[], b: T[]): boolean => {
if (!Array.isArray(a) || ! Array.isArray(b)) {
return false
}
const aIds = a.map(mapFn);
const bIds = b.map(mapFn);
return aIds.length === bIds.length &&
aIds.every((e) => bIds.includes(e)) &&
bIds.every((e) => aIds.includes(e));
};
/**
* Operator for comparing arrays using the object's ids
*/
export const compareArraysUsingIds = <T extends { id: string }>() =>
compareArraysUsing((t: T) => hasValue(t) ? t.id : undefined);
/**
* Fetch the relationships which match the type label given
* @param {string} label Type label
* @returns {(source: Observable<[Relationship[] , RelationshipType[]]>) => Observable<Relationship[]>}
*/
export const filterRelationsByTypeLabel = (label: string) =>
(source: Observable<[Relationship[], RelationshipType[]]>): Observable<Relationship[]> =>
source.pipe(
map(([relsCurrentPage, relTypesCurrentPage]) =>
relsCurrentPage.filter((rel: Relationship, idx: number) =>
hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === label ||
relTypesCurrentPage[idx].rightLabel === label)
)
),
distinctUntilChanged(compareArraysUsingIds())
);
/**
* Operator for turning a list of relationships into a list of the relevant items
* @param {string} thisId The item's id of which the relations belong to
* @param {ItemDataService} ids The ItemDataService to fetch items from the REST API
* @returns {(source: Observable<Relationship[]>) => Observable<Item[]>}
*/
export const relationsToItems = (thisId: string, ids: ItemDataService) =>
(source: Observable<Relationship[]>): Observable<Item[]> =>
source.pipe(
flatMap((rels: Relationship[]) =>
observableZip(
...rels.map((rel: Relationship) => {
let queryId = rel.leftId;
if (rel.leftId === thisId) {
queryId = rel.rightId;
}
return ids.findById(queryId);
})
)
),
map((arr: Array<RemoteData<Item>>) =>
arr
.filter((d: RemoteData<Item>) => d.hasSucceeded)
.map((d: RemoteData<Item>) => d.payload)),
distinctUntilChanged(compareArraysUsingIds()),
);
/**
* Operator for turning a list of relationships into a list of metadatarepresentations given the original metadata
* @param thisId 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 = (thisId: 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];
let queryId = matchingRel.leftId;
if (matchingRel.leftId === thisId) {
queryId = matchingRel.rightId;
}
return ids.findById(queryId).pipe(
getSucceededRemoteData(),
map((d: RemoteData<Item>) => Object.assign(new ItemMetadataRepresentation(itemType), d.payload))
);
}
} else {
return of(Object.assign(new MetadatumRepresentation(itemType), metadatum));
}
})
)
)
);
@Component({ @Component({
selector: 'ds-item', selector: 'ds-item',

View File

@@ -7,7 +7,7 @@ import { ItemMetadataRepresentation } from '../../../core/shared/metadata-repres
const itemType = 'type'; const itemType = 'type';
const metadataRepresentation1 = new MetadatumRepresentation(itemType); const metadataRepresentation1 = new MetadatumRepresentation(itemType);
const metadataRepresentation2 = new ItemMetadataRepresentation(itemType); const metadataRepresentation2 = new ItemMetadataRepresentation();
const representations = [metadataRepresentation1, metadataRepresentation2]; const representations = [metadataRepresentation1, metadataRepresentation2];
describe('MetadataRepresentationListComponent', () => { describe('MetadataRepresentationListComponent', () => {

View File

@@ -1,8 +1,6 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import * as viewMode from '../../../shared/view-mode';
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
import { ItemViewMode } from '../../../shared/items/item-type-decorator';
export const VIEW_MODE_METADATA = 'metadata';
@Component({ @Component({
selector: 'ds-metadata-representation-list', selector: 'ds-metadata-representation-list',
@@ -27,5 +25,5 @@ export class MetadataRepresentationListComponent {
* The view-mode we're currently on * The view-mode we're currently on
* @type {ElementViewMode} * @type {ElementViewMode}
*/ */
viewMode = VIEW_MODE_METADATA; viewMode = ItemViewMode.Metadata;
} }

View File

@@ -1,7 +1,6 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { ItemViewMode } from '../../../shared/items/item-type-decorator';
export const VIEW_MODE_ELEMENT = 'element';
@Component({ @Component({
selector: 'ds-related-items', selector: 'ds-related-items',
@@ -27,5 +26,5 @@ export class RelatedItemsComponent {
* The view-mode we're currently on * The view-mode we're currently on
* @type {ElementViewMode} * @type {ElementViewMode}
*/ */
viewMode = VIEW_MODE_ELEMENT; viewMode = ItemViewMode.Element;
} }

View File

@@ -0,0 +1,4 @@
export enum MyDSpaceConfigurationValueType {
Workspace = 'workspace',
Workflow = 'workflow'
}

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

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

View File

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

View File

@@ -0,0 +1,11 @@
.parent {
display: flex;
}
.upload {
flex: auto;
}
.add {
flex: initial;
}

View File

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

View File

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

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

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

View File

@@ -0,0 +1 @@
@import '../+search-page/search-page.component.scss';

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

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

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -3,7 +3,13 @@ import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angul
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@Injectable() @Injectable()
/**
* Assemble the correct i18n key for the filtered search page's title depending on the current route's filter parameter
* and title data.
* The format of the key will be "{title}{filter}.title" with:
* - title: The prefix of the key stored in route.data
* - filter: The current filter stored in route.params
*/
export class FilteredSearchPageGuard implements CanActivate { export class FilteredSearchPageGuard implements CanActivate {
canActivate( canActivate(
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
@import '../../../../../../styles/variables.scss';
a {
color: $body-color;
&:hover, &focus {
text-decoration: none;
}
span.badge {
vertical-align: text-top;
}
}

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<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>
</a> </a>

View File

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

View File

@@ -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'],

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
@import '../../../../../../styles/variables.scss';
a {
color: $body-color;
&:hover, &focus {
text-decoration: none;
}
span.badge {
vertical-align: text-top;
}
}

View File

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

View File

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

View File

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

View File

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

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