mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'master' into w2p-55990_Move-item-component
This commit is contained in:
@@ -18,9 +18,16 @@ module.exports = {
|
||||
// Caching settings
|
||||
cache: {
|
||||
// NOTE: how long should objects be cached for by default
|
||||
msToLive: 15 * 60 * 1000, // 15 minutes
|
||||
msToLive: {
|
||||
default: 15 * 60 * 1000, // 15 minutes
|
||||
},
|
||||
// msToLive: 1000, // 15 minutes
|
||||
control: 'max-age=60' // revalidate browser
|
||||
control: 'max-age=60', // revalidate browser
|
||||
autoSync: {
|
||||
defaultTime: 0,
|
||||
maxBufferSize: 100,
|
||||
timePerMethod: {'PATCH': 3} //time in seconds
|
||||
}
|
||||
},
|
||||
// Form settings
|
||||
form: {
|
||||
@@ -41,6 +48,68 @@ module.exports = {
|
||||
// NOTE: 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale'
|
||||
animate: 'scale'
|
||||
},
|
||||
// Submission settings
|
||||
submission: {
|
||||
autosave: {
|
||||
// NOTE: which metadata trigger an autosave
|
||||
metadata: ['dc.title', 'dc.identifier.doi', 'dc.identifier.pmid', 'dc.identifier.arxiv'],
|
||||
// NOTE: every how many minutes submission is saved automatically
|
||||
timer: 5
|
||||
},
|
||||
icons: {
|
||||
metadata: [
|
||||
/**
|
||||
* NOTE: example of configuration
|
||||
* {
|
||||
* // NOTE: metadata name
|
||||
* name: 'dc.author',
|
||||
* // NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
||||
* style: 'fa-user'
|
||||
* }
|
||||
*/
|
||||
{
|
||||
name: 'dc.author',
|
||||
style: 'fas fa-user'
|
||||
},
|
||||
// default configuration
|
||||
{
|
||||
name: 'default',
|
||||
style: ''
|
||||
}
|
||||
],
|
||||
authority: {
|
||||
confidence: [
|
||||
/**
|
||||
* NOTE: example of configuration
|
||||
* {
|
||||
* // NOTE: confidence value
|
||||
* value: 'dc.author',
|
||||
* // NOTE: fontawesome (v4.x) icon classes and bootstrap utility classes can be used
|
||||
* style: 'fa-user'
|
||||
* }
|
||||
*/
|
||||
{
|
||||
value: 600,
|
||||
style: 'text-success'
|
||||
},
|
||||
{
|
||||
value: 500,
|
||||
style: 'text-info'
|
||||
},
|
||||
{
|
||||
value: 400,
|
||||
style: 'text-warning'
|
||||
},
|
||||
// default configuration
|
||||
{
|
||||
value: 'default',
|
||||
style: 'text-muted'
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// Angular Universal settings
|
||||
universal: {
|
||||
preboot: true,
|
||||
@@ -52,5 +121,39 @@ module.exports = {
|
||||
// Log directory
|
||||
logDirectory: '.',
|
||||
// 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,
|
||||
}],
|
||||
// Browse-By Pages
|
||||
browseBy: {
|
||||
// Amount of years to display using jumps of one year (current year - oneYearLimit)
|
||||
oneYearLimit: 10,
|
||||
// Limit for years to display using jumps of five years (current year - fiveYearLimit)
|
||||
fiveYearLimit: 30,
|
||||
// The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
|
||||
defaultLowerLimit: 1900
|
||||
},
|
||||
item: {
|
||||
edit: {
|
||||
undoTimeout: 10000 // 10 seconds
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -12,8 +12,8 @@ describe('protractor App', () => {
|
||||
expect<any>(page.getPageTitleText()).toEqual('DSpace Angular :: Home');
|
||||
});
|
||||
|
||||
it('should display header "Welcome to DSpace"', () => {
|
||||
it('should contain a news section', () => {
|
||||
page.navigateTo();
|
||||
expect<any>(page.getFirstHeaderText()).toEqual('Welcome to DSpace');
|
||||
expect<any>(page.getHomePageNewsText()).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
@@ -9,11 +9,7 @@ export class ProtractorPage {
|
||||
return browser.getTitle();
|
||||
}
|
||||
|
||||
getFirstPText() {
|
||||
return element(by.xpath('//p[1]')).getText();
|
||||
}
|
||||
|
||||
getFirstHeaderText() {
|
||||
return element(by.xpath('//h1[1]')).getText();
|
||||
getHomePageNewsText() {
|
||||
return element(by.xpath('//ds-home-news')).getText();
|
||||
}
|
||||
}
|
||||
|
17
package.json
17
package.json
@@ -75,8 +75,8 @@
|
||||
"@angular/router": "^6.1.4",
|
||||
"@angularclass/bootloader": "1.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^2.0.0",
|
||||
"@ng-dynamic-forms/core": "6.0.9",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "6.0.9",
|
||||
"@ng-dynamic-forms/core": "6.2.0",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "6.2.0",
|
||||
"@ngrx/effects": "^6.1.0",
|
||||
"@ngrx/router-store": "^6.1.0",
|
||||
"@ngrx/store": "^6.1.0",
|
||||
@@ -89,13 +89,15 @@
|
||||
"angular2-text-mask": "9.0.0",
|
||||
"angulartics2": "^6.2.0",
|
||||
"body-parser": "1.18.2",
|
||||
"bootstrap": "4.1.3",
|
||||
"bootstrap": "4.3.1",
|
||||
"cerialize": "0.1.18",
|
||||
"compression": "1.7.1",
|
||||
"cookie-parser": "1.4.3",
|
||||
"core-js": "^2.5.7",
|
||||
"express": "4.16.2",
|
||||
"express-session": "1.15.6",
|
||||
"fast-json-patch": "^2.0.7",
|
||||
"file-saver": "^1.3.8",
|
||||
"font-awesome": "4.7.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.10",
|
||||
"http-server": "0.11.1",
|
||||
@@ -110,7 +112,7 @@
|
||||
"ng-mocks": "^6.2.1",
|
||||
"ng2-file-upload": "1.2.1",
|
||||
"ng2-nouislider": "^1.7.11",
|
||||
"ngx-bootstrap": "^3.0.1",
|
||||
"ngx-bootstrap": "^3.2.0",
|
||||
"ngx-infinite-scroll": "6.0.1",
|
||||
"ngx-moment": "^3.1.0",
|
||||
"ngx-pagination": "3.0.3",
|
||||
@@ -118,6 +120,7 @@
|
||||
"pem": "1.12.3",
|
||||
"reflect-metadata": "0.1.12",
|
||||
"rxjs": "6.2.2",
|
||||
"rxjs-spy": "^7.5.1",
|
||||
"sortablejs": "1.7.0",
|
||||
"text-mask-core": "5.0.1",
|
||||
"ts-loader": "^5.2.1",
|
||||
@@ -130,6 +133,7 @@
|
||||
"devDependencies": {
|
||||
"@angular/compiler": "^6.1.4",
|
||||
"@angular/compiler-cli": "^6.1.4",
|
||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||
"@ngrx/entity": "^6.1.0",
|
||||
"@ngrx/schematics": "^6.1.0",
|
||||
"@ngrx/store-devtools": "^6.1.0",
|
||||
@@ -140,6 +144,7 @@
|
||||
"@types/deep-freeze": "0.1.1",
|
||||
"@types/express": "^4.11.1",
|
||||
"@types/express-serve-static-core": "4.16.0",
|
||||
"@types/file-saver": "^1.3.0",
|
||||
"@types/hammerjs": "2.0.35",
|
||||
"@types/jasmine": "^2.8.6",
|
||||
"@types/js-cookie": "2.1.0",
|
||||
@@ -160,6 +165,7 @@
|
||||
"copy-webpack-plugin": "^4.4.1",
|
||||
"coveralls": "3.0.0",
|
||||
"css-loader": "1.0.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"deep-freeze": "0.0.1",
|
||||
"exports-loader": "^0.7.0",
|
||||
"html-webpack-plugin": "^4.0.0-alpha",
|
||||
@@ -182,9 +188,10 @@
|
||||
"karma-webdriver-launcher": "1.0.5",
|
||||
"karma-webpack": "3.0.0",
|
||||
"ngrx-store-freeze": "^0.2.4",
|
||||
"node-sass": "^4.7.2",
|
||||
"node-sass": "^4.11.0",
|
||||
"nodemon": "^1.15.0",
|
||||
"npm-run-all": "4.1.3",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"postcss": "^7.0.2",
|
||||
"postcss-apply": "0.11.0",
|
||||
"postcss-cli": "^6.0.0",
|
||||
|
@@ -13,6 +13,38 @@
|
||||
"head": "Recent Submissions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"title": "Name",
|
||||
"description": "Introductory text (HTML)",
|
||||
"abstract": "Short Description",
|
||||
"rights": "Copyright text (HTML)",
|
||||
"tableofcontents": "News (HTML)",
|
||||
"license": "License",
|
||||
"provenance": "Provenance",
|
||||
"errors": {
|
||||
"title": {
|
||||
"required": "Please enter a collection name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"head": "Edit Collection",
|
||||
"delete": "Delete this collection"
|
||||
},
|
||||
"create": {
|
||||
"head": "Create a Collection",
|
||||
"sub-head": "Create a Collection for Community {{ parent }}"
|
||||
},
|
||||
"delete": {
|
||||
"head": "Delete Collection",
|
||||
"text": "Are you sure you want to delete collection \"{{ dso }}\"",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"notification": {
|
||||
"success": "Successfully deleted collection",
|
||||
"fail": "Collection could not be deleted"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community": {
|
||||
@@ -22,6 +54,39 @@
|
||||
},
|
||||
"sub-collection-list": {
|
||||
"head": "Collections of this Community"
|
||||
},
|
||||
"sub-community-list": {
|
||||
"head": "Communities of this Community"
|
||||
},
|
||||
"form": {
|
||||
"title": "Name",
|
||||
"description": "Introductory text (HTML)",
|
||||
"abstract": "Short Description",
|
||||
"rights": "Copyright text (HTML)",
|
||||
"tableofcontents": "News (HTML)",
|
||||
"errors": {
|
||||
"title": {
|
||||
"required": "Please enter a community name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"head": "Edit Community",
|
||||
"delete": "Delete this community"
|
||||
},
|
||||
"create": {
|
||||
"head": "Create a Community",
|
||||
"sub-head": "Create a Sub-Community for Community {{ parent }}"
|
||||
},
|
||||
"delete": {
|
||||
"head": "Delete Community",
|
||||
"text": "Are you sure you want to delete community \"{{ dso }}\"",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"notification": {
|
||||
"success": "Successfully deleted community",
|
||||
"fail": "Community could not be deleted"
|
||||
}
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
@@ -58,6 +123,7 @@
|
||||
"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.",
|
||||
"title": "Item Edit - Status",
|
||||
"labels": {
|
||||
"id": "Item Internal ID",
|
||||
"handle": "Handle",
|
||||
@@ -73,6 +139,10 @@
|
||||
"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..."
|
||||
@@ -81,6 +151,10 @@
|
||||
"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"
|
||||
@@ -92,16 +166,20 @@
|
||||
}
|
||||
},
|
||||
"bitstreams": {
|
||||
"head": "Item Bitstreams"
|
||||
"head": "Item Bitstreams",
|
||||
"title": "Item Edit - Bitstreams"
|
||||
},
|
||||
"metadata": {
|
||||
"head": "Item Metadata"
|
||||
"head": "Item Metadata",
|
||||
"title": "Item Edit - Metadata"
|
||||
},
|
||||
"view": {
|
||||
"head": "View Item"
|
||||
"head": "View Item",
|
||||
"title": "Item Edit - View"
|
||||
},
|
||||
"curate": {
|
||||
"head": "Curate"
|
||||
"head": "Curate",
|
||||
"title": "Item Edit - Curate"
|
||||
}
|
||||
},
|
||||
"move": {
|
||||
@@ -116,19 +194,115 @@
|
||||
"cancel": "Cancel",
|
||||
"success": "The item has been moved succesfully",
|
||||
"error": "An error occured when attempting to move the item"
|
||||
},
|
||||
"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 occurred 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 occurred 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 occurred 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 occurred 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 occurred while deleting the item"
|
||||
},
|
||||
"metadata": {
|
||||
"add-button": "Add",
|
||||
"discard-button": "Discard",
|
||||
"reinstate-button": "Undo",
|
||||
"save-button": "Save",
|
||||
"headers": {
|
||||
"field": "Field",
|
||||
"value": "Value",
|
||||
"language": "Lang",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"edit": {
|
||||
"buttons": {
|
||||
"edit": "Edit",
|
||||
"unedit": "Stop editing",
|
||||
"remove": "Remove",
|
||||
"undo": "Undo changes"
|
||||
}
|
||||
},
|
||||
"metadatafield": {
|
||||
"invalid": "Please choose a valid metadata field"
|
||||
},
|
||||
"notifications": {
|
||||
"outdated": {
|
||||
"title": "Changed outdated",
|
||||
"content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts"
|
||||
},
|
||||
"discarded": {
|
||||
"title": "Changed discarded",
|
||||
"content": "Your changes were discarded. To reinstate your changes click the 'Undo' button"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "Metadata invalid",
|
||||
"content": "Your changes were not saved. Please make sure all fields are valid before you save."
|
||||
},
|
||||
"saved": {
|
||||
"title": "Metadata saved",
|
||||
"content": "Your changes to this item's metadata were saved."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"browse": {
|
||||
"header": "All of DSpace"
|
||||
},
|
||||
"community-browse": {
|
||||
"header": "By Community"
|
||||
},
|
||||
"statistics": {
|
||||
"header": "Statistics"
|
||||
},
|
||||
"login": "Log In",
|
||||
"logout": "Log Out"
|
||||
"logout": "Log Out",
|
||||
"language": "Language switch",
|
||||
"search": "Search"
|
||||
},
|
||||
"pagination": {
|
||||
"results-per-page": "Results Per Page",
|
||||
"sort-direction": "Sort Options",
|
||||
"showing": {
|
||||
"label": "Now showing items ",
|
||||
"label": "Now showing ",
|
||||
"detail": "{{ range }} of {{ total }}"
|
||||
}
|
||||
},
|
||||
@@ -227,7 +401,46 @@
|
||||
}
|
||||
},
|
||||
"browse": {
|
||||
"title": "Browsing {{ collection }} by {{ field }} {{ value }}"
|
||||
"title": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
||||
"startsWith": {
|
||||
"jump": "Jump to a point in the index:",
|
||||
"choose_year": "(Choose year)",
|
||||
"choose_start": "(Choose start)",
|
||||
"type_date": "Or type in a date (year-month):",
|
||||
"type_text": "Or enter first few letters:",
|
||||
"months": {
|
||||
"none": "(Choose month)",
|
||||
"january": "January",
|
||||
"february": "February",
|
||||
"march": "March",
|
||||
"april": "April",
|
||||
"may": "May",
|
||||
"june": "June",
|
||||
"july": "July",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "October",
|
||||
"november": "November",
|
||||
"december": "December"
|
||||
},
|
||||
"submit": "Go"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Title",
|
||||
"author": "Author",
|
||||
"subject": "Subject",
|
||||
"dateissued": "Issue Date"
|
||||
},
|
||||
"comcol": {
|
||||
"head": "Browse",
|
||||
"by": {
|
||||
"title": "By Title",
|
||||
"dateissued": "By Issue Date",
|
||||
"author": "By Author",
|
||||
"subject": "By Subject"
|
||||
}
|
||||
},
|
||||
"empty": "No items to show."
|
||||
},
|
||||
"admin": {
|
||||
"registries": {
|
||||
@@ -235,11 +448,18 @@
|
||||
"title": "DSpace Angular :: Metadata Registry",
|
||||
"head": "Metadata Registry",
|
||||
"description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.",
|
||||
"form": {
|
||||
"create": "Create metadata schema",
|
||||
"edit": "Edit metadata schema",
|
||||
"namespace": "Namespace",
|
||||
"name": "Name"
|
||||
},
|
||||
"schemas": {
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"namespace": "Namespace",
|
||||
"name": "Name"
|
||||
"name": "Name",
|
||||
"delete": "Delete selected"
|
||||
},
|
||||
"no-items": "No metadata schemas to show."
|
||||
}
|
||||
@@ -248,13 +468,40 @@
|
||||
"title": "DSpace Angular :: Metadata Schema Registry",
|
||||
"head": "Metadata Schema",
|
||||
"description": "This is the metadata schema for \"{{namespace}}\".",
|
||||
"return": "Return",
|
||||
"form": {
|
||||
"create": "Create metadata field",
|
||||
"edit": "Edit metadata field",
|
||||
"element": "Element",
|
||||
"qualifier": "Qualifier",
|
||||
"scopenote": "Scope Note"
|
||||
},
|
||||
"fields": {
|
||||
"head": "Schema metadata fields",
|
||||
"table": {
|
||||
"field": "Field",
|
||||
"scopenote": "Scope Note"
|
||||
"scopenote": "Scope Note",
|
||||
"delete": "Delete selected"
|
||||
},
|
||||
"no-items": "No metadata fields to show."
|
||||
},
|
||||
"notification": {
|
||||
"success": "Success",
|
||||
"failure": "Error",
|
||||
"created": "Successfully created metadata schema \"{{prefix}}\"",
|
||||
"edited": "Successfully edited metadata schema \"{{prefix}}\"",
|
||||
"deleted": {
|
||||
"success": "Successfully deleted {{amount}} metadata schemas",
|
||||
"failure": "Failed to delete {{amount}} metadata schemas"
|
||||
},
|
||||
"field": {
|
||||
"created": "Successfully created metadata field \"{{field}}\"",
|
||||
"edited": "Successfully edited metadata field \"{{field}}\"",
|
||||
"deleted": {
|
||||
"success": "Successfully deleted {{amount}} metadata fields",
|
||||
"failure": "Failed to delete {{amount}} metadata fields"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bitstream-formats": {
|
||||
@@ -278,17 +525,99 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"browse_global_by_subject": "By Subject",
|
||||
"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": {
|
||||
"default": "Loading...",
|
||||
"top-level-communities": "Loading top-level communities...",
|
||||
"community": "Loading community...",
|
||||
"collection": "Loading collection...",
|
||||
"sub-collections": "Loading sub-collections...",
|
||||
"sub-communities": "Loading sub-communities...",
|
||||
"recent-submissions": "Loading recent submissions...",
|
||||
"item": "Loading item...",
|
||||
"objects": "Loading...",
|
||||
"search-results": "Loading search results...",
|
||||
"browse-by": "Loading items..."
|
||||
"browse-by": "Loading items...",
|
||||
"browse-by-page": "Loading page..."
|
||||
},
|
||||
"error": {
|
||||
"default": "Error",
|
||||
@@ -296,6 +625,7 @@
|
||||
"community": "Error fetching community",
|
||||
"collection": "Error fetching collection",
|
||||
"sub-collections": "Error fetching sub-collections",
|
||||
"sub-communities": "Error fetching sub-communities",
|
||||
"recent-submissions": "Error fetching recent submissions",
|
||||
"item": "Error fetching item",
|
||||
"objects": "Error fetching objects",
|
||||
@@ -306,13 +636,25 @@
|
||||
"license": {
|
||||
"notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission."
|
||||
}
|
||||
},
|
||||
"submission": {
|
||||
"sections": {
|
||||
"init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below : <br> <br>"
|
||||
}
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"search-help": "Click here to looking for an existing correspondence",
|
||||
"remove": "Remove",
|
||||
"clear": "Clear",
|
||||
"clear-help": "Click here to remove the selected value",
|
||||
"edit": "Edit",
|
||||
"edit-help": "Click here to edit the selected value",
|
||||
"save": "Save",
|
||||
"save-help": "Save changes",
|
||||
"first-name": "First name",
|
||||
"last-name": "Last name",
|
||||
"loading": "Loading...",
|
||||
@@ -321,7 +663,9 @@
|
||||
"group-collapse": "Collapse",
|
||||
"group-expand": "Expand",
|
||||
"group-collapse-help": "Click here to collapse",
|
||||
"group-expand-help": "Click here to expand and add more elements"
|
||||
"group-expand-help": "Click here to expand and add more elements",
|
||||
"other-information": {
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Login",
|
||||
@@ -348,5 +692,128 @@
|
||||
"errors": {
|
||||
"invalid-user": "Invalid email address or password."
|
||||
}
|
||||
},
|
||||
"chips": {
|
||||
"remove": "Remove chip"
|
||||
},
|
||||
"dso-selector": {
|
||||
"create": {
|
||||
"community": {
|
||||
"head": "New community",
|
||||
"sub-level": "Create a new community in",
|
||||
"top-level": "Create a new top-level community"
|
||||
},
|
||||
"collection": {
|
||||
"head": "New collection"
|
||||
},
|
||||
"item": {
|
||||
"head": "New item"
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"community": {
|
||||
"head": "Edit community"
|
||||
},
|
||||
"collection": {
|
||||
"head": "Edit collection"
|
||||
},
|
||||
"item": {
|
||||
"head": "Edit item"
|
||||
}
|
||||
},
|
||||
"placeholder": "Search for a {{ type }}",
|
||||
"no-results": "No {{ type }} found"
|
||||
},
|
||||
"submission": {
|
||||
"general":{
|
||||
"cannot_submit": "You have not the privilege to make a new submission.",
|
||||
"deposit": "Deposit",
|
||||
"discard": {
|
||||
"submit": "Discard",
|
||||
"confirm": {
|
||||
"cancel": "Cancel",
|
||||
"submit": "Yes, I'm sure",
|
||||
"title": "Discard submission",
|
||||
"info": "This operation can't be undone. Are you sure?"
|
||||
}
|
||||
},
|
||||
"save": "Save",
|
||||
"save-later": "Save for later"
|
||||
},
|
||||
"submit": {
|
||||
"title": "Submission"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit Submission"
|
||||
},
|
||||
"mydspace": {
|
||||
|
||||
},
|
||||
"sections": {
|
||||
|
||||
"general": {
|
||||
"add-more": "Add more",
|
||||
"no-sections": "No options available",
|
||||
"sections_not_valid": "There are incomplete sections.",
|
||||
"collection": "Collection",
|
||||
"no-collection": "No collection found",
|
||||
"search-collection": "Search for a collection",
|
||||
"save_error_notice": "There was an issue when saving the item, please try again later.",
|
||||
"deposit_success_notice": "Submission deposited successfully.",
|
||||
"deposit_error_notice": "There was an issue when submitting the item, please try again later.",
|
||||
"discard_success_notice": "Submission discarded successfully.",
|
||||
"discard_error_notice": "There was an issue when discarding the item, please try again later.",
|
||||
"save_success_notice": "Submission saved successfully.",
|
||||
"metadata-extracted": "New metadata have been extracted and added to the <strong>{{sectionId}}</strong> section.",
|
||||
"metadata-extracted-new-section": "New <strong>{{sectionId}}</strong> section has been added to submission."
|
||||
},
|
||||
"submit.progressbar.describe.stepone": "Describe",
|
||||
"submit.progressbar.describe.steptwo": "Describe",
|
||||
"submit.progressbar.describe.stepcustom": "Describe",
|
||||
"submit.progressbar.describe.recycle": "Recycle",
|
||||
"submit.progressbar.upload": "Upload files",
|
||||
"submit.progressbar.license": "Deposit license",
|
||||
"submit.progressbar.cclicense": "Creative commons license",
|
||||
"submit.progressbar.detect-duplicate": "Potential duplicates",
|
||||
|
||||
"upload": {
|
||||
"no-entry": "No",
|
||||
"no-file-uploaded": "No file uploaded yet.",
|
||||
"info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
|
||||
"drop-message": "Drop files to attach them to the item",
|
||||
"upload-successful": "Upload successful",
|
||||
"upload-failed": "Upload failed",
|
||||
"header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):",
|
||||
"header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):",
|
||||
"form": {
|
||||
"access-condition-label": "Access condition type",
|
||||
"from-label": "Access grant from",
|
||||
"from-placeholder": "From",
|
||||
"until-label": "Access grant until",
|
||||
"until-placeholder": "Until",
|
||||
"group-label": "Group",
|
||||
"group-required": "Group is required.",
|
||||
"date-required": "Date is required."
|
||||
},
|
||||
"save-metadata": "Save metadata",
|
||||
"undo": "Cancel",
|
||||
"delete": {
|
||||
"submit": "Delete",
|
||||
"confirm": {
|
||||
"cancel": "Cancel",
|
||||
"submit": "Yes, I'm sure",
|
||||
"title": "Delete bitstream",
|
||||
"info": "This operation can't be undone. Are you sure?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"uploader": {
|
||||
"drag-message": "Drag & Drop your files here",
|
||||
"or": ", or",
|
||||
"browse": "browse",
|
||||
"queue-lenght": "Queue length",
|
||||
"processing": "Processing"
|
||||
}
|
||||
}
|
||||
|
@@ -40,8 +40,8 @@
|
||||
"description": "Beschrijving:"
|
||||
},
|
||||
"link": {
|
||||
"simple": "Eenvoudige item weergave",
|
||||
"full": "Volledige item weergave"
|
||||
"simple": "Eenvoudige itemweergave",
|
||||
"full": "Volledige itemweergave"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -52,10 +52,10 @@
|
||||
},
|
||||
"pagination": {
|
||||
"results-per-page": "Resultaten per pagina",
|
||||
"sort-direction": "Sorteer mogelijkheden",
|
||||
"sort-direction": "Sorteermogelijkheden",
|
||||
"showing": {
|
||||
"label": "Getoonde items ",
|
||||
"detail": "{{ range }} tot {{ total }}"
|
||||
"label": "Resultaten ",
|
||||
"detail": "{{ range }} van {{ total }}"
|
||||
}
|
||||
},
|
||||
"sorting": {
|
||||
@@ -116,8 +116,8 @@
|
||||
"reset": "Filters verwijderen",
|
||||
"applied": {
|
||||
"f.author": "Auteur",
|
||||
"f.dateIssued.min": "Start datum",
|
||||
"f.dateIssued.max": "Eind datum",
|
||||
"f.dateIssued.min": "Startdatum",
|
||||
"f.dateIssued.max": "Einddatum",
|
||||
"f.subject": "Sleutelwoord",
|
||||
"f.has_content_in_original_bundle": "Heeft bestanden"
|
||||
},
|
||||
@@ -129,7 +129,7 @@
|
||||
"head": "Auteur"
|
||||
},
|
||||
"scope": {
|
||||
"placeholder": "Bereik filter",
|
||||
"placeholder": "Bereikfilter",
|
||||
"head": "Bereik"
|
||||
},
|
||||
"subject": {
|
||||
@@ -159,27 +159,27 @@
|
||||
"metadata": {
|
||||
"title": "DSpace Angular :: Metadata Register",
|
||||
"head": "Metadata Register",
|
||||
"description": "Het metadata register omvat de lijst van alle metadata velden die beschikbaar zijn in het systeem. Deze velden kunnen verspreid zijn over verschillende metadata schema's. Het qualified Dublin Core schema (dc) is een verplicht schema en kan niet worden verwijderd.",
|
||||
"description": "Het metadataregister omvat de lijst van alle metadatavelden die beschikbaar zijn in het systeem. Deze velden kunnen verspreid zijn over verschillende metadataschema's. Het qualified Dublin Core schema (dc) is een verplicht schema en kan niet worden verwijderd.",
|
||||
"schemas": {
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"namespace": "Naamruimte",
|
||||
"name": "Naam"
|
||||
},
|
||||
"no-items": "Er kunnen geen metadata schema's getoond worden."
|
||||
"no-items": "Er kunnen geen metadataschema's getoond worden."
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"title": "DSpace Angular :: Metadata Schema Register",
|
||||
"head": "Metadata Schema",
|
||||
"description": "Dit is het metadata schema voor \"{{namespace}}\".",
|
||||
"description": "Dit is het metadataschema voor \"{{namespace}}\".",
|
||||
"fields": {
|
||||
"head": "Schema metadata velden",
|
||||
"head": "Schema metadatavelden",
|
||||
"table": {
|
||||
"field": "Veld",
|
||||
"scopenote": "Opmerking over bereik"
|
||||
},
|
||||
"no-items": "Er kunnen geen metadata velden getoond worden."
|
||||
"no-items": "Er kunnen geen metadatavelden getoond worden."
|
||||
}
|
||||
},
|
||||
"bitstream-formats": {
|
||||
@@ -198,7 +198,7 @@
|
||||
},
|
||||
"internal": "intern"
|
||||
},
|
||||
"no-items": "Er kunnen geen bitstream formaten getoond worden."
|
||||
"no-items": "Er kunnen geen bitstreamformaten getoond worden."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@
|
||||
"validation": {
|
||||
"pattern": "Deze invoer is niet toegelaten volgens dit patroon: {{ pattern }}.",
|
||||
"license": {
|
||||
"notgranted": "U moet de invoerlicentie goedkeuren om de invoer af te werken. Indien u deze licentie momenteel niet kan of mag goedkeuren, kan u uw werk opslaan en de invoer later afwerken. U kan dit nieuwe item ook verwijderen indien u niet voldoet aan de vereisten van de invoer licentie."
|
||||
"notgranted": "U moet de invoerlicentie goedkeuren om de invoer af te werken. Indien u deze licentie momenteel niet kan of mag goedkeuren, kan u uw werk opslaan en de invoer later afwerken. U kunt dit nieuwe item ook verwijderen indien u niet voldoet aan de vereisten van de invoerlicentie."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -271,7 +271,7 @@
|
||||
"expired": "Uw sessie is vervallen. Gelieve opnieuw aan te melden."
|
||||
},
|
||||
"errors": {
|
||||
"invalid-user": "Ongeldig email adres of wachtwoord."
|
||||
"invalid-user": "Ongeldig e-mailadres of wachtwoord."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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,6 +7,8 @@ import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { BitstreamFormatsComponent } from './bitstream-formats/bitstream-formats.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { MetadataSchemaFormComponent } from './metadata-registry/metadata-schema-form/metadata-schema-form.component';
|
||||
import {MetadataFieldFormComponent} from './metadata-schema/metadata-field-form/metadata-field-form.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -19,7 +21,12 @@ import { SharedModule } from '../../shared/shared.module';
|
||||
declarations: [
|
||||
MetadataRegistryComponent,
|
||||
MetadataSchemaComponent,
|
||||
BitstreamFormatsComponent
|
||||
BitstreamFormatsComponent,
|
||||
MetadataSchemaFormComponent,
|
||||
MetadataFieldFormComponent
|
||||
],
|
||||
entryComponents: [
|
||||
|
||||
]
|
||||
})
|
||||
export class AdminRegistriesModule {
|
||||
|
@@ -6,13 +6,24 @@ import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { BitstreamFormat } from '../../../core/registry/mock-bitstream-format.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
/**
|
||||
* This component renders a list of bitstream formats
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-bitstream-formats',
|
||||
templateUrl: './bitstream-formats.component.html'
|
||||
})
|
||||
export class BitstreamFormatsComponent {
|
||||
|
||||
/**
|
||||
* A paginated list of bitstream formats to be shown on the page
|
||||
*/
|
||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||
|
||||
/**
|
||||
* The current pagination configuration for the page
|
||||
* Currently simply renders all bitstream formats
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-bitstreamformats-pagination',
|
||||
pageSize: 10000
|
||||
@@ -22,11 +33,18 @@ export class BitstreamFormatsComponent {
|
||||
this.updateFormats();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the page is changed, make sure to update the list of bitstreams to match the new page
|
||||
* @param event The page change event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateFormats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to update the bitstream formats that are shown
|
||||
*/
|
||||
private updateFormats() {
|
||||
this.bitstreamFormats = this.registryService.getBitstreamFormats(this.config);
|
||||
}
|
||||
|
@@ -0,0 +1,151 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||
|
||||
/**
|
||||
* 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 MetadataRegistryActionTypes = {
|
||||
|
||||
EDIT_SCHEMA: type('dspace/metadata-registry/EDIT_SCHEMA'),
|
||||
CANCEL_EDIT_SCHEMA: type('dspace/metadata-registry/CANCEL_SCHEMA'),
|
||||
SELECT_SCHEMA: type('dspace/metadata-registry/SELECT_SCHEMA'),
|
||||
DESELECT_SCHEMA: type('dspace/metadata-registry/DESELECT_SCHEMA'),
|
||||
DESELECT_ALL_SCHEMA: type('dspace/metadata-registry/DESELECT_ALL_SCHEMA'),
|
||||
|
||||
EDIT_FIELD: type('dspace/metadata-registry/EDIT_FIELD'),
|
||||
CANCEL_EDIT_FIELD: type('dspace/metadata-registry/CANCEL_FIELD'),
|
||||
SELECT_FIELD: type('dspace/metadata-registry/SELECT_FIELD'),
|
||||
DESELECT_FIELD: type('dspace/metadata-registry/DESELECT_FIELD'),
|
||||
DESELECT_ALL_FIELD: type('dspace/metadata-registry/DESELECT_ALL_FIELD')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to edit a metadata schema in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistryEditSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.EDIT_SCHEMA;
|
||||
|
||||
schema: MetadataSchema;
|
||||
|
||||
constructor(registry: MetadataSchema) {
|
||||
this.schema = registry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to cancel the editing of a metadata schema in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistryCancelSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.CANCEL_EDIT_SCHEMA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to select a single metadata schema in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistrySelectSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.SELECT_SCHEMA;
|
||||
|
||||
schema: MetadataSchema;
|
||||
|
||||
constructor(registry: MetadataSchema) {
|
||||
this.schema = registry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to deselect a single metadata schema in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistryDeselectSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.DESELECT_SCHEMA;
|
||||
|
||||
schema: MetadataSchema;
|
||||
|
||||
constructor(registry: MetadataSchema) {
|
||||
this.schema = registry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to deselect all metadata schemas in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistryDeselectAllSchemaAction implements Action {
|
||||
type = MetadataRegistryActionTypes.DESELECT_ALL_SCHEMA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to edit a metadata field in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistryEditFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.EDIT_FIELD;
|
||||
|
||||
field: MetadataField;
|
||||
|
||||
constructor(registry: MetadataField) {
|
||||
this.field = registry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to cancel the editing of a metadata field in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistryCancelFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.CANCEL_EDIT_FIELD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to select a single metadata field in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistrySelectFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.SELECT_FIELD;
|
||||
|
||||
field: MetadataField;
|
||||
|
||||
constructor(registry: MetadataField) {
|
||||
this.field = registry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to deselect a single metadata field in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistryDeselectFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.DESELECT_FIELD;
|
||||
|
||||
field: MetadataField;
|
||||
|
||||
constructor(registry: MetadataField) {
|
||||
this.field = registry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to deselect all metadata fields in the metadata registry
|
||||
*/
|
||||
export class MetadataRegistryDeselectAllFieldAction implements Action {
|
||||
type = MetadataRegistryActionTypes.DESELECT_ALL_FIELD;
|
||||
}
|
||||
|
||||
/* 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
|
||||
* These are all the actions to perform on the metadata registry state
|
||||
*/
|
||||
export type MetadataRegistryAction
|
||||
= MetadataRegistryEditSchemaAction
|
||||
| MetadataRegistryCancelSchemaAction
|
||||
| MetadataRegistrySelectSchemaAction
|
||||
| MetadataRegistryDeselectSchemaAction
|
||||
| MetadataRegistryEditFieldAction
|
||||
| MetadataRegistryCancelFieldAction
|
||||
| MetadataRegistrySelectFieldAction
|
||||
| MetadataRegistryDeselectFieldAction;
|
@@ -1,42 +1,61 @@
|
||||
<div class="container">
|
||||
<div class="metadata-registry row">
|
||||
<div class="col-12">
|
||||
<div class="metadata-registry row">
|
||||
<div class="col-12">
|
||||
|
||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h2>
|
||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h2>
|
||||
|
||||
<p id="description" class="pb-2">{{'admin.registries.metadata.description' | translate}}</p>
|
||||
<p id="description" class="pb-2">{{'admin.registries.metadata.description' | translate}}</p>
|
||||
|
||||
<ds-metadata-schema-form (submitForm)="forceUpdateSchemas()"></ds-metadata-schema-form>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataSchemas | async)?.payload"
|
||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-schemas" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.namespace' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.name' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
[checked]="isSelected(schema) | async"
|
||||
(change)="selectMetadataSchema(schema, $event)"
|
||||
>
|
||||
</label>
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.namespace}}</a></td>
|
||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(metadataSchemas | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.metadata.schemas.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button *ngIf="(metadataSchemas | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteSchemas()">{{'admin.registries.metadata.schemas.table.delete' | translate}}</button>
|
||||
</div>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataSchemas | async)?.payload"
|
||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-schemas" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.namespace' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.metadata.schemas.table.name' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page">
|
||||
<td><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
||||
<td><a [routerLink]="[schema.prefix]">{{schema.namespace}}</a></td>
|
||||
<td><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
<div *ngIf="(metadataSchemas | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert">
|
||||
{{'admin.registries.metadata.schemas.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,5 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
|
||||
.selectable-row:hover {
|
||||
cursor: pointer;
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { MetadataRegistryComponent } from './metadata-registry.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
@@ -13,6 +13,10 @@ import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||
import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
|
||||
describe('MetadataRegistryComponent', () => {
|
||||
let comp: MetadataRegistryComponent;
|
||||
@@ -33,9 +37,18 @@ describe('MetadataRegistryComponent', () => {
|
||||
}
|
||||
];
|
||||
const mockSchemas = observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
getSelectedMetadataSchemas: () => observableOf([]),
|
||||
editMetadataSchema: (schema) => {},
|
||||
cancelEditMetadataSchema: () => {},
|
||||
deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')),
|
||||
deselectAllMetadataSchema: () => {},
|
||||
clearMetadataSchemaRequests: () => observableOf(undefined)
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -43,8 +56,12 @@ describe('MetadataRegistryComponent', () => {
|
||||
declarations: [MetadataRegistryComponent, PaginationComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
||||
]
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MetadataRegistryComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
@@ -52,19 +69,66 @@ describe('MetadataRegistryComponent', () => {
|
||||
fixture = TestBed.createComponent(MetadataRegistryComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
registryService = (comp as any).service;
|
||||
});
|
||||
|
||||
beforeEach(inject([RegistryService], (s) => {
|
||||
registryService = s;
|
||||
}));
|
||||
|
||||
it('should contain two schemas', () => {
|
||||
const tbody: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas>tbody')).nativeElement;
|
||||
expect(tbody.children.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should contain the correct schemas', () => {
|
||||
const dcName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(1) td:nth-child(3)')).nativeElement;
|
||||
const dcName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(1) td:nth-child(4)')).nativeElement;
|
||||
expect(dcName.textContent).toBe('dc');
|
||||
|
||||
const mockName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(2) td:nth-child(3)')).nativeElement;
|
||||
const mockName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(2) td:nth-child(4)')).nativeElement;
|
||||
expect(mockName.textContent).toBe('mock');
|
||||
});
|
||||
|
||||
describe('when clicking a metadata schema row', () => {
|
||||
let row: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'editMetadataSchema');
|
||||
row = fixture.debugElement.query(By.css('.selectable-row')).nativeElement;
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should start editing the selected schema', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.editMetadataSchema).toHaveBeenCalledWith(mockSchemasList[0]);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should cancel editing the selected schema when clicked again', async(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0]));
|
||||
spyOn(registryService, 'cancelEditMetadataSchema');
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.cancelEditMetadataSchema).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when deleting metadata schemas', () => {
|
||||
const selectedSchemas = Array(mockSchemasList[0]);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
||||
spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas));
|
||||
comp.deleteSchemas();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call deleteMetadataSchema with the selected id', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.deleteMetadataSchema).toHaveBeenCalledWith(selectedSchemas[0].id);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@@ -1,34 +1,173 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { zip } from 'rxjs/internal/observable/zip';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { Route, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-registry',
|
||||
templateUrl: './metadata-registry.component.html'
|
||||
templateUrl: './metadata-registry.component.html',
|
||||
styleUrls: ['./metadata-registry.component.scss']
|
||||
})
|
||||
/**
|
||||
* A component used for managing all existing metadata schemas within the repository.
|
||||
* The admin can create, edit or delete metadata schemas here.
|
||||
*/
|
||||
export class MetadataRegistryComponent {
|
||||
|
||||
/**
|
||||
* A list of all the current metadata schemas within the repository
|
||||
*/
|
||||
metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>;
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of metadata schemas
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-metadataschemas-pagination',
|
||||
pageSize: 10000
|
||||
pageSize: 25
|
||||
});
|
||||
|
||||
constructor(private registryService: RegistryService) {
|
||||
constructor(private registryService: RegistryService,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private translateService: TranslateService) {
|
||||
this.updateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of schemas by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateSchemas() {
|
||||
this.metadataSchemas = this.registryService.getMetadataSchemas(this.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of schemas by first clearing the cache related to metadata schemas, then performing
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateSchemas() {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.updateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing the selected metadata schema
|
||||
* @param schema
|
||||
*/
|
||||
editSchema(schema: MetadataSchema) {
|
||||
this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => {
|
||||
if (schema === activeSchema) {
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
} else {
|
||||
this.registryService.editMetadataSchema(schema);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given metadata schema is active (being edited)
|
||||
* @param schema
|
||||
*/
|
||||
isActive(schema: MetadataSchema): Observable<boolean> {
|
||||
return this.getActiveSchema().pipe(
|
||||
map((activeSchema) => schema === activeSchema)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active metadata schema (being edited)
|
||||
*/
|
||||
getActiveSchema(): Observable<MetadataSchema> {
|
||||
return this.registryService.getActiveMetadataSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a metadata schema within the list (checkbox)
|
||||
* @param schema
|
||||
* @param event
|
||||
*/
|
||||
selectMetadataSchema(schema: MetadataSchema, event) {
|
||||
event.target.checked ?
|
||||
this.registryService.selectMetadataSchema(schema) :
|
||||
this.registryService.deselectMetadataSchema(schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given metadata schema is selected in the list (checkbox)
|
||||
* @param schema
|
||||
*/
|
||||
isSelected(schema: MetadataSchema): Observable<boolean> {
|
||||
return this.registryService.getSelectedMetadataSchemas().pipe(
|
||||
map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the selected metadata schemas
|
||||
*/
|
||||
deleteSchemas() {
|
||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||
(schemas) => {
|
||||
const tasks$ = [];
|
||||
for (const schema of schemas) {
|
||||
if (hasValue(schema.id)) {
|
||||
tasks$.push(this.registryService.deleteMetadataSchema(schema.id));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((responses: RestResponse[]) => {
|
||||
const successResponses = responses.filter((response: RestResponse) => response.isSuccessful);
|
||||
const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
this.registryService.deselectAllMetadataSchema();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
this.forceUpdateSchemas();
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notifications for an amount of deleted metadata schemas
|
||||
* @param success Whether or not the notification should be a success message (error message when false)
|
||||
* @param amount The amount of deleted metadata schemas
|
||||
*/
|
||||
showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount })
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content)
|
||||
} else {
|
||||
this.notificationsService.error(head, content)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
MetadataRegistryCancelFieldAction,
|
||||
MetadataRegistryCancelSchemaAction, MetadataRegistryDeselectAllFieldAction,
|
||||
MetadataRegistryDeselectAllSchemaAction, MetadataRegistryDeselectFieldAction,
|
||||
MetadataRegistryDeselectSchemaAction, MetadataRegistryEditFieldAction,
|
||||
MetadataRegistryEditSchemaAction, MetadataRegistrySelectFieldAction,
|
||||
MetadataRegistrySelectSchemaAction
|
||||
} from './metadata-registry.actions';
|
||||
import { metadataRegistryReducer, MetadataRegistryState } from './metadata-registry.reducers';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||
|
||||
class NullAction extends MetadataRegistryEditSchemaAction {
|
||||
type = null;
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const schema: MetadataSchema = Object.assign(new MetadataSchema(),
|
||||
{
|
||||
id: 'schema-id',
|
||||
self: 'http://rest.self/schema/dc',
|
||||
prefix: 'dc',
|
||||
namespace: 'http://dublincore.org/documents/dcmi-terms/'
|
||||
});
|
||||
|
||||
const schema2: MetadataSchema = Object.assign(new MetadataSchema(),
|
||||
{
|
||||
id: 'another-schema-id',
|
||||
self: 'http://rest.self/schema/dcterms',
|
||||
prefix: 'dcterms',
|
||||
namespace: 'http://purl.org/dc/terms/'
|
||||
});
|
||||
|
||||
const field: MetadataField = Object.assign(new MetadataField(),
|
||||
{
|
||||
id: 'author-field-id',
|
||||
self: 'http://rest.self/field/author',
|
||||
element: 'contributor',
|
||||
qualifier: 'author',
|
||||
scopeNote: 'Author of an item',
|
||||
schema: schema
|
||||
});
|
||||
|
||||
const field2: MetadataField = Object.assign(new MetadataField(),
|
||||
{
|
||||
id: 'title-field-id',
|
||||
self: 'http://rest.self/field/title',
|
||||
element: 'title',
|
||||
qualifier: null,
|
||||
scopeNote: 'Title of an item',
|
||||
schema: schema
|
||||
});
|
||||
|
||||
const initialState: MetadataRegistryState = {
|
||||
editSchema: null,
|
||||
selectedSchemas: [],
|
||||
editField: null,
|
||||
selectedFields: []
|
||||
};
|
||||
|
||||
const editState: MetadataRegistryState = {
|
||||
editSchema: schema,
|
||||
selectedSchemas: [],
|
||||
editField: field,
|
||||
selectedFields: []
|
||||
};
|
||||
|
||||
const selectState: MetadataRegistryState = {
|
||||
editSchema: null,
|
||||
selectedSchemas: [schema2],
|
||||
editField: null,
|
||||
selectedFields: [field2]
|
||||
};
|
||||
|
||||
const moreSelectState: MetadataRegistryState = {
|
||||
editSchema: null,
|
||||
selectedSchemas: [schema, schema2],
|
||||
editField: null,
|
||||
selectedFields: [field, field2]
|
||||
};
|
||||
|
||||
describe('metadataRegistryReducer', () => {
|
||||
|
||||
it('should return the current state when no valid actions have been made', () => {
|
||||
const state = initialState;
|
||||
const action = new NullAction();
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should start with an the initial state', () => {
|
||||
const state = initialState;
|
||||
const action = new NullAction();
|
||||
const initState = metadataRegistryReducer(undefined, action);
|
||||
|
||||
expect(initState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should update the current state to change the editSchema to a new schema when MetadataRegistryEditSchemaAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new MetadataRegistryEditSchemaAction(schema2);
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editSchema).toEqual(schema2);
|
||||
});
|
||||
|
||||
it('should update the current state to remove the editSchema from the state when MetadataRegistryCancelSchemaAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new MetadataRegistryCancelSchemaAction();
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editSchema).toEqual(null);
|
||||
});
|
||||
|
||||
it('should update the current state to add a given schema to the selectedSchemas when MetadataRegistrySelectSchemaAction is dispatched', () => {
|
||||
const state = selectState;
|
||||
const action = new MetadataRegistrySelectSchemaAction(schema);
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.selectedSchemas).toContain(schema);
|
||||
expect(newState.selectedSchemas).toContain(schema2);
|
||||
});
|
||||
|
||||
it('should update the current state to remove a given schema to the selectedSchemas when MetadataRegistryDeselectSchemaAction is dispatched', () => {
|
||||
const state = selectState;
|
||||
const action = new MetadataRegistryDeselectSchemaAction(schema2);
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.selectedSchemas).toEqual([]);
|
||||
});
|
||||
|
||||
it('should update the current state to remove a given schema to the selectedSchemas when MetadataRegistryDeselectAllSchemaAction is dispatched', () => {
|
||||
const state = selectState;
|
||||
const action = new MetadataRegistryDeselectAllSchemaAction();
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.selectedSchemas).toEqual([]);
|
||||
});
|
||||
|
||||
it('should update the current state to change the editField to a new field when MetadataRegistryEditFieldAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new MetadataRegistryEditFieldAction(field2);
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editField).toEqual(field2);
|
||||
});
|
||||
|
||||
it('should update the current state to remove the editField from the state when MetadataRegistryCancelFieldAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new MetadataRegistryCancelFieldAction();
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editField).toEqual(null);
|
||||
});
|
||||
|
||||
it('should update the current state to add a given field to the selectedFields when MetadataRegistrySelectFieldAction is dispatched', () => {
|
||||
const state = selectState;
|
||||
const action = new MetadataRegistrySelectFieldAction(field);
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.selectedFields).toContain(field);
|
||||
expect(newState.selectedFields).toContain(field2);
|
||||
});
|
||||
|
||||
it('should update the current state to remove a given field to the selectedFields when MetadataRegistryDeselectFieldAction is dispatched', () => {
|
||||
const state = selectState;
|
||||
const action = new MetadataRegistryDeselectFieldAction(field2);
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.selectedFields).toEqual([]);
|
||||
});
|
||||
|
||||
it('should update the current state to remove a given field to the selectedFields when MetadataRegistryDeselectAllFieldAction is dispatched', () => {
|
||||
const state = selectState;
|
||||
const action = new MetadataRegistryDeselectAllFieldAction();
|
||||
const newState = metadataRegistryReducer(state, action);
|
||||
|
||||
expect(newState.selectedFields).toEqual([]);
|
||||
});
|
||||
});
|
@@ -0,0 +1,111 @@
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import {
|
||||
MetadataRegistryAction,
|
||||
MetadataRegistryActionTypes,
|
||||
MetadataRegistryDeselectFieldAction,
|
||||
MetadataRegistryDeselectSchemaAction,
|
||||
MetadataRegistryEditFieldAction,
|
||||
MetadataRegistryEditSchemaAction,
|
||||
MetadataRegistrySelectFieldAction,
|
||||
MetadataRegistrySelectSchemaAction
|
||||
} from './metadata-registry.actions';
|
||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||
|
||||
/**
|
||||
* The metadata registry state.
|
||||
* @interface MetadataRegistryState
|
||||
*/
|
||||
export interface MetadataRegistryState {
|
||||
editSchema: MetadataSchema;
|
||||
selectedSchemas: MetadataSchema[];
|
||||
editField: MetadataField;
|
||||
selectedFields: MetadataField[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial state.
|
||||
*/
|
||||
const initialState: MetadataRegistryState = {
|
||||
editSchema: null,
|
||||
selectedSchemas: [],
|
||||
editField: null,
|
||||
selectedFields: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer that handles MetadataRegistryActions to modify metadata schema and/or field states
|
||||
* @param state The current MetadataRegistryState
|
||||
* @param action The MetadataRegistryAction to perform on the state
|
||||
*/
|
||||
export function metadataRegistryReducer(state = initialState, action: MetadataRegistryAction): MetadataRegistryState {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case MetadataRegistryActionTypes.EDIT_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
editSchema: (action as MetadataRegistryEditSchemaAction).schema
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.CANCEL_EDIT_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
editSchema: null
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.SELECT_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
selectedSchemas: [...state.selectedSchemas, (action as MetadataRegistrySelectSchemaAction).schema]
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.DESELECT_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
selectedSchemas: state.selectedSchemas.filter(
|
||||
(selectedSchema) => selectedSchema !== (action as MetadataRegistryDeselectSchemaAction).schema
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.DESELECT_ALL_SCHEMA: {
|
||||
return Object.assign({}, state, {
|
||||
selectedSchemas: []
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.EDIT_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
editField: (action as MetadataRegistryEditFieldAction).field
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.CANCEL_EDIT_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
editField: null
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.SELECT_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
selectedFields: [...state.selectedFields, (action as MetadataRegistrySelectFieldAction).field]
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.DESELECT_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
selectedFields: state.selectedFields.filter(
|
||||
(selectedField) => selectedField !== (action as MetadataRegistryDeselectFieldAction).field
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
case MetadataRegistryActionTypes.DESELECT_ALL_FIELD: {
|
||||
return Object.assign({}, state, {
|
||||
selectedFields: []
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
<div *ngIf="registryService.getActiveMetadataSchema() | async; then editheader; else createHeader"></div>
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h4>{{messagePrefix + '.create' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h4>{{messagePrefix + '.edit' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
(cancel)="onCancel()"
|
||||
(submitForm)="onSubmit()">
|
||||
|
||||
</ds-form>
|
@@ -0,0 +1,106 @@
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
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 { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
|
||||
describe('MetadataSchemaFormComponent', () => {
|
||||
let component: MetadataSchemaFormComponent;
|
||||
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
||||
let registryService: RegistryService;
|
||||
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
||||
cancelEditMetadataSchema: () => {}
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
return {
|
||||
patchValue: () => {}
|
||||
};
|
||||
}
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [ MetadataSchemaFormComponent, EnumKeysPipe ],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: FormBuilderService, useValue: formBuilderServiceStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MetadataSchemaFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
beforeEach(inject([RegistryService], (s) => {
|
||||
registryService = s;
|
||||
}));
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
const namespace = 'fake namespace';
|
||||
const prefix = 'fake';
|
||||
|
||||
const expected = Object.assign(new MetadataSchema(), {
|
||||
namespace: namespace,
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.name.value = prefix;
|
||||
component.namespace.value = namespace;
|
||||
});
|
||||
|
||||
describe('without an active schema', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new schema using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with an active schema', () => {
|
||||
const expectedWithId = Object.assign(new MetadataSchema(), {
|
||||
id: 1,
|
||||
namespace: namespace,
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should edit the existing schema using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,171 @@
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormLayout,
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-schema-form',
|
||||
templateUrl: './metadata-schema-form.component.html'
|
||||
})
|
||||
/**
|
||||
* A form used for creating and editing metadata schemas
|
||||
*/
|
||||
export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* A unique id used for ds-form
|
||||
*/
|
||||
formId = 'metadata-schema-form';
|
||||
|
||||
/**
|
||||
* The prefix for all messages related to this form
|
||||
*/
|
||||
messagePrefix = 'admin.registries.metadata.form';
|
||||
|
||||
/**
|
||||
* A dynamic input model for the name field
|
||||
*/
|
||||
name: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A dynamic input model for the namespace field
|
||||
*/
|
||||
namespace: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A list of all dynamic input models
|
||||
*/
|
||||
formModel: DynamicFormControlModel[];
|
||||
|
||||
/**
|
||||
* Layout used for structuring the form inputs
|
||||
*/
|
||||
formLayout: DynamicFormLayout = {
|
||||
name: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
},
|
||||
namespace: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
*/
|
||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
combineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.name`),
|
||||
this.translateService.get(`${this.messagePrefix}.namespace`)
|
||||
).subscribe(([name, namespace]) => {
|
||||
this.name = new DynamicInputModel({
|
||||
id: 'name',
|
||||
label: name,
|
||||
name: 'name',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[^ ,_]{1,32}$'
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.namespace = new DynamicInputModel({
|
||||
id: 'namespace',
|
||||
label: namespace,
|
||||
name: 'namespace',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.formModel = [
|
||||
this.namespace,
|
||||
this.name
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.registryService.getActiveMetadataSchema().subscribe((schema) => {
|
||||
this.formGroup.patchValue({
|
||||
name: schema != null ? schema.prefix : '',
|
||||
namespace: schema != null ? schema.namespace : ''
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop editing the currently selected metadata schema
|
||||
*/
|
||||
onCancel() {
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form
|
||||
* When the schema has an id attached -> Edit the schema
|
||||
* When the schema has no id attached -> Create new schema
|
||||
* Emit the updated/created schema using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||
(schema) => {
|
||||
const values = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value
|
||||
};
|
||||
if (schema == null) {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), values)).subscribe((newSchema) => {
|
||||
this.submitForm.emit(newSchema);
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), {
|
||||
id: schema.id,
|
||||
prefix: (values.prefix ? values.prefix : schema.prefix),
|
||||
namespace: (values.namespace ? values.namespace : schema.namespace)
|
||||
})).subscribe((updatedSchema) => {
|
||||
this.submitForm.emit(updatedSchema);
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty
|
||||
*/
|
||||
clearFields() {
|
||||
this.formGroup.patchValue({
|
||||
prefix: '',
|
||||
namespace: ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
<div *ngIf="registryService.getActiveMetadataField() | async; then editheader; else createHeader"></div>
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h4>{{messagePrefix + '.create' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h4>{{messagePrefix + '.edit' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
[formGroup]="formGroup"
|
||||
(cancel)="onCancel()"
|
||||
(submit)="onSubmit()">
|
||||
|
||||
</ds-form>
|
@@ -0,0 +1,120 @@
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MetadataFieldFormComponent } from './metadata-field-form.component';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
||||
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 { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
|
||||
describe('MetadataFieldFormComponent', () => {
|
||||
let component: MetadataFieldFormComponent;
|
||||
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
||||
let registryService: RegistryService;
|
||||
|
||||
const metadataSchema = Object.assign(new MetadataSchema(), {
|
||||
id: 1,
|
||||
namespace: 'fake schema',
|
||||
prefix: 'fake'
|
||||
});
|
||||
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
||||
cancelEditMetadataSchema: () => {},
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
return {
|
||||
patchValue: () => {}
|
||||
};
|
||||
}
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [ MetadataFieldFormComponent, EnumKeysPipe ],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: FormBuilderService, useValue: formBuilderServiceStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MetadataFieldFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.metadataSchema = metadataSchema;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
beforeEach(inject([RegistryService], (s) => {
|
||||
registryService = s;
|
||||
}));
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
const element = 'fakeElement';
|
||||
const qualifier = 'fakeQualifier';
|
||||
const scopeNote = 'fakeScopeNote';
|
||||
|
||||
const expected = Object.assign(new MetadataField(), {
|
||||
schema: metadataSchema,
|
||||
element: element,
|
||||
qualifier: qualifier,
|
||||
scopeNote: scopeNote
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.element.value = element;
|
||||
component.qualifier.value = qualifier;
|
||||
component.scopeNote.value = scopeNote;
|
||||
});
|
||||
|
||||
describe('without an active field', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(undefined));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new field using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with an active field', () => {
|
||||
const expectedWithId = Object.assign(new MetadataField(), {
|
||||
id: 1,
|
||||
schema: metadataSchema,
|
||||
element: element,
|
||||
qualifier: qualifier,
|
||||
scopeNote: scopeNote
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(expectedWithId));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should edit the existing field using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,201 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadataschema.model';
|
||||
import {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormLayout,
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-field-form',
|
||||
templateUrl: './metadata-field-form.component.html'
|
||||
})
|
||||
/**
|
||||
* A form used for creating and editing metadata fields
|
||||
*/
|
||||
export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* A unique id used for ds-form
|
||||
*/
|
||||
formId = 'metadata-field-form';
|
||||
|
||||
/**
|
||||
* The prefix for all messages related to this form
|
||||
*/
|
||||
messagePrefix = 'admin.registries.schema.form';
|
||||
|
||||
/**
|
||||
* The metadata schema this field is attached to
|
||||
*/
|
||||
@Input() metadataSchema: MetadataSchema;
|
||||
|
||||
/**
|
||||
* A dynamic input model for the element field
|
||||
*/
|
||||
element: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A dynamic input model for the qualifier field
|
||||
*/
|
||||
qualifier: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A dynamic input model for the scopeNote field
|
||||
*/
|
||||
scopeNote: DynamicInputModel;
|
||||
|
||||
/**
|
||||
* A list of all dynamic input models
|
||||
*/
|
||||
formModel: DynamicFormControlModel[];
|
||||
|
||||
/**
|
||||
* Layout used for structuring the form inputs
|
||||
*/
|
||||
formLayout: DynamicFormLayout = {
|
||||
element: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
},
|
||||
qualifier: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
},
|
||||
scopeNote: {
|
||||
grid: {
|
||||
host: 'col col-sm-12 d-inline-block'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
*/
|
||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
constructor(public registryService: RegistryService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component, setting up the necessary Models for the dynamic form
|
||||
*/
|
||||
ngOnInit() {
|
||||
combineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.element`),
|
||||
this.translateService.get(`${this.messagePrefix}.qualifier`),
|
||||
this.translateService.get(`${this.messagePrefix}.scopenote`)
|
||||
).subscribe(([element, qualifier, scopenote]) => {
|
||||
this.element = new DynamicInputModel({
|
||||
id: 'element',
|
||||
label: element,
|
||||
name: 'element',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.qualifier = new DynamicInputModel({
|
||||
id: 'qualifier',
|
||||
label: qualifier,
|
||||
name: 'qualifier',
|
||||
required: false,
|
||||
});
|
||||
this.scopeNote = new DynamicInputModel({
|
||||
id: 'scopeNote',
|
||||
label: scopenote,
|
||||
name: 'scopeNote',
|
||||
required: false,
|
||||
});
|
||||
this.formModel = [
|
||||
this.element,
|
||||
this.qualifier,
|
||||
this.scopeNote
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.registryService.getActiveMetadataField().subscribe((field) => {
|
||||
this.formGroup.patchValue({
|
||||
element: field != null ? field.element : '',
|
||||
qualifier: field != null ? field.qualifier : '',
|
||||
scopeNote: field != null ? field.scopeNote : ''
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop editing the currently selected metadata field
|
||||
*/
|
||||
onCancel() {
|
||||
this.registryService.cancelEditMetadataField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form
|
||||
* When the field has an id attached -> Edit the field
|
||||
* When the field has no id attached -> Create new field
|
||||
* Emit the updated/created field using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||
(field) => {
|
||||
const values = {
|
||||
schema: this.metadataSchema,
|
||||
element: this.element.value,
|
||||
qualifier: this.qualifier.value,
|
||||
scopeNote: this.scopeNote.value
|
||||
};
|
||||
if (field == null) {
|
||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), values)).subscribe((newField) => {
|
||||
this.submitForm.emit(newField);
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), {
|
||||
id: field.id,
|
||||
schema: this.metadataSchema,
|
||||
element: (values.element ? values.element : field.element),
|
||||
qualifier: (values.qualifier ? values.qualifier : field.qualifier),
|
||||
scopeNote: (values.scopeNote ? values.scopeNote : field.scopeNote)
|
||||
})).subscribe((updatedField) => {
|
||||
this.submitForm.emit(updatedField);
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty
|
||||
*/
|
||||
clearFields() {
|
||||
this.formGroup.patchValue({
|
||||
element: '',
|
||||
qualifier: '',
|
||||
scopeNote: ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
@@ -6,36 +6,56 @@
|
||||
|
||||
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:namespace }}</p>
|
||||
|
||||
<ds-metadata-field-form
|
||||
[metadataSchema]="(metadataSchema | async)?.payload"
|
||||
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
||||
|
||||
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(metadataFields | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataFields | async)?.payload"
|
||||
[collectionSize]="(metadataFields | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-fields" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let field of (metadataFields | async)?.payload?.page">
|
||||
<td>{{(metadataSchema | async)?.payload?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||
<td>{{field.scopeNote}}</td>
|
||||
<tr *ngFor="let field of (metadataFields | async)?.payload?.page"
|
||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
[checked]="isSelected(field) | async"
|
||||
(change)="selectMetadataField(field, $event)">
|
||||
</label>
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{(metadataSchema | async)?.payload?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
<div *ngIf="(metadataFields | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert">
|
||||
|
||||
<div *ngIf="(metadataFields | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.schema.fields.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
||||
<button *ngIf="(metadataFields | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,5 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
|
||||
.selectable-row:hover {
|
||||
cursor: pointer;
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { MetadataSchemaComponent } from './metadata-schema.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
@@ -17,6 +17,10 @@ import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
|
||||
describe('MetadataSchemaComponent', () => {
|
||||
let comp: MetadataSchemaComponent;
|
||||
@@ -38,40 +42,53 @@ describe('MetadataSchemaComponent', () => {
|
||||
];
|
||||
const mockFieldsList = [
|
||||
{
|
||||
id: 1,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8',
|
||||
element: 'contributor',
|
||||
qualifier: 'advisor',
|
||||
scopenote: null,
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9',
|
||||
element: 'contributor',
|
||||
qualifier: 'author',
|
||||
scopenote: null,
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10',
|
||||
element: 'contributor',
|
||||
qualifier: 'editor',
|
||||
scopenote: 'test scope note',
|
||||
scopeNote: 'test scope note',
|
||||
schema: mockSchemasList[1]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11',
|
||||
element: 'contributor',
|
||||
qualifier: 'illustrator',
|
||||
scopenote: null,
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[1]
|
||||
}
|
||||
];
|
||||
const mockSchemas = observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema)))),
|
||||
getMetadataSchemaByName: (schemaName: string) => observableOf(new RemoteData(false, false, true, undefined, mockSchemasList.filter((value) => value.prefix === schemaName)[0]))
|
||||
getMetadataSchemaByName: (schemaName: string) => observableOf(new RemoteData(false, false, true, undefined, mockSchemasList.filter((value) => value.prefix === schemaName)[0])),
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
getSelectedMetadataFields: () => observableOf([]),
|
||||
editMetadataField: (schema) => {},
|
||||
cancelEditMetadataField: () => {},
|
||||
deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')),
|
||||
deselectAllMetadataField: () => {},
|
||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
const schemaNameParam = 'mock';
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({
|
||||
@@ -87,8 +104,10 @@ describe('MetadataSchemaComponent', () => {
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: Router, useValue: new RouterStub() }
|
||||
]
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
@@ -96,9 +115,12 @@ describe('MetadataSchemaComponent', () => {
|
||||
fixture = TestBed.createComponent(MetadataSchemaComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
registryService = (comp as any).service;
|
||||
});
|
||||
|
||||
beforeEach(inject([RegistryService], (s) => {
|
||||
registryService = s;
|
||||
}));
|
||||
|
||||
it('should contain the schema prefix in the header', () => {
|
||||
const header: HTMLElement = fixture.debugElement.query(By.css('.metadata-schema #header')).nativeElement;
|
||||
expect(header.textContent).toContain('mock');
|
||||
@@ -110,10 +132,54 @@ describe('MetadataSchemaComponent', () => {
|
||||
});
|
||||
|
||||
it('should contain the correct fields', () => {
|
||||
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(1)')).nativeElement;
|
||||
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
||||
expect(editorField.textContent).toBe('mock.contributor.editor');
|
||||
|
||||
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(1)')).nativeElement;
|
||||
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(2)')).nativeElement;
|
||||
expect(illustratorField.textContent).toBe('mock.contributor.illustrator');
|
||||
});
|
||||
|
||||
describe('when clicking a metadata field row', () => {
|
||||
let row: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'editMetadataField');
|
||||
row = fixture.debugElement.query(By.css('.selectable-row')).nativeElement;
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should start editing the selected field', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.editMetadataField).toHaveBeenCalledWith(mockFieldsList[2]);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should cancel editing the selected field when clicked again', async(() => {
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2]));
|
||||
spyOn(registryService, 'cancelEditMetadataField');
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.cancelEditMetadataField).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when deleting metadata fields', () => {
|
||||
const selectedFields = Array(mockFieldsList[2]);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'deleteMetadataField').and.callThrough();
|
||||
spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields));
|
||||
comp.deleteFields();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call deleteMetadataField with the selected id', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(registryService.deleteMetadataField).toHaveBeenCalledWith(selectedFields[0].id);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@@ -1,30 +1,59 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { zip } from 'rxjs/internal/observable/zip';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-schema',
|
||||
templateUrl: './metadata-schema.component.html'
|
||||
templateUrl: './metadata-schema.component.html',
|
||||
styleUrls: ['./metadata-schema.component.scss']
|
||||
})
|
||||
/**
|
||||
* A component used for managing all existing metadata fields within the current metadata schema.
|
||||
* The admin can create, edit or delete metadata fields here.
|
||||
*/
|
||||
export class MetadataSchemaComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The namespace of the metadata schema
|
||||
*/
|
||||
namespace;
|
||||
|
||||
/**
|
||||
* The metadata schema
|
||||
*/
|
||||
metadataSchema: Observable<RemoteData<MetadataSchema>>;
|
||||
|
||||
/**
|
||||
* A list of all the fields attached to this metadata schema
|
||||
*/
|
||||
metadataFields: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of metadata fields
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-metadatafields-pagination',
|
||||
pageSize: 10000
|
||||
pageSize: 25,
|
||||
pageSizeOptions: [25, 50, 100, 200]
|
||||
});
|
||||
|
||||
constructor(private registryService: RegistryService, private route: ActivatedRoute) {
|
||||
constructor(private registryService: RegistryService,
|
||||
private route: ActivatedRoute,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private translateService: TranslateService) {
|
||||
|
||||
}
|
||||
|
||||
@@ -34,22 +63,143 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component using the params within the url (schemaName)
|
||||
* @param params
|
||||
*/
|
||||
initialize(params) {
|
||||
this.metadataSchema = this.registryService.getMetadataSchemaByName(params.schemaName);
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of fields by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateFields() {
|
||||
this.metadataSchema.subscribe((schemaData) => {
|
||||
const schema = schemaData.payload;
|
||||
this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, this.config);
|
||||
this.namespace = { namespace: schemaData.payload.namespace };
|
||||
this.namespace = {namespace: schemaData.payload.namespace};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of fields by first clearing the cache related to metadata fields, then performing
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateFields() {
|
||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing the selected metadata field
|
||||
* @param field
|
||||
*/
|
||||
editField(field: MetadataField) {
|
||||
this.getActiveField().pipe(take(1)).subscribe((activeField) => {
|
||||
if (field === activeField) {
|
||||
this.registryService.cancelEditMetadataField();
|
||||
} else {
|
||||
this.registryService.editMetadataField(field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given metadata field is active (being edited)
|
||||
* @param field
|
||||
*/
|
||||
isActive(field: MetadataField): Observable<boolean> {
|
||||
return this.getActiveField().pipe(
|
||||
map((activeField) => field === activeField)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active metadata field (being edited)
|
||||
*/
|
||||
getActiveField(): Observable<MetadataField> {
|
||||
return this.registryService.getActiveMetadataField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a metadata field within the list (checkbox)
|
||||
* @param field
|
||||
* @param event
|
||||
*/
|
||||
selectMetadataField(field: MetadataField, event) {
|
||||
event.target.checked ?
|
||||
this.registryService.selectMetadataField(field) :
|
||||
this.registryService.deselectMetadataField(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given metadata field is selected in the list (checkbox)
|
||||
* @param field
|
||||
*/
|
||||
isSelected(field: MetadataField): Observable<boolean> {
|
||||
return this.registryService.getSelectedMetadataFields().pipe(
|
||||
map((fields) => fields.find((selectedField) => selectedField === field) != null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the selected metadata fields
|
||||
*/
|
||||
deleteFields() {
|
||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
||||
(fields) => {
|
||||
const tasks$ = [];
|
||||
for (const field of fields) {
|
||||
if (hasValue(field.id)) {
|
||||
tasks$.push(this.registryService.deleteMetadataField(field.id));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((responses: RestResponse[]) => {
|
||||
const successResponses = responses.filter((response: RestResponse) => response.isSuccessful);
|
||||
const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
this.registryService.deselectAllMetadataField();
|
||||
this.registryService.cancelEditMetadataField();
|
||||
this.forceUpdateFields();
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notifications for an amount of deleted metadata fields
|
||||
* @param success Whether or not the notification should be a success message (error message when false)
|
||||
* @param amount The amount of deleted metadata fields
|
||||
*/
|
||||
showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount })
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content)
|
||||
} else {
|
||||
this.notificationsService.error(head, content)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,10 @@ import { NgModule } from '@angular/core';
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: 'registries', loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule' }
|
||||
{
|
||||
path: 'registries',
|
||||
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
160
src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
Normal file
160
src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
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';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
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 },
|
||||
{
|
||||
provide: NgbModal, useValue: {
|
||||
open: () => {/*comment*/}
|
||||
}
|
||||
}
|
||||
],
|
||||
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();
|
||||
}));
|
||||
});
|
||||
});
|
499
src/app/+admin/admin-sidebar/admin-sidebar.component.ts
Normal file
499
src/app/+admin/admin-sidebar/admin-sidebar.component.ts
Normal file
@@ -0,0 +1,499 @@
|
||||
import { Component, Injector, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { 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';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
|
||||
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
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.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_collection',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCollectionParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_item',
|
||||
function: () => {
|
||||
this.modalService.open(CreateItemParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
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.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
function: () => {
|
||||
this.modalService.open(EditCommunitySelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_collection',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
function: () => {
|
||||
this.modalService.open(EditCollectionSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_item',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
function: () => {
|
||||
this.modalService.open(EditItemSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
|
||||
/* 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)))
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,12 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
|
||||
import { AdminRoutingModule } from './admin-routing.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AdminRegistriesModule,
|
||||
AdminRoutingModule
|
||||
]
|
||||
AdminRoutingModule,
|
||||
SharedModule,
|
||||
],
|
||||
})
|
||||
export class AdminModule {
|
||||
|
||||
|
@@ -1,11 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="browse-by-author w-100 row">
|
||||
<ds-browse-by class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:{collection: '', field: 'Author', value: (value)? '"' + value + '"': ''} }}"
|
||||
[objects$]="(items$ !== undefined)? items$ : authors$"
|
||||
[currentUrl]="currentUrl"
|
||||
[paginationConfig]="paginationConfig"
|
||||
[sortConfig]="sortConfig">
|
||||
</ds-browse-by>
|
||||
</div>
|
||||
</div>
|
@@ -1,107 +0,0 @@
|
||||
|
||||
import {combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-author-page',
|
||||
styleUrls: ['./browse-by-author-page.component.scss'],
|
||||
templateUrl: './browse-by-author-page.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for browsing (items) by author (dc.contributor.author)
|
||||
*/
|
||||
export class BrowseByAuthorPageComponent implements OnInit {
|
||||
|
||||
authors$: Observable<RemoteData<PaginatedList<BrowseEntry>>>;
|
||||
items$: Observable<RemoteData<PaginatedList<Item>>>;
|
||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'browse-by-author-pagination',
|
||||
currentPage: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
sortConfig: SortOptions = new SortOptions('dc.contributor.author', SortDirection.ASC);
|
||||
subs: Subscription[] = [];
|
||||
currentUrl: string;
|
||||
value = '';
|
||||
|
||||
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute, private browseService: BrowseService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.currentUrl = this.route.snapshot.pathFromRoot
|
||||
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
|
||||
.join('/');
|
||||
this.updatePage({
|
||||
pagination: this.paginationConfig,
|
||||
sort: this.sortConfig
|
||||
});
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
this.route.params,
|
||||
this.route.queryParams,
|
||||
(params, queryParams, ) => {
|
||||
return Object.assign({}, params, queryParams);
|
||||
})
|
||||
.subscribe((params) => {
|
||||
const page = +params.page || this.paginationConfig.currentPage;
|
||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
||||
const sortDirection = params.sortDirection || this.sortConfig.direction;
|
||||
const sortField = params.sortField || this.sortConfig.field;
|
||||
this.value = +params.value || params.value || '';
|
||||
const pagination = Object.assign({},
|
||||
this.paginationConfig,
|
||||
{ currentPage: page, pageSize: pageSize }
|
||||
);
|
||||
const sort = Object.assign({},
|
||||
this.sortConfig,
|
||||
{ direction: sortDirection, field: sortField }
|
||||
);
|
||||
const searchOptions = {
|
||||
pagination: pagination,
|
||||
sort: sort
|
||||
};
|
||||
if (isNotEmpty(this.value)) {
|
||||
this.updatePageWithItems(searchOptions, this.value);
|
||||
} else {
|
||||
this.updatePage(searchOptions);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current page with searchOptions
|
||||
* @param searchOptions Options to narrow down your search:
|
||||
* { pagination: PaginationComponentOptions,
|
||||
* sort: SortOptions }
|
||||
*/
|
||||
updatePage(searchOptions) {
|
||||
this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions);
|
||||
this.items$ = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current page with searchOptions and display items linked to author
|
||||
* @param searchOptions Options to narrow down your search:
|
||||
* { pagination: PaginationComponentOptions,
|
||||
* sort: SortOptions }
|
||||
* @param author The author's name for displaying items
|
||||
*/
|
||||
updatePageWithItems(searchOptions, author: string) {
|
||||
this.items$ = this.browseService.getBrowseItemsFor('author', author, searchOptions);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
import { BrowseByDatePageComponent } from './browse-by-date-page.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
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 { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ENV_CONFIG, GLOBAL_CONFIG } from '../../../config';
|
||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||
import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
|
||||
|
||||
describe('BrowseByDatePageComponent', () => {
|
||||
let comp: BrowseByDatePageComponent;
|
||||
let fixture: ComponentFixture<BrowseByDatePageComponent>;
|
||||
let route: ActivatedRoute;
|
||||
|
||||
const mockCommunity = Object.assign(new Community(), {
|
||||
id: 'test-uuid',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
value: 'test community'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const firstItem = Object.assign(new Item(), {
|
||||
id: 'first-item-id',
|
||||
metadata: {
|
||||
'dc.date.issued': [
|
||||
{
|
||||
value: '1950-01-01'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const mockBrowseService = {
|
||||
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData([]),
|
||||
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData([firstItem]),
|
||||
getFirstItemFor: () => observableOf(new RemoteData(false, false, true, undefined, firstItem))
|
||||
};
|
||||
|
||||
const mockDsoService = {
|
||||
findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
|
||||
};
|
||||
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({}),
|
||||
queryParams: observableOf({}),
|
||||
data: observableOf({ metadata: 'dateissued', metadataField: 'dc.date.issued' })
|
||||
});
|
||||
|
||||
const mockCdRef = Object.assign({
|
||||
detectChanges: () => fixture.detectChanges()
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [BrowseByDatePageComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: BrowseService, useValue: mockBrowseService },
|
||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||
{ provide: Router, useValue: new MockRouter() },
|
||||
{ provide: ChangeDetectorRef, useValue: mockCdRef }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BrowseByDatePageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
route = (comp as any).route;
|
||||
});
|
||||
|
||||
it('should initialize the list of items', () => {
|
||||
comp.items$.subscribe((result) => {
|
||||
expect(result.payload.page).toEqual([firstItem]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a list of startsWith options with the earliest year at the end (rounded down by 10)', () => {
|
||||
expect(comp.startsWithOptions[comp.startsWithOptions.length - 1]).toEqual(1950);
|
||||
});
|
||||
|
||||
it('should create a list of startsWith options with the current year first', () => {
|
||||
expect(comp.startsWithOptions[0]).toEqual(new Date().getFullYear());
|
||||
});
|
||||
});
|
@@ -0,0 +1,115 @@
|
||||
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
||||
import {
|
||||
BrowseByMetadataPageComponent,
|
||||
browseParamsToOptions
|
||||
} from '../+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-date-page',
|
||||
styleUrls: ['../+browse-by-metadata-page/browse-by-metadata-page.component.scss'],
|
||||
templateUrl: '../+browse-by-metadata-page/browse-by-metadata-page.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for browsing items by metadata definition of type 'date'
|
||||
* A metadata definition is a short term used to describe one or multiple metadata fields.
|
||||
* An example would be 'dateissued' for 'dc.date.issued'
|
||||
*/
|
||||
export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
|
||||
/**
|
||||
* The default metadata-field to use for determining the lower limit of the StartsWith dropdown options
|
||||
*/
|
||||
defaultMetadataField = 'dc.date.issued';
|
||||
|
||||
public constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
||||
protected route: ActivatedRoute,
|
||||
protected browseService: BrowseService,
|
||||
protected dsoService: DSpaceObjectDataService,
|
||||
protected router: Router,
|
||||
protected cdRef: ChangeDetectorRef) {
|
||||
super(route, browseService, dsoService, router);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.startsWithType = StartsWithType.date;
|
||||
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
this.route.params,
|
||||
this.route.queryParams,
|
||||
this.route.data,
|
||||
(params, queryParams, data ) => {
|
||||
return Object.assign({}, params, queryParams, data);
|
||||
})
|
||||
.subscribe((params) => {
|
||||
const metadataField = params.metadataField || this.defaultMetadataField;
|
||||
this.metadata = params.metadata || this.defaultMetadata;
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
const searchOptions = browseParamsToOptions(params, Object.assign({}), this.sortConfig, this.metadata);
|
||||
this.updatePageWithItems(searchOptions, this.value);
|
||||
this.updateParent(params.scope);
|
||||
this.updateStartsWithOptions(this.metadata, metadataField, params.scope);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the StartsWith options
|
||||
* In this implementation, it creates a list of years starting from now, going all the way back to the earliest
|
||||
* date found on an item within this scope. The further back in time, the bigger the change in years become to avoid
|
||||
* extremely long lists with a one-year difference.
|
||||
* To determine the change in years, the config found under GlobalConfig.BrowseBy is used for this.
|
||||
* @param definition The metadata definition to fetch the first item for
|
||||
* @param metadataField The metadata field to fetch the earliest date from (expects a date field)
|
||||
* @param scope The scope under which to fetch the earliest item for
|
||||
*/
|
||||
updateStartsWithOptions(definition: string, metadataField: string, scope?: string) {
|
||||
this.subs.push(
|
||||
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
|
||||
let lowerLimit = this.config.browseBy.defaultLowerLimit;
|
||||
if (hasValue(firstItemRD.payload)) {
|
||||
const date = firstItemRD.payload.firstMetadataValue(metadataField);
|
||||
if (hasValue(date) && hasValue(+date.split('-')[0])) {
|
||||
lowerLimit = +date.split('-')[0];
|
||||
}
|
||||
}
|
||||
const options = [];
|
||||
const currentYear = new Date().getFullYear();
|
||||
const oneYearBreak = Math.floor((currentYear - this.config.browseBy.oneYearLimit) / 5) * 5;
|
||||
const fiveYearBreak = Math.floor((currentYear - this.config.browseBy.fiveYearLimit) / 10) * 10;
|
||||
if (lowerLimit <= fiveYearBreak) {
|
||||
lowerLimit -= 10;
|
||||
} else if (lowerLimit <= oneYearBreak) {
|
||||
lowerLimit -= 5;
|
||||
} else {
|
||||
lowerLimit -= 1;
|
||||
}
|
||||
let i = currentYear;
|
||||
while (i > lowerLimit) {
|
||||
options.push(i);
|
||||
if (i <= fiveYearBreak) {
|
||||
i -= 10;
|
||||
} else if (i <= oneYearBreak) {
|
||||
i -= 5;
|
||||
} else {
|
||||
i--;
|
||||
}
|
||||
}
|
||||
if (isNotEmpty(options)) {
|
||||
this.startsWithOptions = options;
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
<div class="container">
|
||||
<div class="browse-by-metadata w-100">
|
||||
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + metadata | translate, value: (value)? '"' + value + '"': ''} }}"
|
||||
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
||||
[paginationConfig]="paginationConfig"
|
||||
[sortConfig]="sortConfig"
|
||||
[type]="startsWithType"
|
||||
[startsWithOptions]="startsWithOptions"
|
||||
[enableArrows]="true"
|
||||
(prev)="goPrev()"
|
||||
(next)="goNext()"
|
||||
(pageSizeChange)="pageSizeChange($event)"
|
||||
(sortDirectionChange)="sortDirectionChange($event)">
|
||||
</ds-browse-by>
|
||||
<ds-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-loading>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,164 @@
|
||||
import { BrowseByMetadataPageComponent, browseParamsToOptions } from './browse-by-metadata-page.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
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 { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||
|
||||
describe('BrowseByMetadataPageComponent', () => {
|
||||
let comp: BrowseByMetadataPageComponent;
|
||||
let fixture: ComponentFixture<BrowseByMetadataPageComponent>;
|
||||
let browseService: BrowseService;
|
||||
let route: ActivatedRoute;
|
||||
|
||||
const mockCommunity = Object.assign(new Community(), {
|
||||
id: 'test-uuid',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
value: 'test community'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const mockEntries = [
|
||||
{
|
||||
type: 'author',
|
||||
authority: null,
|
||||
value: 'John Doe',
|
||||
language: 'en',
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
type: 'author',
|
||||
authority: null,
|
||||
value: 'James Doe',
|
||||
language: 'en',
|
||||
count: 3
|
||||
},
|
||||
{
|
||||
type: 'subject',
|
||||
authority: null,
|
||||
value: 'Fake subject',
|
||||
language: 'en',
|
||||
count: 2
|
||||
}
|
||||
];
|
||||
|
||||
const mockItems = [
|
||||
Object.assign(new Item(), {
|
||||
id: 'fakeId'
|
||||
})
|
||||
];
|
||||
|
||||
const mockBrowseService = {
|
||||
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData(mockEntries.filter((entry) => entry.type === options.metadataDefinition)),
|
||||
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData(mockItems)
|
||||
};
|
||||
|
||||
const mockDsoService = {
|
||||
findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
|
||||
};
|
||||
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({})
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [BrowseByMetadataPageComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: BrowseService, useValue: mockBrowseService },
|
||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||
{ provide: Router, useValue: new MockRouter() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BrowseByMetadataPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
browseService = (comp as any).browseService;
|
||||
route = (comp as any).route;
|
||||
route.params = observableOf({});
|
||||
comp.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should fetch the correct entries depending on the metadata definition', () => {
|
||||
comp.browseEntries$.subscribe((result) => {
|
||||
expect(result.payload.page).toEqual(mockEntries.filter((entry) => entry.type === 'author'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fetch any items when no value is provided', () => {
|
||||
expect(comp.items$).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('when a value is provided', () => {
|
||||
beforeEach(() => {
|
||||
const paramsWithValue = {
|
||||
metadata: 'author',
|
||||
value: 'John Doe'
|
||||
};
|
||||
|
||||
route.params = observableOf(paramsWithValue);
|
||||
comp.ngOnInit();
|
||||
});
|
||||
|
||||
it('should fetch items', () => {
|
||||
comp.items$.subscribe((result) => {
|
||||
expect(result.payload.page).toEqual(mockItems);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('when calling browseParamsToOptions', () => {
|
||||
let result: BrowseEntrySearchOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
const paramsWithPaginationAndScope = {
|
||||
page: 5,
|
||||
pageSize: 10,
|
||||
sortDirection: SortDirection.ASC,
|
||||
sortField: 'fake-field',
|
||||
scope: 'fake-scope'
|
||||
};
|
||||
|
||||
result = browseParamsToOptions(paramsWithPaginationAndScope, Object.assign({}), Object.assign({}), 'author');
|
||||
});
|
||||
|
||||
it('should return BrowseEntrySearchOptions with the correct properties', () => {
|
||||
expect(result.metadataDefinition).toEqual('author');
|
||||
expect(result.pagination.currentPage).toEqual(5);
|
||||
expect(result.pagination.pageSize).toEqual(10);
|
||||
expect(result.sort.direction).toEqual(SortDirection.ASC);
|
||||
expect(result.sort.field).toEqual('fake-field');
|
||||
expect(result.scope).toEqual('fake-scope');
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
export function toRemoteData(objects: any[]): Observable<RemoteData<PaginatedList<any>>> {
|
||||
return observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), objects)));
|
||||
}
|
@@ -0,0 +1,263 @@
|
||||
import {combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||
import { getSucceededRemoteData } from '../../core/shared/operators';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-metadata-page',
|
||||
styleUrls: ['./browse-by-metadata-page.component.scss'],
|
||||
templateUrl: './browse-by-metadata-page.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for browsing (items) by metadata definition
|
||||
* A metadata definition is a short term used to describe one or multiple metadata fields.
|
||||
* An example would be 'author' for 'dc.contributor.*'
|
||||
*/
|
||||
export class BrowseByMetadataPageComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The list of browse-entries to display
|
||||
*/
|
||||
browseEntries$: Observable<RemoteData<PaginatedList<BrowseEntry>>>;
|
||||
|
||||
/**
|
||||
* The list of items to display when a value is present
|
||||
*/
|
||||
items$: Observable<RemoteData<PaginatedList<Item>>>;
|
||||
|
||||
/**
|
||||
* The current Community or Collection we're browsing metadata/items in
|
||||
*/
|
||||
parent$: Observable<RemoteData<DSpaceObject>>;
|
||||
|
||||
/**
|
||||
* The pagination config used to display the values
|
||||
*/
|
||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'browse-by-metadata-pagination',
|
||||
currentPage: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
/**
|
||||
* The sorting config used to sort the values (defaults to Ascending)
|
||||
*/
|
||||
sortConfig: SortOptions = new SortOptions('default', SortDirection.ASC);
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* The default metadata definition to resort to when none is provided
|
||||
*/
|
||||
defaultMetadata = 'author';
|
||||
|
||||
/**
|
||||
* The current metadata definition
|
||||
*/
|
||||
metadata = this.defaultMetadata;
|
||||
|
||||
/**
|
||||
* The type of StartsWith options to render
|
||||
* Defaults to text
|
||||
*/
|
||||
startsWithType = StartsWithType.text;
|
||||
|
||||
/**
|
||||
* The list of StartsWith options
|
||||
* Should be defined after ngOnInit is called!
|
||||
*/
|
||||
startsWithOptions;
|
||||
|
||||
/**
|
||||
* The value we're browing items for
|
||||
* - When the value is not empty, we're browsing items
|
||||
* - When the value is empty, we're browsing browse-entries (values for the given metadata definition)
|
||||
*/
|
||||
value = '';
|
||||
|
||||
/**
|
||||
* The current startsWith option (fetched and updated from query-params)
|
||||
*/
|
||||
startsWith: string;
|
||||
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
protected browseService: BrowseService,
|
||||
protected dsoService: DSpaceObjectDataService,
|
||||
protected router: Router) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
this.route.params,
|
||||
this.route.queryParams,
|
||||
(params, queryParams, ) => {
|
||||
return Object.assign({}, params, queryParams);
|
||||
})
|
||||
.subscribe((params) => {
|
||||
this.metadata = params.metadata || this.defaultMetadata;
|
||||
this.value = +params.value || params.value || '';
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
const searchOptions = browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.metadata);
|
||||
if (isNotEmpty(this.value)) {
|
||||
this.updatePageWithItems(searchOptions, this.value);
|
||||
} else {
|
||||
this.updatePage(searchOptions);
|
||||
}
|
||||
this.updateParent(params.scope);
|
||||
}));
|
||||
this.updateStartsWithTextOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the StartsWith options with text values
|
||||
* It adds the value "0-9" as well as all letters from A to Z
|
||||
*/
|
||||
updateStartsWithTextOptions() {
|
||||
this.startsWithOptions = ['0-9', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current page with searchOptions
|
||||
* @param searchOptions Options to narrow down your search:
|
||||
* { metadata: string
|
||||
* pagination: PaginationComponentOptions,
|
||||
* sort: SortOptions,
|
||||
* scope: string }
|
||||
*/
|
||||
updatePage(searchOptions: BrowseEntrySearchOptions) {
|
||||
this.browseEntries$ = this.browseService.getBrowseEntriesFor(searchOptions);
|
||||
this.items$ = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current page with searchOptions and display items linked to the given value
|
||||
* @param searchOptions Options to narrow down your search:
|
||||
* { metadata: string
|
||||
* pagination: PaginationComponentOptions,
|
||||
* sort: SortOptions,
|
||||
* scope: string }
|
||||
* @param value The value of the browse-entry to display items for
|
||||
*/
|
||||
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
||||
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the parent Community or Collection using their scope
|
||||
* @param scope The UUID of the Community or Collection to fetch
|
||||
*/
|
||||
updateParent(scope: string) {
|
||||
if (hasValue(scope)) {
|
||||
this.parent$ = this.dsoService.findById(scope).pipe(
|
||||
getSucceededRemoteData()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the previous page
|
||||
*/
|
||||
goPrev() {
|
||||
if (this.items$) {
|
||||
this.items$.pipe(take(1)).subscribe((items) => {
|
||||
this.items$ = this.browseService.getPrevBrowseItems(items);
|
||||
});
|
||||
} else if (this.browseEntries$) {
|
||||
this.browseEntries$.pipe(take(1)).subscribe((entries) => {
|
||||
this.browseEntries$ = this.browseService.getPrevBrowseEntries(entries);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the next page
|
||||
*/
|
||||
goNext() {
|
||||
if (this.items$) {
|
||||
this.items$.pipe(take(1)).subscribe((items) => {
|
||||
this.items$ = this.browseService.getNextBrowseItems(items);
|
||||
});
|
||||
} else if (this.browseEntries$) {
|
||||
this.browseEntries$.pipe(take(1)).subscribe((entries) => {
|
||||
this.browseEntries$ = this.browseService.getNextBrowseEntries(entries);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the page size
|
||||
* @param size
|
||||
*/
|
||||
pageSizeChange(size) {
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({ pageSize: size }),
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the sorting direction
|
||||
* @param direction
|
||||
*/
|
||||
sortDirectionChange(direction) {
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({ sortDirection: direction }),
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to transform query and url parameters into searchOptions used to fetch browse entries or items
|
||||
* @param params URL and query parameters
|
||||
* @param paginationConfig Pagination configuration
|
||||
* @param sortConfig Sorting configuration
|
||||
* @param metadata Optional metadata definition to fetch browse entries/items for
|
||||
*/
|
||||
export function browseParamsToOptions(params: any,
|
||||
paginationConfig: PaginationComponentOptions,
|
||||
sortConfig: SortOptions,
|
||||
metadata?: string): BrowseEntrySearchOptions {
|
||||
return new BrowseEntrySearchOptions(
|
||||
metadata,
|
||||
Object.assign({},
|
||||
paginationConfig,
|
||||
{
|
||||
currentPage: +params.page || paginationConfig.currentPage,
|
||||
pageSize: +params.pageSize || paginationConfig.pageSize
|
||||
}
|
||||
),
|
||||
Object.assign({},
|
||||
sortConfig,
|
||||
{
|
||||
direction: params.sortDirection || sortConfig.direction,
|
||||
field: params.sortField || sortConfig.field
|
||||
}
|
||||
),
|
||||
+params.startsWith || params.startsWith,
|
||||
params.scope
|
||||
);
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="browse-by-title w-100 row">
|
||||
<ds-browse-by class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:{collection: '', field: 'Title', value: ''} }}"
|
||||
[objects$]="items$"
|
||||
[currentUrl]="currentUrl"
|
||||
[paginationConfig]="paginationConfig"
|
||||
[sortConfig]="sortConfig">
|
||||
</ds-browse-by>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,90 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
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 { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
|
||||
import { BrowseByTitlePageComponent } from './browse-by-title-page.component';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||
|
||||
describe('BrowseByTitlePageComponent', () => {
|
||||
let comp: BrowseByTitlePageComponent;
|
||||
let fixture: ComponentFixture<BrowseByTitlePageComponent>;
|
||||
let itemDataService: ItemDataService;
|
||||
let route: ActivatedRoute;
|
||||
|
||||
const mockCommunity = Object.assign(new Community(), {
|
||||
id: 'test-uuid',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
value: 'test community'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const mockItems = [
|
||||
Object.assign(new Item(), {
|
||||
id: 'fakeId',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
value: 'Fake Title'
|
||||
}
|
||||
]
|
||||
})
|
||||
];
|
||||
|
||||
const mockBrowseService = {
|
||||
getBrowseItemsFor: () => toRemoteData(mockItems),
|
||||
getBrowseEntriesFor: () => toRemoteData([])
|
||||
};
|
||||
|
||||
const mockDsoService = {
|
||||
findById: () => observableOf(new RemoteData(false, false, true, null, mockCommunity))
|
||||
};
|
||||
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({}),
|
||||
data: observableOf({ metadata: 'title' })
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [BrowseByTitlePageComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: BrowseService, useValue: mockBrowseService },
|
||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||
{ provide: Router, useValue: new MockRouter() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BrowseByTitlePageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
itemDataService = (comp as any).itemDataService;
|
||||
route = (comp as any).route;
|
||||
});
|
||||
|
||||
it('should initialize the list of items', () => {
|
||||
comp.items$.subscribe((result) => {
|
||||
expect(result.payload.page).toEqual(mockItems);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,88 +1,51 @@
|
||||
|
||||
import {combineLatest as observableCombineLatest, Observable , Subscription } from 'rxjs';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { Component } from '@angular/core';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import {
|
||||
BrowseByMetadataPageComponent,
|
||||
browseParamsToOptions
|
||||
} from '../+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-title-page',
|
||||
styleUrls: ['./browse-by-title-page.component.scss'],
|
||||
templateUrl: './browse-by-title-page.component.html'
|
||||
styleUrls: ['../+browse-by-metadata-page/browse-by-metadata-page.component.scss'],
|
||||
templateUrl: '../+browse-by-metadata-page/browse-by-metadata-page.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for browsing items by title (dc.title)
|
||||
*/
|
||||
export class BrowseByTitlePageComponent implements OnInit {
|
||||
|
||||
items$: Observable<RemoteData<PaginatedList<Item>>>;
|
||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'browse-by-title-pagination',
|
||||
currentPage: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
|
||||
subs: Subscription[] = [];
|
||||
currentUrl: string;
|
||||
|
||||
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute) {
|
||||
export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
protected browseService: BrowseService,
|
||||
protected dsoService: DSpaceObjectDataService,
|
||||
protected router: Router) {
|
||||
super(route, browseService, dsoService, router);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.currentUrl = this.route.snapshot.pathFromRoot
|
||||
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
|
||||
.join('/');
|
||||
this.updatePage({
|
||||
pagination: this.paginationConfig,
|
||||
sort: this.sortConfig
|
||||
});
|
||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||
this.updatePage(new BrowseEntrySearchOptions(null, this.paginationConfig, this.sortConfig));
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
this.route.params,
|
||||
this.route.queryParams,
|
||||
(params, queryParams, ) => {
|
||||
return Object.assign({}, params, queryParams);
|
||||
this.route.data,
|
||||
(params, queryParams, data ) => {
|
||||
return Object.assign({}, params, queryParams, data);
|
||||
})
|
||||
.subscribe((params) => {
|
||||
const page = +params.page || this.paginationConfig.currentPage;
|
||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
||||
const sortDirection = params.sortDirection || this.sortConfig.direction;
|
||||
const sortField = params.sortField || this.sortConfig.field;
|
||||
const pagination = Object.assign({},
|
||||
this.paginationConfig,
|
||||
{ currentPage: page, pageSize: pageSize }
|
||||
);
|
||||
const sort = Object.assign({},
|
||||
this.sortConfig,
|
||||
{ direction: sortDirection, field: sortField }
|
||||
);
|
||||
this.updatePage({
|
||||
pagination: pagination,
|
||||
sort: sort
|
||||
});
|
||||
this.metadata = params.metadata || this.defaultMetadata;
|
||||
this.updatePageWithItems(browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.metadata), undefined);
|
||||
this.updateParent(params.scope)
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current page with searchOptions
|
||||
* @param searchOptions Options to narrow down your search:
|
||||
* { pagination: PaginationComponentOptions,
|
||||
* sort: SortOptions }
|
||||
*/
|
||||
updatePage(searchOptions) {
|
||||
this.items$ = this.itemDataService.findAll({
|
||||
currentPage: searchOptions.pagination.currentPage,
|
||||
elementsPerPage: searchOptions.pagination.pageSize,
|
||||
sort: searchOptions.sort
|
||||
});
|
||||
this.updateStartsWithTextOptions();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
125
src/app/+browse-by/browse-by-guard.spec.ts
Normal file
125
src/app/+browse-by/browse-by-guard.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { first } from 'rxjs/operators';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
describe('BrowseByGuard', () => {
|
||||
describe('canActivate', () => {
|
||||
let guard: BrowseByGuard;
|
||||
let dsoService: any;
|
||||
let translateService: any;
|
||||
|
||||
const name = 'An interesting DSO';
|
||||
const title = 'Author';
|
||||
const field = 'Author';
|
||||
const metadata = 'author';
|
||||
const metadataField = 'dc.contributor';
|
||||
const scope = '1234-65487-12354-1235';
|
||||
const value = 'Filter';
|
||||
|
||||
beforeEach(() => {
|
||||
dsoService = {
|
||||
findById: (id: string) => observableOf({ payload: { name: name }, hasSucceeded: true })
|
||||
};
|
||||
|
||||
translateService = {
|
||||
instant: () => field
|
||||
};
|
||||
guard = new BrowseByGuard(dsoService, translateService);
|
||||
});
|
||||
|
||||
it('should return true, and sets up the data correctly, with a scope and value', () => {
|
||||
const scopedRoute = {
|
||||
data: {
|
||||
title: field,
|
||||
metadataField,
|
||||
},
|
||||
params: {
|
||||
metadata,
|
||||
},
|
||||
queryParams: {
|
||||
scope,
|
||||
value
|
||||
}
|
||||
};
|
||||
guard.canActivate(scopedRoute as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) => {
|
||||
const result = {
|
||||
title,
|
||||
metadata,
|
||||
metadataField,
|
||||
collection: name,
|
||||
field,
|
||||
value: '"' + value + '"'
|
||||
};
|
||||
expect(scopedRoute.data).toEqual(result);
|
||||
expect(canActivate).toEqual(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true, and sets up the data correctly, with a scope and without value', () => {
|
||||
const scopedNoValueRoute = {
|
||||
data: {
|
||||
title: field,
|
||||
metadataField,
|
||||
},
|
||||
params: {
|
||||
metadata,
|
||||
},
|
||||
queryParams: {
|
||||
scope
|
||||
}
|
||||
};
|
||||
|
||||
guard.canActivate(scopedNoValueRoute as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) => {
|
||||
const result = {
|
||||
title,
|
||||
metadata,
|
||||
metadataField,
|
||||
collection: name,
|
||||
field,
|
||||
value: ''
|
||||
};
|
||||
expect(scopedNoValueRoute.data).toEqual(result);
|
||||
expect(canActivate).toEqual(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true, and sets up the data correctly, without a scope and with a value', () => {
|
||||
const route = {
|
||||
data: {
|
||||
title: field,
|
||||
metadataField,
|
||||
},
|
||||
params: {
|
||||
metadata,
|
||||
},
|
||||
queryParams: {
|
||||
value
|
||||
}
|
||||
};
|
||||
guard.canActivate(route as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) => {
|
||||
const result = {
|
||||
title,
|
||||
metadata,
|
||||
metadataField,
|
||||
collection: '',
|
||||
field,
|
||||
value: '"' + value + '"'
|
||||
};
|
||||
expect(route.data).toEqual(result);
|
||||
expect(canActivate).toEqual(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
52
src/app/+browse-by/browse-by-guard.ts
Normal file
52
src/app/+browse-by/browse-by-guard.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* A guard taking care of the correct route.data being set for the Browse-By components
|
||||
*/
|
||||
export class BrowseByGuard implements CanActivate {
|
||||
|
||||
constructor(protected dsoService: DSpaceObjectDataService,
|
||||
protected translate: TranslateService) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const title = route.data.title;
|
||||
const metadata = route.params.metadata || route.queryParams.metadata || route.data.metadata;
|
||||
const metadataField = route.data.metadataField;
|
||||
const scope = route.queryParams.scope;
|
||||
const value = route.queryParams.value;
|
||||
const metadataTranslated = this.translate.instant('browse.metadata.' + metadata);
|
||||
if (hasValue(scope)) {
|
||||
const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getSucceededRemoteData());
|
||||
return dsoAndMetadata$.pipe(
|
||||
map((dsoRD) => {
|
||||
const name = dsoRD.payload.name;
|
||||
route.data = this.createData(title, metadata, metadataField, name, metadataTranslated, value);
|
||||
return true;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
route.data = this.createData(title, metadata, metadataField, '', metadataTranslated, value);
|
||||
return observableOf(true);
|
||||
}
|
||||
}
|
||||
|
||||
private createData(title, metadata, metadataField, collection, field, value) {
|
||||
return {
|
||||
title: title,
|
||||
metadata: metadata,
|
||||
metadataField: metadataField,
|
||||
collection: collection,
|
||||
field: field,
|
||||
value: hasValue(value) ? `"${value}"` : ''
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,13 +1,16 @@
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
|
||||
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
|
||||
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { BrowseByDatePageComponent } from './+browse-by-date-page/browse-by-date-page.component';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: 'title', component: BrowseByTitlePageComponent },
|
||||
{ path: 'author', component: BrowseByAuthorPageComponent }
|
||||
{ path: 'title', component: BrowseByTitlePageComponent, canActivate: [BrowseByGuard], data: { metadata: 'title', title: 'browse.title' } },
|
||||
{ path: 'dateissued', component: BrowseByDatePageComponent, canActivate: [BrowseByGuard], data: { metadata: 'dateissued', metadataField: 'dc.date.issued', title: 'browse.title' } },
|
||||
{ path: ':metadata', component: BrowseByMetadataPageComponent, canActivate: [BrowseByGuard], data: { title: 'browse.title' } }
|
||||
])
|
||||
]
|
||||
})
|
||||
|
@@ -4,8 +4,10 @@ import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-ti
|
||||
import { ItemDataService } from '../core/data/item-data.service';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { BrowseByRoutingModule } from './browse-by-routing.module';
|
||||
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
|
||||
import { BrowseService } from '../core/browse/browse.service';
|
||||
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { BrowseByDatePageComponent } from './+browse-by-date-page/browse-by-date-page.component';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -15,11 +17,13 @@ import { BrowseService } from '../core/browse/browse.service';
|
||||
],
|
||||
declarations: [
|
||||
BrowseByTitlePageComponent,
|
||||
BrowseByAuthorPageComponent
|
||||
BrowseByMetadataPageComponent,
|
||||
BrowseByDatePageComponent
|
||||
],
|
||||
providers: [
|
||||
ItemDataService,
|
||||
BrowseService
|
||||
BrowseService,
|
||||
BrowseByGuard
|
||||
]
|
||||
})
|
||||
export class BrowseByModule {
|
||||
|
@@ -0,0 +1,71 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import {
|
||||
DynamicInputModel,
|
||||
DynamicTextAreaModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||
import { ResourceType } from '../../core/shared/resource-type';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
|
||||
/**
|
||||
* Form used for creating and editing collections
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-collection-form',
|
||||
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
})
|
||||
export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||
/**
|
||||
* @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited
|
||||
*/
|
||||
@Input() dso: Collection = new Collection();
|
||||
|
||||
/**
|
||||
* @type {ResourceType.Collection} This is a collection-type form
|
||||
*/
|
||||
protected type = ResourceType.Collection;
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a collection
|
||||
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||
*/
|
||||
formModel: DynamicFormControlModel[] = [
|
||||
new DynamicInputModel({
|
||||
id: 'title',
|
||||
name: 'dc.title',
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'Please enter a name for this title'
|
||||
},
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'dc.description',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'abstract',
|
||||
name: 'dc.description.abstract',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'rights',
|
||||
name: 'dc.rights',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'tableofcontents',
|
||||
name: 'dc.description.tableofcontents',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'license',
|
||||
name: 'dc.rights.license',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'provenance',
|
||||
name: 'dc.description.provenance',
|
||||
}),
|
||||
];
|
||||
}
|
@@ -3,10 +3,57 @@ import { RouterModule } from '@angular/router';
|
||||
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
import { CollectionPageResolver } from './collection-page.resolver';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||
import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { getCollectionModulePath } from '../app-routing.module';
|
||||
|
||||
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
||||
|
||||
export function getCollectionPageRoute(collectionId: string) {
|
||||
return new URLCombiner(getCollectionModulePath(), collectionId).toString();
|
||||
}
|
||||
|
||||
export function getCollectionEditPath(id: string) {
|
||||
return new URLCombiner(getCollectionModulePath(), COLLECTION_EDIT_PATH.replace(/:id/, id)).toString()
|
||||
}
|
||||
|
||||
export function getCollectionCreatePath() {
|
||||
return new URLCombiner(getCollectionModulePath(), COLLECTION_CREATE_PATH).toString()
|
||||
}
|
||||
|
||||
const COLLECTION_CREATE_PATH = 'create';
|
||||
const COLLECTION_EDIT_PATH = ':id/edit';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: COLLECTION_CREATE_PATH,
|
||||
component: CreateCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||
},
|
||||
{
|
||||
path: COLLECTION_EDIT_PATH,
|
||||
pathMatch: 'full',
|
||||
component: EditCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CollectionPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id/delete',
|
||||
pathMatch: 'full',
|
||||
component: DeleteCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CollectionPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: CollectionPageComponent,
|
||||
@@ -19,6 +66,7 @@ import { CollectionPageResolver } from './collection-page.resolver';
|
||||
],
|
||||
providers: [
|
||||
CollectionPageResolver,
|
||||
CreateCollectionPageGuard
|
||||
]
|
||||
})
|
||||
export class CollectionPageRoutingModule {
|
||||
|
@@ -7,6 +7,8 @@
|
||||
<ds-comcol-page-header
|
||||
[name]="collection.name">
|
||||
</ds-comcol-page-header>
|
||||
<!-- Browse-By Links -->
|
||||
<ds-comcol-page-browse-by [id]="collection.id"></ds-comcol-page-browse-by>
|
||||
<!-- Collection logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||
[logo]="(logoRD$ | async)?.payload"
|
||||
@@ -30,7 +32,7 @@
|
||||
</ds-comcol-page-content>
|
||||
<!-- Licence -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.license"
|
||||
[content]="collection.dcLicense"
|
||||
[title]="'collection.page.license'">
|
||||
</ds-comcol-page-content>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
@@ -15,7 +15,7 @@ import { Item } from '../core/shared/item.model';
|
||||
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { filter, flatMap, map } from 'rxjs/operators';
|
||||
import { filter, flatMap, map, tap } from 'rxjs/operators';
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||
import { toDSpaceObjectListRD } from '../core/shared/operators';
|
||||
@@ -55,7 +55,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.collectionRD$ = this.route.data.pipe(
|
||||
map((data) => data.collection)
|
||||
map((data) => data.collection),
|
||||
tap((data) => this.collectionId = data.payload.id)
|
||||
);
|
||||
this.logoRD$ = this.collectionRD$.pipe(
|
||||
map((rd: RemoteData<Collection>) => rd.payload),
|
||||
@@ -75,8 +76,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
pagination: pagination,
|
||||
sort: this.sortConfig
|
||||
});
|
||||
})
|
||||
);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
updatePage(searchOptions) {
|
||||
|
@@ -5,17 +5,24 @@ import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
||||
import { SearchPageModule } from '../+search-page/search-page.module';
|
||||
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SearchPageModule,
|
||||
CollectionPageRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
CreateCollectionPageComponent,
|
||||
EditCollectionPageComponent,
|
||||
DeleteCollectionPageComponent,
|
||||
CollectionFormComponent
|
||||
]
|
||||
})
|
||||
export class CollectionPageModule {
|
||||
|
28
src/app/+collection-page/collection-page.resolver.spec.ts
Normal file
28
src/app/+collection-page/collection-page.resolver.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { first } from 'rxjs/operators';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CollectionPageResolver } from './collection-page.resolver';
|
||||
|
||||
describe('CollectionPageResolver', () => {
|
||||
describe('resolve', () => {
|
||||
let resolver: CollectionPageResolver;
|
||||
let collectionService: any;
|
||||
const uuid = '1234-65487-12354-1235';
|
||||
|
||||
beforeEach(() => {
|
||||
collectionService = {
|
||||
findById: (id: string) => observableOf({ payload: { id }, hasSucceeded: true })
|
||||
};
|
||||
resolver = new CollectionPageResolver(collectionService);
|
||||
});
|
||||
|
||||
it('should resolve a collection with the correct id', () => {
|
||||
resolver.resolve({ params: { id: uuid } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(resolved) => {
|
||||
expect(resolved.payload.id).toEqual(uuid);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,8 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ds-collection-form (submitForm)="onSubmit($event)"></ds-collection-form>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,46 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page.component';
|
||||
|
||||
describe('CreateCollectionPageComponent', () => {
|
||||
let comp: CreateCollectionPageComponent;
|
||||
let fixture: ComponentFixture<CreateCollectionPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [CreateCollectionPageComponent],
|
||||
providers: [
|
||||
{ provide: CollectionDataService, useValue: {} },
|
||||
{
|
||||
provide: CommunityDataService,
|
||||
useValue: { findById: () => observableOf({ payload: { name: 'test' } }) }
|
||||
},
|
||||
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||
{ provide: Router, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CreateCollectionPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,28 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can create a new Collection
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-create-collection',
|
||||
styleUrls: ['./create-collection-page.component.scss'],
|
||||
templateUrl: './create-collection-page.component.html'
|
||||
})
|
||||
export class CreateCollectionPageComponent extends CreateComColPageComponent<Collection> {
|
||||
protected frontendURL = '/collections/';
|
||||
|
||||
public constructor(
|
||||
protected communityDataService: CommunityDataService,
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected routeService: RouteService,
|
||||
protected router: Router
|
||||
) {
|
||||
super(collectionDataService, communityDataService, routeService, router);
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
import { CreateCollectionPageGuard } from './create-collection-page.guard';
|
||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
describe('CreateCollectionPageGuard', () => {
|
||||
describe('canActivate', () => {
|
||||
let guard: CreateCollectionPageGuard;
|
||||
let router;
|
||||
let communityDataServiceStub: any;
|
||||
|
||||
beforeEach(() => {
|
||||
communityDataServiceStub = {
|
||||
findById: (id: string) => {
|
||||
if (id === 'valid-id') {
|
||||
return observableOf(new RemoteData(false, false, true, null, new Community()));
|
||||
} else if (id === 'invalid-id') {
|
||||
return observableOf(new RemoteData(false, false, true, null, undefined));
|
||||
} else if (id === 'error-id') {
|
||||
return observableOf(new RemoteData(false, false, false, null, new Community()));
|
||||
}
|
||||
}
|
||||
};
|
||||
router = new MockRouter();
|
||||
|
||||
guard = new CreateCollectionPageGuard(router, communityDataServiceStub);
|
||||
});
|
||||
|
||||
it('should return true when the parent ID resolves to a community', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when no parent ID has been provided', () => {
|
||||
guard.canActivate({ queryParams: { } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when the parent ID does not resolve to a community', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when the parent ID resolves to an error response', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Prevent creation of a collection without a parent community provided
|
||||
* @class CreateCollectionPageGuard
|
||||
*/
|
||||
@Injectable()
|
||||
export class CreateCollectionPageGuard implements CanActivate {
|
||||
public constructor(private router: Router, private communityService: CommunityDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community
|
||||
* Reroutes to a 404 page when the page cannot be activated
|
||||
* @method canActivate
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
const parentID = route.queryParams.parent;
|
||||
if (hasNoValue(parentID)) {
|
||||
this.router.navigate(['/404']);
|
||||
return observableOf(false);
|
||||
}
|
||||
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||
.pipe(
|
||||
getFinishedRemoteData(),
|
||||
);
|
||||
|
||||
return parent.pipe(
|
||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||
tap((isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
this.router.navigate(['/404']);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||
}}</h2>
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||
{{'community.delete.confirm' |
|
||||
translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,41 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page.component';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
|
||||
describe('DeleteCollectionPageComponent', () => {
|
||||
let comp: DeleteCollectionPageComponent;
|
||||
let fixture: ComponentFixture<DeleteCollectionPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [DeleteCollectionPageComponent],
|
||||
providers: [
|
||||
{ provide: CollectionDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DeleteCollectionPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,29 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can delete an existing Collection
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-delete-collection',
|
||||
styleUrls: ['./delete-collection-page.component.scss'],
|
||||
templateUrl: './delete-collection-page.component.html'
|
||||
})
|
||||
export class DeleteCollectionPageComponent extends DeleteComColPageComponent<Collection> {
|
||||
protected frontendURL = '/collections/';
|
||||
|
||||
public constructor(
|
||||
protected dsoDataService: CollectionDataService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
protected notifications: NotificationsService,
|
||||
protected translate: TranslateService
|
||||
) {
|
||||
super(dsoDataService, router, route, notifications, translate);
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'collection.edit.head' | translate }}</h2>
|
||||
<ds-collection-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-collection-form>
|
||||
<a class="btn btn-danger"
|
||||
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete'
|
||||
| translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { EditCollectionPageComponent } from './edit-collection-page.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
describe('EditCollectionPageComponent', () => {
|
||||
let comp: EditCollectionPageComponent;
|
||||
let fixture: ComponentFixture<EditCollectionPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [EditCollectionPageComponent],
|
||||
providers: [
|
||||
{ provide: CollectionDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditCollectionPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can edit an existing Collection
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-edit-collection',
|
||||
styleUrls: ['./edit-collection-page.component.scss'],
|
||||
templateUrl: './edit-collection-page.component.html'
|
||||
})
|
||||
export class EditCollectionPageComponent extends EditComColPageComponent<Collection> {
|
||||
protected frontendURL = '/collections/';
|
||||
|
||||
public constructor(
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute
|
||||
) {
|
||||
super(collectionDataService, router, route);
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { ResourceType } from '../../core/shared/resource-type';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
|
||||
/**
|
||||
* Form used for creating and editing communities
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-community-form',
|
||||
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
})
|
||||
export class CommunityFormComponent extends ComColFormComponent<Community> {
|
||||
/**
|
||||
* @type {Community} A new community when a community is being created, an existing Input community when a community is being edited
|
||||
*/
|
||||
@Input() dso: Community = new Community();
|
||||
|
||||
/**
|
||||
* @type {ResourceType.Community} This is a community-type form
|
||||
*/
|
||||
protected type = ResourceType.Community;
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a community
|
||||
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||
*/
|
||||
formModel: DynamicFormControlModel[] = [
|
||||
new DynamicInputModel({
|
||||
id: 'title',
|
||||
name: 'dc.title',
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'Please enter a name for this title'
|
||||
},
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'dc.description',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'abstract',
|
||||
name: 'dc.description.abstract',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'rights',
|
||||
name: 'dc.rights',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'tableofcontents',
|
||||
name: 'dc.description.tableofcontents',
|
||||
}),
|
||||
];
|
||||
}
|
@@ -3,10 +3,57 @@ import { RouterModule } from '@angular/router';
|
||||
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageResolver } from './community-page.resolver';
|
||||
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||
import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard';
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { getCommunityModulePath } from '../app-routing.module';
|
||||
|
||||
export const COMMUNITY_PARENT_PARAMETER = 'parent';
|
||||
|
||||
export function getCommunityPageRoute(communityId: string) {
|
||||
return new URLCombiner(getCommunityModulePath(), communityId).toString();
|
||||
}
|
||||
|
||||
export function getCommunityEditPath(id: string) {
|
||||
return new URLCombiner(getCommunityModulePath(), COMMUNITY_EDIT_PATH.replace(/:id/, id)).toString()
|
||||
}
|
||||
|
||||
export function getCommunityCreatePath() {
|
||||
return new URLCombiner(getCommunityModulePath(), COMMUNITY_CREATE_PATH).toString()
|
||||
}
|
||||
|
||||
const COMMUNITY_CREATE_PATH = 'create';
|
||||
const COMMUNITY_EDIT_PATH = ':id/edit';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: COMMUNITY_CREATE_PATH,
|
||||
component: CreateCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||
},
|
||||
{
|
||||
path: COMMUNITY_EDIT_PATH,
|
||||
pathMatch: 'full',
|
||||
component: EditCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CommunityPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id/delete',
|
||||
pathMatch: 'full',
|
||||
component: DeleteCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CommunityPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: CommunityPageComponent,
|
||||
@@ -19,6 +66,7 @@ import { CommunityPageResolver } from './community-page.resolver';
|
||||
],
|
||||
providers: [
|
||||
CommunityPageResolver,
|
||||
CreateCommunityPageGuard
|
||||
]
|
||||
})
|
||||
export class CommunityPageRoutingModule {
|
||||
|
@@ -3,6 +3,8 @@
|
||||
<div *ngIf="communityRD?.payload; let communityPayload">
|
||||
<!-- Community name -->
|
||||
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
||||
<!-- Browse-By Links -->
|
||||
<ds-comcol-page-browse-by [id]="communityPayload.id"></ds-comcol-page-browse-by>
|
||||
<!-- Community logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||
[logo]="(logoRD$ | async)?.payload"
|
||||
@@ -24,9 +26,11 @@
|
||||
[content]="communityPayload.copyrightText"
|
||||
[hasInnerHtml]="true">
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
||||
</div>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import {mergeMap, filter, map} from 'rxjs/operators';
|
||||
import { mergeMap, filter, map } from 'rxjs/operators';
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@@ -21,11 +21,19 @@ import { hasValue } from '../shared/empty.util';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [fadeInOut]
|
||||
})
|
||||
export class CommunityPageComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* This component represents a detail page for a single community
|
||||
*/
|
||||
export class CommunityPageComponent implements OnInit {
|
||||
/**
|
||||
* The community displayed on this page
|
||||
*/
|
||||
communityRD$: Observable<RemoteData<Community>>;
|
||||
logoRD$: Observable<RemoteData<Bitstream>>;
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* The logo of this community
|
||||
*/
|
||||
logoRD$: Observable<RemoteData<Bitstream>>;
|
||||
constructor(
|
||||
private communityDataService: CommunityDataService,
|
||||
private metadata: MetadataService,
|
||||
@@ -42,8 +50,4 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
|
||||
mergeMap((community: Community) => community.logo));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,6 +6,11 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
||||
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
||||
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||
import { CommunityFormComponent } from './community-form/community-form.component';
|
||||
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -16,8 +21,14 @@ import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||
declarations: [
|
||||
CommunityPageComponent,
|
||||
CommunityPageSubCollectionListComponent,
|
||||
CommunityPageSubCommunityListComponent,
|
||||
CreateCommunityPageComponent,
|
||||
EditCommunityPageComponent,
|
||||
DeleteCommunityPageComponent,
|
||||
CommunityFormComponent
|
||||
]
|
||||
})
|
||||
|
||||
export class CommunityPageModule {
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 pb-4">
|
||||
<ng-container *ngVar="(parentRD$ | async)?.payload as parent">
|
||||
<h2 *ngIf="!parent" id="header" class="border-bottom pb-2">{{ 'community.create.head' | translate }}</h2>
|
||||
<h2 *ngIf="parent" id="sub-header" class="border-bottom pb-2">{{ 'community.create.sub-head' | translate:{ parent: parent.name } }}</h2>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<ds-community-form (submitForm)="onSubmit($event)"></ds-community-form>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,42 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { CreateCommunityPageComponent } from './create-community-page.component';
|
||||
|
||||
describe('CreateCommunityPageComponent', () => {
|
||||
let comp: CreateCommunityPageComponent;
|
||||
let fixture: ComponentFixture<CreateCommunityPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [CreateCommunityPageComponent],
|
||||
providers: [
|
||||
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
|
||||
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||
{ provide: Router, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CreateCommunityPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can create a new Community
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-create-community',
|
||||
styleUrls: ['./create-community-page.component.scss'],
|
||||
templateUrl: './create-community-page.component.html'
|
||||
})
|
||||
export class CreateCommunityPageComponent extends CreateComColPageComponent<Community> {
|
||||
protected frontendURL = '/communities/';
|
||||
|
||||
public constructor(
|
||||
protected communityDataService: CommunityDataService,
|
||||
protected routeService: RouteService,
|
||||
protected router: Router
|
||||
) {
|
||||
super(communityDataService, communityDataService, routeService, router);
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
import { CreateCommunityPageGuard } from './create-community-page.guard';
|
||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
describe('CreateCommunityPageGuard', () => {
|
||||
describe('canActivate', () => {
|
||||
let guard: CreateCommunityPageGuard;
|
||||
let router;
|
||||
let communityDataServiceStub: any;
|
||||
|
||||
beforeEach(() => {
|
||||
communityDataServiceStub = {
|
||||
findById: (id: string) => {
|
||||
if (id === 'valid-id') {
|
||||
return observableOf(new RemoteData(false, false, true, null, new Community()));
|
||||
} else if (id === 'invalid-id') {
|
||||
return observableOf(new RemoteData(false, false, true, null, undefined));
|
||||
} else if (id === 'error-id') {
|
||||
return observableOf(new RemoteData(false, false, false, null, new Community()));
|
||||
}
|
||||
}
|
||||
};
|
||||
router = new MockRouter();
|
||||
|
||||
guard = new CreateCommunityPageGuard(router, communityDataServiceStub);
|
||||
});
|
||||
|
||||
it('should return true when the parent ID resolves to a community', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true when no parent ID has been provided', () => {
|
||||
guard.canActivate({ queryParams: { } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when the parent ID does not resolve to a community', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when the parent ID resolves to an error response', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Prevent creation of a community with an invalid parent community provided
|
||||
* @class CreateCommunityPageGuard
|
||||
*/
|
||||
@Injectable()
|
||||
export class CreateCommunityPageGuard implements CanActivate {
|
||||
public constructor(private router: Router, private communityService: CommunityDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community
|
||||
* Reroutes to a 404 page when the page cannot be activated
|
||||
* @method canActivate
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
const parentID = route.queryParams.parent;
|
||||
if (hasNoValue(parentID)) {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||
.pipe(
|
||||
getFinishedRemoteData(),
|
||||
);
|
||||
|
||||
return parent.pipe(
|
||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||
tap((isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
this.router.navigate(['/404']);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||
}}</h2>
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||
{{'community.delete.confirm' |
|
||||
translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,42 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page.component';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
|
||||
describe('DeleteCommunityPageComponent', () => {
|
||||
let comp: DeleteCommunityPageComponent;
|
||||
let fixture: ComponentFixture<DeleteCommunityPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [DeleteCommunityPageComponent],
|
||||
providers: [
|
||||
{ provide: CommunityDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DeleteCommunityPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,29 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can delete an existing Community
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-delete-community',
|
||||
styleUrls: ['./delete-community-page.component.scss'],
|
||||
templateUrl: './delete-community-page.component.html'
|
||||
})
|
||||
export class DeleteCommunityPageComponent extends DeleteComColPageComponent<Community> {
|
||||
protected frontendURL = '/communities/';
|
||||
|
||||
public constructor(
|
||||
protected dsoDataService: CommunityDataService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
protected notifications: NotificationsService,
|
||||
protected translate: TranslateService
|
||||
) {
|
||||
super(dsoDataService, router, route, notifications, translate);
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.edit.head' | translate }}</h2>
|
||||
<ds-community-form (submitForm)="onSubmit($event)"
|
||||
[dso]="(dsoRD$ | async)?.payload"></ds-community-form>
|
||||
<a class="btn btn-danger"
|
||||
[routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete'
|
||||
| translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { EditCommunityPageComponent } from './edit-community-page.component';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
|
||||
describe('EditCommunityPageComponent', () => {
|
||||
let comp: EditCommunityPageComponent;
|
||||
let fixture: ComponentFixture<EditCommunityPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [EditCommunityPageComponent],
|
||||
providers: [
|
||||
{ provide: CommunityDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditCommunityPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can edit an existing Community
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-edit-community',
|
||||
styleUrls: ['./edit-community-page.component.scss'],
|
||||
templateUrl: './edit-community-page.component.html'
|
||||
})
|
||||
export class EditCommunityPageComponent extends EditComColPageComponent<Community> {
|
||||
protected frontendURL = '/communities/';
|
||||
|
||||
public constructor(
|
||||
protected communityDataService: CommunityDataService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute
|
||||
) {
|
||||
super(communityDataService, router, route);
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<ul>
|
||||
<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,90 @@
|
||||
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(), {
|
||||
id: '123456789-1',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ language: 'en_US', value: 'SubCommunity 1' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
Object.assign(new Community(), {
|
||||
id: '123456789-2',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ language: 'en_US', value: 'SubCommunity 2' }
|
||||
]
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
const emptySubCommunitiesCommunity = Object.assign(new Community(), {
|
||||
metadata: {
|
||||
'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: {
|
||||
'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);
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user