mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'master' into w2p-62589_Item-mapper-update
Conflicts: resources/i18n/en.json src/app/+collection-page/collection-page-routing.module.ts src/app/+collection-page/collection-page.module.ts src/app/+community-page/community-page.component.ts src/app/+item-page/edit-item-page/edit-item-page.component.ts src/app/+item-page/edit-item-page/edit-item-page.module.ts src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts src/app/+item-page/edit-item-page/item-private/item-private.component.ts src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts src/app/+item-page/edit-item-page/item-public/item-public.component.ts src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts src/app/+item-page/edit-item-page/item-status/item-status.component.ts src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts src/app/+item-page/item-page-routing.module.ts src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts src/app/+search-page/search-filters/search-filter/search-filter.component.ts src/app/+search-page/search-service/search.service.ts src/app/app.reducer.ts src/app/core/auth/auth-response-parsing.service.ts src/app/core/auth/auth.service.ts src/app/core/auth/server-auth.service.ts src/app/core/cache/builders/remote-data-build.service.ts src/app/core/cache/object-cache.service.ts src/app/core/core.module.ts src/app/core/data/collection-data.service.ts src/app/core/data/data.service.ts src/app/core/data/item-data.service.spec.ts src/app/core/data/item-data.service.ts src/app/core/data/request.models.ts src/app/core/data/request.reducer.ts src/app/core/data/request.service.spec.ts src/app/core/data/request.service.ts src/app/core/index/index.effects.ts src/app/core/index/index.reducer.ts src/app/core/metadata/metadata.service.spec.ts src/app/core/shared/operators.spec.ts src/app/core/shared/operators.ts src/app/header/header.component.spec.ts src/app/shared/shared.module.ts
This commit is contained in:
@@ -14,7 +14,7 @@ If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [h
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
**Ensure you're running [Node](https://nodejs.org) >= `v8.0.x`, [npm](https://www.npmjs.com/) >= `v3.x` and [yarn](https://yarnpkg.com) >= `v0.20.x`**
|
||||
**Ensure you're running [Node](https://nodejs.org) >= `v8.0.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`**
|
||||
|
||||
```bash
|
||||
# clone the repo
|
||||
@@ -65,7 +65,7 @@ Requirements
|
||||
------------
|
||||
|
||||
- [Node.js](https://nodejs.org), [npm](https://www.npmjs.com/), and [yarn](https://yarnpkg.com)
|
||||
- Ensure you're running node >= `v5.x`, npm >= `v3.x` and yarn >= `v0.20.x`
|
||||
- Ensure you're running node >= `v8.x`, npm >= `v5.x` and yarn >= `v1.x`
|
||||
|
||||
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
||||
|
||||
|
@@ -10,7 +10,7 @@ module.exports = {
|
||||
// The REST API server settings.
|
||||
rest: {
|
||||
ssl: true,
|
||||
host: 'dspace7.4science.it',
|
||||
host: 'dspace7.4science.cloud',
|
||||
port: 443,
|
||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: '/dspace-spring-rest/api'
|
||||
@@ -48,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,
|
||||
@@ -59,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();
|
||||
}
|
||||
}
|
||||
|
23
package.json
23
package.json
@@ -23,9 +23,9 @@
|
||||
"prebuild": "yarn run clean:dist",
|
||||
"prebuild:aot": "yarn run prebuild",
|
||||
"prebuild:prod": "yarn run prebuild",
|
||||
"build": "webpack --progress --mode development",
|
||||
"build:aot": "webpack --env.aot --env.server --mode development && webpack --env.aot --env.client --mode development",
|
||||
"build:prod": "webpack --env.aot --env.server --mode production && webpack --env.aot --env.client --mode production",
|
||||
"build": "node ./webpack/run-webpack.js --progress --mode development",
|
||||
"build:aot": "node ./webpack/run-webpack.js --env.aot --env.server --mode development && node ./webpack/run-webpack.js --env.aot --env.client --mode development",
|
||||
"build:prod": "node ./webpack/run-webpack.js --env.aot --env.server --mode production && node ./webpack/run-webpack.js --env.aot --env.client --mode production",
|
||||
"postbuild:prod": "yarn run rollup",
|
||||
"rollup": "rollup -c rollup.config.js",
|
||||
"prestart": "yarn run build:prod",
|
||||
@@ -40,7 +40,7 @@
|
||||
"server": "node dist/server.js",
|
||||
"server:watch": "nodemon dist/server.js",
|
||||
"server:watch:debug": "nodemon --debug dist/server.js",
|
||||
"webpack:watch": "webpack -w --mode development",
|
||||
"webpack:watch": "node ./webpack/run-webpack.js -w --mode development",
|
||||
"watch": "yarn run build && npm-run-all -p webpack:watch server:watch",
|
||||
"watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug",
|
||||
"predebug": "yarn run build",
|
||||
@@ -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,7 +89,7 @@
|
||||
"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",
|
||||
@@ -97,6 +97,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",
|
||||
@@ -107,11 +108,11 @@
|
||||
"jwt-decode": "^2.2.0",
|
||||
"methods": "1.1.2",
|
||||
"moment": "^2.22.1",
|
||||
"morgan": "1.9.0",
|
||||
"morgan": "^1.9.1",
|
||||
"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",
|
||||
@@ -119,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",
|
||||
@@ -142,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",
|
||||
@@ -162,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",
|
||||
@@ -187,6 +191,7 @@
|
||||
"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",
|
||||
|
@@ -14,6 +14,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"item-mapper": {
|
||||
"head": "Item Mapper - Map Items from Other Collections",
|
||||
"collection": "Collection: \"<b>{{name}}</b>\"",
|
||||
@@ -65,16 +97,48 @@
|
||||
},
|
||||
"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": {
|
||||
"page": {
|
||||
"author": "Author",
|
||||
"author": "Authors",
|
||||
"abstract": "Abstract",
|
||||
"date": "Date",
|
||||
"uri": "URI",
|
||||
"files": "Files",
|
||||
"collections": "Collections",
|
||||
"subject": "Keywords",
|
||||
"citation": "Citation",
|
||||
"filesection": {
|
||||
"download": "Download",
|
||||
"name": "Name:",
|
||||
@@ -101,6 +165,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",
|
||||
@@ -143,16 +208,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"
|
||||
}
|
||||
},
|
||||
"modify.overview": {
|
||||
@@ -166,7 +235,7 @@
|
||||
"confirm": "Withdraw",
|
||||
"cancel": "Cancel",
|
||||
"success": "The item was withdrawn successfully",
|
||||
"error": "An error occured while withdrawing the item"
|
||||
"error": "An error occurred while withdrawing the item"
|
||||
},
|
||||
"reinstate": {
|
||||
"header": "Reinstate item: {{ id }}",
|
||||
@@ -174,7 +243,7 @@
|
||||
"confirm": "Reinstate",
|
||||
"cancel": "Cancel",
|
||||
"success": "The item was reinstated successfully",
|
||||
"error": "An error occured while reinstating the item"
|
||||
"error": "An error occurred while reinstating the item"
|
||||
},
|
||||
"private": {
|
||||
"header": "Make item private: {{ id }}",
|
||||
@@ -182,7 +251,7 @@
|
||||
"confirm": "Make it Private",
|
||||
"cancel": "Cancel",
|
||||
"success": "The item is now private",
|
||||
"error": "An error occured while making the item private"
|
||||
"error": "An error occurred while making the item private"
|
||||
},
|
||||
"public": {
|
||||
"header": "Make item public: {{ id }}",
|
||||
@@ -190,7 +259,7 @@
|
||||
"confirm": "Make it Public",
|
||||
"cancel": "Cancel",
|
||||
"success": "The item is now public",
|
||||
"error": "An error occured while making the item public"
|
||||
"error": "An error occurred while making the item public"
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete item: {{ id }}",
|
||||
@@ -198,7 +267,48 @@
|
||||
"confirm": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"success": "The item has been deleted",
|
||||
"error": "An error occured while deleting the item"
|
||||
"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."
|
||||
}
|
||||
}
|
||||
},
|
||||
"item-mapper": {
|
||||
"head": "Item Mapper - Map Item to Collections",
|
||||
@@ -238,6 +348,114 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"relationships": {
|
||||
"isPublicationOf": "Publications",
|
||||
"isProjectOf": "Research Projects",
|
||||
"isOrgUnitOf": "Organizational Units",
|
||||
"isAuthorOf": "Authors",
|
||||
"isPersonOf": "Authors",
|
||||
"isJournalOf": "Journals",
|
||||
"isSingleJournalOf": "Journal",
|
||||
"isVolumeOf": "Journal Volumes",
|
||||
"isSingleVolumeOf": "Journal Volume",
|
||||
"isIssueOf": "Journal Issues",
|
||||
"isJournalIssueOf": "Journal Issue",
|
||||
"isPublicationOfJournalIssue": "Articles"
|
||||
},
|
||||
"person": {
|
||||
"page": {
|
||||
"titleprefix": "Person: ",
|
||||
"jobtitle": "Job Title",
|
||||
"lastname": "Last Name",
|
||||
"firstname": "First Name",
|
||||
"email": "Email Address",
|
||||
"orcid": "ORCID",
|
||||
"birthdate": "Birth Date",
|
||||
"staffid": "Staff ID",
|
||||
"link": {
|
||||
"full": "Show all metadata"
|
||||
}
|
||||
},
|
||||
"listelement": {
|
||||
"badge": "Person"
|
||||
}
|
||||
},
|
||||
"project": {
|
||||
"page": {
|
||||
"titleprefix": "Research Project: ",
|
||||
"status": "Status",
|
||||
"contributor": "Contributors",
|
||||
"funder": "Funders",
|
||||
"id": "ID",
|
||||
"expectedcompletion": "Expected Completion",
|
||||
"description": "Description",
|
||||
"keyword": "Keywords"
|
||||
},
|
||||
"listelement": {
|
||||
"badge": "Research Project"
|
||||
}
|
||||
},
|
||||
"orgunit": {
|
||||
"page": {
|
||||
"titleprefix": "Organizational Unit: ",
|
||||
"dateestablished": "Date established",
|
||||
"city": "City",
|
||||
"country": "Country",
|
||||
"id": "ID",
|
||||
"description": "Description"
|
||||
},
|
||||
"listelement": {
|
||||
"badge": "Organizational Unit"
|
||||
}
|
||||
},
|
||||
"journal": {
|
||||
"page": {
|
||||
"titleprefix": "Journal: ",
|
||||
"issn": "ISSN",
|
||||
"publisher": "Publisher",
|
||||
"description": "Description",
|
||||
"editor": "Editor-in-Chief"
|
||||
},
|
||||
"listelement": {
|
||||
"badge": "Journal"
|
||||
}
|
||||
},
|
||||
"journalvolume": {
|
||||
"page": {
|
||||
"titleprefix": "Journal Volume: ",
|
||||
"volume": "Volume",
|
||||
"issuedate": "Issue Date",
|
||||
"description": "Description"
|
||||
},
|
||||
"listelement": {
|
||||
"badge": "Journal Volume"
|
||||
}
|
||||
},
|
||||
"journalissue": {
|
||||
"page": {
|
||||
"titleprefix": "Journal Issue: ",
|
||||
"number": "Number",
|
||||
"issuedate": "Issue Date",
|
||||
"description": "Description",
|
||||
"keyword": "Keywords",
|
||||
"journal-title": "Journal Title",
|
||||
"journal-issn": "Journal ISSN"
|
||||
},
|
||||
"listelement": {
|
||||
"badge": "Journal Issue"
|
||||
}
|
||||
},
|
||||
"publication": {
|
||||
"page": {
|
||||
"titleprefix": "Publication: ",
|
||||
"journal-title": "Journal Title",
|
||||
"journal-issn": "Journal ISSN",
|
||||
"volume-title": "Volume Title"
|
||||
},
|
||||
"listelement": {
|
||||
"badge": "Publication"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"browse": {
|
||||
"header": "All of DSpace"
|
||||
@@ -250,6 +468,7 @@
|
||||
},
|
||||
"login": "Log In",
|
||||
"logout": "Log Out",
|
||||
"mydspace": "MyDSpace",
|
||||
"language": "Language switch",
|
||||
"search": "Search"
|
||||
},
|
||||
@@ -257,7 +476,7 @@
|
||||
"results-per-page": "Results Per Page",
|
||||
"sort-direction": "Sort Options",
|
||||
"showing": {
|
||||
"label": "Now showing items ",
|
||||
"label": "Now showing ",
|
||||
"detail": "{{ range }} of {{ total }}"
|
||||
}
|
||||
},
|
||||
@@ -286,12 +505,82 @@
|
||||
"help": "Select a community to browse its collections."
|
||||
}
|
||||
},
|
||||
"mydspace": {
|
||||
"title": "MyDSpace",
|
||||
"description": "",
|
||||
"new-submission": "New submission",
|
||||
"results": {
|
||||
"head": "Your submissions",
|
||||
"no-results": "There were no items to show",
|
||||
"no-title": "No title",
|
||||
"no-authors": "No Authors",
|
||||
"no-date": "No Date",
|
||||
"no-abstract": "No Abstract",
|
||||
"no-files": "No Files",
|
||||
"no-uri": "No Uri",
|
||||
"no-collections": "No Collections"
|
||||
},
|
||||
"messages": {
|
||||
"title": "Messages",
|
||||
"to": "To",
|
||||
"hide-msg": "Hide message",
|
||||
"show-msg": "Show message",
|
||||
"no-messages": "No messages yet.",
|
||||
"no-content": "No content.",
|
||||
"send-btn": "Send",
|
||||
"subject-placeholder": "Subject...",
|
||||
"description-placeholder": "Insert your message here...",
|
||||
"mark-as-read": "Mark as read",
|
||||
"mark-as-unread": "Mark as unread",
|
||||
"submitter-help": "Select this option to send a message to controller.",
|
||||
"controller-help": "Select this option to send a message to item's submitter."
|
||||
},
|
||||
"show": {
|
||||
"workspace": "Your Submissions",
|
||||
"workflow": "All tasks"
|
||||
},
|
||||
"status": {
|
||||
"workflow": "Workflow",
|
||||
"validation": "Validation",
|
||||
"waiting-for-controller": "Waiting for controller",
|
||||
"workspace": "Workspace",
|
||||
"archived": "Archived"
|
||||
},
|
||||
"view-btn": "View",
|
||||
"general": {
|
||||
"text-here": "HERE"
|
||||
},
|
||||
"upload": {
|
||||
"upload-successful": "New workspace item created. Click {{here}} for edit it.",
|
||||
"upload-multiple-successful": "{{qty}} new workspace items created.",
|
||||
"upload-failed": "Error creating new workspace. Please verify the content uploaded before retry."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"journal": {
|
||||
"title": "DSpace Angular :: Journal Search",
|
||||
"results": {
|
||||
"head": "Journal Search Results"
|
||||
}
|
||||
},
|
||||
"person": {
|
||||
"title": "DSpace Angular :: Person Search",
|
||||
"results": {
|
||||
"head": "Person Search Results"
|
||||
}
|
||||
},
|
||||
"publication": {
|
||||
"title": "DSpace Angular :: Publication Search",
|
||||
"results": {
|
||||
"head": "Publication Search Results"
|
||||
}
|
||||
},
|
||||
"title": "DSpace Angular :: Search",
|
||||
"description": "",
|
||||
"form": {
|
||||
"search": "Search",
|
||||
"search_dspace": "Search DSpace"
|
||||
"search_dspace": "Search DSpace",
|
||||
"search_mydspace": "Search MyDSpace"
|
||||
},
|
||||
"results": {
|
||||
"head": "Search Results",
|
||||
@@ -311,9 +600,13 @@
|
||||
"rpp": "Results per page"
|
||||
}
|
||||
},
|
||||
"switch-configuration": {
|
||||
"title":"Show"
|
||||
},
|
||||
"view-switch": {
|
||||
"show-list": "Show as list",
|
||||
"show-grid": "Show as grid"
|
||||
"show-grid": "Show as grid",
|
||||
"show-detail": "Show detail"
|
||||
},
|
||||
"filters": {
|
||||
"head": "Filters",
|
||||
@@ -323,7 +616,12 @@
|
||||
"f.dateIssued.min": "Start date",
|
||||
"f.dateIssued.max": "End date",
|
||||
"f.subject": "Subject",
|
||||
"f.has_content_in_original_bundle": "Has files"
|
||||
"f.has_content_in_original_bundle": "Has files",
|
||||
"f.entityType": "Item Type",
|
||||
"f.namedresourcetype": "Status",
|
||||
"f.dateSubmitted": "Date submitted",
|
||||
"f.itemtype": "Type",
|
||||
"f.submitter": "Submitter"
|
||||
},
|
||||
"filter": {
|
||||
"show-more": "Show more",
|
||||
@@ -351,12 +649,75 @@
|
||||
},
|
||||
"has_content_in_original_bundle": {
|
||||
"head": "Has files"
|
||||
},
|
||||
"entityType": {
|
||||
"placeholder": "Item Type",
|
||||
"head": "Item Type"
|
||||
},
|
||||
"namedresourcetype": {
|
||||
"placeholder": "Status",
|
||||
"head": "Status"
|
||||
},
|
||||
"dateSubmitted": {
|
||||
"placeholder": "Date submitted",
|
||||
"head": "Date submitted"
|
||||
},
|
||||
"itemtype": {
|
||||
"placeholder": "Type",
|
||||
"head": "Type"
|
||||
},
|
||||
"submitter": {
|
||||
"placeholder": "Submitter",
|
||||
"head": "Submitter"
|
||||
},
|
||||
"objectpeople": {
|
||||
"placeholder": "People",
|
||||
"head": "People"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -364,11 +725,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."
|
||||
}
|
||||
@@ -377,13 +745,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": {
|
||||
@@ -453,6 +848,7 @@
|
||||
"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",
|
||||
@@ -497,7 +893,9 @@
|
||||
"item": "Loading item...",
|
||||
"objects": "Loading...",
|
||||
"search-results": "Loading search results...",
|
||||
"browse-by": "Loading items..."
|
||||
"mydspace-results": "Loading items...",
|
||||
"browse-by": "Loading items...",
|
||||
"browse-by-page": "Loading page..."
|
||||
},
|
||||
"error": {
|
||||
"default": "Error",
|
||||
@@ -516,13 +914,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...",
|
||||
@@ -531,7 +941,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",
|
||||
@@ -561,5 +973,168 @@
|
||||
},
|
||||
"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?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"workflow": {
|
||||
"generic": {
|
||||
"delete": "Delete",
|
||||
"delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.",
|
||||
"edit": "Edit",
|
||||
"edit-help": "Select this option to change the item's metadata.",
|
||||
"view": "View",
|
||||
"view-help": "Select this option to view the item's metadata."
|
||||
},
|
||||
"tasks": {
|
||||
"generic": {
|
||||
"processing": "Processing...",
|
||||
"success": "Operation successful",
|
||||
"error": "Error occurred during operation...",
|
||||
"submitter": "Submitter"
|
||||
},
|
||||
"claimed": {
|
||||
"approve": "Approve",
|
||||
"approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".",
|
||||
"edit": "Edit",
|
||||
"edit_help": "Select this option to change the item's metadata.",
|
||||
"reject": {
|
||||
"submit": "Reject",
|
||||
"reason": {
|
||||
"submit": "Reject item",
|
||||
"title": "Reason",
|
||||
"info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.",
|
||||
"placeholder": "Describe the reason of reject"
|
||||
}
|
||||
},
|
||||
"reject_help": "If you have reviewed the item and found it is <strong>not</strong> suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.",
|
||||
"return": "Return to pool",
|
||||
"return_help": "Return the task to the pool so that another user may perform the task."
|
||||
|
||||
},
|
||||
"pool": {
|
||||
"claim": "Claim",
|
||||
"claim_help": "Assign this task to yourself.",
|
||||
"show-detail": "Show detail",
|
||||
"hide-detail": "Hide detail"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"uploader": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
resources/images/orgunit-placeholder.svg
Normal file
13
resources/images/orgunit-placeholder.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="125.348px" height="161.348px" viewBox="62.326 80.326 125.348 161.348"
|
||||
enable-background="new 62.326 80.326 125.348 161.348" xml:space="preserve">
|
||||
<rect x="62.5" y="80.5" fill="#FFFFFF" stroke="#000000" stroke-width="0.3478" stroke-miterlimit="10" width="125" height="161"/>
|
||||
<path fill="#43515F" d="M135.5,171.5h-21c-2.899,0-5.25,2.352-5.25,5.25v21c0,2.898,2.351,5.25,5.25,5.25h21
|
||||
c2.898,0,5.25-2.352,5.25-5.25v-21C140.75,173.852,138.398,171.5,135.5,171.5z M104,124.25c0-2.899-2.351-5.25-5.25-5.25h-21
|
||||
c-2.899,0-5.25,2.351-5.25,5.25v21c0,2.899,2.351,5.25,5.25,5.25h15.704l12.002,21.007c1.822-3.127,5.171-5.257,9.043-5.257h0.046
|
||||
L104,147.794V140h36.75v-10.5H104V124.25z M172.25,119h-21c-2.898,0-5.25,2.351-5.25,5.25v21c0,2.899,2.352,5.25,5.25,5.25h21
|
||||
c2.898,0,5.25-2.351,5.25-5.25v-21C177.5,121.351,175.148,119,172.25,119z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
13
resources/images/person-placeholder.svg
Normal file
13
resources/images/person-placeholder.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="125.348px" height="161.348px" viewBox="62.674 80.674 125.348 161.348"
|
||||
enable-background="new 62.674 80.674 125.348 161.348" xml:space="preserve">
|
||||
<rect x="62.847" y="80.847" fill="#FFFFFF" stroke="#000000" stroke-width="0.3478" stroke-miterlimit="10" width="125" height="161"/>
|
||||
<path fill="#43515F" d="M125.347,167.91c16.304,0,29.531-13.228,29.531-29.531c0-16.303-13.228-29.531-29.531-29.531
|
||||
c-16.303,0-29.531,13.227-29.531,29.531S109.044,167.91,125.347,167.91z M151.597,174.472h-11.301
|
||||
c-4.552,2.092-9.617,3.281-14.95,3.281c-5.332,0-10.377-1.189-14.95-3.281h-11.3c-14.499,0-26.25,11.751-26.25,26.25v3.281
|
||||
c0,5.435,4.409,9.844,9.844,9.844h85.312c5.435,0,9.844-4.408,9.844-9.844v-3.281C177.847,186.223,166.096,174.472,151.597,174.472z
|
||||
"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
118
resources/images/project-placeholder.svg
Normal file
118
resources/images/project-placeholder.svg
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="125.5px" height="161.5px" viewBox="62.25 80.25 125.5 161.5" enable-background="new 62.25 80.25 125.5 161.5"
|
||||
xml:space="preserve">
|
||||
<rect x="62.5" y="80.5" fill="#FFFFFF" stroke="#000000" stroke-width="0.5" stroke-miterlimit="10" width="125" height="161"/>
|
||||
<g>
|
||||
<path fill="#43515F" d="M176.007,108.235h-65.078c-0.972,0-1.759,0.788-1.759,1.759v7.035h-7.035c-0.972,0-1.759,0.788-1.759,1.759
|
||||
v9.246c-5.482,3.937-8.794,10.281-8.794,17.136c0,3.284,0.808,6.52,2.278,9.449c-0.313,0.902-0.52,1.855-0.52,2.863
|
||||
c0,0.32,0.062,0.625,0.095,0.936l-9.667,9.666c-0.323-0.029-0.65-0.049-0.981-0.049c-5.82,0-10.553,4.733-10.553,10.553
|
||||
c0,3.115,1.364,5.91,3.517,7.844v25.574c0,0.971,0.787,1.759,1.759,1.759h10.553c0.973,0,1.759-0.788,1.759-1.759v-25.574
|
||||
c2.153-1.932,3.518-4.727,3.518-7.844c0-1.349-0.264-2.635-0.727-3.82l7.762-7.764v45.002c0,0.971,0.786,1.759,1.759,1.759h65.077
|
||||
c0.973,0,1.759-0.787,1.759-1.759v-5.275h7.036c0.972,0,1.759-0.789,1.759-1.76v-94.978
|
||||
C177.765,109.023,176.979,108.235,176.007,108.235z M102.965,130.539c0.044-0.022,0.092-0.037,0.134-0.063l0.193-0.129l3.835,3.834
|
||||
c-4.083,2.049-6.752,6.234-6.752,10.99c0,0.586,0.048,1.169,0.132,1.749c0.029,0.197,0.083,0.387,0.122,0.583
|
||||
c0.074,0.376,0.148,0.752,0.257,1.12c0.014,0.051,0.02,0.104,0.035,0.153c-0.103,0.014-0.201,0.049-0.302,0.067
|
||||
c-0.204,0.035-0.401,0.084-0.6,0.134c-0.3,0.075-0.592,0.167-0.881,0.274c-0.219,0.082-0.437,0.157-0.646,0.253
|
||||
c-0.256,0.118-0.496,0.261-0.739,0.401c-0.371,0.214-0.718,0.454-1.052,0.719c-0.192,0.153-0.393,0.292-0.57,0.461
|
||||
c-0.681-1.891-1.031-3.896-1.031-5.914C95.1,139.241,98.084,133.785,102.965,130.539z M106.321,154.306
|
||||
c0.119,0.157,0.227,0.318,0.329,0.487c0.063,0.107,0.122,0.217,0.177,0.327c0.08,0.156,0.155,0.313,0.218,0.477
|
||||
c0.116,0.304,0.208,0.611,0.268,0.914c0.06,0.317,0.099,0.639,0.099,0.971c0,0.406-0.058,0.803-0.148,1.191
|
||||
c-0.028,0.118-0.072,0.227-0.105,0.341c-0.081,0.265-0.176,0.521-0.297,0.77c-0.06,0.122-0.126,0.242-0.197,0.362
|
||||
c-0.135,0.229-0.291,0.448-0.459,0.653c-0.081,0.099-0.157,0.202-0.243,0.296c-0.266,0.281-0.556,0.544-0.883,0.767
|
||||
c-0.843,0.566-1.855,0.898-2.944,0.898c-0.397,0-0.781-0.052-1.152-0.136c-0.009,0-0.018-0.007-0.026-0.01
|
||||
c-0.115-0.027-0.222-0.069-0.333-0.102c-0.132-0.041-0.266-0.073-0.392-0.121c-0.164-0.064-0.318-0.145-0.473-0.225
|
||||
c-0.103-0.052-0.202-0.105-0.301-0.163c-0.153-0.091-0.305-0.187-0.447-0.292c-0.075-0.056-0.146-0.119-0.218-0.181
|
||||
c-0.259-0.215-0.496-0.45-0.708-0.709c-0.06-0.072-0.123-0.141-0.177-0.216c-0.105-0.143-0.203-0.294-0.294-0.449
|
||||
c-0.059-0.098-0.111-0.198-0.164-0.301c-0.079-0.154-0.158-0.31-0.222-0.473c-0.056-0.146-0.097-0.299-0.141-0.45
|
||||
c-0.026-0.094-0.063-0.181-0.084-0.276c-0.001-0.007-0.005-0.014-0.007-0.021c-0.084-0.369-0.137-0.754-0.137-1.152
|
||||
c0-0.776,0.178-1.507,0.48-2.172c0.005-0.012,0.016-0.021,0.021-0.033c0.193-0.417,0.442-0.793,0.726-1.138
|
||||
c0.049-0.058,0.095-0.119,0.146-0.176c0.127-0.142,0.264-0.274,0.404-0.401c0.077-0.069,0.158-0.134,0.241-0.198
|
||||
c0.141-0.113,0.28-0.226,0.431-0.322c0.214-0.137,0.438-0.261,0.672-0.364c0.118-0.052,0.241-0.091,0.362-0.137
|
||||
c0.178-0.065,0.359-0.124,0.544-0.169c0.107-0.026,0.214-0.052,0.323-0.072c0.303-0.053,0.608-0.093,0.925-0.093
|
||||
c0.365,0,0.739,0.044,1.147,0.135h0.001h0.002c0.533,0.118,1.025,0.329,1.481,0.593c0.063,0.037,0.132,0.065,0.191,0.104
|
||||
c0.179,0.115,0.343,0.248,0.506,0.383c0.078,0.063,0.156,0.122,0.229,0.19c0.149,0.139,0.287,0.29,0.421,0.443
|
||||
C106.186,154.139,106.256,154.22,106.321,154.306z M110.928,157.357c0.58,0.083,1.168,0.125,1.76,0.125
|
||||
c4.754,0,8.938-2.667,10.988-6.752l3.863,3.862c-3.22,5.069-8.797,8.166-14.851,8.166c-1.101,0-2.2-0.107-3.289-0.32
|
||||
c0.009-0.012,0.014-0.026,0.023-0.038c0.007-0.011,0.012-0.024,0.021-0.035c0.06-0.09,0.105-0.19,0.164-0.281
|
||||
c0.091-0.15,0.179-0.3,0.262-0.455c0.102-0.187,0.198-0.377,0.285-0.572c0.049-0.111,0.095-0.224,0.14-0.337
|
||||
c0.052-0.131,0.104-0.263,0.15-0.399c0.088-0.255,0.152-0.517,0.216-0.781c0.032-0.13,0.074-0.253,0.099-0.385
|
||||
c0.014-0.074,0.028-0.146,0.041-0.22c0.081-0.479,0.13-0.962,0.13-1.453C110.929,157.44,110.929,157.398,110.928,157.357z
|
||||
M86.306,210.247h-7.035v-21.722c0.545,0.194,1.11,0.346,1.692,0.447c0.035,0.006,0.069,0.011,0.102,0.016
|
||||
c0.563,0.094,1.136,0.153,1.724,0.153c0.587,0,1.161-0.06,1.722-0.153c0.035-0.006,0.069-0.009,0.103-0.016
|
||||
c0.583-0.102,1.148-0.253,1.693-0.447V210.247z M86.978,184.207c-0.003,0.002-0.007,0.005-0.011,0.006
|
||||
c-0.287,0.215-0.589,0.398-0.898,0.562c-0.039,0.021-0.078,0.044-0.118,0.063c-0.301,0.151-0.608,0.28-0.924,0.387
|
||||
c-0.055,0.018-0.109,0.033-0.163,0.051c-1.353,0.421-2.8,0.421-4.153,0c-0.054-0.018-0.109-0.033-0.164-0.051
|
||||
c-0.315-0.106-0.623-0.234-0.923-0.387c-0.041-0.021-0.079-0.043-0.118-0.063c-0.31-0.163-0.61-0.349-0.898-0.562
|
||||
c-0.003-0.001-0.007-0.004-0.011-0.006c-1.715-1.283-2.844-3.314-2.844-5.619c0-3.879,3.155-7.034,7.036-7.034
|
||||
c0.542,0,1.064,0.075,1.573,0.192c0.012,0.003,0.024,0.01,0.037,0.014c0.48,0.111,0.943,0.275,1.382,0.486
|
||||
c0.004,0.002,0.007,0.004,0.011,0.006c1.319,0.632,2.427,1.678,3.148,3c0.003,0.006,0.009,0.01,0.012,0.015
|
||||
c0.539,0.995,0.873,2.115,0.873,3.322C89.823,180.893,88.694,182.922,86.978,184.207z M90.738,171.67
|
||||
c-0.875-1.003-1.932-1.84-3.124-2.456l7.077-7.079c0.009,0.016,0.024,0.027,0.034,0.042c0.695,1.095,1.62,2.018,2.714,2.714
|
||||
c0.016,0.011,0.027,0.025,0.042,0.033L90.738,171.67z M165.453,210.247h-61.559v-44.148c0.002,0,0.003-0.002,0.005-0.002
|
||||
c0.031-0.007,0.06-0.021,0.092-0.027c0.506-0.11,0.996-0.265,1.465-0.456c0.167-0.068,0.32-0.16,0.481-0.239
|
||||
c0.101-0.05,0.201-0.097,0.299-0.147c2.11,0.684,4.275,1.05,6.451,1.05c7.784,0,14.913-4.267,18.602-11.138
|
||||
c0.367-0.684,0.244-1.526-0.306-2.075l-6.775-6.775c-0.452-0.452-1.111-0.625-1.729-0.447c-0.616,0.178-1.085,0.674-1.228,1.296
|
||||
c-0.921,4.021-4.442,6.827-8.563,6.827c-0.939,0-1.852-0.187-2.737-0.479c-0.07-0.135-0.161-0.257-0.239-0.389
|
||||
c-0.144-0.248-0.283-0.495-0.45-0.725c-0.113-0.155-0.245-0.296-0.366-0.443c-0.348-0.424-0.731-0.811-1.154-1.163
|
||||
c-0.168-0.143-0.331-0.293-0.512-0.422c-0.218-0.157-0.45-0.287-0.683-0.422c-0.218-0.128-0.436-0.251-0.664-0.362
|
||||
c-0.236-0.113-0.476-0.209-0.721-0.301c-0.099-0.036-0.188-0.088-0.289-0.119c-0.05-0.095-0.102-0.188-0.147-0.287
|
||||
c-0.063-0.135-0.123-0.272-0.18-0.41c-0.088-0.216-0.165-0.438-0.235-0.659c-0.042-0.134-0.086-0.268-0.122-0.403
|
||||
c-0.065-0.244-0.113-0.494-0.155-0.744c-0.02-0.118-0.048-0.233-0.063-0.352c-0.049-0.371-0.078-0.742-0.078-1.115
|
||||
c0-4.121,2.807-7.644,6.825-8.563c0.622-0.143,1.12-0.614,1.296-1.228c0.176-0.614,0.007-1.277-0.446-1.729l-6.775-6.775
|
||||
c-0.132-0.132-0.283-0.237-0.443-0.318c-0.053-0.026-0.11-0.037-0.166-0.06c-0.097-0.037-0.19-0.084-0.292-0.104v-5.846h7.036
|
||||
h54.523v89.7H165.453z M174.247,203.212h-5.276v-84.424c0-0.971-0.786-1.759-1.76-1.759h-54.523v-5.277h61.559V203.212
|
||||
L174.247,203.212z"/>
|
||||
<path fill="#43515F" d="M144.347,196.177c0-0.97-0.786-1.759-1.759-1.759h-5.277v-7.035c0-0.971-0.785-1.759-1.759-1.759h-5.276
|
||||
v-12.312c0-0.972-0.786-1.76-1.759-1.76h-7.035c-0.973,0-1.759,0.788-1.759,1.76v8.793h-5.276c-0.973,0-1.759,0.788-1.759,1.76
|
||||
v17.589h-3.518v3.517h38.695v-3.517h-3.518V196.177L144.347,196.177z M116.206,185.624h3.518v15.829h-3.518V185.624z
|
||||
M123.241,183.864v-8.794h3.518v12.312v14.07h-3.518V183.864z M130.276,189.141h3.519v7.036v5.276h-3.519V189.141z
|
||||
M137.312,201.453v-3.518h3.519v3.518H137.312z"/>
|
||||
<path fill="#43515F" d="M161.844,169.476c0.008-0.064,0.021-0.128,0.029-0.193c0.042-0.411,0.062-0.829,0.062-1.247
|
||||
c0-6.789-5.523-12.312-12.312-12.312c-6.787,0-12.312,5.523-12.312,12.312c0,0.418,0.021,0.836,0.063,1.247
|
||||
c0.007,0.065,0.021,0.129,0.028,0.193c0.04,0.349,0.089,0.693,0.158,1.034c0.003,0.016,0.009,0.031,0.012,0.05
|
||||
c0.481,2.325,1.625,4.464,3.346,6.182c0.009,0.009,0.02,0.011,0.029,0.02c2.227,2.215,5.294,3.587,8.675,3.587
|
||||
s6.448-1.372,8.675-3.587c0.01-0.009,0.021-0.011,0.03-0.02c1.719-1.719,2.862-3.856,3.346-6.182
|
||||
c0.003-0.018,0.009-0.034,0.011-0.05C161.754,170.169,161.804,169.824,161.844,169.476z M158.256,169.635
|
||||
c-0.024,0.137-0.06,0.271-0.09,0.406c-0.097,0.414-0.221,0.817-0.376,1.206c-0.038,0.1-0.072,0.202-0.115,0.301
|
||||
c-0.204,0.465-0.446,0.911-0.724,1.329l-2.565-2.566c0.017-0.037,0.023-0.08,0.042-0.119c0.094-0.209,0.159-0.43,0.227-0.654
|
||||
c0.038-0.13,0.092-0.253,0.119-0.387c0.081-0.36,0.127-0.731,0.127-1.115c0-0.332-0.039-0.654-0.099-0.969
|
||||
c-0.018-0.099-0.046-0.191-0.071-0.288c-0.052-0.216-0.116-0.429-0.196-0.634c-0.038-0.1-0.077-0.197-0.123-0.293
|
||||
c-0.095-0.206-0.204-0.404-0.323-0.595c-0.046-0.073-0.084-0.149-0.134-0.22c-0.177-0.254-0.37-0.49-0.586-0.709
|
||||
c-0.036-0.039-0.081-0.069-0.12-0.105c-0.184-0.176-0.38-0.34-0.589-0.488c-0.083-0.057-0.169-0.108-0.255-0.163
|
||||
c-0.182-0.114-0.369-0.216-0.566-0.307c-0.095-0.045-0.188-0.089-0.287-0.129c-0.058-0.023-0.111-0.055-0.17-0.075v-3.641
|
||||
c0.001,0,0.003,0,0.005,0c0.032,0.007,0.06,0.021,0.092,0.028c0.507,0.109,0.995,0.264,1.465,0.456
|
||||
c0.168,0.068,0.32,0.16,0.482,0.239c0.321,0.156,0.64,0.318,0.939,0.51c0.174,0.113,0.334,0.239,0.499,0.365
|
||||
c0.262,0.197,0.517,0.401,0.757,0.624c0.155,0.146,0.302,0.301,0.448,0.459c0.218,0.236,0.422,0.484,0.612,0.742
|
||||
c0.126,0.173,0.251,0.347,0.366,0.53c0.177,0.278,0.33,0.571,0.473,0.868c0.09,0.187,0.186,0.369,0.263,0.563
|
||||
c0.134,0.338,0.235,0.689,0.325,1.047c0.046,0.173,0.105,0.341,0.141,0.521c0.105,0.535,0.17,1.094,0.17,1.665
|
||||
C158.418,168.582,158.353,169.113,158.256,169.635z M148.381,169.279L148.381,169.279c-0.166-0.164-0.291-0.355-0.379-0.562
|
||||
c-0.088-0.21-0.137-0.44-0.137-0.682c0-0.969,0.788-1.758,1.759-1.758s1.759,0.789,1.759,1.758c0,0.241-0.049,0.472-0.137,0.682
|
||||
c-0.088,0.207-0.213,0.398-0.378,0.562l0,0c-0.318,0.318-0.759,0.516-1.243,0.516C149.14,169.795,148.698,169.597,148.381,169.279z
|
||||
M140.998,166.364c0.035-0.179,0.097-0.347,0.141-0.521c0.092-0.355,0.191-0.707,0.325-1.045c0.077-0.193,0.175-0.377,0.265-0.564
|
||||
c0.144-0.297,0.297-0.592,0.47-0.869c0.114-0.18,0.239-0.355,0.368-0.529c0.192-0.259,0.395-0.506,0.612-0.742
|
||||
c0.146-0.156,0.292-0.312,0.448-0.459c0.237-0.223,0.494-0.427,0.757-0.623c0.165-0.123,0.325-0.251,0.499-0.364
|
||||
c0.299-0.192,0.615-0.354,0.938-0.51c0.162-0.079,0.315-0.171,0.482-0.24c0.469-0.193,0.958-0.346,1.465-0.456
|
||||
c0.031-0.007,0.06-0.021,0.092-0.028c0.001,0,0.003,0,0.005,0v3.641c-0.06,0.021-0.112,0.053-0.171,0.076
|
||||
c-0.098,0.038-0.191,0.082-0.286,0.126c-0.198,0.093-0.386,0.193-0.566,0.308c-0.087,0.055-0.173,0.105-0.255,0.164
|
||||
c-0.209,0.147-0.405,0.311-0.59,0.487c-0.038,0.036-0.082,0.067-0.119,0.105c-0.217,0.218-0.409,0.456-0.586,0.709
|
||||
c-0.05,0.071-0.088,0.146-0.134,0.22c-0.12,0.191-0.229,0.388-0.323,0.596c-0.044,0.097-0.084,0.193-0.121,0.292
|
||||
c-0.079,0.206-0.145,0.417-0.198,0.636c-0.022,0.096-0.051,0.189-0.069,0.288c-0.061,0.317-0.099,0.64-0.099,0.972
|
||||
c0,0.385,0.046,0.756,0.123,1.115c0.028,0.134,0.082,0.259,0.121,0.389c0.067,0.222,0.134,0.443,0.227,0.652
|
||||
c0.018,0.039,0.023,0.082,0.043,0.119l-2.567,2.566c-0.277-0.418-0.519-0.863-0.722-1.329c-0.043-0.099-0.075-0.2-0.116-0.301
|
||||
c-0.153-0.389-0.278-0.793-0.377-1.206c-0.031-0.136-0.064-0.27-0.089-0.406c-0.095-0.521-0.16-1.053-0.16-1.6
|
||||
C140.83,167.464,140.893,166.906,140.998,166.364z M144.781,175.364l2.58-2.582c0.689,0.329,1.449,0.53,2.262,0.53
|
||||
s1.572-0.2,2.261-0.53l2.58,2.582c-1.39,0.923-3.052,1.466-4.841,1.466C147.835,176.83,146.173,176.286,144.781,175.364z"/>
|
||||
<path fill="#43515F" d="M156.658,124.064c-2.908,0-5.276,2.367-5.276,5.276c0,0.219,0.039,0.426,0.065,0.639l-8.724,4.362
|
||||
c-0.949-0.913-2.233-1.483-3.653-1.483c-1.921,0-3.588,1.043-4.512,2.581l-7.875-1.576c-0.369-2.55-2.549-4.521-5.201-4.521
|
||||
c-2.909,0-5.276,2.367-5.276,5.276s2.367,5.277,5.276,5.277c1.921,0,3.588-1.043,4.511-2.581l7.876,1.576
|
||||
c0.369,2.55,2.549,4.521,5.201,4.521c2.908,0,5.276-2.367,5.276-5.276c0-0.218-0.038-0.427-0.064-0.64l8.724-4.362
|
||||
c0.949,0.914,2.233,1.484,3.652,1.484c2.909,0,5.277-2.368,5.277-5.277S159.568,124.064,156.658,124.064z M121.482,136.376
|
||||
c-0.971,0-1.759-0.79-1.759-1.759c0-0.969,0.788-1.759,1.759-1.759s1.759,0.79,1.759,1.759S122.453,136.376,121.482,136.376z
|
||||
M139.071,139.894c-0.972,0-1.76-0.79-1.76-1.759c0-0.969,0.788-1.759,1.76-1.759s1.759,0.79,1.759,1.759
|
||||
C140.83,139.104,140.042,139.894,139.071,139.894z M156.658,131.1c-0.971,0-1.758-0.788-1.758-1.759l0,0
|
||||
c0-0.969,0.787-1.759,1.758-1.759s1.76,0.79,1.76,1.759S157.63,131.1,156.658,131.1z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
@@ -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,126 @@
|
||||
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),
|
||||
cancelEditMetadataField: () => {},
|
||||
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;
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
component = null;
|
||||
registryService = null
|
||||
})
|
||||
|
||||
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,29 +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 { 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) {
|
||||
|
||||
}
|
||||
|
||||
@@ -33,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'
|
||||
}
|
||||
])
|
||||
]
|
||||
})
|
||||
|
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -26,7 +27,12 @@ describe('AdminSidebarComponent', () => {
|
||||
{ provide: Injector, useValue: {} },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: AuthService, useClass: AuthServiceStub }
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{
|
||||
provide: NgbModal, useValue: {
|
||||
open: () => {/*comment*/}
|
||||
}
|
||||
}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(AdminSidebarComponent, {
|
||||
@@ -96,7 +102,10 @@ describe('AdminSidebarComponent', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleMenu');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('a.shortcut-icon'));
|
||||
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
|
||||
sidebarToggler.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call toggleMenu on the menuService', () => {
|
||||
@@ -108,7 +117,10 @@ describe('AdminSidebarComponent', () => {
|
||||
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: () => {/**/}});
|
||||
sidebarToggler.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call toggleMenu on the menuService', () => {
|
||||
@@ -120,7 +132,10 @@ describe('AdminSidebarComponent', () => {
|
||||
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: () => {/**/}});
|
||||
sidebarToggler.triggerEventHandler('mouseenter', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick(99);
|
||||
expect(menuService.expandMenuPreview).not.toHaveBeenCalled();
|
||||
tick(1);
|
||||
@@ -132,7 +147,10 @@ describe('AdminSidebarComponent', () => {
|
||||
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: () => {/**/}});
|
||||
sidebarToggler.triggerEventHandler('mouseleave', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick(399);
|
||||
expect(menuService.collapseMenuPreview).not.toHaveBeenCalled();
|
||||
tick(1);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, Injector, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { slide, slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||
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';
|
||||
@@ -10,6 +10,14 @@ 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
|
||||
@@ -52,7 +60,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
constructor(protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
private variableService: CSSVariableService,
|
||||
private authService: AuthService
|
||||
private authService: AuthService,
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
super(menuService, injector);
|
||||
}
|
||||
@@ -104,10 +113,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
link: '/communities/submission'
|
||||
} as LinkMenuItemModel,
|
||||
function: () => {
|
||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_collection',
|
||||
@@ -115,20 +126,29 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
link: '/collections/submission'
|
||||
} as LinkMenuItemModel,
|
||||
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,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_item',
|
||||
link: '/items/submission'
|
||||
link: '/submit'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
@@ -139,7 +159,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_item_version',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
@@ -161,10 +181,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
function: () => {
|
||||
this.modalService.open(EditCommunitySelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_collection',
|
||||
@@ -172,10 +194,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
function: () => {
|
||||
this.modalService.open(EditCollectionSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_item',
|
||||
@@ -183,10 +207,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
function: () => {
|
||||
this.modalService.open(EditItemSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
|
||||
/* Import */
|
||||
@@ -209,7 +235,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_metadata',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
@@ -220,10 +246,9 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_batch',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Export */
|
||||
{
|
||||
id: 'export',
|
||||
@@ -244,7 +269,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_community',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
@@ -255,7 +280,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_collection',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
@@ -266,7 +291,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_item',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
}, {
|
||||
id: 'export_metadata',
|
||||
@@ -276,7 +301,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_metadata',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
@@ -300,7 +325,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_people',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
@@ -311,7 +336,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_groups',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
@@ -322,7 +347,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_authorizations',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
@@ -357,7 +382,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.find_withdrawn_items',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
@@ -368,7 +393,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.find_private_items',
|
||||
link: '/admin/items'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
@@ -415,7 +440,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.curation_task',
|
||||
link: '/curation'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'filter',
|
||||
index: 7
|
||||
@@ -429,7 +454,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics_task',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'chart-bar',
|
||||
index: 8
|
||||
@@ -443,7 +468,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.control_panel',
|
||||
link: '#'
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'cogs',
|
||||
index: 9
|
||||
|
@@ -18,8 +18,8 @@ import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorat
|
||||
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 {
|
||||
/**
|
||||
|
@@ -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,
|
||||
]
|
||||
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,12 +3,58 @@ import { RouterModule } from '@angular/router';
|
||||
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
import { CollectionPageResolver } from './collection-page.resolver';
|
||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||
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';
|
||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||
|
||||
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,
|
||||
@@ -30,6 +76,7 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
],
|
||||
providers: [
|
||||
CollectionPageResolver,
|
||||
CreateCollectionPageGuard
|
||||
]
|
||||
})
|
||||
export class CollectionPageRoutingModule {
|
||||
|
@@ -1,56 +1,62 @@
|
||||
<div class="container">
|
||||
<div class="collection-page"
|
||||
*ngVar="(collectionRD$ | async) as collectionRD">
|
||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="collectionRD?.payload as collection">
|
||||
<!-- Collection Name -->
|
||||
<ds-comcol-page-header
|
||||
[name]="collection.name">
|
||||
</ds-comcol-page-header>
|
||||
<!-- Collection logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||
[logo]="(logoRD$ | async)?.payload"
|
||||
[alternateText]="'Collection Logo'">
|
||||
</ds-comcol-page-logo>
|
||||
<!-- Introductionary text -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.introductoryText"
|
||||
[hasInnerHtml]="true">
|
||||
</ds-comcol-page-content>
|
||||
<!-- News -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.sidebarText"
|
||||
[hasInnerHtml]="true"
|
||||
[title]="'community.page.news'">
|
||||
</ds-comcol-page-content>
|
||||
<!-- Copyright -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.copyrightText"
|
||||
[hasInnerHtml]="true">
|
||||
</ds-comcol-page-content>
|
||||
<!-- Licence -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.license"
|
||||
[title]="'collection.page.license'">
|
||||
</ds-comcol-page-content>
|
||||
</div>
|
||||
<div class="collection-page"
|
||||
*ngVar="(collectionRD$ | async) as collectionRD">
|
||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="collectionRD?.payload as collection">
|
||||
<!-- Collection Name -->
|
||||
<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"
|
||||
[alternateText]="'Collection Logo'">
|
||||
</ds-comcol-page-logo>
|
||||
<!-- Introductionary text -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.introductoryText"
|
||||
[hasInnerHtml]="true">
|
||||
</ds-comcol-page-content>
|
||||
<!-- News -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.sidebarText"
|
||||
[hasInnerHtml]="true"
|
||||
[title]="'community.page.news'">
|
||||
</ds-comcol-page-content>
|
||||
<!-- Copyright -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.copyrightText"
|
||||
[hasInnerHtml]="true">
|
||||
</ds-comcol-page-content>
|
||||
<!-- Licence -->
|
||||
<ds-comcol-page-content
|
||||
[content]="collection.dcLicense"
|
||||
[title]="'collection.page.license'">
|
||||
</ds-comcol-page-content>
|
||||
</div>
|
||||
<br>
|
||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
||||
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
||||
<ds-viewable-collection
|
||||
[config]="paginationConfig"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="itemRD"
|
||||
[hideGear]="true"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed"
|
||||
message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading"
|
||||
message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ds-error *ngIf="collectionRD?.hasFailed"
|
||||
message="{{'error.collection' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="collectionRD?.isLoading"
|
||||
message="{{'loading.collection' | translate}}"></ds-loading>
|
||||
</div>
|
||||
<ds-error *ngIf="collectionRD?.hasFailed" message="{{'error.collection' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="collectionRD?.isLoading" message="{{'loading.collection' | translate}}"></ds-loading>
|
||||
<br>
|
||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
||||
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
||||
<ds-viewable-collection
|
||||
[config]="paginationConfig"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="itemRD"
|
||||
[hideGear]="true"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BehaviorSubject, of as observableOf, Observable, Subject } from 'rxjs';
|
||||
import { filter, flatMap, map, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
@@ -10,16 +13,17 @@ import { MetadataService } from '../core/metadata/metadata.service';
|
||||
import { Bitstream } from '../core/shared/bitstream.model';
|
||||
|
||||
import { Collection } from '../core/shared/collection.model';
|
||||
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import {
|
||||
getSucceededRemoteData,
|
||||
redirectToPageNotFoundOn404,
|
||||
toDSpaceObjectListRD
|
||||
} from '../core/shared/operators';
|
||||
|
||||
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { 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';
|
||||
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-page',
|
||||
@@ -31,20 +35,23 @@ import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
||||
fadeInOut
|
||||
]
|
||||
})
|
||||
export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
export class CollectionPageComponent implements OnInit {
|
||||
collectionRD$: Observable<RemoteData<Collection>>;
|
||||
itemRD$: Observable<RemoteData<PaginatedList<Item>>>;
|
||||
logoRD$: Observable<RemoteData<Bitstream>>;
|
||||
paginationConfig: PaginationComponentOptions;
|
||||
sortConfig: SortOptions;
|
||||
private subs: Subscription[] = [];
|
||||
private collectionId: string;
|
||||
private paginationChanges$: Subject<{
|
||||
paginationConfig: PaginationComponentOptions,
|
||||
sortConfig: SortOptions
|
||||
}>;
|
||||
|
||||
constructor(
|
||||
private collectionDataService: CollectionDataService,
|
||||
private searchService: SearchService,
|
||||
private metadata: MetadataService,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {
|
||||
this.paginationConfig = new PaginationComponentOptions();
|
||||
this.paginationConfig.id = 'collection-page-pagination';
|
||||
@@ -55,42 +62,43 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.collectionRD$ = this.route.data.pipe(
|
||||
map((data) => data.collection)
|
||||
map((data) => data.collection as RemoteData<Collection>),
|
||||
redirectToPageNotFoundOn404(this.router),
|
||||
take(1)
|
||||
);
|
||||
this.logoRD$ = this.collectionRD$.pipe(
|
||||
map((rd: RemoteData<Collection>) => rd.payload),
|
||||
filter((collection: Collection) => hasValue(collection)),
|
||||
flatMap((collection: Collection) => collection.logo)
|
||||
);
|
||||
this.subs.push(
|
||||
this.route.queryParams.subscribe((params) => {
|
||||
this.metadata.processRemoteData(this.collectionRD$);
|
||||
const page = +params.page || this.paginationConfig.currentPage;
|
||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
||||
const pagination = Object.assign({},
|
||||
this.paginationConfig,
|
||||
{ currentPage: page, pageSize: pageSize }
|
||||
);
|
||||
this.updatePage({
|
||||
pagination: pagination,
|
||||
sort: this.sortConfig
|
||||
});
|
||||
}));
|
||||
|
||||
}
|
||||
this.paginationChanges$ = new BehaviorSubject({
|
||||
paginationConfig: this.paginationConfig,
|
||||
sortConfig: this.sortConfig
|
||||
});
|
||||
|
||||
updatePage(searchOptions) {
|
||||
this.itemRD$ = this.searchService.search(
|
||||
new PaginatedSearchOptions({
|
||||
scope: this.collectionId,
|
||||
pagination: searchOptions.pagination,
|
||||
sort: searchOptions.sort,
|
||||
dsoType: DSpaceObjectType.ITEM
|
||||
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
|
||||
}
|
||||
this.itemRD$ = this.paginationChanges$.pipe(
|
||||
switchMap((dto) => this.collectionRD$.pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((rd) => rd.payload.id),
|
||||
switchMap((id: string) => {
|
||||
return this.searchService.search(
|
||||
new PaginatedSearchOptions({
|
||||
scope: id,
|
||||
pagination: dto.paginationConfig,
|
||||
sort: dto.sortConfig,
|
||||
dsoType: DSpaceObjectType.ITEM
|
||||
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>
|
||||
}),
|
||||
startWith(undefined) // Make sure switching pages shows loading component
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
||||
this.metadata.processRemoteData(this.collectionRD$);
|
||||
this.onPaginationChange(params);
|
||||
})
|
||||
}
|
||||
|
||||
isNotEmpty(object: any) {
|
||||
@@ -98,15 +106,14 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onPaginationChange(event) {
|
||||
this.updatePage({
|
||||
pagination: {
|
||||
currentPage: event.page,
|
||||
pageSize: event.pageSize
|
||||
},
|
||||
sort: {
|
||||
field: event.sortField,
|
||||
direction: event.sortDirection
|
||||
}
|
||||
})
|
||||
this.paginationConfig.currentPage = +event.page || this.paginationConfig.currentPage;
|
||||
this.paginationConfig.pageSize = +event.pageSize || this.paginationConfig.pageSize;
|
||||
this.sortConfig.direction = event.sortDirection || this.sortConfig.direction;
|
||||
this.sortConfig.field = event.sortField || this.sortConfig.field;
|
||||
|
||||
this.paginationChanges$.next({
|
||||
paginationConfig: this.paginationConfig,
|
||||
sortConfig: this.sortConfig
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -5,19 +5,29 @@ import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||
import { SearchPageModule } from '../+search-page/search-page.module';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
||||
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SearchPageModule,
|
||||
CollectionPageRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
CreateCollectionPageComponent,
|
||||
EditCollectionPageComponent,
|
||||
DeleteCollectionPageComponent,
|
||||
CollectionFormComponent,
|
||||
CollectionItemMapperComponent
|
||||
],
|
||||
providers: [
|
||||
SearchService
|
||||
]
|
||||
})
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -4,7 +4,8 @@ import { Collection } from '../core/shared/collection.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { find } from 'rxjs/operators';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific collection before the route is activated
|
||||
@@ -18,11 +19,12 @@ export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
|
||||
* Method for resolving a collection based on the parameters in the current route
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route
|
||||
* @returns Observable<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route,
|
||||
* or an error if something went wrong
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
|
||||
return this.collectionService.findById(route.params.id).pipe(
|
||||
getSucceededRemoteData()
|
||||
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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,25 @@
|
||||
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 { 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"
|
||||
@@ -28,7 +30,7 @@
|
||||
<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>
|
||||
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { mergeMap, filter, map, first, tap } from 'rxjs/operators';
|
||||
import { mergeMap, filter, map } from 'rxjs/operators';
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { Subscription, Observable } from 'rxjs';
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
@@ -13,6 +13,7 @@ import { MetadataService } from '../core/metadata/metadata.service';
|
||||
|
||||
import { fadeInOut } from '../shared/animations/fade';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { redirectToPageNotFoundOn404 } from '../core/shared/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-page',
|
||||
@@ -21,30 +22,37 @@ 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>>;
|
||||
|
||||
/**
|
||||
* The logo of this community
|
||||
*/
|
||||
logoRD$: Observable<RemoteData<Bitstream>>;
|
||||
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private communityDataService: CommunityDataService,
|
||||
private metadata: MetadataService,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.communityRD$ = this.route.data.pipe(map((data) => data.community));
|
||||
this.communityRD$ = this.route.data.pipe(
|
||||
map((data) => data.community as RemoteData<Community>),
|
||||
redirectToPageNotFoundOn404(this.router)
|
||||
);
|
||||
this.logoRD$ = this.communityRD$.pipe(
|
||||
map((rd: RemoteData<Community>) => rd.payload),
|
||||
filter((community: Community) => hasValue(community)),
|
||||
mergeMap((community: Community) => community.logo));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -7,6 +7,10 @@ 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: [
|
||||
@@ -18,8 +22,13 @@ import {CommunityPageSubCommunityListComponent} from './sub-community-list/commu
|
||||
CommunityPageComponent,
|
||||
CommunityPageSubCollectionListComponent,
|
||||
CommunityPageSubCommunityListComponent,
|
||||
CreateCommunityPageComponent,
|
||||
EditCommunityPageComponent,
|
||||
DeleteCommunityPageComponent,
|
||||
CommunityFormComponent
|
||||
]
|
||||
})
|
||||
|
||||
export class CommunityPageModule {
|
||||
|
||||
}
|
||||
|
@@ -2,9 +2,10 @@ import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { Community } from '../core/shared/community.model';
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { find } from 'rxjs/operators';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific community before the route is activated
|
||||
@@ -18,11 +19,12 @@ export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
|
||||
* Method for resolving a community based on the parameters in the current route
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<<RemoteData<Community>> Emits the found community based on the parameters in the current route
|
||||
* @returns Observable<<RemoteData<Community>> Emits the found community based on the parameters in the current route,
|
||||
* or an error if something went wrong
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
|
||||
return this.communityService.findById(route.params.id).pipe(
|
||||
getSucceededRemoteData()
|
||||
find((RD) => hasValue(RD.error) || RD.hasSucceeded)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -17,45 +17,39 @@ describe('SubCommunityList Component', () => {
|
||||
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
||||
|
||||
const subcommunities = [Object.assign(new Community(), {
|
||||
name: 'SubCommunity 1',
|
||||
id: '123456789-1',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'SubCommunity 1'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ language: 'en_US', value: 'SubCommunity 1' }
|
||||
]
|
||||
}
|
||||
}),
|
||||
Object.assign(new Community(), {
|
||||
name: 'SubCommunity 2',
|
||||
id: '123456789-2',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'SubCommunity 2'
|
||||
}]
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ language: 'en_US', value: 'SubCommunity 2' }
|
||||
]
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
const emptySubCommunitiesCommunity = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}],
|
||||
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: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}],
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{ language: 'en_US', value: 'Test title' }
|
||||
]
|
||||
},
|
||||
subcommunities: observableOf(new RemoteData(true, true, true,
|
||||
undefined, new PaginatedList(new PageInfo(), subcommunities)))
|
||||
})
|
||||
|
@@ -3,16 +3,17 @@
|
||||
<div class="d-flex flex-wrap">
|
||||
<img class="mr-4 dspace-logo" src="assets/images/dspace-logo.svg" alt="" />
|
||||
<div>
|
||||
<h1 class="display-3">Welcome to DSpace</h1>
|
||||
<p class="lead">DSpace is an open source software platform that enables organisations to:</p>
|
||||
<h1 class="display-3">Welcome to the DSpace 7 Preview</h1>
|
||||
<p class="lead">DSpace is the world leading open source repository platform that enables organisations to:</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li>capture and describe digital material using a submission workflow module, or a variety of programmatic ingest options
|
||||
<li>easily ingest documents, audio, video, datasets and their corresponding Dublin Core metadata
|
||||
</li>
|
||||
<li>distribute an organisation's digital assets over the web through a search and retrieval system
|
||||
<li>open up this content to local and global audiences, thanks to the OAI-PMH interface and Google Scholar optimizations
|
||||
</li>
|
||||
<li>preserve digital assets over the long term</li>
|
||||
<li>issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI</li>
|
||||
</ul>
|
||||
<p>Join an international community of <A HREF="https://wiki.duraspace.org/display/DSPACE/DSpace+Positioning" TARGET="_NEW">leading institutions using DSpace</A>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -5,6 +5,10 @@ import { Component } from '@angular/core';
|
||||
styleUrls: ['./home-news.component.scss'],
|
||||
templateUrl: './home-news.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* Component to render the news section on the home page
|
||||
*/
|
||||
export class HomeNewsComponent {
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<ds-home-news></ds-home-news>
|
||||
<div class="container">
|
||||
<ds-search-form></ds-search-form>
|
||||
<ds-search-form [inPlaceSearch]="false"></ds-search-form>
|
||||
<ds-top-level-community-list></ds-top-level-community-list>
|
||||
</div>
|
||||
|
@@ -1,12 +1,13 @@
|
||||
<ng-container *ngVar="(communitiesRDObs | async) as communitiesRD">
|
||||
<div *ngIf="communitiesRD?.hasSucceeded " @fadeInOut>
|
||||
<ng-container *ngVar="(communitiesRD$ | async) as communitiesRD">
|
||||
<div *ngIf="communitiesRD?.hasSucceeded ">
|
||||
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
||||
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
||||
<ds-viewable-collection
|
||||
[config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="communitiesRD"
|
||||
(paginationChange)="updatePage($event)">
|
||||
[objects]="communitiesRD$ | async"
|
||||
[hideGear]="true"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="communitiesRD?.hasFailed " message="{{'error.top-level-communites' | translate}}"></ds-error>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
@@ -9,7 +9,11 @@ import { Community } from '../../core/shared/community.model';
|
||||
|
||||
import { fadeInOut } from '../../shared/animations/fade';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* this component renders the Top-Level Community list
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-top-level-community-list',
|
||||
styleUrls: ['./top-level-community-list.component.scss'],
|
||||
@@ -18,9 +22,20 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
||||
animations: [fadeInOut]
|
||||
})
|
||||
|
||||
export class TopLevelCommunityListComponent {
|
||||
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
||||
export class TopLevelCommunityListComponent implements OnInit {
|
||||
/**
|
||||
* A list of remote data objects of all top communities
|
||||
*/
|
||||
communitiesRD$: BehaviorSubject<RemoteData<PaginatedList<Community>>> = new BehaviorSubject<RemoteData<PaginatedList<Community>>>({} as any);
|
||||
|
||||
/**
|
||||
* The pagination configuration
|
||||
*/
|
||||
config: PaginationComponentOptions;
|
||||
|
||||
/**
|
||||
* The sorting configuration
|
||||
*/
|
||||
sortConfig: SortOptions;
|
||||
|
||||
constructor(private cds: CommunityDataService) {
|
||||
@@ -29,20 +44,34 @@ export class TopLevelCommunityListComponent {
|
||||
this.config.pageSize = 5;
|
||||
this.config.currentPage = 1;
|
||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||
|
||||
this.updatePage({
|
||||
page: this.config.currentPage,
|
||||
pageSize: this.config.pageSize,
|
||||
sortField: this.sortConfig.field,
|
||||
direction: this.sortConfig.direction
|
||||
});
|
||||
}
|
||||
|
||||
updatePage(data) {
|
||||
this.communitiesRDObs = this.cds.findTop({
|
||||
currentPage: data.page,
|
||||
elementsPerPage: data.pageSize,
|
||||
sort: { field: data.sortField, direction: data.sortDirection }
|
||||
ngOnInit() {
|
||||
this.updatePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the pagination settings is changed
|
||||
* @param event The new pagination data
|
||||
*/
|
||||
onPaginationChange(event) {
|
||||
this.config.currentPage = event.page;
|
||||
this.config.pageSize = event.pageSize;
|
||||
this.sortConfig.field = event.sortField;
|
||||
this.sortConfig.direction = event.sortDirection;
|
||||
this.updatePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of top communities
|
||||
*/
|
||||
updatePage() {
|
||||
this.cds.findTop({
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize,
|
||||
sort: { field: this.sortConfig.field, direction: this.sortConfig.direction }
|
||||
}).pipe(take(1)).subscribe((results) => {
|
||||
this.communitiesRD$.next(results);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,36 +1,24 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="border-bottom">{{'item.edit.head' | translate}}</h2>
|
||||
<div class="pt-2">
|
||||
<ngb-tabset>
|
||||
<ngb-tab title="{{'item.edit.tabs.status.head' | translate}}">
|
||||
<ng-template ngbTabContent>
|
||||
<ds-item-status [item]="(itemRD$ | async)?.payload"></ds-item-status>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
<ngb-tab title="{{'item.edit.tabs.bitstreams.head' | translate}}">
|
||||
<ng-template ngbTabContent>
|
||||
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
<ngb-tab title="{{'item.edit.tabs.metadata.head' | translate}}">
|
||||
<ng-template ngbTabContent>
|
||||
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
<ngb-tab title="{{'item.edit.tabs.view.head' | translate}}">
|
||||
<ng-template ngbTabContent>
|
||||
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
<ngb-tab title="{{'item.edit.tabs.curate.head' | translate}}">
|
||||
<ng-template ngbTabContent>
|
||||
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
</ngb-tabset>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="border-bottom">{{'item.edit.head' | translate}}</h2>
|
||||
<div class="pt-2">
|
||||
<ul class="nav nav-tabs justify-content-start">
|
||||
<li *ngFor="let page of pages" class="nav-item">
|
||||
<a class="nav-link"
|
||||
[ngClass]="{'active' : page === currentPage}"
|
||||
[routerLink]="['./' + page]">
|
||||
{{'item.edit.tabs.' + page + '.head' | translate}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-pane active">
|
||||
<div class="mb-4">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<a [routerLink]="getItemPage((itemRD$ | async)?.payload)" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,5 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
|
||||
.btn {
|
||||
min-width: $edit-item-button-min-width;
|
||||
}
|
@@ -1,10 +1,12 @@
|
||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { getItemPageRoute } from '../item-page-routing.module';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-page',
|
||||
@@ -25,11 +27,34 @@ export class EditItemPageComponent implements OnInit {
|
||||
*/
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
constructor(private route: ActivatedRoute) {
|
||||
/**
|
||||
* The current page outlet string
|
||||
*/
|
||||
currentPage: string;
|
||||
|
||||
/**
|
||||
* All possible page outlet strings
|
||||
*/
|
||||
pages: string[];
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router) {
|
||||
this.router.events.subscribe(() => {
|
||||
this.currentPage = this.route.snapshot.firstChild.routeConfig.path;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pages = this.route.routeConfig.children
|
||||
.map((child: any) => child.path)
|
||||
.filter((path: string) => isNotEmpty(path)); // ignore reroutes
|
||||
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item page url
|
||||
* @param item The item for which the url is requested
|
||||
*/
|
||||
getItemPage(item: Item): string {
|
||||
return getItemPageRoute(item.id)
|
||||
}
|
||||
}
|
||||
|
@@ -2,18 +2,21 @@ import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
||||
import { SearchPageModule } from '../../+search-page/search-page.module';
|
||||
import { EditItemPageComponent } from './edit-item-page.component';
|
||||
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
|
||||
import { ItemStatusComponent } from './item-status/item-status.component';
|
||||
import { ItemOperationComponent } from './item-operation/item-operation.component';
|
||||
import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract-simple-item-action.component';
|
||||
import { ModifyItemOverviewComponent } from './modify-item-overview/modify-item-overview.component';
|
||||
import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component';
|
||||
import { ItemReinstateComponent } from './item-reinstate/item-reinstate.component';
|
||||
import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract-simple-item-action.component';
|
||||
import { ItemPrivateComponent } from './item-private/item-private.component';
|
||||
import { ItemPublicComponent } from './item-public/item-public.component';
|
||||
import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
||||
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
||||
import { SearchPageModule } from '../../+search-page/search-page.module';
|
||||
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Item page administrator functionality
|
||||
@@ -36,6 +39,9 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
||||
ItemPublicComponent,
|
||||
ItemDeleteComponent,
|
||||
ItemStatusComponent,
|
||||
ItemMetadataComponent,
|
||||
ItemBitstreamsComponent,
|
||||
EditInPlaceFieldComponent,
|
||||
ItemCollectionMapperComponent
|
||||
]
|
||||
})
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user