mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'master' into configurable_entities
This commit is contained in:
@@ -52,5 +52,25 @@ module.exports = {
|
|||||||
// Log directory
|
// Log directory
|
||||||
logDirectory: '.',
|
logDirectory: '.',
|
||||||
// NOTE: will log all redux actions and transfers in console
|
// NOTE: will log all redux actions and transfers in console
|
||||||
debug: false
|
debug: false,
|
||||||
|
// Default Language in which the UI will be rendered if the user's browser language is not an active language
|
||||||
|
defaultLanguage: 'en',
|
||||||
|
// Languages. DSpace Angular holds a message catalog for each of the following languages. When set to active, users will be able to switch to the use of this language in the user interface.
|
||||||
|
languages: [{
|
||||||
|
code: 'en',
|
||||||
|
label: 'English',
|
||||||
|
active: true,
|
||||||
|
}, {
|
||||||
|
code: 'de',
|
||||||
|
label: 'Deutsch',
|
||||||
|
active: true,
|
||||||
|
}, {
|
||||||
|
code: 'cs',
|
||||||
|
label: 'Čeština',
|
||||||
|
active: true,
|
||||||
|
}, {
|
||||||
|
code: 'nl',
|
||||||
|
label: 'Nederlands',
|
||||||
|
active: false,
|
||||||
|
}]
|
||||||
};
|
};
|
||||||
|
@@ -130,6 +130,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/compiler": "^6.1.4",
|
"@angular/compiler": "^6.1.4",
|
||||||
"@angular/compiler-cli": "^6.1.4",
|
"@angular/compiler-cli": "^6.1.4",
|
||||||
|
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||||
"@ngrx/entity": "^6.1.0",
|
"@ngrx/entity": "^6.1.0",
|
||||||
"@ngrx/schematics": "^6.1.0",
|
"@ngrx/schematics": "^6.1.0",
|
||||||
"@ngrx/store-devtools": "^6.1.0",
|
"@ngrx/store-devtools": "^6.1.0",
|
||||||
@@ -182,7 +183,7 @@
|
|||||||
"karma-webdriver-launcher": "1.0.5",
|
"karma-webdriver-launcher": "1.0.5",
|
||||||
"karma-webpack": "3.0.0",
|
"karma-webpack": "3.0.0",
|
||||||
"ngrx-store-freeze": "^0.2.4",
|
"ngrx-store-freeze": "^0.2.4",
|
||||||
"node-sass": "^4.7.2",
|
"node-sass": "^4.11.0",
|
||||||
"nodemon": "^1.15.0",
|
"nodemon": "^1.15.0",
|
||||||
"npm-run-all": "4.1.3",
|
"npm-run-all": "4.1.3",
|
||||||
"postcss": "^7.0.2",
|
"postcss": "^7.0.2",
|
||||||
|
@@ -22,6 +22,9 @@
|
|||||||
},
|
},
|
||||||
"sub-collection-list": {
|
"sub-collection-list": {
|
||||||
"head": "Collections of this Community"
|
"head": "Collections of this Community"
|
||||||
|
},
|
||||||
|
"sub-community-list": {
|
||||||
|
"head": "Communities of this Community"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
@@ -43,6 +46,120 @@
|
|||||||
"simple": "Simple item page",
|
"simple": "Simple item page",
|
||||||
"full": "Full item page"
|
"full": "Full item page"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"select": {
|
||||||
|
"table": {
|
||||||
|
"collection": "Collection",
|
||||||
|
"author": "Author",
|
||||||
|
"title": "Title"
|
||||||
|
},
|
||||||
|
"confirm": "Confirm selected"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"head": "Edit Item",
|
||||||
|
"tabs": {
|
||||||
|
"status": {
|
||||||
|
"head": "Item Status",
|
||||||
|
"description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.",
|
||||||
|
"labels": {
|
||||||
|
"id": "Item Internal ID",
|
||||||
|
"handle": "Handle",
|
||||||
|
"lastModified": "Last Modified",
|
||||||
|
"itemPage": "Item Page"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"authorizations": {
|
||||||
|
"label": "Edit item's authorization policies",
|
||||||
|
"button": "Authorizations..."
|
||||||
|
},
|
||||||
|
"withdraw": {
|
||||||
|
"label": "Withdraw item from the repository",
|
||||||
|
"button": "Withdraw..."
|
||||||
|
},
|
||||||
|
"reinstate": {
|
||||||
|
"label": "Reinstate item into the repository",
|
||||||
|
"button": "Reinstate..."
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"label": "Move item to another collection",
|
||||||
|
"button": "Move..."
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"label": "Make item private",
|
||||||
|
"button": "Make it private..."
|
||||||
|
},
|
||||||
|
"public": {
|
||||||
|
"label": "Make item public",
|
||||||
|
"button": "Make it public..."
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"label": "Completely expunge item",
|
||||||
|
"button": "Permanently delete"
|
||||||
|
},
|
||||||
|
"mappedCollections": {
|
||||||
|
"label": "Manage mapped collections",
|
||||||
|
"button": "Mapped collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bitstreams": {
|
||||||
|
"head": "Item Bitstreams"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"head": "Item Metadata"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"head": "View Item"
|
||||||
|
},
|
||||||
|
"curate": {
|
||||||
|
"head": "Curate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modify.overview": {
|
||||||
|
"field": "Field",
|
||||||
|
"value": "Value",
|
||||||
|
"language": "Language"
|
||||||
|
},
|
||||||
|
"withdraw": {
|
||||||
|
"header": "Withdraw item: {{ id }}",
|
||||||
|
"description": "Are you sure this item should be withdrawn from the archive?",
|
||||||
|
"confirm": "Withdraw",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"success": "The item was withdrawn successfully",
|
||||||
|
"error": "An error occured while withdrawing the item"
|
||||||
|
},
|
||||||
|
"reinstate": {
|
||||||
|
"header": "Reinstate item: {{ id }}",
|
||||||
|
"description": "Are you sure this item should be reinstated to the archive?",
|
||||||
|
"confirm": "Reinstate",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"success": "The item was reinstated successfully",
|
||||||
|
"error": "An error occured while reinstating the item"
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"header": "Make item private: {{ id }}",
|
||||||
|
"description": "Are you sure this item should be made private in the archive?",
|
||||||
|
"confirm": "Make it Private",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"success": "The item is now private",
|
||||||
|
"error": "An error occured while making the item private"
|
||||||
|
},
|
||||||
|
"public": {
|
||||||
|
"header": "Make item public: {{ id }}",
|
||||||
|
"description": "Are you sure this item should be made public in the archive?",
|
||||||
|
"confirm": "Make it Public",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"success": "The item is now public",
|
||||||
|
"error": "An error occured while making the item public"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"header": "Delete item: {{ id }}",
|
||||||
|
"description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.",
|
||||||
|
"confirm": "Delete",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"success": "The item has been deleted",
|
||||||
|
"error": "An error occured while deleting the item"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"relationships": {
|
"relationships": {
|
||||||
@@ -114,9 +231,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"home": "Home",
|
"browse": {
|
||||||
|
"header": "All of DSpace"
|
||||||
|
},
|
||||||
|
"community-browse": {
|
||||||
|
"header": "By Community"
|
||||||
|
},
|
||||||
|
"statistics": {
|
||||||
|
"header": "Statistics"
|
||||||
|
},
|
||||||
"login": "Log In",
|
"login": "Log In",
|
||||||
"logout": "Log Out"
|
"logout": "Log Out",
|
||||||
|
"language": "Language switch",
|
||||||
|
"search": "Search"
|
||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"results-per-page": "Results Per Page",
|
"results-per-page": "Results Per Page",
|
||||||
@@ -295,12 +422,92 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"menu": {
|
||||||
|
"header": {
|
||||||
|
"admin": "Admin",
|
||||||
|
"image": {
|
||||||
|
"logo": "Repository logo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"section": {
|
||||||
|
"pin": "Pin sidebar",
|
||||||
|
"unpin": "Unpin sidebar",
|
||||||
|
"new": "New",
|
||||||
|
"new_community": "Community",
|
||||||
|
"new_collection": "Collection",
|
||||||
|
"new_item": "Item",
|
||||||
|
"new_item_version": "Item Version",
|
||||||
|
"edit": "Edit",
|
||||||
|
"edit_community": "Community",
|
||||||
|
"edit_collection": "Collection",
|
||||||
|
"edit_item": "Item",
|
||||||
|
"import": "Import",
|
||||||
|
"import_metadata": "Metadata",
|
||||||
|
"import_batch": "Batch Import (ZIP)",
|
||||||
|
"export": "Export",
|
||||||
|
"export_community": "Community",
|
||||||
|
"export_collection": "Collection",
|
||||||
|
"export_item": "Item",
|
||||||
|
"export_metadata": "Metadata",
|
||||||
|
"access_control": "Access Control",
|
||||||
|
"access_control_people": "People",
|
||||||
|
"access_control_groups": "Groups",
|
||||||
|
"access_control_authorizations": "Authorizations",
|
||||||
|
"find": "Find",
|
||||||
|
"find_items": "Items",
|
||||||
|
"find_withdrawn_items": "Withdrawn Items",
|
||||||
|
"find_private_items": "Private Items",
|
||||||
|
"registries": "Registries",
|
||||||
|
"registries_metadata": "Metadata",
|
||||||
|
"registries_format": "Format",
|
||||||
|
"curation_task": "Curation Task",
|
||||||
|
"statistics_task": "Statistics Task",
|
||||||
|
"control_panel": "Control Panel",
|
||||||
|
"browse_global": "All of DSpace",
|
||||||
|
"browse_global_communities_and_collections": "Communities & Collections",
|
||||||
|
"browse_global_by_issue_date": "By Issue Date",
|
||||||
|
"browse_global_by_author": "By Author",
|
||||||
|
"browse_global_by_title": "By Title",
|
||||||
|
"statistics": "Statistics",
|
||||||
|
"browse_community": "This Community",
|
||||||
|
"browse_community_by_issue_date": "By Issue Date",
|
||||||
|
"browse_community_by_author": "By Author",
|
||||||
|
"browse_community_by_title": "By Title",
|
||||||
|
"icon": {
|
||||||
|
"pin": "Pin sidebar",
|
||||||
|
"unpin": "Unpin sidebar",
|
||||||
|
"new": "New menu section",
|
||||||
|
"edit": "Edit menu section",
|
||||||
|
"import": "Import menu section",
|
||||||
|
"export": "Export menu section",
|
||||||
|
"access_control": "Access Control menu section",
|
||||||
|
"find": "Find menu section",
|
||||||
|
"registries": "Registries menu section",
|
||||||
|
"curation_task": "Curation Task menu section",
|
||||||
|
"statistics_task": "Statistics Task menu section",
|
||||||
|
"control_panel": "Control Panel menu section"
|
||||||
|
},
|
||||||
|
"toggle": {
|
||||||
|
"new": "Toggle New section",
|
||||||
|
"edit": "Toggle Edit section",
|
||||||
|
"import": "Toggle Import section",
|
||||||
|
"export": "Toggle Export section",
|
||||||
|
"access_control": "Toggle Access Control section",
|
||||||
|
"find": "Toggle Find section",
|
||||||
|
"registries": "Toggle Registries section",
|
||||||
|
"curation_task": "Toggle Curation Task section",
|
||||||
|
"statistics_task": "Toggle Statistics Task section",
|
||||||
|
"control_panel": "Toggle Control Panel section"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"default": "Loading...",
|
"default": "Loading...",
|
||||||
"top-level-communities": "Loading top-level communities...",
|
"top-level-communities": "Loading top-level communities...",
|
||||||
"community": "Loading community...",
|
"community": "Loading community...",
|
||||||
"collection": "Loading collection...",
|
"collection": "Loading collection...",
|
||||||
"sub-collections": "Loading sub-collections...",
|
"sub-collections": "Loading sub-collections...",
|
||||||
|
"sub-communities": "Loading sub-communities...",
|
||||||
"recent-submissions": "Loading recent submissions...",
|
"recent-submissions": "Loading recent submissions...",
|
||||||
"item": "Loading item...",
|
"item": "Loading item...",
|
||||||
"objects": "Loading...",
|
"objects": "Loading...",
|
||||||
@@ -313,6 +520,7 @@
|
|||||||
"community": "Error fetching community",
|
"community": "Error fetching community",
|
||||||
"collection": "Error fetching collection",
|
"collection": "Error fetching collection",
|
||||||
"sub-collections": "Error fetching sub-collections",
|
"sub-collections": "Error fetching sub-collections",
|
||||||
|
"sub-communities": "Error fetching sub-communities",
|
||||||
"recent-submissions": "Error fetching recent submissions",
|
"recent-submissions": "Error fetching recent submissions",
|
||||||
"item": "Error fetching item",
|
"item": "Error fetching item",
|
||||||
"objects": "Error fetching objects",
|
"objects": "Error fetching objects",
|
||||||
@@ -365,5 +573,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"invalid-user": "Invalid email address or password."
|
"invalid-user": "Invalid email address or password."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"chips": {
|
||||||
|
"remove": "Remove chip"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
resources/images/dspace-logo-mini.svg
Normal file
23
resources/images/dspace-logo-mini.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?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="97.562px" height="91.866px" viewBox="67.165 0 97.562 91.866" enable-background="new 67.165 0 97.562 91.866"
|
||||||
|
xml:space="preserve">
|
||||||
|
<path fill="#92C642" d="M120.726,58.569l0.109-0.006l0.116-0.01l0.106-0.013l0.11-0.01l0.11-0.023l0.109-0.019l0.106-0.023
|
||||||
|
l0.106-0.029l0.105-0.023l0.106-0.033l0.103-0.034l0.097-0.035l0.104-0.04l0.101-0.042l0.1-0.042v-0.001l0.096-0.045l0,0
|
||||||
|
l0.095-0.044l0.097-0.049l0.091-0.056v-0.001l0.094-0.05v-0.002l0.09-0.056v-0.001l0.093-0.06l0.083-0.056v-0.001l0.085-0.063
|
||||||
|
l0.088-0.065v-0.002l0.087-0.062v-0.001c0.816-0.683,1.393-1.646,1.561-2.738l0.013-0.104V54.72l0.014-0.101v-0.011l0.009-0.098
|
||||||
|
v-0.012l0.009-0.101V54.38l0.005-0.095v-0.016l0.002-0.105v-16.46l-0.002-0.105v-0.016l-0.005-0.095v-0.013l-0.009-0.101v-0.012
|
||||||
|
l-0.009-0.098v-0.011l-0.014-0.1v-0.01l-0.013-0.104c-0.167-1.092-0.744-2.057-1.561-2.738V34.3l-0.087-0.063v-0.002l-0.088-0.065
|
||||||
|
l-0.085-0.063v-0.001l-0.083-0.056l-0.093-0.061l0,0l-0.09-0.056V33.93l-0.094-0.05v-0.001l-0.091-0.056l-0.097-0.049l-0.095-0.043
|
||||||
|
V33.73l-0.096-0.045v-0.001l-0.1-0.043l-0.101-0.042l-0.104-0.04l-0.097-0.035l-0.103-0.031l-0.106-0.036l-0.105-0.023l-0.106-0.028
|
||||||
|
l-0.106-0.024l-0.109-0.019l-0.11-0.023l-0.11-0.009l-0.106-0.014l-0.116-0.01l-0.109-0.006l-0.114-0.005h-7.89
|
||||||
|
c-9.716,0-15.858-7.838-15.858-17.15V6.92c0-3.812-3.102-6.915-6.914-6.915H74.085c-3.813,0-6.92,3.106-6.92,6.915v16.682
|
||||||
|
c0,3.806,3.104,6.909,6.92,6.909h8.414c9.169,0,16.906,5.95,17.146,15.403v0.04c-0.24,9.453-7.978,15.402-17.146,15.402h-8.414
|
||||||
|
c-3.815,0-6.92,3.103-6.92,6.909v16.682c0,3.81,3.106,6.915,6.92,6.915H89.95c3.812,0,6.914-3.104,6.914-6.915v-9.223
|
||||||
|
c0-9.312,6.144-17.149,15.858-17.149h7.89L120.726,58.569z M154.772,9.956C148.631,3.814,140.15,0,130.816,0h-15.024v17.424h15.024
|
||||||
|
c4.526,0,8.647,1.858,11.64,4.849c2.99,2.99,4.849,7.112,4.849,11.639v24.042c0,4.538-1.853,8.665-4.832,11.655l-0.017-0.016
|
||||||
|
c-2.991,2.991-7.113,4.849-11.64,4.849h-15.024v17.424h15.024c9.333,0,17.814-3.814,23.956-9.956v-0.033
|
||||||
|
c6.142-6.143,9.955-14.614,9.955-23.923V33.912C164.727,24.578,160.914,16.097,154.772,9.956z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8.4 KiB |
37
resources/images/dspace-logo.svg
Normal file
37
resources/images/dspace-logo.svg
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?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="231.892px" height="167.458px" viewBox="0 0 231.892 167.458" enable-background="new 0 0 231.892 167.458"
|
||||||
|
xml:space="preserve">
|
||||||
|
<path fill="#6D6E71" d="M51.733,143.32c0-1.941,1.651-3.203,4.563-3.203c3.302,0,6.798,1.116,9.419,3.543l3.835-5.146
|
||||||
|
c-3.203-2.962-7.476-4.516-12.622-4.516c-7.622,0-12.284,4.468-12.284,9.856c0,12.187,18.644,8.254,18.644,13.886
|
||||||
|
c0,1.894-1.797,3.593-5.632,3.593c-4.466,0-8.011-2.039-10.292-4.419l-3.787,5.39c3.058,3.059,7.525,5.154,13.788,5.154
|
||||||
|
c8.691,0,12.964-4.474,12.964-10.397C70.329,144.971,51.733,148.418,51.733,143.32z M100.682,134.484H85.534v32.385h6.894v-11.556
|
||||||
|
h8.254c6.991,0,10.875-4.759,10.875-10.391C111.558,139.242,107.722,134.484,100.682,134.484z M99.71,149.244h-7.283v-8.69h7.283
|
||||||
|
c2.719,0,4.807,1.651,4.807,4.369C104.518,147.593,102.43,149.244,99.71,149.244z M180.759,140.067c3.301,0,6.214,2.089,7.573,4.71
|
||||||
|
l5.923-2.913c-2.281-4.078-6.408-7.914-13.496-7.914c-9.759,0-17.284,6.75-17.284,16.75c0,9.954,7.525,16.759,17.284,16.759
|
||||||
|
c7.088,0,11.215-3.94,13.496-7.97l-5.923-2.865c-1.359,2.622-4.272,4.71-7.573,4.71c-5.924,0-10.195-4.516-10.195-10.634
|
||||||
|
C170.564,144.583,174.835,140.067,180.759,140.067z M131.958,134.484l-12.486,32.385h7.824l2.04-5.486h13.886l2.039,5.486h7.816
|
||||||
|
l-12.479-32.385H131.958z M131.228,155.313l5.05-13.935l5.05,13.935H131.228z M231.892,140.553v-6.069h-22.916v32.385h22.916v-6.069
|
||||||
|
H215.87v-7.38h15.683v-6.069H215.87v-6.797H231.892z"/>
|
||||||
|
<path fill="#92C642" d="M29.956,150.652c0-9.71-7.04-16.168-17.187-16.168H0v32.385h12.817
|
||||||
|
C22.916,166.869,29.956,160.458,29.956,150.652z M12.769,160.799H6.894v-20.246h5.924c6.603,0,10.098,4.418,10.098,10.099
|
||||||
|
C22.916,156.187,19.177,160.799,12.769,160.799z"/>
|
||||||
|
<path fill="#92C642" d="M120.726,58.569l0.11-0.006l0.116-0.01l0.106-0.013l0.111-0.01l0.11-0.023l0.109-0.019l0.107-0.023
|
||||||
|
l0.106-0.029l0.106-0.023l0.106-0.033l0.103-0.034l0.096-0.035l0.104-0.04l0.101-0.042l0.099-0.042v-0.001l0.096-0.045v0
|
||||||
|
l0.095-0.044l0.096-0.049l0.091-0.056v-0.001l0.094-0.05v-0.002l0.09-0.056v-0.001l0.092-0.06l0.083-0.056v-0.001l0.085-0.063
|
||||||
|
l0.088-0.065v-0.002l0.087-0.063v-0.001c0.817-0.683,1.393-1.646,1.561-2.738l0.012-0.104v-0.009l0.014-0.101v-0.011l0.009-0.098
|
||||||
|
v-0.012l0.009-0.101V54.38l0.005-0.095v-0.016l0.002-0.105v-16.46l-0.002-0.105v-0.016l-0.005-0.095v-0.013l-0.009-0.101v-0.012
|
||||||
|
l-0.009-0.098v-0.011l-0.014-0.1v-0.01l-0.012-0.104c-0.167-1.092-0.744-2.057-1.561-2.738v-0.001l-0.087-0.063v-0.002l-0.088-0.065
|
||||||
|
l-0.085-0.063v-0.001l-0.083-0.056l-0.092-0.061v0l-0.09-0.056v-0.003l-0.094-0.05v-0.001l-0.091-0.056l-0.096-0.049l-0.095-0.043
|
||||||
|
v-0.001l-0.096-0.045v-0.001l-0.099-0.043l-0.101-0.042l-0.104-0.04l-0.096-0.035l-0.103-0.031l-0.106-0.036l-0.106-0.023
|
||||||
|
l-0.106-0.028l-0.107-0.024l-0.109-0.019l-0.11-0.023l-0.111-0.009l-0.106-0.014l-0.116-0.01l-0.11-0.006l-0.114-0.005h-7.89
|
||||||
|
c-9.715,0-15.858-7.838-15.858-17.15V6.92c0-3.812-3.102-6.915-6.914-6.915H74.085c-3.814,0-6.92,3.106-6.92,6.915v16.682
|
||||||
|
c0,3.806,3.104,6.909,6.92,6.909h8.414c9.169,0,16.906,5.95,17.146,15.403v0.04c-0.24,9.453-7.977,15.402-17.146,15.402h-8.414
|
||||||
|
c-3.816,0-6.92,3.103-6.92,6.909v16.682c0,3.809,3.106,6.915,6.92,6.915H89.95c3.812,0,6.914-3.104,6.914-6.915v-9.223
|
||||||
|
c0-9.312,6.144-17.149,15.858-17.149h7.89L120.726,58.569z M154.772,9.956C148.631,3.814,140.15,0,130.816,0h-15.024v17.424h15.024
|
||||||
|
c4.527,0,8.648,1.858,11.64,4.849c2.99,2.99,4.848,7.112,4.848,11.639v24.042c0,4.538-1.852,8.665-4.832,11.655l-0.016-0.016
|
||||||
|
c-2.991,2.991-7.113,4.849-11.64,4.849h-15.024v17.424h15.024c9.333,0,17.815-3.814,23.956-9.956v-0.033
|
||||||
|
c6.142-6.143,9.955-14.614,9.955-23.923V33.912C164.727,24.578,160.914,16.097,154.772,9.956z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
@@ -7,7 +7,6 @@ import { PaginatedList } from '../../../core/data/paginated-list';
|
|||||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-metadata-schema',
|
selector: 'ds-metadata-schema',
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
<li class="sidebar-section">
|
||||||
|
<a class="nav-item nav-link shortcut-icon" [routerLink]="itemModel.link">
|
||||||
|
<i class="fas fa-{{section.icon}} fa-fw" [title]="('menu.section.icon.' + section.id) | translate"></i>
|
||||||
|
</a>
|
||||||
|
<div class="sidebar-collapsible">
|
||||||
|
<span class="section-header-text">
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
@@ -0,0 +1,60 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
|
import { MenuServiceStub } from '../../../shared/testing/menu-service-stub';
|
||||||
|
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||||
|
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service-stub';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { AdminSidebarSectionComponent } from './admin-sidebar-section.component';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
describe('AdminSidebarSectionComponent', () => {
|
||||||
|
let component: AdminSidebarSectionComponent;
|
||||||
|
let fixture: ComponentFixture<AdminSidebarSectionComponent>;
|
||||||
|
const menuService = new MenuServiceStub();
|
||||||
|
const iconString = 'test';
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
|
||||||
|
declarations: [AdminSidebarSectionComponent, TestComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: 'sectionDataProvider', useValue: { model: { link: 'google.com' }, icon: iconString } },
|
||||||
|
{ provide: MenuService, useValue: menuService },
|
||||||
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
|
]
|
||||||
|
}).overrideComponent(AdminSidebarSectionComponent, {
|
||||||
|
set: {
|
||||||
|
entryComponents: [TestComponent]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the right icon', () => {
|
||||||
|
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
||||||
|
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// declare a test component
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-test-cmp',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
class TestComponent {
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||||
|
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
|
||||||
|
import { MenuID } from '../../../shared/menu/initial-menus-state';
|
||||||
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
|
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||||
|
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
||||||
|
import { MenuSection } from '../../../shared/menu/menu.reducer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a non-expandable section in the admin sidebar
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-sidebar-section',
|
||||||
|
templateUrl: './admin-sidebar-section.component.html',
|
||||||
|
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||||
|
|
||||||
|
})
|
||||||
|
@rendersSectionForMenu(MenuID.ADMIN, false)
|
||||||
|
export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This section resides in the Admin Sidebar
|
||||||
|
*/
|
||||||
|
menuID: MenuID = MenuID.ADMIN;
|
||||||
|
itemModel;
|
||||||
|
constructor(@Inject('sectionDataProvider') menuSection: MenuSection, protected menuService: MenuService, protected injector: Injector,) {
|
||||||
|
super(menuSection, menuService, injector);
|
||||||
|
this.itemModel = menuSection.model as LinkMenuItemModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
}
|
||||||
|
}
|
52
src/app/+admin/admin-sidebar/admin-sidebar.component.html
Normal file
52
src/app/+admin/admin-sidebar/admin-sidebar.component.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<nav @slideHorizontal class="navbar navbar-dark bg-dark p-0"
|
||||||
|
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
||||||
|
[@slideSidebar]="{
|
||||||
|
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||||
|
params: {sidebarWidth: (sidebarWidth | async)}
|
||||||
|
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
|
||||||
|
*ngIf="menuVisible | async" (mouseenter)="expandPreview($event)"
|
||||||
|
(mouseleave)="collapsePreview($event)">
|
||||||
|
<div class="sidebar-top-level-items">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="admin-menu-header sidebar-section">
|
||||||
|
<a class="shortcut-icon navbar-brand mr-0" href="#">
|
||||||
|
<span class="logo-wrapper">
|
||||||
|
<img class="admin-logo" src="assets/images/dspace-logo-mini.svg"
|
||||||
|
[alt]="('menu.header.image.logo') | translate">
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="sidebar-collapsible">
|
||||||
|
<a class="navbar-brand mr-0" href="#">
|
||||||
|
<h4 class="section-header-text mb-0">{{'menu.header.admin' |
|
||||||
|
translate}}</h4>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<ng-container *ngFor="let section of (sections | async)">
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="sectionComponents.get(section.id); injector: sectionInjectors.get(section.id);"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<div class="sidebar-section" id="sidebar-collapse-toggle">
|
||||||
|
<a class="nav-item nav-link shortcut-icon"
|
||||||
|
href="#"
|
||||||
|
(click)="toggle($event)">
|
||||||
|
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
|
||||||
|
[title]="'menu.section.icon.pin' | translate"></i>
|
||||||
|
<i *ngIf="!(menuCollapsed | async)" class="fas fa-fw fa-angle-double-left"
|
||||||
|
[title]="'menu.section.icon.unpin' | translate"></i>
|
||||||
|
</a>
|
||||||
|
<div class="sidebar-collapsible">
|
||||||
|
<a class="nav-item nav-link sidebar-section"
|
||||||
|
href="#"
|
||||||
|
(click)="toggle($event)">
|
||||||
|
<span *ngIf="menuCollapsed | async" class="section-header-text">{{'menu.section.pin' | translate }}</span>
|
||||||
|
<span *ngIf="!(menuCollapsed | async)" class="section-header-text">{{'menu.section.unpin' | translate }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
77
src/app/+admin/admin-sidebar/admin-sidebar.component.scss
Normal file
77
src/app/+admin/admin-sidebar/admin-sidebar.component.scss
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../styles/mixins.scss';
|
||||||
|
$icon-z-index: 10;
|
||||||
|
|
||||||
|
:host {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
nav {
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
&.sidebar-top-level-items {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
@include dark-scrollbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive ::ng-deep .sidebar-collapsible {
|
||||||
|
margin-left: -#{$sidebar-items-width};
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
.admin-menu-header {
|
||||||
|
background-color: $admin-sidebar-header-bg;
|
||||||
|
.logo-wrapper {
|
||||||
|
img {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.section-header-text {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.navbar-nav {
|
||||||
|
.sidebar-section {
|
||||||
|
display: flex;
|
||||||
|
align-content: stretch;
|
||||||
|
background-color: $dark;
|
||||||
|
.nav-item {
|
||||||
|
padding-top: $spacer;
|
||||||
|
padding-bottom: $spacer;
|
||||||
|
}
|
||||||
|
.shortcut-icon {
|
||||||
|
padding-left: $icon-padding;
|
||||||
|
padding-right: $icon-padding;
|
||||||
|
}
|
||||||
|
.shortcut-icon, .icon-wrapper {
|
||||||
|
background-color: inherit;
|
||||||
|
z-index: $icon-z-index;
|
||||||
|
}
|
||||||
|
.sidebar-collapsible {
|
||||||
|
width: $sidebar-items-width;
|
||||||
|
position: relative;
|
||||||
|
a {
|
||||||
|
padding-right: $spacer;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.active > .sidebar-collapsible > .nav-link {
|
||||||
|
color: $navbar-dark-active-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
142
src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
Normal file
142
src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||||
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
|
import { MenuServiceStub } from '../../shared/testing/menu-service-stub';
|
||||||
|
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||||
|
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service-stub';
|
||||||
|
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('AdminSidebarComponent', () => {
|
||||||
|
let comp: AdminSidebarComponent;
|
||||||
|
let fixture: ComponentFixture<AdminSidebarComponent>;
|
||||||
|
const menuService = new MenuServiceStub();
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), NoopAnimationsModule],
|
||||||
|
declarations: [AdminSidebarComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: Injector, useValue: {} },
|
||||||
|
{ provide: MenuService, useValue: menuService },
|
||||||
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(AdminSidebarComponent, {
|
||||||
|
set: {
|
||||||
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
|
}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'getMenuTopSections').and.returnValue(observableOf([]));
|
||||||
|
fixture = TestBed.createComponent(AdminSidebarComponent);
|
||||||
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
|
comp.sections = observableOf([]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('startSlide', () => {
|
||||||
|
describe('when expanding', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.sidebarClosed = true;
|
||||||
|
comp.startSlide({ toState: 'expanded' } as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the sidebarClosed to false', () => {
|
||||||
|
expect(comp.sidebarClosed).toBeFalsy();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when collapsing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.sidebarClosed = false;
|
||||||
|
comp.startSlide({ toState: 'collapsed' } as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the sidebarOpen to false', () => {
|
||||||
|
expect(comp.sidebarOpen).toBeFalsy();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('finishSlide', () => {
|
||||||
|
describe('when expanding', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.sidebarClosed = true;
|
||||||
|
comp.startSlide({ fromState: 'expanded' } as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the sidebarClosed to true', () => {
|
||||||
|
expect(comp.sidebarClosed).toBeTruthy();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when collapsing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.sidebarClosed = false;
|
||||||
|
comp.startSlide({ fromState: 'collapsed' } as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the sidebarOpen to true', () => {
|
||||||
|
expect(comp.sidebarOpen).toBeTruthy();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the collapse icon is clicked', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'toggleMenu');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('a.shortcut-icon'));
|
||||||
|
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call toggleMenu on the menuService', () => {
|
||||||
|
expect(menuService.toggleMenu).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the collapse link is clicked', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'toggleMenu');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('.sidebar-collapsible')).query(By.css('a'));
|
||||||
|
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call toggleMenu on the menuService', () => {
|
||||||
|
expect(menuService.toggleMenu).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the the mouse enters the nav tag', () => {
|
||||||
|
it('should call expandPreview on the menuService after 100ms', fakeAsync(() => {
|
||||||
|
spyOn(menuService, 'expandMenuPreview');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar'));
|
||||||
|
sidebarToggler.triggerEventHandler('mouseenter', {preventDefault: () => {/**/}});
|
||||||
|
tick(99);
|
||||||
|
expect(menuService.expandMenuPreview).not.toHaveBeenCalled();
|
||||||
|
tick(1);
|
||||||
|
expect(menuService.expandMenuPreview).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the the mouse leaves the nav tag', () => {
|
||||||
|
it('should call collapseMenuPreview on the menuService after 400ms', fakeAsync(() => {
|
||||||
|
spyOn(menuService, 'collapseMenuPreview');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar'));
|
||||||
|
sidebarToggler.triggerEventHandler('mouseleave', {preventDefault: () => {/**/}});
|
||||||
|
tick(399);
|
||||||
|
expect(menuService.collapseMenuPreview).not.toHaveBeenCalled();
|
||||||
|
tick(1);
|
||||||
|
expect(menuService.collapseMenuPreview).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
479
src/app/+admin/admin-sidebar/admin-sidebar.component.ts
Normal file
479
src/app/+admin/admin-sidebar/admin-sidebar.component.ts
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
import { Component, Injector, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { slide, slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||||
|
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||||
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
|
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
|
||||||
|
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||||
|
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
|
||||||
|
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component representing the admin sidebar
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-sidebar',
|
||||||
|
templateUrl: './admin-sidebar.component.html',
|
||||||
|
styleUrls: ['./admin-sidebar.component.scss'],
|
||||||
|
animations: [slideHorizontal, slideSidebar]
|
||||||
|
})
|
||||||
|
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The menu ID of the Navbar is PUBLIC
|
||||||
|
* @type {MenuID.ADMIN}
|
||||||
|
*/
|
||||||
|
menuID = MenuID.ADMIN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits the width of the collapsible menu sections
|
||||||
|
*/
|
||||||
|
sidebarWidth: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is true when the sidebar is open, is false when the sidebar is animating or closed
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
sidebarOpen = true; // Open in UI, animation finished
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is true when the sidebar is closed, is false when the sidebar is animating or open
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
sidebarClosed = !this.sidebarOpen; // Closed in UI, animation finished
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits true when either the menu OR the menu's preview is expanded, else emits false
|
||||||
|
*/
|
||||||
|
sidebarExpanded: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(protected menuService: MenuService,
|
||||||
|
protected injector: Injector,
|
||||||
|
private variableService: CSSVariableService,
|
||||||
|
private authService: AuthService
|
||||||
|
) {
|
||||||
|
super(menuService, injector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set and calculate all initial values of the instance variables
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.createMenu();
|
||||||
|
super.ngOnInit();
|
||||||
|
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
||||||
|
this.authService.isAuthenticated()
|
||||||
|
.subscribe((loggedIn: boolean) => {
|
||||||
|
if (loggedIn) {
|
||||||
|
this.menuService.showMenu(this.menuID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.menuCollapsed.pipe(first())
|
||||||
|
.subscribe((collapsed: boolean) => {
|
||||||
|
this.sidebarOpen = !collapsed;
|
||||||
|
this.sidebarClosed = collapsed;
|
||||||
|
});
|
||||||
|
this.sidebarExpanded = combineLatestObservable(this.menuCollapsed, this.menuPreviewCollapsed)
|
||||||
|
.pipe(
|
||||||
|
map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all menu sections and items for this menu
|
||||||
|
*/
|
||||||
|
private createMenu() {
|
||||||
|
const menuList = [
|
||||||
|
/* News */
|
||||||
|
{
|
||||||
|
id: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.new'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'plus-circle',
|
||||||
|
index: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new_community',
|
||||||
|
parentID: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.new_community',
|
||||||
|
link: '/communities/submission'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new_collection',
|
||||||
|
parentID: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.new_collection',
|
||||||
|
link: '/collections/submission'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new_item',
|
||||||
|
parentID: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.new_item',
|
||||||
|
link: '/items/submission'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new_item_version',
|
||||||
|
parentID: 'new',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.new_item_version',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Edit */
|
||||||
|
{
|
||||||
|
id: 'edit',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.edit'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'pencil-alt',
|
||||||
|
index: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit_community',
|
||||||
|
parentID: 'edit',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.edit_community',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit_collection',
|
||||||
|
parentID: 'edit',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.edit_collection',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit_item',
|
||||||
|
parentID: 'edit',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.edit_item',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Import */
|
||||||
|
{
|
||||||
|
id: 'import',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.import'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'sign-in-alt',
|
||||||
|
index: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'import_metadata',
|
||||||
|
parentID: 'import',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.import_metadata',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'import_batch',
|
||||||
|
parentID: 'import',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.import_batch',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Export */
|
||||||
|
{
|
||||||
|
id: 'export',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.export'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'sign-out-alt',
|
||||||
|
index: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'export_community',
|
||||||
|
parentID: 'export',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.export_community',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'export_collection',
|
||||||
|
parentID: 'export',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.export_collection',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'export_item',
|
||||||
|
parentID: 'export',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.export_item',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
}, {
|
||||||
|
id: 'export_metadata',
|
||||||
|
parentID: 'export',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.export_metadata',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Access Control */
|
||||||
|
{
|
||||||
|
id: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.access_control'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'key',
|
||||||
|
index: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'access_control_people',
|
||||||
|
parentID: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.access_control_people',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'access_control_groups',
|
||||||
|
parentID: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.access_control_groups',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'access_control_authorizations',
|
||||||
|
parentID: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.access_control_authorizations',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Search */
|
||||||
|
{
|
||||||
|
id: 'find',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.find'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'search',
|
||||||
|
index: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'find_items',
|
||||||
|
parentID: 'find',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.find_items',
|
||||||
|
link: '/search'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'find_withdrawn_items',
|
||||||
|
parentID: 'find',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.find_withdrawn_items',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'find_private_items',
|
||||||
|
parentID: 'find',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.find_private_items',
|
||||||
|
link: '/admin/items'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Registries */
|
||||||
|
{
|
||||||
|
id: 'registries',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.registries'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'list',
|
||||||
|
index: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'registries_metadata',
|
||||||
|
parentID: 'registries',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.registries_metadata',
|
||||||
|
link: 'admin/registries/metadata'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'registries_format',
|
||||||
|
parentID: 'registries',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.registries_format',
|
||||||
|
link: 'admin/registries/bitstream-formats'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Curation tasks */
|
||||||
|
{
|
||||||
|
id: 'curation_tasks',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.curation_task',
|
||||||
|
link: '/curation'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'filter',
|
||||||
|
index: 7
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Statistics */
|
||||||
|
{
|
||||||
|
id: 'statistics_task',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.statistics_task',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'chart-bar',
|
||||||
|
index: 8
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Control Panel */
|
||||||
|
{
|
||||||
|
id: 'control_panel',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.control_panel',
|
||||||
|
link: '#'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'cogs',
|
||||||
|
index: 9
|
||||||
|
},
|
||||||
|
];
|
||||||
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to change this.collapsed to false when the slide animation ends and is sliding open
|
||||||
|
* @param event The animation event
|
||||||
|
*/
|
||||||
|
startSlide(event: any): void {
|
||||||
|
if (event.toState === 'expanded') {
|
||||||
|
this.sidebarClosed = false;
|
||||||
|
} else if (event.toState === 'collapsed') {
|
||||||
|
this.sidebarOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to change this.collapsed to false when the slide animation ends and is sliding open
|
||||||
|
* @param event The animation event
|
||||||
|
*/
|
||||||
|
finishSlide(event: any): void {
|
||||||
|
if (event.fromState === 'expanded') {
|
||||||
|
this.sidebarClosed = true;
|
||||||
|
} else if (event.fromState === 'collapsed') {
|
||||||
|
this.sidebarOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
<li class="sidebar-section" [ngClass]="{'expanded': (expanded | async)}"
|
||||||
|
[@bgColor]="{
|
||||||
|
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
|
||||||
|
params: {endColor: (sidebarActiveBg | async)}}">
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<a class="nav-item nav-link shortcut-icon" (click)="toggleSection($event)" href="#">
|
||||||
|
<i class="fas fa-{{section.icon}} fa-fw" [title]="('menu.section.icon.' + section.id) | translate"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-collapsible">
|
||||||
|
<a class="nav-item nav-link" href="#"
|
||||||
|
(click)="toggleSection($event)">
|
||||||
|
<span class="section-header-text">
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||||
|
</span>
|
||||||
|
<i class="fas fa-chevron-right fa-pull-right"
|
||||||
|
[@rotate]="(expanded | async) ? 'expanded' : 'collapsed'" [title]="('menu.section.toggle.' + section.id) | translate"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="sidebar-sub-level-items list-unstyled" @slide *ngIf="(expanded | async)">
|
||||||
|
<li *ngFor="let subSection of (subSections | async)">
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="itemComponents.get(subSection.id); injector: itemInjectors.get(subSection.id);"></ng-container>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
@@ -0,0 +1,21 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.fa-chevron-right {
|
||||||
|
padding-left: $spacer/2;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
line-height: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-sub-level-items {
|
||||||
|
list-style: disc;
|
||||||
|
color: $navbar-dark-color;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-collapsible {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,84 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
|
||||||
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
|
import { MenuServiceStub } from '../../../shared/testing/menu-service-stub';
|
||||||
|
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||||
|
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service-stub';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
describe('ExpandableAdminSidebarSectionComponent', () => {
|
||||||
|
let component: ExpandableAdminSidebarSectionComponent;
|
||||||
|
let fixture: ComponentFixture<ExpandableAdminSidebarSectionComponent>;
|
||||||
|
const menuService = new MenuServiceStub();
|
||||||
|
const iconString = 'test';
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
|
||||||
|
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: 'sectionDataProvider', useValue: {icon: iconString} },
|
||||||
|
{ provide: MenuService, useValue: menuService },
|
||||||
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
|
]
|
||||||
|
}).overrideComponent(ExpandableAdminSidebarSectionComponent, {
|
||||||
|
set: {
|
||||||
|
entryComponents: [TestComponent]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||||
|
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the right icon', () => {
|
||||||
|
const icon = fixture.debugElement.query(By.css('.icon-wrapper')).query(By.css('i.fas'));
|
||||||
|
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the icon is clicked', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('a.shortcut-icon'));
|
||||||
|
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call toggleActiveSection on the menuService', () => {
|
||||||
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the header text is clicked', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-collapsible')).query(By.css('a'));
|
||||||
|
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call toggleActiveSection on the menuService', () => {
|
||||||
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// declare a test component
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-test-cmp',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
class TestComponent {
|
||||||
|
}
|
@@ -0,0 +1,69 @@
|
|||||||
|
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||||
|
import { rotate } from '../../../shared/animations/rotate';
|
||||||
|
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
||||||
|
import { slide } from '../../../shared/animations/slide';
|
||||||
|
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||||
|
import { bgColor } from '../../../shared/animations/bgColor';
|
||||||
|
import { MenuID } from '../../../shared/menu/initial-menus-state';
|
||||||
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
|
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a expandable section in the sidebar
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-expandable-admin-sidebar-section',
|
||||||
|
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||||
|
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||||
|
animations: [rotate, slide, bgColor]
|
||||||
|
|
||||||
|
})
|
||||||
|
@rendersSectionForMenu(MenuID.ADMIN, true)
|
||||||
|
export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* This section resides in the Admin Sidebar
|
||||||
|
*/
|
||||||
|
menuID = MenuID.ADMIN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The background color of the section when it's active
|
||||||
|
*/
|
||||||
|
sidebarActiveBg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits true when the sidebar is currently collapsed, true when it's expanded
|
||||||
|
*/
|
||||||
|
sidebarCollapsed: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits true when the sidebar's preview is currently collapsed, true when it's expanded
|
||||||
|
*/
|
||||||
|
sidebarPreviewCollapsed: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits true when the menu section is expanded, else emits false
|
||||||
|
* This is true when the section is active AND either the sidebar or it's preview is open
|
||||||
|
*/
|
||||||
|
expanded: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(@Inject('sectionDataProvider') menuSection, protected menuService: MenuService,
|
||||||
|
private variableService: CSSVariableService, protected injector: Injector) {
|
||||||
|
super(menuSection, menuService, injector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set initial values for instance variables
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.sidebarActiveBg = this.variableService.getVariable('adminSidebarActiveBg');
|
||||||
|
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||||
|
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||||
|
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
|
||||||
|
.pipe(
|
||||||
|
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -5,7 +5,7 @@ import { AdminRoutingModule } from './admin-routing.module';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
AdminRegistriesModule,
|
AdminRegistriesModule,
|
||||||
AdminRoutingModule
|
AdminRoutingModule,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminModule {
|
export class AdminModule {
|
||||||
|
@@ -24,6 +24,7 @@
|
|||||||
[content]="communityPayload.copyrightText"
|
[content]="communityPayload.copyrightText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
|
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
|
||||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -6,6 +6,7 @@ import { SharedModule } from '../shared/shared.module';
|
|||||||
import { CommunityPageComponent } from './community-page.component';
|
import { CommunityPageComponent } from './community-page.component';
|
||||||
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
||||||
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||||
|
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -16,6 +17,7 @@ import { CommunityPageRoutingModule } from './community-page-routing.module';
|
|||||||
declarations: [
|
declarations: [
|
||||||
CommunityPageComponent,
|
CommunityPageComponent,
|
||||||
CommunityPageSubCollectionListComponent,
|
CommunityPageSubCollectionListComponent,
|
||||||
|
CommunityPageSubCommunityListComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CommunityPageModule {
|
export class CommunityPageModule {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
|
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
|
||||||
<div *ngIf="subCollectionsRD?.hasSucceeded" @fadeIn>
|
<div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn>
|
||||||
<h2>{{'community.sub-collection-list.head' | translate}}</h2>
|
<h2>{{'community.sub-collection-list.head' | translate}}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let collection of subCollectionsRD?.payload.page">
|
<li *ngFor="let collection of subCollectionsRD?.payload.page">
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
<ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD">
|
||||||
|
<div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn>
|
||||||
|
<h2>{{'community.sub-community-list.head' | translate}}</h2>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let community of subCommunitiesRD?.payload.page">
|
||||||
|
<p>
|
||||||
|
<span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br>
|
||||||
|
<span class="text-muted">{{community.shortDescription}}</span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
|
||||||
|
<ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading>
|
||||||
|
</ng-container>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
@@ -0,0 +1,96 @@
|
|||||||
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {NO_ERRORS_SCHEMA} from '@angular/core';
|
||||||
|
import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component';
|
||||||
|
import {Community} from '../../core/shared/community.model';
|
||||||
|
import {RemoteData} from '../../core/data/remote-data';
|
||||||
|
import {PaginatedList} from '../../core/data/paginated-list';
|
||||||
|
import {PageInfo} from '../../core/shared/page-info.model';
|
||||||
|
import {SharedModule} from '../../shared/shared.module';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {of as observableOf, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
describe('SubCommunityList Component', () => {
|
||||||
|
let comp: CommunityPageSubCommunityListComponent;
|
||||||
|
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
||||||
|
|
||||||
|
const subcommunities = [Object.assign(new Community(), {
|
||||||
|
name: 'SubCommunity 1',
|
||||||
|
id: '123456789-1',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'SubCommunity 1'
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
Object.assign(new Community(), {
|
||||||
|
name: 'SubCommunity 2',
|
||||||
|
id: '123456789-2',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'SubCommunity 2'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const emptySubCommunitiesCommunity = Object.assign(new Community(), {
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Test title'
|
||||||
|
}],
|
||||||
|
subcommunities: observableOf(new RemoteData(true, true, true,
|
||||||
|
undefined, new PaginatedList(new PageInfo(), [])))
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCommunity = Object.assign(new Community(), {
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Test title'
|
||||||
|
}],
|
||||||
|
subcommunities: observableOf(new RemoteData(true, true, true,
|
||||||
|
undefined, new PaginatedList(new PageInfo(), subcommunities)))
|
||||||
|
})
|
||||||
|
;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule,
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
NoopAnimationsModule],
|
||||||
|
declarations: [CommunityPageSubCommunityListComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a list of subCommunities', () => {
|
||||||
|
comp.community = mockCommunity;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const subComList = fixture.debugElement.queryAll(By.css('li'));
|
||||||
|
expect(subComList.length).toEqual(2);
|
||||||
|
expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1');
|
||||||
|
expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display the header when subCommunities are empty', () => {
|
||||||
|
comp.community = emptySubCommunitiesCommunity;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const subComHead = fixture.debugElement.queryAll(By.css('h2'));
|
||||||
|
expect(subComHead.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
|
||||||
|
import { fadeIn } from '../../shared/animations/fade';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-community-page-sub-community-list',
|
||||||
|
styleUrls: ['./community-page-sub-community-list.component.scss'],
|
||||||
|
templateUrl: './community-page-sub-community-list.component.html',
|
||||||
|
animations:[fadeIn]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component to render the sub-communities of a Community
|
||||||
|
*/
|
||||||
|
export class CommunityPageSubCommunityListComponent implements OnInit {
|
||||||
|
@Input() community: Community;
|
||||||
|
subCommunitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subCommunitiesRDObs = this.community.subcommunities;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,7 @@
|
|||||||
<div class="jumbotron jumbotron-fluid">
|
<div class="jumbotron jumbotron-fluid">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex">
|
<div class="d-flex flex-wrap">
|
||||||
<div class="dspace-logo-container">
|
<img class="mr-4 dspace-logo" src="assets/images/dspace-logo.svg" alt="" />
|
||||||
<img src="assets/images/dspace-logo.png" />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 class="display-3">Welcome to DSpace</h1>
|
<h1 class="display-3">Welcome to DSpace</h1>
|
||||||
<p class="lead">DSpace is an open source software platform that enables organisations to:</p>
|
<p class="lead">DSpace is an open source software platform that enables organisations to:</p>
|
||||||
|
@@ -6,11 +6,11 @@
|
|||||||
margin-bottom: -$content-spacing;
|
margin-bottom: -$content-spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dspace-logo-container {
|
.display-3 {
|
||||||
margin: 10px 20px 0px 20px;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dspace-logo-container img {
|
.dspace-logo {
|
||||||
max-height: 110px;
|
height: 110px;
|
||||||
max-width: 110px;
|
width: 110px;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,35 @@
|
|||||||
|
import {RemoteData} from '../../core/data/remote-data';
|
||||||
|
import {hot} from 'jasmine-marbles';
|
||||||
|
import {Item} from '../../core/shared/item.model';
|
||||||
|
import {findSuccessfulAccordingTo} from './edit-item-operators';
|
||||||
|
|
||||||
|
describe('findSuccessfulAccordingTo', () => {
|
||||||
|
let mockItem1;
|
||||||
|
let mockItem2;
|
||||||
|
let predicate;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockItem1 = new Item();
|
||||||
|
mockItem1.isWithdrawn = true;
|
||||||
|
|
||||||
|
mockItem2 = new Item();
|
||||||
|
mockItem1.isWithdrawn = false;
|
||||||
|
|
||||||
|
predicate = (rd: RemoteData<Item>) => rd.payload.isWithdrawn;
|
||||||
|
});
|
||||||
|
it('should return first successful RemoteData Observable that complies to predicate', () => {
|
||||||
|
const testRD = {
|
||||||
|
a: new RemoteData(false, false, true, null, undefined),
|
||||||
|
b: new RemoteData(false, false, false, null, mockItem1),
|
||||||
|
c: new RemoteData(false, false, true, null, mockItem2),
|
||||||
|
d: new RemoteData(false, false, true, null, mockItem1),
|
||||||
|
e: new RemoteData(false, false, true, null, mockItem2),
|
||||||
|
};
|
||||||
|
|
||||||
|
const source = hot('abcde', testRD);
|
||||||
|
const result = source.pipe(findSuccessfulAccordingTo(predicate));
|
||||||
|
|
||||||
|
result.subscribe((value) => expect(value).toEqual(testRD.d));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
13
src/app/+item-page/edit-item-page/edit-item-operators.ts
Normal file
13
src/app/+item-page/edit-item-page/edit-item-operators.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import {RemoteData} from '../../core/data/remote-data';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {getAllSucceededRemoteData} from '../../core/shared/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return first Observable of a RemoteData object that complies to the provided predicate
|
||||||
|
* @param predicate
|
||||||
|
*/
|
||||||
|
export const findSuccessfulAccordingTo = <T>(predicate: (rd: RemoteData<T>) => boolean) =>
|
||||||
|
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
|
source.pipe(getAllSucceededRemoteData(),
|
||||||
|
first(predicate));
|
@@ -0,0 +1,36 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="border-bottom">{{'item.edit.head' | translate}}</h2>
|
||||||
|
<div class="pt-2">
|
||||||
|
<ngb-tabset>
|
||||||
|
<ngb-tab title="{{'item.edit.tabs.status.head' | translate}}">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
<ds-item-status [item]="(itemRD$ | async)?.payload"></ds-item-status>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
|
<ngb-tab title="{{'item.edit.tabs.bitstreams.head' | translate}}">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
|
<ngb-tab title="{{'item.edit.tabs.metadata.head' | translate}}">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
|
<ngb-tab title="{{'item.edit.tabs.view.head' | translate}}">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
|
<ngb-tab title="{{'item.edit.tabs.curate.head' | translate}}">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
|
</ngb-tabset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,35 @@
|
|||||||
|
import {fadeIn, fadeInOut} from '../../shared/animations/fade';
|
||||||
|
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
|
||||||
|
import {ActivatedRoute} from '@angular/router';
|
||||||
|
import {RemoteData} from '../../core/data/remote-data';
|
||||||
|
import {Item} from '../../core/shared/item.model';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {map} from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-item-page',
|
||||||
|
templateUrl: './edit-item-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [
|
||||||
|
fadeIn,
|
||||||
|
fadeInOut
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Page component for editing an item
|
||||||
|
*/
|
||||||
|
export class EditItemPageComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to edit
|
||||||
|
*/
|
||||||
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
src/app/+item-page/edit-item-page/edit-item-page.module.ts
Normal file
40
src/app/+item-page/edit-item-page/edit-item-page.module.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {SharedModule} from '../../shared/shared.module';
|
||||||
|
import {EditItemPageRoutingModule} from './edit-item-page.routing.module';
|
||||||
|
import {EditItemPageComponent} from './edit-item-page.component';
|
||||||
|
import {ItemStatusComponent} from './item-status/item-status.component';
|
||||||
|
import {ItemOperationComponent} from './item-operation/item-operation.component';
|
||||||
|
import {ModifyItemOverviewComponent} from './modify-item-overview/modify-item-overview.component';
|
||||||
|
import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component';
|
||||||
|
import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component';
|
||||||
|
import {AbstractSimpleItemActionComponent} from './simple-item-action/abstract-simple-item-action.component';
|
||||||
|
import {ItemPrivateComponent} from './item-private/item-private.component';
|
||||||
|
import {ItemPublicComponent} from './item-public/item-public.component';
|
||||||
|
import {ItemDeleteComponent} from './item-delete/item-delete.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module that contains all components related to the Edit Item page administrator functionality
|
||||||
|
*/
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
EditItemPageRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
EditItemPageComponent,
|
||||||
|
ItemOperationComponent,
|
||||||
|
AbstractSimpleItemActionComponent,
|
||||||
|
ModifyItemOverviewComponent,
|
||||||
|
ItemWithdrawComponent,
|
||||||
|
ItemReinstateComponent,
|
||||||
|
ItemPrivateComponent,
|
||||||
|
ItemPublicComponent,
|
||||||
|
ItemDeleteComponent,
|
||||||
|
ItemStatusComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class EditItemPageModule {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,72 @@
|
|||||||
|
import {ItemPageResolver} from '../item-page.resolver';
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {RouterModule} from '@angular/router';
|
||||||
|
import {EditItemPageComponent} from './edit-item-page.component';
|
||||||
|
import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component';
|
||||||
|
import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component';
|
||||||
|
import {ItemPrivateComponent} from './item-private/item-private.component';
|
||||||
|
import {ItemPublicComponent} from './item-public/item-public.component';
|
||||||
|
import {ItemDeleteComponent} from './item-delete/item-delete.component';
|
||||||
|
|
||||||
|
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||||
|
const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||||
|
const ITEM_EDIT_PRIVATE_PATH = 'private';
|
||||||
|
const ITEM_EDIT_PUBLIC_PATH = 'public';
|
||||||
|
const ITEM_EDIT_DELETE_PATH = 'delete';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||||
|
*/
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: EditItemPageComponent,
|
||||||
|
resolve: {
|
||||||
|
item: ItemPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_WITHDRAW_PATH,
|
||||||
|
component: ItemWithdrawComponent,
|
||||||
|
resolve: {
|
||||||
|
item: ItemPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_REINSTATE_PATH,
|
||||||
|
component: ItemReinstateComponent,
|
||||||
|
resolve: {
|
||||||
|
item: ItemPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_PRIVATE_PATH,
|
||||||
|
component: ItemPrivateComponent,
|
||||||
|
resolve: {
|
||||||
|
item: ItemPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_PUBLIC_PATH,
|
||||||
|
component: ItemPublicComponent,
|
||||||
|
resolve: {
|
||||||
|
item: ItemPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_DELETE_PATH,
|
||||||
|
component: ItemDeleteComponent,
|
||||||
|
resolve: {
|
||||||
|
item: ItemPageResolver
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ItemPageResolver,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class EditItemPageRoutingModule {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,118 @@
|
|||||||
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||||
|
import {of as observableOf} from 'rxjs';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||||
|
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||||
|
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {ItemDeleteComponent} from './item-delete.component';
|
||||||
|
import {getItemEditPath} from '../../item-page-routing.module';
|
||||||
|
|
||||||
|
let comp: ItemDeleteComponent;
|
||||||
|
let fixture: ComponentFixture<ItemDeleteComponent>;
|
||||||
|
|
||||||
|
let mockItem;
|
||||||
|
let itemPageUrl;
|
||||||
|
let routerStub;
|
||||||
|
let mockItemDataService: ItemDataService;
|
||||||
|
let routeStub;
|
||||||
|
let notificationsServiceStub;
|
||||||
|
let successfulRestResponse;
|
||||||
|
let failRestResponse;
|
||||||
|
|
||||||
|
describe('ItemDeleteComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
|
||||||
|
mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
isWithdrawn: true
|
||||||
|
});
|
||||||
|
|
||||||
|
itemPageUrl = `fake-url/${mockItem.id}`;
|
||||||
|
routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: `${itemPageUrl}/edit`
|
||||||
|
});
|
||||||
|
|
||||||
|
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
||||||
|
delete: observableOf(new RestResponse(true, '200'))
|
||||||
|
});
|
||||||
|
|
||||||
|
routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
item: new RemoteData(false, false, true, null, {
|
||||||
|
id: 'fake-id'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [ItemDeleteComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
{provide: ItemDataService, useValue: mockItemDataService},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||||
|
], schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
successfulRestResponse = new RestResponse(true, '200');
|
||||||
|
failRestResponse = new RestResponse(false, '500');
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ItemDeleteComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a page with messages based on the \'delete\' messageKey', () => {
|
||||||
|
const header = fixture.debugElement.query(By.css('h2')).nativeElement;
|
||||||
|
expect(header.innerHTML).toContain('item.edit.delete.header');
|
||||||
|
const description = fixture.debugElement.query(By.css('p')).nativeElement;
|
||||||
|
expect(description.innerHTML).toContain('item.edit.delete.description');
|
||||||
|
const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
|
||||||
|
expect(confirmButton.innerHTML).toContain('item.edit.delete.confirm');
|
||||||
|
const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
|
||||||
|
expect(cancelButton.innerHTML).toContain('item.edit.delete.cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('performAction', () => {
|
||||||
|
it('should call delete function from the ItemDataService', () => {
|
||||||
|
spyOn(comp, 'processRestResponse');
|
||||||
|
comp.performAction();
|
||||||
|
|
||||||
|
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id);
|
||||||
|
expect(comp.processRestResponse).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('processRestResponse', () => {
|
||||||
|
it('should navigate to the homepage on successful deletion of the item', () => {
|
||||||
|
comp.processRestResponse(successfulRestResponse);
|
||||||
|
expect(routerStub.navigate).toHaveBeenCalledWith(['']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('processRestResponse', () => {
|
||||||
|
it('should navigate to the item edit page on failed deletion of the item', () => {
|
||||||
|
comp.processRestResponse(failRestResponse);
|
||||||
|
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath('fake-id')]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,43 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
|
import {getItemEditPath} from '../../item-page-routing.module';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-delete',
|
||||||
|
templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component responsible for rendering the item delete page
|
||||||
|
*/
|
||||||
|
export class ItemDeleteComponent extends AbstractSimpleItemActionComponent {
|
||||||
|
|
||||||
|
protected messageKey = 'delete';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the delete action to the item
|
||||||
|
*/
|
||||||
|
performAction() {
|
||||||
|
this.itemDataService.delete(this.item.id).pipe(first()).subscribe(
|
||||||
|
(response: RestResponse) => {
|
||||||
|
this.processRestResponse(response);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the RestResponse retrieved from the server.
|
||||||
|
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
|
||||||
|
* @param response
|
||||||
|
*/
|
||||||
|
processRestResponse(response: RestResponse) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
|
||||||
|
this.router.navigate(['']);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
|
||||||
|
this.router.navigate([getItemEditPath(this.item.id)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="col-3 float-left d-flex h-100 action-label">
|
||||||
|
<span class="justify-content-center align-self-center">
|
||||||
|
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
|
||||||
|
<a class="btn btn-outline-secondary" href="{{operation.operationUrl}}">
|
||||||
|
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="operation.disabled" class="col-9 float-left action-button">
|
||||||
|
<span class="btn btn-danger">
|
||||||
|
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||||
|
</span>
|
||||||
|
</div>
|
@@ -0,0 +1,45 @@
|
|||||||
|
import {ItemOperation} from './itemOperation.model';
|
||||||
|
import {async, TestBed} from '@angular/core/testing';
|
||||||
|
import {ItemOperationComponent} from './item-operation.component';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('ItemOperationComponent', () => {
|
||||||
|
let itemOperation: ItemOperation;
|
||||||
|
|
||||||
|
let fixture;
|
||||||
|
let comp;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [ItemOperationComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
itemOperation = new ItemOperation('key1', 'url1');
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ItemOperationComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.operation = itemOperation;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render operation row', () => {
|
||||||
|
const span = fixture.debugElement.query(By.css('span')).nativeElement;
|
||||||
|
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
|
||||||
|
const link = fixture.debugElement.query(By.css('a')).nativeElement;
|
||||||
|
expect(link.href).toContain('url1');
|
||||||
|
expect(link.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
||||||
|
});
|
||||||
|
it('should render disabled operation row', () => {
|
||||||
|
itemOperation.setDisabled(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const span = fixture.debugElement.query(By.css('span')).nativeElement;
|
||||||
|
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
|
||||||
|
const span2 = fixture.debugElement.query(By.css('span.btn-danger')).nativeElement;
|
||||||
|
expect(span2.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,15 @@
|
|||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
import {ItemOperation} from './itemOperation.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-operation',
|
||||||
|
templateUrl: './item-operation.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Operation that can be performed on an item
|
||||||
|
*/
|
||||||
|
export class ItemOperationComponent {
|
||||||
|
|
||||||
|
@Input() operation: ItemOperation;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Represents an item operation used on the edit item page with a key, an operation URL to which will be navigated
|
||||||
|
* when performing the action and an option to disable the operation.
|
||||||
|
*/
|
||||||
|
export class ItemOperation {
|
||||||
|
|
||||||
|
operationKey: string;
|
||||||
|
operationUrl: string;
|
||||||
|
disabled: boolean;
|
||||||
|
|
||||||
|
constructor(operationKey: string, operationUrl: string) {
|
||||||
|
this.operationKey = operationKey;
|
||||||
|
this.operationUrl = operationUrl;
|
||||||
|
this.setDisabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether this operation should be disabled
|
||||||
|
* @param disabled
|
||||||
|
*/
|
||||||
|
setDisabled(disabled: boolean): void {
|
||||||
|
this.disabled = disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,105 @@
|
|||||||
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||||
|
import {of as observableOf} from 'rxjs';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||||
|
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||||
|
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {ItemPrivateComponent} from './item-private.component';
|
||||||
|
|
||||||
|
let comp: ItemPrivateComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPrivateComponent>;
|
||||||
|
|
||||||
|
let mockItem;
|
||||||
|
let itemPageUrl;
|
||||||
|
let routerStub;
|
||||||
|
let mockItemDataService: ItemDataService;
|
||||||
|
let routeStub;
|
||||||
|
let notificationsServiceStub;
|
||||||
|
let successfulRestResponse;
|
||||||
|
let failRestResponse;
|
||||||
|
|
||||||
|
describe('ItemPrivateComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
|
||||||
|
mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
isWithdrawn: true
|
||||||
|
});
|
||||||
|
|
||||||
|
itemPageUrl = `fake-url/${mockItem.id}`;
|
||||||
|
routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: `${itemPageUrl}/edit`
|
||||||
|
});
|
||||||
|
|
||||||
|
mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
|
||||||
|
setDiscoverable: observableOf(new RestResponse(true, '200'))
|
||||||
|
});
|
||||||
|
|
||||||
|
routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
item: new RemoteData(false, false, true, null, {
|
||||||
|
id: 'fake-id'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [ItemPrivateComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
{provide: ItemDataService, useValue: mockItemDataService},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||||
|
], schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
successfulRestResponse = new RestResponse(true, '200');
|
||||||
|
failRestResponse = new RestResponse(false, '500');
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ItemPrivateComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a page with messages based on the \'private\' messageKey', () => {
|
||||||
|
const header = fixture.debugElement.query(By.css('h2')).nativeElement;
|
||||||
|
expect(header.innerHTML).toContain('item.edit.private.header');
|
||||||
|
const description = fixture.debugElement.query(By.css('p')).nativeElement;
|
||||||
|
expect(description.innerHTML).toContain('item.edit.private.description');
|
||||||
|
const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
|
||||||
|
expect(confirmButton.innerHTML).toContain('item.edit.private.confirm');
|
||||||
|
const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
|
||||||
|
expect(cancelButton.innerHTML).toContain('item.edit.private.cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('performAction', () => {
|
||||||
|
it('should call setDiscoverable function from the ItemDataService', () => {
|
||||||
|
spyOn(comp, 'processRestResponse');
|
||||||
|
comp.performAction();
|
||||||
|
|
||||||
|
expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem.id, false);
|
||||||
|
expect(comp.processRestResponse).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,30 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-private',
|
||||||
|
templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component responsible for rendering the make item private page
|
||||||
|
*/
|
||||||
|
export class ItemPrivateComponent extends AbstractSimpleItemActionComponent {
|
||||||
|
|
||||||
|
protected messageKey = 'private';
|
||||||
|
protected predicate = (rd: RemoteData<Item>) => !rd.payload.isDiscoverable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the make private action to the item
|
||||||
|
*/
|
||||||
|
performAction() {
|
||||||
|
this.itemDataService.setDiscoverable(this.item.id, false).pipe(first()).subscribe(
|
||||||
|
(response: RestResponse) => {
|
||||||
|
this.processRestResponse(response);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,105 @@
|
|||||||
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||||
|
import {of as observableOf} from 'rxjs';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||||
|
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||||
|
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {ItemPublicComponent} from './item-public.component';
|
||||||
|
|
||||||
|
let comp: ItemPublicComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPublicComponent>;
|
||||||
|
|
||||||
|
let mockItem;
|
||||||
|
let itemPageUrl;
|
||||||
|
let routerStub;
|
||||||
|
let mockItemDataService: ItemDataService;
|
||||||
|
let routeStub;
|
||||||
|
let notificationsServiceStub;
|
||||||
|
let successfulRestResponse;
|
||||||
|
let failRestResponse;
|
||||||
|
|
||||||
|
describe('ItemPublicComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
|
||||||
|
mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
isWithdrawn: true
|
||||||
|
});
|
||||||
|
|
||||||
|
itemPageUrl = `fake-url/${mockItem.id}`;
|
||||||
|
routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: `${itemPageUrl}/edit`
|
||||||
|
});
|
||||||
|
|
||||||
|
mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
|
||||||
|
setDiscoverable: observableOf(new RestResponse(true, '200'))
|
||||||
|
});
|
||||||
|
|
||||||
|
routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
item: new RemoteData(false, false, true, null, {
|
||||||
|
id: 'fake-id'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [ItemPublicComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
{provide: ItemDataService, useValue: mockItemDataService},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||||
|
], schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
successfulRestResponse = new RestResponse(true, '200');
|
||||||
|
failRestResponse = new RestResponse(false, '500');
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ItemPublicComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a page with messages based on the \'public\' messageKey', () => {
|
||||||
|
const header = fixture.debugElement.query(By.css('h2')).nativeElement;
|
||||||
|
expect(header.innerHTML).toContain('item.edit.public.header');
|
||||||
|
const description = fixture.debugElement.query(By.css('p')).nativeElement;
|
||||||
|
expect(description.innerHTML).toContain('item.edit.public.description');
|
||||||
|
const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
|
||||||
|
expect(confirmButton.innerHTML).toContain('item.edit.public.confirm');
|
||||||
|
const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
|
||||||
|
expect(cancelButton.innerHTML).toContain('item.edit.public.cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('performAction', () => {
|
||||||
|
it('should call setDiscoverable function from the ItemDataService', () => {
|
||||||
|
spyOn(comp, 'processRestResponse');
|
||||||
|
comp.performAction();
|
||||||
|
|
||||||
|
expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem.id, true);
|
||||||
|
expect(comp.processRestResponse).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,30 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-public',
|
||||||
|
templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component responsible for rendering the make item public page
|
||||||
|
*/
|
||||||
|
export class ItemPublicComponent extends AbstractSimpleItemActionComponent {
|
||||||
|
|
||||||
|
protected messageKey = 'public';
|
||||||
|
protected predicate = (rd: RemoteData<Item>) => rd.payload.isDiscoverable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the make public action to the item
|
||||||
|
*/
|
||||||
|
performAction() {
|
||||||
|
this.itemDataService.setDiscoverable(this.item.id, true).pipe(first()).subscribe(
|
||||||
|
(response: RestResponse) => {
|
||||||
|
this.processRestResponse(response);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,105 @@
|
|||||||
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||||
|
import {of as observableOf} from 'rxjs';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||||
|
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||||
|
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {ItemReinstateComponent} from './item-reinstate.component';
|
||||||
|
|
||||||
|
let comp: ItemReinstateComponent;
|
||||||
|
let fixture: ComponentFixture<ItemReinstateComponent>;
|
||||||
|
|
||||||
|
let mockItem;
|
||||||
|
let itemPageUrl;
|
||||||
|
let routerStub;
|
||||||
|
let mockItemDataService: ItemDataService;
|
||||||
|
let routeStub;
|
||||||
|
let notificationsServiceStub;
|
||||||
|
let successfulRestResponse;
|
||||||
|
let failRestResponse;
|
||||||
|
|
||||||
|
describe('ItemReinstateComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
|
||||||
|
mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
isWithdrawn: true
|
||||||
|
});
|
||||||
|
|
||||||
|
itemPageUrl = `fake-url/${mockItem.id}`;
|
||||||
|
routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: `${itemPageUrl}/edit`
|
||||||
|
});
|
||||||
|
|
||||||
|
mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
|
||||||
|
setWithDrawn: observableOf(new RestResponse(true, '200'))
|
||||||
|
});
|
||||||
|
|
||||||
|
routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
item: new RemoteData(false, false, true, null, {
|
||||||
|
id: 'fake-id'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [ItemReinstateComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
{provide: ItemDataService, useValue: mockItemDataService},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||||
|
], schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
successfulRestResponse = new RestResponse(true, '200');
|
||||||
|
failRestResponse = new RestResponse(false, '500');
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ItemReinstateComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a page with messages based on the \'reinstate\' messageKey', () => {
|
||||||
|
const header = fixture.debugElement.query(By.css('h2')).nativeElement;
|
||||||
|
expect(header.innerHTML).toContain('item.edit.reinstate.header');
|
||||||
|
const description = fixture.debugElement.query(By.css('p')).nativeElement;
|
||||||
|
expect(description.innerHTML).toContain('item.edit.reinstate.description');
|
||||||
|
const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
|
||||||
|
expect(confirmButton.innerHTML).toContain('item.edit.reinstate.confirm');
|
||||||
|
const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
|
||||||
|
expect(cancelButton.innerHTML).toContain('item.edit.reinstate.cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('performAction', () => {
|
||||||
|
it('should call setWithdrawn function from the ItemDataService', () => {
|
||||||
|
spyOn(comp, 'processRestResponse');
|
||||||
|
comp.performAction();
|
||||||
|
|
||||||
|
expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(mockItem.id, false);
|
||||||
|
expect(comp.processRestResponse).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,30 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-reinstate',
|
||||||
|
templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component responsible for rendering the Item Reinstate page
|
||||||
|
*/
|
||||||
|
export class ItemReinstateComponent extends AbstractSimpleItemActionComponent {
|
||||||
|
|
||||||
|
protected messageKey = 'reinstate';
|
||||||
|
protected predicate = (rd: RemoteData<Item>) => !rd.payload.isWithdrawn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the reinstate action to the item
|
||||||
|
*/
|
||||||
|
performAction() {
|
||||||
|
this.itemDataService.setWithDrawn(this.item.id, false).pipe(first()).subscribe(
|
||||||
|
(response: RestResponse) => {
|
||||||
|
this.processRestResponse(response);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
<p class="mt-2">{{'item.edit.tabs.status.description' | translate}}</p>
|
||||||
|
<div class="row">
|
||||||
|
<div *ngFor="let statusKey of statusDataKeys" class="w-100">
|
||||||
|
<div class="col-3 float-left status-label">
|
||||||
|
{{'item.edit.tabs.status.labels.' + statusKey | translate}}:
|
||||||
|
</div>
|
||||||
|
<div class="col-9 float-left status-data" id="status-{{statusKey}}">
|
||||||
|
{{statusData[statusKey]}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 float-left status-label">
|
||||||
|
{{'item.edit.tabs.status.labels.itemPage' | translate}}:
|
||||||
|
</div>
|
||||||
|
<div class="col-9 float-left status-data" id="status-itemPage">
|
||||||
|
<a href="{{getItemPage()}}">{{getItemPage()}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngFor="let operation of operations" class="w-100 pt-3">
|
||||||
|
<ds-item-operation [operation]="operation"></ds-item-operation>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { ItemStatusComponent } from './item-status.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||||
|
import { HostWindowService } from '../../../shared/host-window.service';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
|
|
||||||
|
describe('ItemStatusComponent', () => {
|
||||||
|
let comp: ItemStatusComponent;
|
||||||
|
let fixture: ComponentFixture<ItemStatusComponent>;
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018'
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemPageUrl = `fake-url/${mockItem.id}`;
|
||||||
|
const routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: `${itemPageUrl}/edit`
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [ItemStatusComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: Router, useValue: routerStub },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
||||||
|
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemStatusComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItem;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the item\'s internal id', () => {
|
||||||
|
const statusId: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-id')).nativeElement;
|
||||||
|
expect(statusId.textContent).toContain(mockItem.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the item\'s handle', () => {
|
||||||
|
const statusHandle: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-handle')).nativeElement;
|
||||||
|
expect(statusHandle.textContent).toContain(mockItem.handle);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the item\'s last modified date', () => {
|
||||||
|
const statusLastModified: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-lastModified')).nativeElement;
|
||||||
|
expect(statusLastModified.textContent).toContain(mockItem.lastModified);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the item\'s page url', () => {
|
||||||
|
const statusItemPage: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-itemPage')).nativeElement;
|
||||||
|
expect(statusItemPage.textContent).toContain(itemPageUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,95 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
|
||||||
|
import {fadeIn, fadeInOut} from '../../../shared/animations/fade';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {ItemOperation} from '../item-operation/itemOperation.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-status',
|
||||||
|
templateUrl: './item-status.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [
|
||||||
|
fadeIn,
|
||||||
|
fadeInOut
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for displaying an item's status
|
||||||
|
*/
|
||||||
|
export class ItemStatusComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display the status for
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data to show in the status
|
||||||
|
*/
|
||||||
|
statusData: any;
|
||||||
|
/**
|
||||||
|
* The keys of the data (to loop over)
|
||||||
|
*/
|
||||||
|
statusDataKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible actions that can be performed on the item
|
||||||
|
* key: id value: url to action's component
|
||||||
|
*/
|
||||||
|
operations: ItemOperation[];
|
||||||
|
/**
|
||||||
|
* The keys of the actions (to loop over)
|
||||||
|
*/
|
||||||
|
actionsKeys;
|
||||||
|
|
||||||
|
constructor(private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.statusData = Object.assign({
|
||||||
|
id: this.item.id,
|
||||||
|
handle: this.item.handle,
|
||||||
|
lastModified: this.item.lastModified
|
||||||
|
});
|
||||||
|
this.statusDataKeys = Object.keys(this.statusData);
|
||||||
|
|
||||||
|
/*
|
||||||
|
The key is used to build messages
|
||||||
|
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
||||||
|
The value is supposed to be a href for the button
|
||||||
|
*/
|
||||||
|
this.operations = [];
|
||||||
|
if (this.item.isWithdrawn) {
|
||||||
|
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl() + '/reinstate'));
|
||||||
|
} else {
|
||||||
|
this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl() + '/withdraw'));
|
||||||
|
}
|
||||||
|
if (this.item.isDiscoverable) {
|
||||||
|
this.operations.push(new ItemOperation('private', this.getCurrentUrl() + '/private'));
|
||||||
|
} else {
|
||||||
|
this.operations.push(new ItemOperation('public', this.getCurrentUrl() + '/public'));
|
||||||
|
}
|
||||||
|
this.operations.push(new ItemOperation('delete', this.getCurrentUrl() + '/delete'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the url to the simple item page
|
||||||
|
* @returns {string} url
|
||||||
|
*/
|
||||||
|
getItemPage(): string {
|
||||||
|
return this.router.url.substr(0, this.router.url.lastIndexOf('/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current url without query params
|
||||||
|
* @returns {string} url
|
||||||
|
*/
|
||||||
|
getCurrentUrl(): string {
|
||||||
|
if (this.router.url.indexOf('?') > -1) {
|
||||||
|
return this.router.url.substr(0, this.router.url.indexOf('?'));
|
||||||
|
} else {
|
||||||
|
return this.router.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,105 @@
|
|||||||
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||||
|
import {of as observableOf} from 'rxjs';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||||
|
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||||
|
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
|
import {ItemWithdrawComponent} from './item-withdraw.component';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
let comp: ItemWithdrawComponent;
|
||||||
|
let fixture: ComponentFixture<ItemWithdrawComponent>;
|
||||||
|
|
||||||
|
let mockItem;
|
||||||
|
let itemPageUrl;
|
||||||
|
let routerStub;
|
||||||
|
let mockItemDataService: ItemDataService;
|
||||||
|
let routeStub;
|
||||||
|
let notificationsServiceStub;
|
||||||
|
let successfulRestResponse;
|
||||||
|
let failRestResponse;
|
||||||
|
|
||||||
|
describe('ItemWithdrawComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
|
||||||
|
mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
isWithdrawn: true
|
||||||
|
});
|
||||||
|
|
||||||
|
itemPageUrl = `fake-url/${mockItem.id}`;
|
||||||
|
routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: `${itemPageUrl}/edit`
|
||||||
|
});
|
||||||
|
|
||||||
|
mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
|
||||||
|
setWithDrawn: observableOf(new RestResponse(true, '200'))
|
||||||
|
});
|
||||||
|
|
||||||
|
routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
item: new RemoteData(false, false, true, null, {
|
||||||
|
id: 'fake-id'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot(),],
|
||||||
|
declarations: [ItemWithdrawComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
{provide: ItemDataService, useValue: mockItemDataService},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||||
|
], schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
successfulRestResponse = new RestResponse(true, '200');
|
||||||
|
failRestResponse = new RestResponse(false, '500');
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ItemWithdrawComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a page with messages based on the \'withdraw\' messageKey', () => {
|
||||||
|
const header = fixture.debugElement.query(By.css('h2')).nativeElement;
|
||||||
|
expect(header.innerHTML).toContain('item.edit.withdraw.header');
|
||||||
|
const description = fixture.debugElement.query(By.css('p')).nativeElement;
|
||||||
|
expect(description.innerHTML).toContain('item.edit.withdraw.description');
|
||||||
|
const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
|
||||||
|
expect(confirmButton.innerHTML).toContain('item.edit.withdraw.confirm');
|
||||||
|
const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
|
||||||
|
expect(cancelButton.innerHTML).toContain('item.edit.withdraw.cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('performAction', () => {
|
||||||
|
it('should call setWithdrawn function from the ItemDataService', () => {
|
||||||
|
spyOn(comp, 'processRestResponse');
|
||||||
|
comp.performAction();
|
||||||
|
|
||||||
|
expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(mockItem.id, true);
|
||||||
|
expect(comp.processRestResponse).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
;
|
@@ -0,0 +1,30 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {first} from 'rxjs/operators';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-withdraw',
|
||||||
|
templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component responsible for rendering the Item Withdraw page
|
||||||
|
*/
|
||||||
|
export class ItemWithdrawComponent extends AbstractSimpleItemActionComponent {
|
||||||
|
|
||||||
|
protected messageKey = 'withdraw';
|
||||||
|
protected predicate = (rd: RemoteData<Item>) => rd.payload.isWithdrawn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the withdraw action to the item
|
||||||
|
*/
|
||||||
|
performAction() {
|
||||||
|
this.itemDataService.setWithDrawn(this.item.id, true).pipe(first()).subscribe(
|
||||||
|
(response: RestResponse) => {
|
||||||
|
this.processRestResponse(response);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
<table id="metadata" class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{{'item.edit.modify.overview.field'| translate}}</th>
|
||||||
|
<th scope="col">{{'item.edit.modify.overview.value'| translate}}</th>
|
||||||
|
<th scope="col">{{'item.edit.modify.overview.language'| translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let metadatum of metadata" class="metadata-row">
|
||||||
|
<td>{{metadatum.key}}</td>
|
||||||
|
<td>{{metadatum.value}}</td>
|
||||||
|
<td>{{metadatum.language}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
@@ -0,0 +1,55 @@
|
|||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {ModifyItemOverviewComponent} from './modify-item-overview.component';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
|
||||||
|
let comp: ModifyItemOverviewComponent;
|
||||||
|
let fixture: ComponentFixture<ModifyItemOverviewComponent>;
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
metadata: [
|
||||||
|
{key: 'dc.title', value: 'Mock item title', language: 'en'},
|
||||||
|
{key: 'dc.contributor.author', value: 'Mayer, Ed', language: ''}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ModifyItemOverviewComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [ModifyItemOverviewComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ModifyItemOverviewComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItem;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should render a table of existing metadata fields in the item', () => {
|
||||||
|
|
||||||
|
const metadataRows = fixture.debugElement.queryAll(By.css('tr.metadata-row'));
|
||||||
|
expect(metadataRows.length).toEqual(2);
|
||||||
|
|
||||||
|
const titleRow = metadataRows[0].queryAll(By.css('td'));
|
||||||
|
expect(titleRow.length).toEqual(3);
|
||||||
|
|
||||||
|
expect(titleRow[0].nativeElement.innerHTML).toContain('dc.title');
|
||||||
|
expect(titleRow[1].nativeElement.innerHTML).toContain('Mock item title');
|
||||||
|
expect(titleRow[2].nativeElement.innerHTML).toContain('en');
|
||||||
|
|
||||||
|
const authorRow = metadataRows[1].queryAll(By.css('td'));
|
||||||
|
expect(authorRow.length).toEqual(3);
|
||||||
|
|
||||||
|
expect(authorRow[0].nativeElement.innerHTML).toContain('dc.contributor.author');
|
||||||
|
expect(authorRow[1].nativeElement.innerHTML).toContain('Mayer, Ed');
|
||||||
|
expect(authorRow[2].nativeElement.innerHTML).toEqual('');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,20 @@
|
|||||||
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {Metadatum} from '../../../core/shared/metadatum.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-modify-item-overview',
|
||||||
|
templateUrl: './modify-item-overview.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component responsible for rendering a table containing the metadatavalues from the to be edited item
|
||||||
|
*/
|
||||||
|
export class ModifyItemOverviewComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() item: Item;
|
||||||
|
metadata: Metadatum[];
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.metadata = this.item.metadata;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2>{{headerMessage | translate: {id: item.handle} }}</h2>
|
||||||
|
<p>{{descriptionMessage | translate}}</p>
|
||||||
|
<ds-modify-item-overview [item]="item"></ds-modify-item-overview>
|
||||||
|
<button (click)="performAction()" class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
|
||||||
|
</button>
|
||||||
|
<button [routerLink]="['/items/', item.id, 'edit']" class="btn btn-outline-secondary cancel">
|
||||||
|
{{cancelMessage| translate}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1,142 @@
|
|||||||
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {RouterTestingModule} from '@angular/router/testing';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||||
|
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {AbstractSimpleItemActionComponent} from './abstract-simple-item-action.component';
|
||||||
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {of as observableOf} from 'rxjs';
|
||||||
|
import {getItemEditPath} from '../../item-page-routing.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test component that implements the AbstractSimpleItemActionComponent used to test the
|
||||||
|
* AbstractSimpleItemActionComponent component
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-simple-action',
|
||||||
|
templateUrl: './abstract-simple-item-action.component.html'
|
||||||
|
})
|
||||||
|
export class MySimpleItemActionComponent extends AbstractSimpleItemActionComponent {
|
||||||
|
|
||||||
|
protected messageKey = 'myEditAction';
|
||||||
|
protected predicate = (rd: RemoteData<Item>) => rd.payload.isWithdrawn;
|
||||||
|
|
||||||
|
performAction() {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let comp: MySimpleItemActionComponent;
|
||||||
|
let fixture: ComponentFixture<MySimpleItemActionComponent>;
|
||||||
|
|
||||||
|
let mockItem;
|
||||||
|
let itemPageUrl;
|
||||||
|
let routerStub;
|
||||||
|
let mockItemDataService;
|
||||||
|
let routeStub;
|
||||||
|
let notificationsServiceStub;
|
||||||
|
let successfulRestResponse;
|
||||||
|
let failRestResponse;
|
||||||
|
|
||||||
|
describe('AbstractSimpleItemActionComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
|
||||||
|
mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
isWithdrawn: true
|
||||||
|
});
|
||||||
|
|
||||||
|
itemPageUrl = `fake-url/${mockItem.id}`;
|
||||||
|
routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: `${itemPageUrl}/edit`
|
||||||
|
});
|
||||||
|
|
||||||
|
mockItemDataService = jasmine.createSpyObj({
|
||||||
|
findById: observableOf(new RemoteData(false, false, true, undefined, mockItem))
|
||||||
|
});
|
||||||
|
|
||||||
|
routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
item: new RemoteData(false, false, true, null, {
|
||||||
|
id: 'fake-id'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [MySimpleItemActionComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: routerStub},
|
||||||
|
{provide: ItemDataService, useValue: mockItemDataService},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||||
|
], schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
successfulRestResponse = new RestResponse(true, '200');
|
||||||
|
failRestResponse = new RestResponse(false, '500');
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(MySimpleItemActionComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a page with messages based on the provided messageKey', () => {
|
||||||
|
const header = fixture.debugElement.query(By.css('h2')).nativeElement;
|
||||||
|
expect(header.innerHTML).toContain('item.edit.myEditAction.header');
|
||||||
|
|
||||||
|
const description = fixture.debugElement.query(By.css('p')).nativeElement;
|
||||||
|
expect(description.innerHTML).toContain('item.edit.myEditAction.description');
|
||||||
|
|
||||||
|
const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
|
||||||
|
expect(confirmButton.innerHTML).toContain('item.edit.myEditAction.confirm');
|
||||||
|
|
||||||
|
const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
|
||||||
|
expect(cancelButton.innerHTML).toContain('item.edit.myEditAction.cancel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform action when the button is clicked', () => {
|
||||||
|
spyOn(comp, 'performAction');
|
||||||
|
const performButton = fixture.debugElement.query(By.css('.perform-action'));
|
||||||
|
performButton.triggerEventHandler('click', null);
|
||||||
|
|
||||||
|
expect(comp.performAction).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process a RestResponse to navigate and display success notification', () => {
|
||||||
|
spyOn(notificationsServiceStub, 'success');
|
||||||
|
comp.processRestResponse(successfulRestResponse);
|
||||||
|
|
||||||
|
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
||||||
|
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath(mockItem.id)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process a RestResponse to navigate and display success notification', () => {
|
||||||
|
spyOn(notificationsServiceStub, 'error');
|
||||||
|
comp.processRestResponse(failRestResponse);
|
||||||
|
|
||||||
|
expect(notificationsServiceStub.error).toHaveBeenCalled();
|
||||||
|
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath(mockItem.id)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,84 @@
|
|||||||
|
import {Component, OnInit, Predicate} from '@angular/core';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||||
|
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||||
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
import {Item} from '../../../core/shared/item.model';
|
||||||
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {getSucceededRemoteData} from '../../../core/shared/operators';
|
||||||
|
import {first, map} from 'rxjs/operators';
|
||||||
|
import {RestResponse} from '../../../core/cache/response-cache.models';
|
||||||
|
import {findSuccessfulAccordingTo} from '../edit-item-operators';
|
||||||
|
import {getItemEditPath} from '../../item-page-routing.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render and handle simple item edit actions such as withdrawal and reinstatement.
|
||||||
|
* This component is not meant to be used itself but to be extended.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-simple-action',
|
||||||
|
templateUrl: './abstract-simple-item-action.component.html'
|
||||||
|
})
|
||||||
|
export class AbstractSimpleItemActionComponent implements OnInit {
|
||||||
|
|
||||||
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
item: Item;
|
||||||
|
|
||||||
|
protected messageKey: string;
|
||||||
|
confirmMessage: string;
|
||||||
|
cancelMessage: string;
|
||||||
|
headerMessage: string;
|
||||||
|
descriptionMessage: string;
|
||||||
|
|
||||||
|
protected predicate: Predicate<RemoteData<Item>>;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected itemDataService: ItemDataService,
|
||||||
|
protected translateService: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.itemRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.item),
|
||||||
|
getSucceededRemoteData()
|
||||||
|
)as Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
|
this.itemRD$.pipe(first()).subscribe((rd) => {
|
||||||
|
this.item = rd.payload;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.confirmMessage = 'item.edit.' + this.messageKey + '.confirm';
|
||||||
|
this.cancelMessage = 'item.edit.' + this.messageKey + '.cancel';
|
||||||
|
this.headerMessage = 'item.edit.' + this.messageKey + '.header';
|
||||||
|
this.descriptionMessage = 'item.edit.' + this.messageKey + '.description';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the operation linked to this action
|
||||||
|
*/
|
||||||
|
performAction() {
|
||||||
|
// Overwrite in subclasses
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the response obtained during the performAction method and navigate back to the edit page
|
||||||
|
* @param response from the action in the performAction method
|
||||||
|
*/
|
||||||
|
processRestResponse(response: RestResponse) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
this.itemDataService.findById(this.item.id).pipe(
|
||||||
|
findSuccessfulAccordingTo(this.predicate)).subscribe(() => {
|
||||||
|
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
|
||||||
|
this.router.navigate([getItemEditPath(this.item.id)]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
|
||||||
|
this.router.navigate([getItemEditPath(this.item.id)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -4,6 +4,18 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { ItemPageComponent } from './simple/item-page.component';
|
import { ItemPageComponent } from './simple/item-page.component';
|
||||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||||
import { ItemPageResolver } from './item-page.resolver';
|
import { ItemPageResolver } from './item-page.resolver';
|
||||||
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
|
import {URLCombiner} from '../core/url-combiner/url-combiner';
|
||||||
|
import {getItemModulePath} from '../app-routing.module';
|
||||||
|
|
||||||
|
export function getItemPageRoute(itemId: string) {
|
||||||
|
return new URLCombiner(getItemModulePath(), itemId).toString();
|
||||||
|
}
|
||||||
|
export function getItemEditPath(id: string) {
|
||||||
|
return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEM_EDIT_PATH = ':id/edit';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -22,6 +34,11 @@ import { ItemPageResolver } from './item-page.resolver';
|
|||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver
|
item: ItemPageResolver
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_PATH,
|
||||||
|
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@@ -29,11 +29,13 @@ import { JournalComponent } from './simple/entity-types/journal/journal.componen
|
|||||||
import { JournalVolumeComponent } from './simple/entity-types/journal-volume/journal-volume.component';
|
import { JournalVolumeComponent } from './simple/entity-types/journal-volume/journal-volume.component';
|
||||||
import { JournalIssueComponent } from './simple/entity-types/journal-issue/journal-issue.component';
|
import { JournalIssueComponent } from './simple/entity-types/journal-issue/journal-issue.component';
|
||||||
import { EntityComponent } from './simple/entity-types/shared/entity.component';
|
import { EntityComponent } from './simple/entity-types/shared/entity.component';
|
||||||
|
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
EditItemPageModule,
|
||||||
ItemPageRoutingModule,
|
ItemPageRoutingModule,
|
||||||
SearchPageModule
|
SearchPageModule
|
||||||
],
|
],
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<div class="container w-100 h-100">
|
<div class="container w-100 h-100">
|
||||||
<div class="text-center mt-5 row justify-content-md-center">
|
<div class="text-center mt-5 row justify-content-md-center">
|
||||||
<div>
|
<div class="mx-auto">
|
||||||
<img class="mb-4 login-logo" src="assets/images/dspace-logo.png">
|
<img class="mb-4 login-logo" src="assets/images/dspace-logo.png">
|
||||||
<h1 class="h3 mb-0 font-weight-normal">{{"logout.form.header" | translate}}</h1>
|
<h1 class="h3 mb-0 font-weight-normal">{{"logout.form.header" | translate}}</h1>
|
||||||
<ds-log-out></ds-log-out>
|
<ds-log-out></ds-log-out>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fa float-right"
|
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fas float-right"
|
||||||
[ngClass]="(isCollapsed() | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
[ngClass]="(isCollapsed() | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
||||||
<div [@slide]="(isCollapsed() | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : collapsed}">
|
<div [@slide]="(isCollapsed() | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : collapsed}">
|
||||||
<ds-search-facet-filter-wrapper [filterConfig]="filter"></ds-search-facet-filter-wrapper>
|
<ds-search-facet-filter-wrapper [filterConfig]="filter"></ds-search-facet-filter-wrapper>
|
||||||
|
@@ -2,11 +2,12 @@
|
|||||||
@import '../../../../styles/mixins.scss';
|
@import '../../../../styles/mixins.scss';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
border: 1px solid map-get($theme-colors, light);
|
border: 1px solid map-get($theme-colors, light);
|
||||||
.search-filter-wrapper.closed {
|
cursor: pointer;
|
||||||
overflow: hidden;
|
.search-filter-wrapper.closed {
|
||||||
}
|
overflow: hidden;
|
||||||
.filter-toggle {
|
}
|
||||||
line-height: $line-height-base;
|
.filter-toggle {
|
||||||
}
|
line-height: $line-height-base;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@@ -58,7 +58,6 @@ export class SearchFiltersComponent {
|
|||||||
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
||||||
*/
|
*/
|
||||||
isActive(filterConfig: SearchFilterConfig): Observable<boolean> {
|
isActive(filterConfig: SearchFilterConfig): Observable<boolean> {
|
||||||
// console.log(filter.name);
|
|
||||||
return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
|
return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
|
||||||
mergeMap((isActive) => {
|
mergeMap((isActive) => {
|
||||||
if (isNotEmpty(isActive)) {
|
if (isNotEmpty(isActive)) {
|
||||||
|
@@ -26,7 +26,7 @@
|
|||||||
<ds-view-mode-switch></ds-view-mode-switch>
|
<ds-view-mode-switch></ds-view-mode-switch>
|
||||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||||
class="fa fa-sliders"></i> {{"search.sidebar.open"
|
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||||
| translate}}
|
| translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<button (click)="toggleSidebar.emit()"
|
<button (click)="toggleSidebar.emit()"
|
||||||
aria-controls="#search-body"
|
aria-controls="#search-body"
|
||||||
class="btn btn-outline-primary float-right close-sidebar"><i
|
class="btn btn-outline-primary float-right close-sidebar"><i
|
||||||
class="fa fa-arrow-right"></i> {{"search.sidebar.close" | translate}}
|
class="fas fa-arrow-right" [title]="'search.sidebar.close' | translate"></i> {{"search.sidebar.close" | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="search-sidebar-content">
|
<div id="search-sidebar-content">
|
||||||
|
@@ -3,6 +3,10 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||||
|
|
||||||
|
const ITEM_MODULE_PATH = 'items';
|
||||||
|
export function getItemModulePath() {
|
||||||
|
return `/${ITEM_MODULE_PATH}`;
|
||||||
|
}
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
@@ -10,7 +14,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
|||||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
||||||
{ path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
{ path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
||||||
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
||||||
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
||||||
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
|
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
|
||||||
|
@@ -1,20 +1,22 @@
|
|||||||
<div class="outer-wrapper">
|
<div class="outer-wrapper">
|
||||||
<div class="inner-wrapper">
|
<ds-admin-sidebar></ds-admin-sidebar>
|
||||||
<ds-header></ds-header>
|
<div class="inner-wrapper" [@slideSidebarPadding]="{
|
||||||
|
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
|
||||||
|
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
|
||||||
|
}">
|
||||||
|
<ds-header-navbar-wrapper></ds-header-navbar-wrapper>
|
||||||
|
|
||||||
<ds-notifications-board
|
<ds-notifications-board
|
||||||
[options]="config.notifications">
|
[options]="config.notifications">
|
||||||
</ds-notifications-board>
|
</ds-notifications-board>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<div class="container" *ngIf="isLoading">
|
<div class="container" *ngIf="isLoading">
|
||||||
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<ds-footer></ds-footer>
|
<ds-footer></ds-footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
@import '../styles/variables.scss';
|
@import '../styles/variables.scss';
|
||||||
|
@import '../styles/helpers/font_awesome_imports.scss';
|
||||||
@import '../../node_modules/bootstrap/scss/bootstrap.scss';
|
@import '../../node_modules/bootstrap/scss/bootstrap.scss';
|
||||||
@import '../../node_modules/nouislider/distribute/nouislider.min.css';
|
@import '../../node_modules/nouislider/distribute/nouislider.min';
|
||||||
@import "../../node_modules/font-awesome/scss/font-awesome.scss";
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -11,8 +11,8 @@ html {
|
|||||||
body {
|
body {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
// Sticky Footer
|
|
||||||
|
|
||||||
|
// Sticky Footer
|
||||||
.outer-wrapper {
|
.outer-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -25,10 +25,22 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
|
z-index: $main-z-index;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
margin-top: $content-spacing;
|
margin-top: $content-spacing;
|
||||||
margin-bottom: $content-spacing;
|
margin-bottom: $content-spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ds-header-navbar-wrapper {
|
||||||
|
z-index: $nav-z-index;
|
||||||
|
}
|
||||||
|
|
||||||
|
ds-admin-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
z-index: $sidebar-z-index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -35,11 +35,18 @@ import { AngularticsMock } from './shared/mocks/mock-angulartics.service';
|
|||||||
import { AuthServiceMock } from './shared/mocks/mock-auth.service';
|
import { AuthServiceMock } from './shared/mocks/mock-auth.service';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
|
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||||
|
import { CSSVariableServiceStub } from './shared/testing/css-variable-service-stub';
|
||||||
|
import { MenuServiceStub } from './shared/testing/menu-service-stub';
|
||||||
|
import { HostWindowService } from './shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from './shared/testing/host-window-service-stub';
|
||||||
|
|
||||||
let comp: AppComponent;
|
let comp: AppComponent;
|
||||||
let fixture: ComponentFixture<AppComponent>;
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
let de: DebugElement;
|
let de: DebugElement;
|
||||||
let el: HTMLElement;
|
let el: HTMLElement;
|
||||||
|
const menuService = new MenuServiceStub();
|
||||||
|
|
||||||
describe('App component', () => {
|
describe('App component', () => {
|
||||||
|
|
||||||
@@ -64,6 +71,9 @@ describe('App component', () => {
|
|||||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
||||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
{ provide: Router, useValue: {} },
|
{ provide: Router, useValue: {} },
|
||||||
|
{ provide: MenuService, useValue: menuService },
|
||||||
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
AppComponent
|
AppComponent
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
@@ -75,7 +85,6 @@ describe('App component', () => {
|
|||||||
fixture = TestBed.createComponent(AppComponent);
|
fixture = TestBed.createComponent(AppComponent);
|
||||||
|
|
||||||
comp = fixture.componentInstance; // component test instance
|
comp = fixture.componentInstance; // component test instance
|
||||||
|
|
||||||
// query for the <div class='outer-wrapper'> by CSS element selector
|
// query for the <div class='outer-wrapper'> by CSS element selector
|
||||||
de = fixture.debugElement.query(By.css('div.outer-wrapper'));
|
de = fixture.debugElement.query(By.css('div.outer-wrapper'));
|
||||||
el = de.nativeElement;
|
el = de.nativeElement;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { filter, first, take } from 'rxjs/operators';
|
import { filter, first, map, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -23,16 +23,29 @@ import { NativeWindowRef, NativeWindowService } from './shared/services/window.s
|
|||||||
import { isAuthenticated } from './core/auth/selectors';
|
import { isAuthenticated } from './core/auth/selectors';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
|
import variables from '../styles/_exposed_variables.scss';
|
||||||
|
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||||
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
|
import { MenuID } from './shared/menu/initial-menus-state';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { slideSidebarPadding } from './shared/animations/slide';
|
||||||
|
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||||
|
import { HostWindowService } from './shared/host-window.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
animations: [slideSidebarPadding]
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, AfterViewInit {
|
export class AppComponent implements OnInit, AfterViewInit {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
sidebarVisible: Observable<boolean>;
|
||||||
|
slideSidebarOver: Observable<boolean>;
|
||||||
|
collapsedSidebarWidth: Observable<string>;
|
||||||
|
totalSidebarWidth: Observable<string>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
||||||
@@ -42,21 +55,35 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private metadata: MetadataService,
|
private metadata: MetadataService,
|
||||||
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private router: Router
|
private router: Router,
|
||||||
|
private cssService: CSSVariableService,
|
||||||
|
private menuService: MenuService,
|
||||||
|
private windowService: HostWindowService
|
||||||
) {
|
) {
|
||||||
// this language will be used as a fallback when a translation isn't found in the current language
|
// Load all the languages that are defined as active from the config file
|
||||||
translate.setDefaultLang('en');
|
translate.addLangs(config.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
||||||
// the lang to use, if the lang isn't available, it will use the current loader to get them
|
|
||||||
translate.use('en');
|
// Load the default language from the config file
|
||||||
|
translate.setDefaultLang(config.defaultLanguage);
|
||||||
|
|
||||||
|
// Attempt to get the browser language from the user
|
||||||
|
if (translate.getLangs().includes(translate.getBrowserLang())) {
|
||||||
|
translate.use(translate.getBrowserLang());
|
||||||
|
} else {
|
||||||
|
translate.use(config.defaultLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
metadata.listenForRouteChange();
|
metadata.listenForRouteChange();
|
||||||
|
|
||||||
if (config.debug) {
|
if (config.debug) {
|
||||||
console.info(config);
|
console.info(config);
|
||||||
}
|
}
|
||||||
|
this.storeCSSVariables();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|
||||||
const env: string = this.config.production ? 'Production' : 'Development';
|
const env: string = this.config.production ? 'Production' : 'Development';
|
||||||
const color: string = this.config.production ? 'red' : 'green';
|
const color: string = this.config.production ? 'red' : 'green';
|
||||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||||
@@ -67,7 +94,23 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
first(),
|
first(),
|
||||||
filter((authenticated) => !authenticated)
|
filter((authenticated) => !authenticated)
|
||||||
).subscribe((authenticated) => this.authService.checkAuthenticationToken());
|
).subscribe((authenticated) => this.authService.checkAuthenticationToken());
|
||||||
|
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
||||||
|
|
||||||
|
this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth');
|
||||||
|
this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth');
|
||||||
|
|
||||||
|
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
||||||
|
this.slideSidebarOver = combineLatestObservable(sidebarCollapsed, this.windowService.isXsOrSm())
|
||||||
|
.pipe(
|
||||||
|
map(([collapsed, mobile]) => collapsed || mobile)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private storeCSSVariables() {
|
||||||
|
const vars = variables.locals || {};
|
||||||
|
Object.keys(vars).forEach((name: string) => {
|
||||||
|
this.cssService.addCSSVariable(name, vars[name]);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
|
|
||||||
import { HeaderEffects } from './header/header.effects';
|
|
||||||
import { StoreEffects } from './store.effects';
|
import { StoreEffects } from './store.effects';
|
||||||
import { NotificationsEffects } from './shared/notifications/notifications.effects';
|
import { NotificationsEffects } from './shared/notifications/notifications.effects';
|
||||||
|
import { NavbarEffects } from './navbar/navbar.effects';
|
||||||
|
|
||||||
export const appEffects = [
|
export const appEffects = [
|
||||||
StoreEffects,
|
StoreEffects,
|
||||||
HeaderEffects,
|
NavbarEffects,
|
||||||
NotificationsEffects
|
NotificationsEffects,
|
||||||
];
|
];
|
||||||
|
@@ -31,6 +31,11 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s
|
|||||||
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
||||||
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||||
import { SharedModule } from './shared/shared.module';
|
import { SharedModule } from './shared/shared.module';
|
||||||
|
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
|
||||||
|
import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.component';
|
||||||
|
import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||||
|
import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||||
|
import { NavbarModule } from './navbar/navbar.module';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return ENV_CONFIG;
|
return ENV_CONFIG;
|
||||||
@@ -48,6 +53,7 @@ export function getMetaReducers(config: GlobalConfig): Array<MetaReducer<AppStat
|
|||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
NavbarModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
CoreModule.forRoot(),
|
CoreModule.forRoot(),
|
||||||
@@ -88,6 +94,10 @@ const PROVIDERS = [
|
|||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
|
HeaderNavbarWrapperComponent,
|
||||||
|
AdminSidebarComponent,
|
||||||
|
AdminSidebarSectionComponent,
|
||||||
|
ExpandableAdminSidebarSectionComponent,
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
PageNotFoundComponent,
|
PageNotFoundComponent,
|
||||||
NotificationComponent,
|
NotificationComponent,
|
||||||
@@ -106,10 +116,14 @@ const EXPORTS = [
|
|||||||
...PROVIDERS
|
...PROVIDERS
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS
|
...DECLARATIONS,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...EXPORTS
|
...EXPORTS
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AdminSidebarSectionComponent,
|
||||||
|
ExpandableAdminSidebarSectionComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
import { ActionReducerMap } from '@ngrx/store';
|
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
import * as fromRouter from '@ngrx/router-store';
|
||||||
|
|
||||||
import { headerReducer, HeaderState } from './header/header.reducer';
|
|
||||||
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
||||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||||
import {
|
import {
|
||||||
@@ -12,29 +10,47 @@ import {
|
|||||||
filterReducer,
|
filterReducer,
|
||||||
SearchFiltersState
|
SearchFiltersState
|
||||||
} from './+search-page/search-filters/search-filter/search-filter.reducer';
|
} from './+search-page/search-filters/search-filter/search-filter.reducer';
|
||||||
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
|
import {
|
||||||
|
notificationsReducer,
|
||||||
|
NotificationsState
|
||||||
|
} from './shared/notifications/notifications.reducers';
|
||||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||||
|
import { hasValue } from './shared/empty.util';
|
||||||
|
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
||||||
|
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
router: fromRouter.RouterReducerState;
|
router: fromRouter.RouterReducerState;
|
||||||
hostWindow: HostWindowState;
|
hostWindow: HostWindowState;
|
||||||
header: HeaderState;
|
|
||||||
forms: FormState;
|
forms: FormState;
|
||||||
notifications: NotificationsState;
|
notifications: NotificationsState;
|
||||||
searchSidebar: SearchSidebarState;
|
searchSidebar: SearchSidebarState;
|
||||||
searchFilter: SearchFiltersState;
|
searchFilter: SearchFiltersState;
|
||||||
truncatable: TruncatablesState;
|
truncatable: TruncatablesState;
|
||||||
|
cssVariables: CSSVariablesState;
|
||||||
|
menus: MenusState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appReducers: ActionReducerMap<AppState> = {
|
export const appReducers: ActionReducerMap<AppState> = {
|
||||||
router: fromRouter.routerReducer,
|
router: fromRouter.routerReducer,
|
||||||
hostWindow: hostWindowReducer,
|
hostWindow: hostWindowReducer,
|
||||||
header: headerReducer,
|
|
||||||
forms: formReducer,
|
forms: formReducer,
|
||||||
notifications: notificationsReducer,
|
notifications: notificationsReducer,
|
||||||
searchSidebar: sidebarReducer,
|
searchSidebar: sidebarReducer,
|
||||||
searchFilter: filterReducer,
|
searchFilter: filterReducer,
|
||||||
truncatable: truncatableReducer
|
truncatable: truncatableReducer,
|
||||||
|
cssVariables: cssVariablesReducer,
|
||||||
|
menus: menusReducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routerStateSelector = (state: AppState) => state.router;
|
export const routerStateSelector = (state: AppState) => state.router;
|
||||||
|
|
||||||
|
export function keySelector<T>(key: string, selector): MemoizedSelector<AppState, T> {
|
||||||
|
return createSelector(selector, (state) => {
|
||||||
|
if (hasValue(state)) {
|
||||||
|
return state[key];
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -41,7 +41,6 @@ export class ServerAuthService extends AuthService {
|
|||||||
|
|
||||||
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
|
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
|
||||||
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
|
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
|
||||||
// person$.subscribe(() => console.log('test'));
|
|
||||||
return person$.pipe(map((eperson) => eperson.payload));
|
return person$.pipe(map((eperson) => eperson.payload));
|
||||||
} else {
|
} else {
|
||||||
throw(new Error('Not authenticated'));
|
throw(new Error('Not authenticated'));
|
||||||
|
@@ -65,6 +65,8 @@ import { UploaderService } from '../shared/uploader/uploader.service';
|
|||||||
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
|
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
|
||||||
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
||||||
import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service';
|
import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service';
|
||||||
|
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
||||||
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -129,6 +131,8 @@ const PROVIDERS = [
|
|||||||
UploaderService,
|
UploaderService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
DSpaceObjectDataService,
|
DSpaceObjectDataService,
|
||||||
|
CSSVariableService,
|
||||||
|
MenuService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
@@ -1,24 +1,44 @@
|
|||||||
import { Store } from '@ngrx/store';
|
import {Store} from '@ngrx/store';
|
||||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
import {cold, getTestScheduler} from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import {TestScheduler} from 'rxjs/testing';
|
||||||
import { BrowseService } from '../browse/browse.service';
|
import {BrowseService} from '../browse/browse.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import {RemoteDataBuildService} from '../cache/builders/remote-data-build.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import {ResponseCacheService} from '../cache/response-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import {CoreState} from '../core.reducers';
|
||||||
import { ItemDataService } from './item-data.service';
|
import {ItemDataService} from './item-data.service';
|
||||||
import { RequestService } from './request.service';
|
import {RequestService} from './request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import {HALEndpointService} from '../shared/hal-endpoint.service';
|
||||||
import { FindAllOptions } from './request.models';
|
import {FindAllOptions, RestRequest} from './request.models';
|
||||||
|
import {Observable, of as observableOf} from 'rxjs';
|
||||||
|
import {ResponseCacheEntry} from '../cache/response-cache.reducer';
|
||||||
|
import {RestResponse} from '../cache/response-cache.models';
|
||||||
|
|
||||||
describe('ItemDataService', () => {
|
describe('ItemDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: ItemDataService;
|
let service: ItemDataService;
|
||||||
let bs: BrowseService;
|
let bs: BrowseService;
|
||||||
const requestService = {} as RequestService;
|
const requestService = {
|
||||||
const responseCache = {} as ResponseCacheService;
|
generateRequestId(): string {
|
||||||
|
return scopeID;
|
||||||
|
},
|
||||||
|
configure(request: RestRequest) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
} as RequestService;
|
||||||
|
const responseCache = {
|
||||||
|
get(href: string) {
|
||||||
|
const responseCacheEntry = new ResponseCacheEntry();
|
||||||
|
responseCacheEntry.response = new RestResponse(true, '200');
|
||||||
|
return observableOf(responseCacheEntry);
|
||||||
|
}
|
||||||
|
} as ResponseCacheService;
|
||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
const store = {} as Store<CoreState>;
|
const store = {} as Store<CoreState>;
|
||||||
const halEndpointService = {} as HALEndpointService;
|
const halEndpointService = {
|
||||||
|
getEndpoint(linkPath: string): Observable<string> {
|
||||||
|
return cold('a', {a: itemEndpoint});
|
||||||
|
}
|
||||||
|
} as HALEndpointService;
|
||||||
|
|
||||||
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
||||||
const options = Object.assign(new FindAllOptions(), {
|
const options = Object.assign(new FindAllOptions(), {
|
||||||
@@ -34,10 +54,12 @@ describe('ItemDataService', () => {
|
|||||||
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
|
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
|
||||||
const serviceEndpoint = `https://rest.api/core/items`;
|
const serviceEndpoint = `https://rest.api/core/items`;
|
||||||
const browseError = new Error('getBrowseURL failed');
|
const browseError = new Error('getBrowseURL failed');
|
||||||
|
const itemEndpoint = 'https://rest.api/core/items';
|
||||||
|
const ScopedItemEndpoint = `https://rest.api/core/items/${scopeID}`;
|
||||||
|
|
||||||
function initMockBrowseService(isSuccessful: boolean) {
|
function initMockBrowseService(isSuccessful: boolean) {
|
||||||
const obs = isSuccessful ?
|
const obs = isSuccessful ?
|
||||||
cold('--a-', { a: itemBrowseEndpoint }) :
|
cold('--a-', {a: itemBrowseEndpoint}) :
|
||||||
cold('--#-', undefined, browseError);
|
cold('--#-', undefined, browseError);
|
||||||
return jasmine.createSpyObj('bs', {
|
return jasmine.createSpyObj('bs', {
|
||||||
getBrowseURLFor: obs
|
getBrowseURLFor: obs
|
||||||
@@ -65,7 +87,7 @@ describe('ItemDataService', () => {
|
|||||||
service = initTestService();
|
service = initTestService();
|
||||||
|
|
||||||
const result = service.getBrowseEndpoint(options);
|
const result = service.getBrowseEndpoint(options);
|
||||||
const expected = cold('--b-', { b: scopedEndpoint });
|
const expected = cold('--b-', {b: scopedEndpoint});
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -83,4 +105,70 @@ describe('ItemDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getItemWithdrawEndpoint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the endpoint to withdraw and reinstate items', () => {
|
||||||
|
const result = service.getItemWithdrawEndpoint(scopeID);
|
||||||
|
const expected = cold('a', {a: ScopedItemEndpoint});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setWithDrawn', () => {
|
||||||
|
const expected = new RestResponse(true, '200');
|
||||||
|
const result = service.setWithDrawn(scopeID, true);
|
||||||
|
result.subscribe((v) => expect(v).toEqual(expected));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getItemDiscoverableEndpoint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the endpoint to make an item private or public', () => {
|
||||||
|
const result = service.getItemDiscoverableEndpoint(scopeID);
|
||||||
|
const expected = cold('a', {a: ScopedItemEndpoint});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setDiscoverable', () => {
|
||||||
|
const expected = new RestResponse(true, '200');
|
||||||
|
const result = service.setDiscoverable(scopeID, false);
|
||||||
|
result.subscribe((v) => expect(v).toEqual(expected));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getItemDeleteEndpoint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the endpoint to make an item private or public', () => {
|
||||||
|
const result = service.getItemDeleteEndpoint(scopeID);
|
||||||
|
const expected = cold('a', {a: ScopedItemEndpoint});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete the item', () => {
|
||||||
|
const expected = new RestResponse(true, '200');
|
||||||
|
const result = service.delete(scopeID);
|
||||||
|
result.subscribe((v) => expect(v).toEqual(expected));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,21 +1,22 @@
|
|||||||
|
import {distinctUntilChanged, filter, map} from 'rxjs/operators';
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {Store} from '@ngrx/store';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {isNotEmpty} from '../../shared/empty.util';
|
||||||
|
import {BrowseService} from '../browse/browse.service';
|
||||||
|
import {RemoteDataBuildService} from '../cache/builders/remote-data-build.service';
|
||||||
|
import {NormalizedItem} from '../cache/models/normalized-item.model';
|
||||||
|
import {ResponseCacheService} from '../cache/response-cache.service';
|
||||||
|
import {CoreState} from '../core.reducers';
|
||||||
|
import {Item} from '../shared/item.model';
|
||||||
|
import {URLCombiner} from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
import {distinctUntilChanged, map, filter} from 'rxjs/operators';
|
import {DataService} from './data.service';
|
||||||
import { Injectable } from '@angular/core';
|
import {RequestService} from './request.service';
|
||||||
import { Store } from '@ngrx/store';
|
import {HALEndpointService} from '../shared/hal-endpoint.service';
|
||||||
import { Observable } from 'rxjs';
|
import {DeleteRequest, FindAllOptions, PatchRequest, RestRequest} from './request.models';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import {configureRequest, getResponseFromSelflink} from '../shared/operators';
|
||||||
import { BrowseService } from '../browse/browse.service';
|
import {ResponseCacheEntry} from '../cache/response-cache.reducer';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { NormalizedItem } from '../cache/models/normalized-item.model';
|
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { Item } from '../shared/item.model';
|
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
|
||||||
|
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { RequestService } from './request.service';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { FindAllOptions } from './request.models';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||||
@@ -48,4 +49,93 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
|
|||||||
distinctUntilChanged(),);
|
distinctUntilChanged(),);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for item withdrawal and reinstatement
|
||||||
|
* @param itemId
|
||||||
|
*/
|
||||||
|
public getItemWithdrawEndpoint(itemId: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getFindByIDHref(endpoint, itemId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint to make item private and public
|
||||||
|
* @param itemId
|
||||||
|
*/
|
||||||
|
public getItemDiscoverableEndpoint(itemId: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getFindByIDHref(endpoint, itemId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint to delete the item
|
||||||
|
* @param itemId
|
||||||
|
*/
|
||||||
|
public getItemDeleteEndpoint(itemId: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getFindByIDHref(endpoint, itemId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the isWithdrawn state of an item to a specified state
|
||||||
|
* @param itemId
|
||||||
|
* @param withdrawn
|
||||||
|
*/
|
||||||
|
public setWithDrawn(itemId: string, withdrawn: boolean) {
|
||||||
|
const patchOperation = [{
|
||||||
|
op: 'replace', path: '/withdrawn', value: withdrawn
|
||||||
|
}];
|
||||||
|
return this.getItemWithdrawEndpoint(itemId).pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) =>
|
||||||
|
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
||||||
|
),
|
||||||
|
configureRequest(this.requestService),
|
||||||
|
map((request: RestRequest) => request.href),
|
||||||
|
getResponseFromSelflink(this.responseCache),
|
||||||
|
map((responseCacheEntry: ResponseCacheEntry) => responseCacheEntry.response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the isDiscoverable state of an item to a specified state
|
||||||
|
* @param itemId
|
||||||
|
* @param discoverable
|
||||||
|
*/
|
||||||
|
public setDiscoverable(itemId: string, discoverable: boolean) {
|
||||||
|
const patchOperation = [{
|
||||||
|
op: 'replace', path: '/discoverable', value: discoverable
|
||||||
|
}];
|
||||||
|
return this.getItemDiscoverableEndpoint(itemId).pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) =>
|
||||||
|
new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation)
|
||||||
|
),
|
||||||
|
configureRequest(this.requestService),
|
||||||
|
map((request: RestRequest) => request.href),
|
||||||
|
getResponseFromSelflink(this.responseCache),
|
||||||
|
map((responseCacheEntry: ResponseCacheEntry) => responseCacheEntry.response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the item
|
||||||
|
* @param itemId
|
||||||
|
*/
|
||||||
|
public delete(itemId: string) {
|
||||||
|
return this.getItemDeleteEndpoint(itemId).pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) =>
|
||||||
|
new DeleteRequest(this.requestService.generateRequestId(), endpointURL)
|
||||||
|
),
|
||||||
|
configureRequest(this.requestService),
|
||||||
|
map((request: RestRequest) => request.href),
|
||||||
|
getResponseFromSelflink(this.responseCache),
|
||||||
|
map((responseCacheEntry: ResponseCacheEntry) => responseCacheEntry.response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { filter, map } from 'rxjs/operators';
|
import { filter, map, tap } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Effect, Actions, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ObjectCacheActionTypes, AddToObjectCacheAction,
|
AddToObjectCacheAction,
|
||||||
|
ObjectCacheActionTypes,
|
||||||
RemoveFromObjectCacheAction
|
RemoveFromObjectCacheAction
|
||||||
} from '../cache/object-cache.actions';
|
} from '../cache/object-cache.actions';
|
||||||
import { RequestActionTypes, RequestConfigureAction } from '../data/request.actions';
|
import { RequestActionTypes, RequestConfigureAction } from '../data/request.actions';
|
||||||
@@ -11,6 +12,11 @@ import { RestRequestMethod } from '../data/request.models';
|
|||||||
import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions';
|
import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { IndexName } from './index.reducer';
|
import { IndexName } from './index.reducer';
|
||||||
|
import {
|
||||||
|
AddMenuSectionAction,
|
||||||
|
MenuActionTypes,
|
||||||
|
RemoveMenuSectionAction
|
||||||
|
} from '../../shared/menu/menu.actions';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UUIDIndexEffects {
|
export class UUIDIndexEffects {
|
||||||
@@ -52,17 +58,6 @@ export class UUIDIndexEffects {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// @Effect() removeRequest$ = this.actions$
|
|
||||||
// .pipe(
|
|
||||||
// ofType(ObjectCacheActionTypes.REMOVE),
|
|
||||||
// map((action: RemoveFromObjectCacheAction) => {
|
|
||||||
// return new RemoveFromIndexByValueAction(
|
|
||||||
// IndexName.OBJECT,
|
|
||||||
// action.payload
|
|
||||||
// );
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
|
|
||||||
constructor(private actions$: Actions) {
|
constructor(private actions$: Actions) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,8 @@ describe('requestReducer', () => {
|
|||||||
const testState: IndexState = {
|
const testState: IndexState = {
|
||||||
[IndexName.OBJECT]: {
|
[IndexName.OBJECT]: {
|
||||||
[key1]: val1
|
[key1]: val1
|
||||||
|
},
|
||||||
|
[IndexName.REQUEST]: {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
deepFreeze(testState);
|
deepFreeze(testState);
|
||||||
|
@@ -7,13 +7,11 @@ import {
|
|||||||
|
|
||||||
export enum IndexName {
|
export enum IndexName {
|
||||||
OBJECT = 'object/uuid-to-self-link',
|
OBJECT = 'object/uuid-to-self-link',
|
||||||
REQUEST = 'get-request/href-to-uuid'
|
REQUEST = 'get-request/href-to-uuid',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IndexState {
|
export type IndexState = {
|
||||||
// TODO this should be `[name in IndexName]: {` but that's currently broken,
|
[name in IndexName]: {
|
||||||
// see https://github.com/Microsoft/TypeScript/issues/13042
|
|
||||||
[name: string]: {
|
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -61,6 +61,6 @@ export class Community extends DSpaceObject {
|
|||||||
|
|
||||||
collections: Observable<RemoteData<PaginatedList<Collection>>>;
|
collections: Observable<RemoteData<PaginatedList<Collection>>>;
|
||||||
|
|
||||||
subcommunities: Observable<RemoteData<PaginatedList<Collection>>>;
|
subcommunities: Observable<RemoteData<PaginatedList<Community>>>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,23 @@
|
|||||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
import {cold, getTestScheduler, hot} from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import {TestScheduler} from 'rxjs/testing';
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import {getMockRequestService} from '../../shared/mocks/mock-request.service';
|
||||||
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
import {getMockResponseCacheService} from '../../shared/mocks/mock-response-cache.service';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import {ResponseCacheEntry} from '../cache/response-cache.reducer';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import {ResponseCacheService} from '../cache/response-cache.service';
|
||||||
import { GetRequest, RestRequest } from '../data/request.models';
|
import {GetRequest} from '../data/request.models';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import {RequestEntry} from '../data/request.reducer';
|
||||||
import { RequestService } from '../data/request.service';
|
import {RequestService} from '../data/request.service';
|
||||||
import {
|
import {
|
||||||
configureRequest,
|
configureRequest,
|
||||||
filterSuccessfulResponses, getRemoteDataPayload,
|
filterSuccessfulResponses,
|
||||||
getRequestFromSelflink, getResourceLinksFromResponse,
|
getAllSucceededRemoteData,
|
||||||
getResponseFromSelflink
|
getRemoteDataPayload,
|
||||||
|
getRequestFromSelflink,
|
||||||
|
getResourceLinksFromResponse,
|
||||||
|
getResponseFromSelflink,
|
||||||
|
getSucceededRemoteData
|
||||||
} from './operators';
|
} from './operators';
|
||||||
|
import {RemoteData} from '../data/remote-data';
|
||||||
|
|
||||||
describe('Core Module - RxJS Operators', () => {
|
describe('Core Module - RxJS Operators', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -20,11 +25,11 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
const testSelfLink = 'https://rest.api/';
|
const testSelfLink = 'https://rest.api/';
|
||||||
|
|
||||||
const testRCEs = {
|
const testRCEs = {
|
||||||
a: { response: { isSuccessful: true, resourceSelfLinks: ['a', 'b', 'c', 'd'] } },
|
a: {response: {isSuccessful: true, resourceSelfLinks: ['a', 'b', 'c', 'd']}},
|
||||||
b: { response: { isSuccessful: false, resourceSelfLinks: ['e', 'f'] } },
|
b: {response: {isSuccessful: false, resourceSelfLinks: ['e', 'f']}},
|
||||||
c: { response: { isSuccessful: undefined, resourceSelfLinks: ['g', 'h', 'i'] } },
|
c: {response: {isSuccessful: undefined, resourceSelfLinks: ['g', 'h', 'i']}},
|
||||||
d: { response: { isSuccessful: true, resourceSelfLinks: ['j', 'k', 'l', 'm', 'n'] } },
|
d: {response: {isSuccessful: true, resourceSelfLinks: ['j', 'k', 'l', 'm', 'n']}},
|
||||||
e: { response: { isSuccessful: 1, resourceSelfLinks: [] } }
|
e: {response: {isSuccessful: 1, resourceSelfLinks: []}}
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -36,31 +41,31 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
it('should return the RequestEntry corresponding to the self link in the source', () => {
|
it('should return the RequestEntry corresponding to the self link in the source', () => {
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
|
|
||||||
const source = hot('a', { a: testSelfLink });
|
const source = hot('a', {a: testSelfLink});
|
||||||
const result = source.pipe(getRequestFromSelflink(requestService));
|
const result = source.pipe(getRequestFromSelflink(requestService));
|
||||||
const expected = cold('a', { a: new RequestEntry()});
|
const expected = cold('a', {a: new RequestEntry()});
|
||||||
|
|
||||||
expect(result).toBeObservable(expected)
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the requestService to fetch the request by its self link', () => {
|
it('should use the requestService to fetch the request by its self link', () => {
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
|
|
||||||
const source = hot('a', { a: testSelfLink });
|
const source = hot('a', {a: testSelfLink});
|
||||||
scheduler.schedule(() => source.pipe(getRequestFromSelflink(requestService)).subscribe());
|
scheduler.schedule(() => source.pipe(getRequestFromSelflink(requestService)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.getByHref).toHaveBeenCalledWith(testSelfLink)
|
expect(requestService.getByHref).toHaveBeenCalledWith(testSelfLink);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouldn\'t return anything if there is no request matching the self link', () => {
|
it('shouldn\'t return anything if there is no request matching the self link', () => {
|
||||||
requestService = getMockRequestService(cold('a', { a: undefined }));
|
requestService = getMockRequestService(cold('a', {a: undefined}));
|
||||||
|
|
||||||
const source = hot('a', { a: testSelfLink });
|
const source = hot('a', {a: testSelfLink});
|
||||||
const result = source.pipe(getRequestFromSelflink(requestService));
|
const result = source.pipe(getRequestFromSelflink(requestService));
|
||||||
const expected = cold('-');
|
const expected = cold('-');
|
||||||
|
|
||||||
expect(result).toBeObservable(expected)
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,31 +79,31 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
it('should return the ResponseCacheEntry corresponding to the self link in the source', () => {
|
it('should return the ResponseCacheEntry corresponding to the self link in the source', () => {
|
||||||
responseCacheService = getMockResponseCacheService();
|
responseCacheService = getMockResponseCacheService();
|
||||||
|
|
||||||
const source = hot('a', { a: testSelfLink });
|
const source = hot('a', {a: testSelfLink});
|
||||||
const result = source.pipe(getResponseFromSelflink(responseCacheService));
|
const result = source.pipe(getResponseFromSelflink(responseCacheService));
|
||||||
const expected = cold('a', { a: new ResponseCacheEntry()});
|
const expected = cold('a', {a: new ResponseCacheEntry()});
|
||||||
|
|
||||||
expect(result).toBeObservable(expected)
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the responseCacheService to fetch the response by the request\'s link', () => {
|
it('should use the responseCacheService to fetch the response by the request\'s link', () => {
|
||||||
responseCacheService = getMockResponseCacheService();
|
responseCacheService = getMockResponseCacheService();
|
||||||
|
|
||||||
const source = hot('a', { a: testSelfLink });
|
const source = hot('a', {a: testSelfLink});
|
||||||
scheduler.schedule(() => source.pipe(getResponseFromSelflink(responseCacheService)).subscribe());
|
scheduler.schedule(() => source.pipe(getResponseFromSelflink(responseCacheService)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(responseCacheService.get).toHaveBeenCalledWith(testSelfLink)
|
expect(responseCacheService.get).toHaveBeenCalledWith(testSelfLink);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouldn\'t return anything if there is no response matching the request\'s link', () => {
|
it('shouldn\'t return anything if there is no response matching the request\'s link', () => {
|
||||||
responseCacheService = getMockResponseCacheService(undefined, cold('a', { a: undefined }));
|
responseCacheService = getMockResponseCacheService(undefined, cold('a', {a: undefined}));
|
||||||
|
|
||||||
const source = hot('a', { a: testSelfLink });
|
const source = hot('a', {a: testSelfLink});
|
||||||
const result = source.pipe(getResponseFromSelflink(responseCacheService));
|
const result = source.pipe(getResponseFromSelflink(responseCacheService));
|
||||||
const expected = cold('-');
|
const expected = cold('-');
|
||||||
|
|
||||||
expect(result).toBeObservable(expected)
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,7 +113,7 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
const result = source.pipe(filterSuccessfulResponses());
|
const result = source.pipe(filterSuccessfulResponses());
|
||||||
const expected = cold('a--d-', testRCEs);
|
const expected = cold('a--d-', testRCEs);
|
||||||
|
|
||||||
expect(result).toBeObservable(expected)
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,7 +126,7 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
d: testRCEs.d.response.resourceSelfLinks
|
d: testRCEs.d.response.resourceSelfLinks
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBeObservable(expected)
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,24 +134,61 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
it('should call requestService.configure with the source request', () => {
|
it('should call requestService.configure with the source request', () => {
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
const testRequest = new GetRequest('6b789e31-f026-4ff8-8993-4eb3b730c841', testSelfLink);
|
const testRequest = new GetRequest('6b789e31-f026-4ff8-8993-4eb3b730c841', testSelfLink);
|
||||||
const source = hot('a', { a: testRequest });
|
const source = hot('a', {a: testRequest});
|
||||||
scheduler.schedule(() => source.pipe(configureRequest(requestService)).subscribe());
|
scheduler.schedule(() => source.pipe(configureRequest(requestService)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(testRequest)
|
expect(requestService.configure).toHaveBeenCalledWith(testRequest);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getRemoteDataPayload', () => {
|
describe('getRemoteDataPayload', () => {
|
||||||
it('should return the payload of the source RemoteData', () => {
|
it('should return the payload of the source RemoteData', () => {
|
||||||
const testRD = { a: { payload: 'a' } };
|
const testRD = {a: {payload: 'a'}};
|
||||||
const source = hot('a', testRD);
|
const source = hot('a', testRD);
|
||||||
const result = source.pipe(getRemoteDataPayload());
|
const result = source.pipe(getRemoteDataPayload());
|
||||||
const expected = cold('a', {
|
const expected = cold('a', {
|
||||||
a: testRD.a.payload,
|
a: testRD.a.payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBeObservable(expected)
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getSucceededRemoteData', () => {
|
||||||
|
it('should return the first() hasSucceeded RemoteData Observable', () => {
|
||||||
|
const testRD = {
|
||||||
|
a: new RemoteData(false, false, true, null, undefined),
|
||||||
|
b: new RemoteData(false, false, false, null, 'b'),
|
||||||
|
c: new RemoteData(false, false, undefined, null, 'c'),
|
||||||
|
d: new RemoteData(false, false, true, null, 'd'),
|
||||||
|
e: new RemoteData(false, false, true, null, 'e'),
|
||||||
|
};
|
||||||
|
const source = hot('abcde', testRD);
|
||||||
|
const result = source.pipe(getSucceededRemoteData());
|
||||||
|
|
||||||
|
result.subscribe((value) => expect(value)
|
||||||
|
.toEqual(new RemoteData(false, false, true, null, 'd')));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
describe('getAllSucceededRemoteData', () => {
|
||||||
|
it('should return all hasSucceeded RemoteData Observables', () => {
|
||||||
|
const testRD = {
|
||||||
|
a: new RemoteData(false, false, true, null, undefined),
|
||||||
|
b: new RemoteData(false, false, false, null, 'b'),
|
||||||
|
c: new RemoteData(false, false, undefined, null, 'c'),
|
||||||
|
d: new RemoteData(false, false, true, null, 'd'),
|
||||||
|
e: new RemoteData(false, false, true, null, 'e'),
|
||||||
|
};
|
||||||
|
const source = hot('abcde', testRD);
|
||||||
|
const result = source.pipe(getAllSucceededRemoteData());
|
||||||
|
const expected = cold('---de', testRD);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -54,12 +54,16 @@ export const getSucceededRemoteData = () =>
|
|||||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded));
|
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded));
|
||||||
|
|
||||||
|
export const getAllSucceededRemoteData = () =>
|
||||||
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
|
source.pipe(filter((rd: RemoteData<T>) => rd.hasSucceeded));
|
||||||
|
|
||||||
export const toDSpaceObjectListRD = () =>
|
export const toDSpaceObjectListRD = () =>
|
||||||
<T extends DSpaceObject>(source: Observable<RemoteData<PaginatedList<SearchResult<T>>>>): Observable<RemoteData<PaginatedList<T>>> =>
|
<T extends DSpaceObject>(source: Observable<RemoteData<PaginatedList<SearchResult<T>>>>): Observable<RemoteData<PaginatedList<T>>> =>
|
||||||
source.pipe(
|
source.pipe(
|
||||||
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
|
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
|
||||||
const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.dspaceObject);
|
const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.dspaceObject);
|
||||||
const payload = Object.assign(rd.payload, { page: dsoPage }) as any;
|
const payload = Object.assign(rd.payload, {page: dsoPage}) as any;
|
||||||
return Object.assign(rd, {payload: payload});
|
return Object.assign(rd, {payload: payload});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}">
|
||||||
|
<ds-header></ds-header>
|
||||||
|
<ds-navbar></ds-navbar>
|
||||||
|
</div>
|
@@ -0,0 +1,9 @@
|
|||||||
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
|
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||||
|
:host.open {
|
||||||
|
background-color: $white;
|
||||||
|
top: 0;
|
||||||
|
position: sticky;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,40 @@
|
|||||||
|
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '../app.reducer';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
|
import { MenuID } from '../shared/menu/initial-menus-state';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component represents a wrapper for the horizontal navbar and the header
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-header-navbar-wrapper',
|
||||||
|
styleUrls: ['header-navbar-wrapper.component.scss'],
|
||||||
|
templateUrl: 'header-navbar-wrapper.component.html',
|
||||||
|
})
|
||||||
|
export class HeaderNavbarWrapperComponent implements OnInit, OnDestroy {
|
||||||
|
@HostBinding('class.open') isOpen = false;
|
||||||
|
private sub: Subscription;
|
||||||
|
public isNavBarCollapsed: Observable<boolean>;
|
||||||
|
menuID = MenuID.PUBLIC;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private store: Store<AppState>,
|
||||||
|
private menuService: MenuService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.isNavBarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||||
|
this.sub = this.isNavBarCollapsed.subscribe((isCollapsed) => this.isOpen = !isCollapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (hasValue(this.sub)) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,40 +0,0 @@
|
|||||||
import { Action } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { type } from '../shared/ngrx/type';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For each action type in an action group, make a simple
|
|
||||||
* enum object for all of this group's action types.
|
|
||||||
*
|
|
||||||
* The 'type' utility function coerces strings into string
|
|
||||||
* literal types and runs a simple check to guarantee all
|
|
||||||
* action types in the application are unique.
|
|
||||||
*/
|
|
||||||
export const HeaderActionTypes = {
|
|
||||||
COLLAPSE: type('dspace/header/COLLAPSE'),
|
|
||||||
EXPAND: type('dspace/header/EXPAND'),
|
|
||||||
TOGGLE: type('dspace/header/TOGGLE')
|
|
||||||
};
|
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
|
||||||
export class HeaderCollapseAction implements Action {
|
|
||||||
type = HeaderActionTypes.COLLAPSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HeaderExpandAction implements Action {
|
|
||||||
type = HeaderActionTypes.EXPAND;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HeaderToggleAction implements Action {
|
|
||||||
type = HeaderActionTypes.TOGGLE;
|
|
||||||
}
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export a type alias of all actions in this action group
|
|
||||||
* so that reducers can easily compose action types
|
|
||||||
*/
|
|
||||||
export type HeaderAction
|
|
||||||
= HeaderCollapseAction
|
|
||||||
| HeaderExpandAction
|
|
||||||
| HeaderToggleAction
|
|
@@ -1,18 +1,20 @@
|
|||||||
<header>
|
<header>
|
||||||
<nav class="navbar navbar-dark bg-primary navbar-expand-md">
|
<div class="container">
|
||||||
<div [ngClass]="{'clearfix': !(isNavBarCollapsed | async)}">
|
<a class="navbar-brand my-2" routerLink="/home">
|
||||||
<a class="navbar-brand" routerLink="/home">{{ 'title' | translate }}</a>
|
<img src="assets/images/dspace-logo.svg"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-light navbar-expand-md float-right px-0">
|
||||||
|
<a href="#" class="px-1"><i class="fas fa-search fa-lg fa-fw" [title]="'nav.search' | translate"></i></a>
|
||||||
|
<ds-lang-switch></ds-lang-switch>
|
||||||
|
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||||
|
<div class="pl-2">
|
||||||
|
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
||||||
|
aria-controls="collapsingNav"
|
||||||
|
aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<button class="navbar-toggler" type="button" (click)="toggle()" aria-controls="collapsingNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon fa fa-bars fa-fw" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
<div [ngbCollapse]="(isNavBarCollapsed | async)" class="collapse navbar-collapse" id="collapsingNav">
|
|
||||||
<ul class="navbar-nav mr-auto">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" routerLink="/home" routerLinkActive="active"><i class="fa fa-home fa-fw" aria-hidden="true"></i> {{ 'nav.home' | translate }}<span class="sr-only">(current)</span></a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ds-auth-nav-menu></ds-auth-nav-menu>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
</header>
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
@import '../../styles/variables.scss';
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
header nav.navbar {
|
.navbar-brand img {
|
||||||
border-radius: 0;
|
height: $header-logo-height;
|
||||||
|
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
|
||||||
|
height: $header-logo-height-xs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.navbar-toggler .navbar-toggler-icon {
|
||||||
header nav.navbar .navbar-toggler:hover {
|
background-image: none !important;
|
||||||
cursor: pointer;
|
line-height: 1.5;
|
||||||
}
|
|
||||||
|
|
||||||
header nav.navbar .navbar-toggler .navbar-toggler-icon {
|
|
||||||
background-image: none !important;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
@@ -1,46 +1,31 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { HeaderComponent } from './header.component';
|
|
||||||
import { HeaderState } from './header.reducer';
|
|
||||||
import { HeaderToggleAction } from './header.actions';
|
|
||||||
import { AuthNavMenuComponent } from '../shared/auth-nav-menu/auth-nav-menu.component';
|
|
||||||
import { LogInComponent } from '../shared/log-in/log-in.component';
|
|
||||||
import { LogOutComponent } from '../shared/log-out/log-out.component';
|
|
||||||
import { LoadingComponent } from '../shared/loading/loading.component';
|
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
|
||||||
import { HostWindowServiceStub } from '../shared/testing/host-window-service-stub';
|
|
||||||
import { RouterStub } from '../shared/testing/router-stub';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import * as ngrx from '@ngrx/store';
|
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
|
import { MenuServiceStub } from '../shared/testing/menu-service-stub';
|
||||||
|
import { HeaderComponent } from './header.component';
|
||||||
|
|
||||||
let comp: HeaderComponent;
|
let comp: HeaderComponent;
|
||||||
let fixture: ComponentFixture<HeaderComponent>;
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
let store: Store<HeaderState>;
|
|
||||||
|
|
||||||
describe('HeaderComponent', () => {
|
describe('HeaderComponent', () => {
|
||||||
|
const menuService = new MenuServiceStub();
|
||||||
|
|
||||||
// async beforeEach
|
// async beforeEach
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
StoreModule.forRoot({}),
|
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
NgbCollapseModule.forRoot(),
|
|
||||||
NoopAnimationsModule,
|
NoopAnimationsModule,
|
||||||
ReactiveFormsModule],
|
ReactiveFormsModule],
|
||||||
declarations: [HeaderComponent],
|
declarations: [HeaderComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
{ provide: MenuService, useValue: menuService }
|
||||||
{ provide: Router, useClass: RouterStub },
|
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
@@ -49,63 +34,25 @@ describe('HeaderComponent', () => {
|
|||||||
|
|
||||||
// synchronous beforeEach
|
// synchronous beforeEach
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'getMenuTopSections').and.returnValue(observableOf([]));
|
||||||
|
|
||||||
fixture = TestBed.createComponent(HeaderComponent);
|
fixture = TestBed.createComponent(HeaderComponent);
|
||||||
|
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
store = fixture.debugElement.injector.get(Store) as Store<HeaderState>;
|
|
||||||
spyOn(store, 'dispatch');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the toggle button is clicked', () => {
|
describe('when the toggle button is clicked', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'toggleMenu');
|
||||||
const navbarToggler = fixture.debugElement.query(By.css('.navbar-toggler'));
|
const navbarToggler = fixture.debugElement.query(By.css('.navbar-toggler'));
|
||||||
navbarToggler.triggerEventHandler('click', null);
|
navbarToggler.triggerEventHandler('click', null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a HeaderToggleAction', () => {
|
it('should call toggleMenu on the menuService', () => {
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new HeaderToggleAction());
|
expect(menuService.toggleMenu).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when navCollapsed in the store is true', () => {
|
|
||||||
let menu: HTMLElement;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement;
|
|
||||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
|
||||||
return () => {
|
|
||||||
return () => observableOf({ navCollapsed: true })
|
|
||||||
};
|
|
||||||
});
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should close the menu', () => {
|
|
||||||
expect(menu.classList).not.toContain('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when navCollapsed in the store is false', () => {
|
|
||||||
let menu: HTMLElement;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement;
|
|
||||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
|
||||||
return () => {
|
|
||||||
return () => observableOf(false)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should open the menu', () => {
|
|
||||||
expect(menu.classList).toContain('show');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,43 +1,31 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { Observable } from 'rxjs';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { RouterReducerState } from '@ngrx/router-store';
|
import { MenuID } from '../shared/menu/initial-menus-state';
|
||||||
|
|
||||||
import { HeaderState } from './header.reducer';
|
|
||||||
import { HeaderToggleAction } from './header.actions';
|
|
||||||
import { AppState } from '../app.reducer';
|
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
|
||||||
|
|
||||||
const headerStateSelector = (state: AppState) => state.header;
|
|
||||||
const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the header with the logo and simple navigation
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-header',
|
selector: 'ds-header',
|
||||||
styleUrls: ['header.component.scss'],
|
styleUrls: ['header.component.scss'],
|
||||||
templateUrl: 'header.component.html',
|
templateUrl: 'header.component.html',
|
||||||
})
|
})
|
||||||
export class HeaderComponent implements OnInit {
|
export class HeaderComponent {
|
||||||
/**
|
/**
|
||||||
* Whether user is authenticated.
|
* Whether user is authenticated.
|
||||||
* @type {Observable<string>}
|
* @type {Observable<string>}
|
||||||
*/
|
*/
|
||||||
public isAuthenticated: Observable<boolean>;
|
public isAuthenticated: Observable<boolean>;
|
||||||
public isNavBarCollapsed: Observable<boolean>;
|
|
||||||
public showAuth = false;
|
public showAuth = false;
|
||||||
|
menuID = MenuID.PUBLIC;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private store: Store<AppState>,
|
private menuService: MenuService
|
||||||
private windowService: HostWindowService
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
public toggleNavbar(): void {
|
||||||
// set loading
|
this.menuService.toggleMenu(this.menuID);
|
||||||
this.isNavBarCollapsed = this.store.pipe(select(navCollapsedSelector));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggle(): void {
|
|
||||||
this.store.dispatch(new HeaderToggleAction());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,28 +0,0 @@
|
|||||||
import { map } from 'rxjs/operators';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Effect, Actions, ofType } from '@ngrx/effects'
|
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
|
||||||
|
|
||||||
import { HostWindowActionTypes } from '../shared/host-window.actions';
|
|
||||||
import { HeaderCollapseAction } from './header.actions';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HeaderEffects {
|
|
||||||
|
|
||||||
@Effect() resize$ = this.actions$
|
|
||||||
.pipe(
|
|
||||||
ofType(HostWindowActionTypes.RESIZE),
|
|
||||||
map(() => new HeaderCollapseAction())
|
|
||||||
);
|
|
||||||
|
|
||||||
@Effect() routeChange$ = this.actions$
|
|
||||||
.pipe(
|
|
||||||
ofType(fromRouter.ROUTER_NAVIGATION),
|
|
||||||
map(() => new HeaderCollapseAction())
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(private actions$: Actions) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,89 +0,0 @@
|
|||||||
import * as deepFreeze from 'deep-freeze';
|
|
||||||
|
|
||||||
import { headerReducer } from './header.reducer';
|
|
||||||
import {
|
|
||||||
HeaderCollapseAction,
|
|
||||||
HeaderExpandAction,
|
|
||||||
HeaderToggleAction
|
|
||||||
} from './header.actions';
|
|
||||||
|
|
||||||
class NullAction extends HeaderCollapseAction {
|
|
||||||
type = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('headerReducer', () => {
|
|
||||||
it('should return the current state when no valid actions have been made', () => {
|
|
||||||
const state = { navCollapsed: false };
|
|
||||||
const action = new NullAction();
|
|
||||||
const newState = headerReducer(state, action);
|
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start with navCollapsed = true', () => {
|
|
||||||
const action = new NullAction();
|
|
||||||
const initialState = headerReducer(undefined, action);
|
|
||||||
|
|
||||||
// The navigation starts collapsed
|
|
||||||
expect(initialState.navCollapsed).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set navCollapsed to true in response to the COLLAPSE action', () => {
|
|
||||||
const state = { navCollapsed: false };
|
|
||||||
const action = new HeaderCollapseAction();
|
|
||||||
const newState = headerReducer(state, action);
|
|
||||||
|
|
||||||
expect(newState.navCollapsed).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should perform the COLLAPSE action without affecting the previous state', () => {
|
|
||||||
const state = { navCollapsed: false };
|
|
||||||
deepFreeze(state);
|
|
||||||
|
|
||||||
const action = new HeaderCollapseAction();
|
|
||||||
headerReducer(state, action);
|
|
||||||
|
|
||||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
|
||||||
// is mutated, and any uncaught exception will cause the test to fail
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set navCollapsed to false in response to the EXPAND action', () => {
|
|
||||||
const state = { navCollapsed: true };
|
|
||||||
const action = new HeaderExpandAction();
|
|
||||||
const newState = headerReducer(state, action);
|
|
||||||
|
|
||||||
expect(newState.navCollapsed).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should perform the EXPAND action without affecting the previous state', () => {
|
|
||||||
const state = { navCollapsed: true };
|
|
||||||
deepFreeze(state);
|
|
||||||
|
|
||||||
const action = new HeaderExpandAction();
|
|
||||||
headerReducer(state, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should flip the value of navCollapsed in response to the TOGGLE action', () => {
|
|
||||||
const state1 = { navCollapsed: true };
|
|
||||||
const action = new HeaderToggleAction();
|
|
||||||
|
|
||||||
const state2 = headerReducer(state1, action);
|
|
||||||
const state3 = headerReducer(state2, action);
|
|
||||||
|
|
||||||
expect(state2.navCollapsed).toEqual(false);
|
|
||||||
expect(state3.navCollapsed).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should perform the TOGGLE action without affecting the previous state', () => {
|
|
||||||
const state = { navCollapsed: true };
|
|
||||||
deepFreeze(state);
|
|
||||||
|
|
||||||
const action = new HeaderToggleAction();
|
|
||||||
headerReducer(state, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@@ -1,38 +0,0 @@
|
|||||||
import { HeaderAction, HeaderActionTypes } from './header.actions';
|
|
||||||
|
|
||||||
export interface HeaderState {
|
|
||||||
navCollapsed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: HeaderState = {
|
|
||||||
navCollapsed: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export function headerReducer(state = initialState, action: HeaderAction): HeaderState {
|
|
||||||
switch (action.type) {
|
|
||||||
|
|
||||||
case HeaderActionTypes.COLLAPSE: {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
navCollapsed: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
case HeaderActionTypes.EXPAND: {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
navCollapsed: false
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
case HeaderActionTypes.TOGGLE: {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
navCollapsed: !state.navCollapsed
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,17 @@
|
|||||||
|
<li class="nav-item dropdown"
|
||||||
|
(mouseenter)="activateSection($event)"
|
||||||
|
(mouseleave)="deactivateSection($event)">
|
||||||
|
<a href="#" class="nav-link dropdown-toggle" routerLinkActive="active"
|
||||||
|
id="browseDropdown" (click)="toggleSection($event)"
|
||||||
|
data-toggle="dropdown">
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||||
|
</a>
|
||||||
|
<ul @slide *ngIf="(active | async)" (click)="deactivateSection($event)"
|
||||||
|
class="m-0 shadow-none border-top-0 dropdown-menu show">
|
||||||
|
<ng-container *ngFor="let subSection of (subSections | async)">
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="itemComponents.get(subSection.id); injector: itemInjectors.get(subSection.id);"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</li>
|
@@ -0,0 +1,27 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 100%;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
::ng-deep a.nav-link {
|
||||||
|
padding-right: $spacer;
|
||||||
|
padding-left: $spacer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mobile menu styling **/
|
||||||
|
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||||
|
.dropdown-toggle {
|
||||||
|
&:after {
|
||||||
|
float: right;
|
||||||
|
margin-top: $spacer/2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dropdown-menu {
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,176 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { MenuServiceStub } from '../../shared/testing/menu-service-stub';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
|
import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
describe('ExpandableNavbarSectionComponent', () => {
|
||||||
|
let component: ExpandableNavbarSectionComponent;
|
||||||
|
let fixture: ComponentFixture<ExpandableNavbarSectionComponent>;
|
||||||
|
const menuService = new MenuServiceStub();
|
||||||
|
|
||||||
|
describe('on larger screens', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAnimationsModule],
|
||||||
|
declarations: [ExpandableNavbarSectionComponent, TestComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: 'sectionDataProvider', useValue: {} },
|
||||||
|
{ provide: MenuService, useValue: menuService },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }
|
||||||
|
]
|
||||||
|
}).overrideComponent(ExpandableNavbarSectionComponent, {
|
||||||
|
set: {
|
||||||
|
entryComponents: [TestComponent]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ExpandableNavbarSectionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the mouse enters the section header', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'activateSection');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown'));
|
||||||
|
sidebarToggler.triggerEventHandler('mouseenter', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call activateSection on the menuService', () => {
|
||||||
|
expect(menuService.activateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the mouse leaves the section header', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'deactivateSection');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown'));
|
||||||
|
sidebarToggler.triggerEventHandler('mouseleave', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call deactivateSection on the menuService', () => {
|
||||||
|
expect(menuService.deactivateSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a click occurs on the section header', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown')).query(By.css('a'));
|
||||||
|
sidebarToggler.triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call toggleActiveSection on the menuService', () => {
|
||||||
|
expect(menuService.toggleActiveSection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on smaller, mobile screens', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAnimationsModule],
|
||||||
|
declarations: [ExpandableNavbarSectionComponent, TestComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: 'sectionDataProvider', useValue: {} },
|
||||||
|
{ provide: MenuService, useValue: menuService },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(300) }
|
||||||
|
]
|
||||||
|
}).overrideComponent(ExpandableNavbarSectionComponent, {
|
||||||
|
set: {
|
||||||
|
entryComponents: [TestComponent]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ExpandableNavbarSectionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the mouse enters the section header', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'activateSection');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown'));
|
||||||
|
sidebarToggler.triggerEventHandler('mouseenter', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call activateSection on the menuService', () => {
|
||||||
|
expect(menuService.activateSection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the mouse leaves the section header', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'deactivateSection');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown'));
|
||||||
|
sidebarToggler.triggerEventHandler('mouseleave', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call deactivateSection on the menuService', () => {
|
||||||
|
expect(menuService.deactivateSection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a click occurs on the section header link', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown')).query(By.css('a'));
|
||||||
|
sidebarToggler.triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call toggleActiveSection on the menuService', () => {
|
||||||
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// declare a test component
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-test-cmp',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
class TestComponent {
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||||
|
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
|
||||||
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
|
import { MenuID } from '../../shared/menu/initial-menus-state';
|
||||||
|
import { slide } from '../../shared/animations/slide';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
|
import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an expandable section in the navbar
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-expandable-navbar-section',
|
||||||
|
templateUrl: './expandable-navbar-section.component.html',
|
||||||
|
styleUrls: ['./expandable-navbar-section.component.scss'],
|
||||||
|
animations: [slide]
|
||||||
|
})
|
||||||
|
@rendersSectionForMenu(MenuID.PUBLIC, true)
|
||||||
|
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* This section resides in the Public Navbar
|
||||||
|
*/
|
||||||
|
menuID = MenuID.PUBLIC;
|
||||||
|
|
||||||
|
constructor(@Inject('sectionDataProvider') menuSection,
|
||||||
|
protected menuService: MenuService,
|
||||||
|
protected injector: Injector,
|
||||||
|
private windowService: HostWindowService
|
||||||
|
) {
|
||||||
|
super(menuSection, menuService, injector);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the super function that activates this section (triggered on hover)
|
||||||
|
* Has an extra check to make sure the section can only be activated on non-mobile devices
|
||||||
|
* @param {Event} event The user event that triggered this function
|
||||||
|
*/
|
||||||
|
activateSection(event): void {
|
||||||
|
this.windowService.isXsOrSm().pipe(
|
||||||
|
first()
|
||||||
|
).subscribe((isMobile) => {
|
||||||
|
if (!isMobile) {
|
||||||
|
super.activateSection(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the super function that deactivates this section (triggered on hover)
|
||||||
|
* Has an extra check to make sure the section can only be deactivated on non-mobile devices
|
||||||
|
* @param {Event} event The user event that triggered this function
|
||||||
|
*/
|
||||||
|
deactivateSection(event): void {
|
||||||
|
this.windowService.isXsOrSm().pipe(
|
||||||
|
first()
|
||||||
|
).subscribe((isMobile) => {
|
||||||
|
if (!isMobile) {
|
||||||
|
super.deactivateSection(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the super function that toggles this section (triggered on click)
|
||||||
|
* Has an extra check to make sure the section can only be toggled on mobile devices
|
||||||
|
* @param {Event} event The user event that triggered this function
|
||||||
|
*/
|
||||||
|
toggleSection(event): void {
|
||||||
|
event.preventDefault();
|
||||||
|
this.windowService.isXsOrSm().pipe(
|
||||||
|
first()
|
||||||
|
).subscribe((isMobile) => {
|
||||||
|
if (isMobile) {
|
||||||
|
super.toggleSection(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
<li class="nav-item">
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||||
|
</li>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user