mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'origin/main' into CST-4510-entity-selection-porting
# Conflicts: # src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts
This commit is contained in:
15
cypress/integration/breadcrumbs.spec.ts
Normal file
15
cypress/integration/breadcrumbs.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Breadcrumbs', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Visit an Item, as those have more breadcrumbs
|
||||||
|
cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION);
|
||||||
|
|
||||||
|
// Wait for breadcrumbs to be visible
|
||||||
|
cy.get('ds-breadcrumbs').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-breadcrumbs> for accessibility
|
||||||
|
testA11y('ds-breadcrumbs');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/integration/browse-by-author.spec.ts
Normal file
13
cypress/integration/browse-by-author.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Browse By Author', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/browse/author');
|
||||||
|
|
||||||
|
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||||
|
cy.get('ds-browse-by-metadata-page').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||||
|
testA11y('ds-browse-by-metadata-page');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/integration/browse-by-dateissued.spec.ts
Normal file
13
cypress/integration/browse-by-dateissued.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Browse By Date Issued', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/browse/dateissued');
|
||||||
|
|
||||||
|
// Wait for <ds-browse-by-date-page> to be visible
|
||||||
|
cy.get('ds-browse-by-date-page').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-browse-by-date-page> for accessibility
|
||||||
|
testA11y('ds-browse-by-date-page');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/integration/browse-by-subject.spec.ts
Normal file
13
cypress/integration/browse-by-subject.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Browse By Subject', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/browse/subject');
|
||||||
|
|
||||||
|
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||||
|
cy.get('ds-browse-by-metadata-page').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||||
|
testA11y('ds-browse-by-metadata-page');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/integration/browse-by-title.spec.ts
Normal file
13
cypress/integration/browse-by-title.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Browse By Title', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/browse/title');
|
||||||
|
|
||||||
|
// Wait for <ds-browse-by-title-page> to be visible
|
||||||
|
cy.get('ds-browse-by-title-page').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-browse-by-title-page> for accessibility
|
||||||
|
testA11y('ds-browse-by-title-page');
|
||||||
|
});
|
||||||
|
});
|
15
cypress/integration/collection-page.spec.ts
Normal file
15
cypress/integration/collection-page.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { TEST_COLLECTION } from 'cypress/support';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Collection Page', () => {
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/collections/' + TEST_COLLECTION);
|
||||||
|
|
||||||
|
// <ds-collection-page> tag must be loaded
|
||||||
|
cy.get('ds-collection-page').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-collection-page> for accessibility issues
|
||||||
|
testA11y('ds-collection-page');
|
||||||
|
});
|
||||||
|
});
|
32
cypress/integration/collection-statistics.spec.ts
Normal file
32
cypress/integration/collection-statistics.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { TEST_COLLECTION } from 'cypress/support';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Collection Statistics Page', () => {
|
||||||
|
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/' + TEST_COLLECTION;
|
||||||
|
|
||||||
|
it('should load if you click on "Statistics" from a Collection page', () => {
|
||||||
|
cy.visit('/collections/' + TEST_COLLECTION);
|
||||||
|
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||||
|
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits" section', () => {
|
||||||
|
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||||
|
cy.get('.' + TEST_COLLECTION + '_TotalVisits').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits per month" section', () => {
|
||||||
|
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||||
|
cy.get('.' + TEST_COLLECTION + '_TotalVisitsPerMonth').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||||
|
|
||||||
|
// <ds-collection-statistics-page> tag must be loaded
|
||||||
|
cy.get('ds-collection-statistics-page').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-collection-statistics-page> for accessibility issues
|
||||||
|
testA11y('ds-collection-statistics-page');
|
||||||
|
});
|
||||||
|
});
|
25
cypress/integration/community-list.spec.ts
Normal file
25
cypress/integration/community-list.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Community List Page', () => {
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/community-list');
|
||||||
|
|
||||||
|
// <ds-community-list-page> tag must be loaded
|
||||||
|
cy.get('ds-community-list-page').should('exist');
|
||||||
|
|
||||||
|
// Open first Community (to show Collections)...that way we scan sub-elements as well
|
||||||
|
cy.get('ds-community-list :nth-child(1) > .btn-group > .btn').click();
|
||||||
|
|
||||||
|
// Analyze <ds-community-list-page> for accessibility issues
|
||||||
|
// Disable heading-order checks until it is fixed
|
||||||
|
testA11y('ds-community-list-page',
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'heading-order': { enabled: false }
|
||||||
|
}
|
||||||
|
} as Options
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
15
cypress/integration/community-page.spec.ts
Normal file
15
cypress/integration/community-page.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { TEST_COMMUNITY } from 'cypress/support';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Community Page', () => {
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/communities/' + TEST_COMMUNITY);
|
||||||
|
|
||||||
|
// <ds-community-page> tag must be loaded
|
||||||
|
cy.get('ds-community-page').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-community-page> for accessibility issues
|
||||||
|
testA11y('ds-community-page',);
|
||||||
|
});
|
||||||
|
});
|
32
cypress/integration/community-statistics.spec.ts
Normal file
32
cypress/integration/community-statistics.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { TEST_COMMUNITY } from 'cypress/support';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Community Statistics Page', () => {
|
||||||
|
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/' + TEST_COMMUNITY;
|
||||||
|
|
||||||
|
it('should load if you click on "Statistics" from a Community page', () => {
|
||||||
|
cy.visit('/communities/' + TEST_COMMUNITY);
|
||||||
|
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||||
|
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits" section', () => {
|
||||||
|
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||||
|
cy.get('.' + TEST_COMMUNITY + '_TotalVisits').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits per month" section', () => {
|
||||||
|
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||||
|
cy.get('.' + TEST_COMMUNITY + '_TotalVisitsPerMonth').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||||
|
|
||||||
|
// <ds-community-statistics-page> tag must be loaded
|
||||||
|
cy.get('ds-community-statistics-page').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-community-statistics-page> for accessibility issues
|
||||||
|
testA11y('ds-community-statistics-page');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/integration/footer.spec.ts
Normal file
13
cypress/integration/footer.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Footer', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
// Footer must first be visible
|
||||||
|
cy.get('ds-footer').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-footer> for accessibility
|
||||||
|
testA11y('ds-footer');
|
||||||
|
});
|
||||||
|
});
|
19
cypress/integration/header.spec.ts
Normal file
19
cypress/integration/header.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Header', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
// Header must first be visible
|
||||||
|
cy.get('ds-header').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-header> for accessibility
|
||||||
|
testA11y({
|
||||||
|
include: ['ds-header'],
|
||||||
|
exclude: [
|
||||||
|
['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174
|
||||||
|
['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
19
cypress/integration/homepage-statistics.spec.ts
Normal file
19
cypress/integration/homepage-statistics.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Site Statistics Page', () => {
|
||||||
|
it('should load if you click on "Statistics" from homepage', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||||
|
cy.location('pathname').should('eq', '/statistics');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/statistics');
|
||||||
|
|
||||||
|
// <ds-site-statistics-page> tag must be loaded
|
||||||
|
cy.get('ds-site-statistics-page').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-site-statistics-page> for accessibility issues
|
||||||
|
testA11y('ds-site-statistics-page');
|
||||||
|
});
|
||||||
|
});
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Homepage', () => {
|
describe('Homepage', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// All tests start with visiting homepage
|
// All tests start with visiting homepage
|
||||||
@@ -20,18 +22,11 @@ describe('Homepage', () => {
|
|||||||
cy.url().should('include', 'query=' + encodeURI(queryString));
|
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||||
});
|
});
|
||||||
|
|
||||||
// it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
// // first must inject Axe into current page
|
// Wait for homepage tag to appear
|
||||||
// cy.injectAxe();
|
cy.get('ds-home-page').should('be.visible');
|
||||||
|
|
||||||
// // Analyze entire page for accessibility issues
|
// Analyze <ds-home-page> for accessibility issues
|
||||||
// // NOTE: this test checks accessibility of header/footer as well
|
testA11y('ds-home-page');
|
||||||
// cy.checkA11y({
|
});
|
||||||
// exclude: [
|
|
||||||
// ['#klaro'], // Klaro plugin (privacy policy popup) has color contrast issues
|
|
||||||
// ['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174
|
|
||||||
// ['.dropdownLogin'] // "Log in" link in header has color contrast issues
|
|
||||||
// ],
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
|
@@ -1,15 +1,31 @@
|
|||||||
describe('Item Page', () => {
|
import { Options } from 'cypress-axe';
|
||||||
const ITEMPAGE = '/items/e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||||
const ENTITYPAGE = '/entities/publication/e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
it('should contain element ds-item-page when navigating to an item page', () => {
|
describe('Item Page', () => {
|
||||||
cy.visit(ENTITYPAGE);
|
const ITEMPAGE = '/items/' + TEST_ENTITY_PUBLICATION;
|
||||||
cy.get('ds-item-page').should('exist');
|
const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
|
||||||
});
|
|
||||||
|
|
||||||
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
|
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
|
||||||
it('should redirect to the entity page when navigating to an item page', () => {
|
it('should redirect to the entity page when navigating to an item page', () => {
|
||||||
cy.visit(ITEMPAGE);
|
cy.visit(ITEMPAGE);
|
||||||
cy.location('pathname').should('eq', ENTITYPAGE);
|
cy.location('pathname').should('eq', ENTITYPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit(ENTITYPAGE);
|
||||||
|
|
||||||
|
// <ds-item-page> tag must be loaded
|
||||||
|
cy.get('ds-item-page').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-item-page> for accessibility issues
|
||||||
|
// Disable heading-order checks until it is fixed
|
||||||
|
testA11y('ds-item-page',
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'heading-order': { enabled: false }
|
||||||
|
}
|
||||||
|
} as Options
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,14 @@
|
|||||||
|
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Item Statistics Page', () => {
|
describe('Item Statistics Page', () => {
|
||||||
const ITEMUUID = 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
const ITEMSTATISTICSPAGE = '/statistics/items/' + TEST_ENTITY_PUBLICATION;
|
||||||
const ITEMSTATISTICSPAGE = '/statistics/items/' + ITEMUUID;
|
|
||||||
|
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
||||||
|
cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION);
|
||||||
|
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||||
|
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||||
|
});
|
||||||
|
|
||||||
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
|
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
@@ -8,18 +16,23 @@ describe('Item Statistics Page', () => {
|
|||||||
cy.get('ds-item-page').should('not.exist');
|
cy.get('ds-item-page').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain the item statistics page url when navigating to an item statistics page', () => {
|
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
|
||||||
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a "Total visits" section', () => {
|
it('should contain a "Total visits" section', () => {
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
cy.get('.' + ITEMUUID + '_TotalVisits').should('exist');
|
cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisits').should('exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a "Total visits per month" section', () => {
|
it('should contain a "Total visits per month" section', () => {
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
cy.get('.' + ITEMUUID + '_TotalVisitsPerMonth').should('exist');
|
cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisitsPerMonth').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
|
|
||||||
|
// <ds-item-statistics-page> tag must be loaded
|
||||||
|
cy.get('ds-item-statistics-page').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-item-statistics-page> for accessibility issues
|
||||||
|
testA11y('ds-item-statistics-page');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Search Page', () => {
|
describe('Search Page', () => {
|
||||||
// unique ID of the search form (for selecting specific elements below)
|
// unique ID of the search form (for selecting specific elements below)
|
||||||
const SEARCHFORM_ID = '#search-form';
|
const SEARCHFORM_ID = '#search-form';
|
||||||
@@ -16,4 +19,54 @@ describe('Search Page', () => {
|
|||||||
cy.get(SEARCHFORM_ID + ' button.search-button').click();
|
cy.get(SEARCHFORM_ID + ' button.search-button').click();
|
||||||
cy.url().should('include', 'query=' + encodeURI(queryString));
|
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/search');
|
||||||
|
|
||||||
|
// <ds-search-page> tag must be loaded
|
||||||
|
cy.get('ds-search-page').should('exist');
|
||||||
|
|
||||||
|
// Click each filter toggle to open *every* filter
|
||||||
|
// (As we want to scan filter section for accessibility issues as well)
|
||||||
|
cy.get('.filter-toggle').click({ multiple: true });
|
||||||
|
|
||||||
|
// Analyze <ds-search-page> for accessibility issues
|
||||||
|
testA11y(
|
||||||
|
{
|
||||||
|
include: ['ds-search-page'],
|
||||||
|
exclude: [
|
||||||
|
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Search filters fail these two "moderate" impact rules
|
||||||
|
'heading-order': { enabled: false },
|
||||||
|
'landmark-unique': { enabled: false }
|
||||||
|
}
|
||||||
|
} as Options
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests in Grid view', () => {
|
||||||
|
cy.visit('/search');
|
||||||
|
|
||||||
|
// Click to display grid view
|
||||||
|
// TODO: These buttons should likely have an easier way to uniquely select
|
||||||
|
cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?spc.sf=score&spc.sd=DESC&view=grid"] > .fas').click();
|
||||||
|
|
||||||
|
// <ds-search-page> tag must be loaded
|
||||||
|
cy.get('ds-search-page').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-search-page> for accessibility issues
|
||||||
|
testA11y('ds-search-page',
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Search filters fail these two "moderate" impact rules
|
||||||
|
'heading-order': { enabled: false },
|
||||||
|
'landmark-unique': { enabled: false }
|
||||||
|
}
|
||||||
|
} as Options
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,16 @@
|
|||||||
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
|
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
|
||||||
// For more info, visit https://on.cypress.io/plugins-api
|
// For more info, visit https://on.cypress.io/plugins-api
|
||||||
/* tslint:disable:no-empty */
|
module.exports = (on, config) => {
|
||||||
module.exports = (on, config) => { };
|
// Define "log" and "table" tasks, used for logging accessibility errors during CI
|
||||||
/* tslint:enable:no-empty */
|
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
|
||||||
|
on('task', {
|
||||||
|
log(message: string) {
|
||||||
|
console.log(message);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
table(message: string) {
|
||||||
|
console.table(message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@@ -19,3 +19,8 @@
|
|||||||
// Import Cypress Axe tools for all tests
|
// Import Cypress Axe tools for all tests
|
||||||
// https://github.com/component-driven/cypress-axe
|
// https://github.com/component-driven/cypress-axe
|
||||||
import 'cypress-axe';
|
import 'cypress-axe';
|
||||||
|
|
||||||
|
// Global constants used in tests
|
||||||
|
export const TEST_COLLECTION = '282164f5-d325-4740-8dd1-fa4d6d3e7200';
|
||||||
|
export const TEST_COMMUNITY = '0958c910-2037-42a9-81c7-dca80e3892b4';
|
||||||
|
export const TEST_ENTITY_PUBLICATION = 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||||
|
44
cypress/support/utils.ts
Normal file
44
cypress/support/utils.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Result } from 'axe-core';
|
||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
|
||||||
|
// Log violations to terminal/commandline in a table format.
|
||||||
|
// Uses 'log' and 'table' tasks defined in ../plugins/index.ts
|
||||||
|
// Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file
|
||||||
|
function terminalLog(violations: Result[]) {
|
||||||
|
cy.task(
|
||||||
|
'log',
|
||||||
|
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`
|
||||||
|
);
|
||||||
|
// pluck specific keys to keep the table readable
|
||||||
|
const violationData = violations.map(
|
||||||
|
({ id, impact, description, helpUrl, nodes }) => ({
|
||||||
|
id,
|
||||||
|
impact,
|
||||||
|
description,
|
||||||
|
helpUrl,
|
||||||
|
nodes: nodes.length,
|
||||||
|
html: nodes.map(node => node.html)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Print violations as an array, since 'node.html' above often breaks table alignment
|
||||||
|
cy.task('log', violationData);
|
||||||
|
// Optionally, uncomment to print as a table
|
||||||
|
// cy.task('table', violationData);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom "testA11y()" method which checks accessibility using cypress-axe
|
||||||
|
// while also ensuring any violations are logged to the terminal (see terminalLog above)
|
||||||
|
// This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load
|
||||||
|
export const testA11y = (context?: any, options?: Options) => {
|
||||||
|
cy.injectAxe();
|
||||||
|
cy.configureAxe({
|
||||||
|
rules: [
|
||||||
|
// Disable color contrast checks as they are inaccurate / result in a lot of false positives
|
||||||
|
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
|
||||||
|
{ id: 'color-contrast', enabled: false },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
cy.checkA11y(context, options, terminalLog);
|
||||||
|
};
|
@@ -6,7 +6,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": [
|
"types": [
|
||||||
"cypress",
|
"cypress",
|
||||||
"cypress-axe"
|
"cypress-axe",
|
||||||
|
"node"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -152,7 +152,7 @@
|
|||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"css-loader": "3.4.0",
|
"css-loader": "3.4.0",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"cypress": "8.3.1",
|
"cypress": "8.6.0",
|
||||||
"cypress-axe": "^0.13.0",
|
"cypress-axe": "^0.13.0",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
@@ -28,6 +28,9 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
|||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
|
import { FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
|
||||||
|
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||||
|
|
||||||
|
|
||||||
describe('EPersonFormComponent', () => {
|
describe('EPersonFormComponent', () => {
|
||||||
let component: EPersonFormComponent;
|
let component: EPersonFormComponent;
|
||||||
@@ -99,12 +102,78 @@ describe('EPersonFormComponent', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return createSuccessfulRemoteDataObject$(ePerson);
|
return createSuccessfulRemoteDataObject$(ePerson);
|
||||||
|
},
|
||||||
|
getEPersonByEmail(email): Observable<RemoteData<EPerson>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
builderService = getMockFormBuilderService();
|
builderService = Object.assign(getMockFormBuilderService(),{
|
||||||
|
createFormGroup(formModel, options = null) {
|
||||||
|
const controls = {};
|
||||||
|
formModel.forEach( model => {
|
||||||
|
model.parent = parent;
|
||||||
|
const controlModel = model;
|
||||||
|
const controlState = { value: controlModel.value, disabled: controlModel.disabled };
|
||||||
|
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
|
||||||
|
controls[model.id] = new FormControl(controlState, controlOptions);
|
||||||
|
});
|
||||||
|
return new FormGroup(controls, options);
|
||||||
|
},
|
||||||
|
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
|
||||||
|
return {
|
||||||
|
validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getValidators(validatorsConfig) {
|
||||||
|
return this.getValidatorFns(validatorsConfig);
|
||||||
|
},
|
||||||
|
getValidatorFns(validatorsConfig, validatorsToken = this._NG_VALIDATORS) {
|
||||||
|
let validatorFns = [];
|
||||||
|
if (this.isObject(validatorsConfig)) {
|
||||||
|
validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => {
|
||||||
|
const validatorConfigValue = validatorsConfig[validatorConfigKey];
|
||||||
|
if (this.isValidatorDescriptor(validatorConfigValue)) {
|
||||||
|
const descriptor = validatorConfigValue;
|
||||||
|
return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken);
|
||||||
|
}
|
||||||
|
return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return validatorFns;
|
||||||
|
},
|
||||||
|
getValidatorFn(validatorName, validatorArgs = null, validatorsToken = this._NG_VALIDATORS) {
|
||||||
|
let validatorFn;
|
||||||
|
if (Validators.hasOwnProperty(validatorName)) { // Built-in Angular Validators
|
||||||
|
validatorFn = Validators[validatorName];
|
||||||
|
} else { // Custom Validators
|
||||||
|
if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) {
|
||||||
|
validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName);
|
||||||
|
} else if (validatorsToken) {
|
||||||
|
validatorFn = validatorsToken.find(validator => validator.name === validatorName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (validatorFn === undefined) { // throw when no validator could be resolved
|
||||||
|
throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`);
|
||||||
|
}
|
||||||
|
if (validatorArgs !== null) {
|
||||||
|
return validatorFn(validatorArgs);
|
||||||
|
}
|
||||||
|
return validatorFn;
|
||||||
|
},
|
||||||
|
isValidatorDescriptor(value) {
|
||||||
|
if (this.isObject(value)) {
|
||||||
|
return value.hasOwnProperty('name') && value.hasOwnProperty('args');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
isObject(value) {
|
||||||
|
return typeof value === 'object' && value !== null;
|
||||||
|
}
|
||||||
|
});
|
||||||
authService = new AuthServiceStub();
|
authService = new AuthServiceStub();
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
isAuthorized: observableOf(true)
|
isAuthorized: observableOf(true),
|
||||||
|
|
||||||
});
|
});
|
||||||
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
||||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
@@ -146,6 +215,131 @@ describe('EPersonFormComponent', () => {
|
|||||||
expect(component).toBeDefined();
|
expect(component).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('check form validation', () => {
|
||||||
|
let firstName;
|
||||||
|
let lastName;
|
||||||
|
let email;
|
||||||
|
let canLogIn;
|
||||||
|
let requireCertificate;
|
||||||
|
|
||||||
|
let expected;
|
||||||
|
beforeEach(() => {
|
||||||
|
firstName = 'testName';
|
||||||
|
lastName = 'testLastName';
|
||||||
|
email = 'testEmail@test.com';
|
||||||
|
canLogIn = false;
|
||||||
|
requireCertificate = false;
|
||||||
|
|
||||||
|
expected = Object.assign(new EPerson(), {
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
value: firstName
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
value: lastName
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
email: email,
|
||||||
|
canLogIn: canLogIn,
|
||||||
|
requireCertificate: requireCertificate,
|
||||||
|
});
|
||||||
|
spyOn(component.submitForm, 'emit');
|
||||||
|
component.canLogIn.value = canLogIn;
|
||||||
|
component.requireCertificate.value = requireCertificate;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.initialisePage();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
describe('firstName, lastName and email should be required', () => {
|
||||||
|
it('form should be invalid because the firstName is required', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.firstName.valid).toBeFalse();
|
||||||
|
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
it('form should be invalid because the lastName is required', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.lastName.valid).toBeFalse();
|
||||||
|
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
it('form should be invalid because the email is required', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||||
|
expect(component.formGroup.controls.email.errors.required).toBeTrue();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after inserting information firstName,lastName and email not required', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.formGroup.controls.firstName.setValue('test');
|
||||||
|
component.formGroup.controls.lastName.setValue('test');
|
||||||
|
component.formGroup.controls.email.setValue('test@test.com');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('firstName should be valid because the firstName is set', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.firstName.valid).toBeTrue();
|
||||||
|
expect(component.formGroup.controls.firstName.errors).toBeNull();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
it('lastName should be valid because the lastName is set', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.lastName.valid).toBeTrue();
|
||||||
|
expect(component.formGroup.controls.lastName.errors).toBeNull();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
it('email should be valid because the email is set', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.email.valid).toBeTrue();
|
||||||
|
expect(component.formGroup.controls.email.errors).toBeNull();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('after inserting email wrong should show pattern validation error', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.formGroup.controls.email.setValue('test@test');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('email should not be valid because the email pattern', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||||
|
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after already utilized email', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const ePersonServiceWithEperson = Object.assign(ePersonDataServiceStub,{
|
||||||
|
getEPersonByEmail(): Observable<RemoteData<EPerson>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(EPersonMock);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
component.formGroup.controls.email.setValue('test@test.com');
|
||||||
|
component.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(ePersonServiceWithEperson));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('email should not be valid because email is already taken', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||||
|
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
describe('when submitting the form', () => {
|
describe('when submitting the form', () => {
|
||||||
let firstName;
|
let firstName;
|
||||||
let lastName;
|
let lastName;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import {
|
import {
|
||||||
DynamicCheckboxModel,
|
DynamicCheckboxModel,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import { switchMap, take } from 'rxjs/operators';
|
import { debounceTime, switchMap, take } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||||
@@ -33,10 +33,11 @@ import { RequestService } from '../../../core/data/request.service';
|
|||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-eperson-form',
|
selector: 'ds-eperson-form',
|
||||||
templateUrl: './eperson-form.component.html'
|
templateUrl: './eperson-form.component.html',
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* A form used for creating and editing EPeople
|
* A form used for creating and editing EPeople
|
||||||
@@ -161,7 +162,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
isImpersonated = false;
|
isImpersonated = false;
|
||||||
|
|
||||||
constructor(public epersonService: EPersonDataService,
|
/**
|
||||||
|
* Subscription to email field value change
|
||||||
|
*/
|
||||||
|
emailValueChangeSubscribe: Subscription;
|
||||||
|
|
||||||
|
constructor(protected changeDetectorRef: ChangeDetectorRef,
|
||||||
|
public epersonService: EPersonDataService,
|
||||||
public groupsDataService: GroupDataService,
|
public groupsDataService: GroupDataService,
|
||||||
private formBuilderService: FormBuilderService,
|
private formBuilderService: FormBuilderService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
@@ -187,6 +194,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* This method will initialise the page
|
* This method will initialise the page
|
||||||
*/
|
*/
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
|
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||||
@@ -219,9 +227,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
name: 'email',
|
name: 'email',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'
|
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$',
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
|
errorMessages: {
|
||||||
|
emailTaken: 'error.validation.emailTaken',
|
||||||
|
pattern: 'error.validation.NotValidEmail'
|
||||||
|
},
|
||||||
hint: emailHint
|
hint: emailHint
|
||||||
});
|
});
|
||||||
this.canLogIn = new DynamicCheckboxModel(
|
this.canLogIn = new DynamicCheckboxModel(
|
||||||
@@ -260,11 +272,18 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
canLogIn: eperson != null ? eperson.canLogIn : true,
|
canLogIn: eperson != null ? eperson.canLogIn : true,
|
||||||
requireCertificate: eperson != null ? eperson.requireCertificate : false
|
requireCertificate: eperson != null ? eperson.requireCertificate : false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (eperson === null && !!this.formGroup.controls.email) {
|
||||||
|
this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService));
|
||||||
|
this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const activeEPerson$ = this.epersonService.getActiveEPerson();
|
const activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||||
|
|
||||||
this.groups = activeEPerson$.pipe(
|
this.groups = activeEPerson$.pipe(
|
||||||
switchMap((eperson) => {
|
switchMap((eperson) => {
|
||||||
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
@@ -280,7 +299,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.canImpersonate$ = activeEPerson$.pipe(
|
this.canImpersonate$ = activeEPerson$.pipe(
|
||||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, hasValue(eperson) ? eperson.self : undefined))
|
switchMap((eperson) => {
|
||||||
|
if (hasValue(eperson)) {
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
||||||
|
} else {
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
this.canDelete$ = activeEPerson$.pipe(
|
this.canDelete$ = activeEPerson$.pipe(
|
||||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
||||||
@@ -343,10 +368,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
getFirstCompletedRemoteData()
|
getFirstCompletedRemoteData()
|
||||||
).subscribe((rd: RemoteData<EPerson>) => {
|
).subscribe((rd: RemoteData<EPerson>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', {name: ePersonToCreate.name}));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name }));
|
||||||
this.submitForm.emit(ePersonToCreate);
|
this.submitForm.emit(ePersonToCreate);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', {name: ePersonToCreate.name}));
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name }));
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -382,10 +407,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
const response = this.epersonService.updateEPerson(editedEperson);
|
const response = this.epersonService.updateEPerson(editedEperson);
|
||||||
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => {
|
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', {name: editedEperson.name}));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name }));
|
||||||
this.submitForm.emit(editedEperson);
|
this.submitForm.emit(editedEperson);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', {name: editedEperson.name}));
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name }));
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -395,6 +420,87 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event triggered when the user changes page
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
onPageChange(event) {
|
||||||
|
this.updateGroups({
|
||||||
|
currentPage: event,
|
||||||
|
elementsPerPage: this.config.pageSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start impersonating the EPerson
|
||||||
|
*/
|
||||||
|
impersonate() {
|
||||||
|
this.authService.impersonate(this.epersonInitial.id);
|
||||||
|
this.isImpersonated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
|
||||||
|
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||||
|
*/
|
||||||
|
delete() {
|
||||||
|
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
||||||
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
|
modalRef.componentInstance.dso = eperson;
|
||||||
|
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
|
||||||
|
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
|
||||||
|
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
|
||||||
|
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
|
||||||
|
modalRef.componentInstance.brandColor = 'danger';
|
||||||
|
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
||||||
|
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
||||||
|
if (confirm) {
|
||||||
|
if (hasValue(eperson.id)) {
|
||||||
|
this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
||||||
|
if (restResponse.hasSucceeded) {
|
||||||
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name }));
|
||||||
|
this.submitForm.emit();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||||
|
}
|
||||||
|
this.cancelForm.emit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop impersonating the EPerson
|
||||||
|
*/
|
||||||
|
stopImpersonating() {
|
||||||
|
this.authService.stopImpersonatingAndRefresh();
|
||||||
|
this.isImpersonated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.onCancel();
|
||||||
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
|
this.paginationService.clearPagination(this.config.id);
|
||||||
|
if (hasValue(this.emailValueChangeSubscribe)) {
|
||||||
|
this.emailValueChangeSubscribe.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will ensure that the page gets reset and that the cache is cleared
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
||||||
|
this.requestService.removeByHrefSubstring(eperson.self);
|
||||||
|
});
|
||||||
|
this.initialisePage();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
||||||
* and shows notification if this is the case
|
* and shows notification if this is the case
|
||||||
@@ -417,17 +523,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Event triggered when the user changes page
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
onPageChange(event) {
|
|
||||||
this.updateGroups({
|
|
||||||
currentPage: event,
|
|
||||||
elementsPerPage: this.config.pageSize
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the list of groups by fetching it from the rest api or cache
|
* Update the list of groups by fetching it from the rest api or cache
|
||||||
*/
|
*/
|
||||||
@@ -436,71 +531,4 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options);
|
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start impersonating the EPerson
|
|
||||||
*/
|
|
||||||
impersonate() {
|
|
||||||
this.authService.impersonate(this.epersonInitial.id);
|
|
||||||
this.isImpersonated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
|
|
||||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
|
||||||
*/
|
|
||||||
delete() {
|
|
||||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
|
||||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
|
||||||
modalRef.componentInstance.dso = eperson;
|
|
||||||
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
|
|
||||||
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
|
|
||||||
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
|
|
||||||
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
|
|
||||||
modalRef.componentInstance.brandColor = 'danger';
|
|
||||||
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
|
||||||
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
|
||||||
if (confirm) {
|
|
||||||
if (hasValue(eperson.id)) {
|
|
||||||
this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
|
||||||
if (restResponse.hasSucceeded) {
|
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name }));
|
|
||||||
this.submitForm.emit();
|
|
||||||
} else {
|
|
||||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
|
||||||
}
|
|
||||||
this.cancelForm.emit();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop impersonating the EPerson
|
|
||||||
*/
|
|
||||||
stopImpersonating() {
|
|
||||||
this.authService.stopImpersonatingAndRefresh();
|
|
||||||
this.isImpersonated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
|
||||||
*/
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.onCancel();
|
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
|
||||||
this.paginationService.clearPagination(this.config.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will ensure that the page gets reset and that the cache is cleared
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
|
||||||
this.requestService.removeByHrefSubstring(eperson.self);
|
|
||||||
});
|
|
||||||
this.initialisePage();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { AbstractControl, ValidationErrors } from '@angular/forms';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
|
import { getFirstSucceededRemoteData, } from '../../../../core/shared/operators';
|
||||||
|
|
||||||
|
export class ValidateEmailNotTaken {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will create the validator with the ePersonDataService requested from component
|
||||||
|
* @param ePersonDataService the service with DI in the component that this validator is being utilized.
|
||||||
|
*/
|
||||||
|
static createValidator(ePersonDataService: EPersonDataService) {
|
||||||
|
return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
|
||||||
|
return ePersonDataService.getEPersonByEmail(control.value)
|
||||||
|
.pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
map(res => {
|
||||||
|
return !!res.payload ? { emailTaken: true } : null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@ import { Collection } from './core/shared/collection.model';
|
|||||||
import { Item } from './core/shared/item.model';
|
import { Item } from './core/shared/item.model';
|
||||||
import { getCommunityPageRoute } from './community-page/community-page-routing-paths';
|
import { getCommunityPageRoute } from './community-page/community-page-routing-paths';
|
||||||
import { getCollectionPageRoute } from './collection-page/collection-page-routing-paths';
|
import { getCollectionPageRoute } from './collection-page/collection-page-routing-paths';
|
||||||
import { getItemPageRoute } from './item-page/item-page-routing-paths';
|
import { getItemModuleRoute, getItemPageRoute } from './item-page/item-page-routing-paths';
|
||||||
import { hasValue } from './shared/empty.util';
|
import { hasValue } from './shared/empty.util';
|
||||||
import { URLCombiner } from './core/url-combiner/url-combiner';
|
import { URLCombiner } from './core/url-combiner/url-combiner';
|
||||||
|
|
||||||
@@ -22,6 +22,15 @@ export function getBitstreamModuleRoute() {
|
|||||||
export function getBitstreamDownloadRoute(bitstream): string {
|
export function getBitstreamDownloadRoute(bitstream): string {
|
||||||
return new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString();
|
return new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString();
|
||||||
}
|
}
|
||||||
|
export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: string, queryParams: any } {
|
||||||
|
const url = new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString();
|
||||||
|
return {
|
||||||
|
routerLink: url,
|
||||||
|
queryParams: {
|
||||||
|
bitstream: bitstream.uuid
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const ADMIN_MODULE_PATH = 'admin';
|
export const ADMIN_MODULE_PATH = 'admin';
|
||||||
|
|
||||||
@@ -90,3 +99,8 @@ export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
|||||||
export function getAccessControlModuleRoute() {
|
export function getAccessControlModuleRoute() {
|
||||||
return `/${ACCESS_CONTROL_MODULE_PATH}`;
|
return `/${ACCESS_CONTROL_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const REQUEST_COPY_MODULE_PATH = 'request-a-copy';
|
||||||
|
export function getRequestCopyModulePath() {
|
||||||
|
return `/${REQUEST_COPY_MODULE_PATH}`;
|
||||||
|
}
|
||||||
|
@@ -14,7 +14,7 @@ import {
|
|||||||
PROFILE_MODULE_PATH,
|
PROFILE_MODULE_PATH,
|
||||||
REGISTER_PATH,
|
REGISTER_PATH,
|
||||||
WORKFLOW_ITEM_MODULE_PATH,
|
WORKFLOW_ITEM_MODULE_PATH,
|
||||||
LEGACY_BITSTREAM_MODULE_PATH,
|
LEGACY_BITSTREAM_MODULE_PATH, REQUEST_COPY_MODULE_PATH,
|
||||||
} from './app-routing-paths';
|
} from './app-routing-paths';
|
||||||
import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths';
|
import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths';
|
||||||
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
|
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
|
||||||
@@ -180,6 +180,11 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
|||||||
path: INFO_MODULE_PATH,
|
path: INFO_MODULE_PATH,
|
||||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule),
|
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: REQUEST_COPY_MODULE_PATH,
|
||||||
|
loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule),
|
||||||
|
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: FORBIDDEN_PATH,
|
path: FORBIDDEN_PATH,
|
||||||
component: ThemedForbiddenComponent
|
component: ThemedForbiddenComponent
|
||||||
|
@@ -7,7 +7,11 @@ import { EffectsModule } from '@ngrx/effects';
|
|||||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||||
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
|
import {
|
||||||
|
DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||||
|
DYNAMIC_MATCHER_PROVIDERS,
|
||||||
|
DynamicErrorMessagesMatcher
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
|
||||||
@@ -52,6 +56,7 @@ import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
|||||||
|
|
||||||
import { UUIDService } from './core/shared/uuid.service';
|
import { UUIDService } from './core/shared/uuid.service';
|
||||||
import { CookieService } from './core/services/cookie.service';
|
import { CookieService } from './core/services/cookie.service';
|
||||||
|
import { AbstractControl } from '@angular/forms';
|
||||||
|
|
||||||
export function getBase() {
|
export function getBase() {
|
||||||
return environment.ui.nameSpace;
|
return environment.ui.nameSpace;
|
||||||
@@ -61,6 +66,14 @@ export function getMetaReducers(): MetaReducer<AppState>[] {
|
|||||||
return environment.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
|
return environment.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition for displaying error messages on email form field
|
||||||
|
*/
|
||||||
|
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
||||||
|
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
||||||
|
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
|
||||||
|
};
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
@@ -146,6 +159,10 @@ const PROVIDERS = [
|
|||||||
multi: true,
|
multi: true,
|
||||||
deps: [ CookieService, UUIDService ]
|
deps: [ CookieService, UUIDService ]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||||
|
useValue: ValidateEmailErrorStateMatcher
|
||||||
|
},
|
||||||
...DYNAMIC_MATCHER_PROVIDERS,
|
...DYNAMIC_MATCHER_PROVIDERS,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -14,6 +14,7 @@ export enum FeatureID {
|
|||||||
IsCollectionAdmin = 'isCollectionAdmin',
|
IsCollectionAdmin = 'isCollectionAdmin',
|
||||||
IsCommunityAdmin = 'isCommunityAdmin',
|
IsCommunityAdmin = 'isCommunityAdmin',
|
||||||
CanDownload = 'canDownload',
|
CanDownload = 'canDownload',
|
||||||
|
CanRequestACopy = 'canRequestACopy',
|
||||||
CanManageVersions = 'canManageVersions',
|
CanManageVersions = 'canManageVersions',
|
||||||
CanManageBitstreamBundles = 'canManageBitstreamBundles',
|
CanManageBitstreamBundles = 'canManageBitstreamBundles',
|
||||||
CanManageRelationships = 'canManageRelationships',
|
CanManageRelationships = 'canManageRelationships',
|
||||||
@@ -21,4 +22,7 @@ export enum FeatureID {
|
|||||||
CanManagePolicies = 'canManagePolicies',
|
CanManagePolicies = 'canManagePolicies',
|
||||||
CanMakePrivate = 'canMakePrivate',
|
CanMakePrivate = 'canMakePrivate',
|
||||||
CanMove = 'canMove',
|
CanMove = 'canMove',
|
||||||
|
CanEditVersion = 'canEditVersion',
|
||||||
|
CanDeleteVersion = 'canDeleteVersion',
|
||||||
|
CanCreateVersion = 'canCreateVersion',
|
||||||
}
|
}
|
||||||
|
@@ -31,7 +31,7 @@ describe('ItemDataService', () => {
|
|||||||
},
|
},
|
||||||
removeByHrefSubstring(href: string) {
|
removeByHrefSubstring(href: string) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
},
|
||||||
}) as RequestService;
|
}) as RequestService;
|
||||||
const rdbService = getMockRemoteDataBuildService();
|
const rdbService = getMockRemoteDataBuildService();
|
||||||
|
|
||||||
@@ -184,4 +184,14 @@ describe('ItemDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when cache is invalidated', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
it('should call setStaleByHrefSubstring', () => {
|
||||||
|
service.invalidateItemCache('uuid');
|
||||||
|
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('item/uuid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -59,6 +59,7 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
* Get the endpoint for browsing items
|
* Get the endpoint for browsing items
|
||||||
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
|
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
|
||||||
* @param {FindListOptions} options
|
* @param {FindListOptions} options
|
||||||
|
* @param linkPath
|
||||||
* @returns {Observable<string>}
|
* @returns {Observable<string>}
|
||||||
*/
|
*/
|
||||||
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||||
@@ -287,4 +288,13 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`))
|
switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the cache of the item
|
||||||
|
* @param itemUUID
|
||||||
|
*/
|
||||||
|
invalidateItemCache(itemUUID: string) {
|
||||||
|
this.requestService.setStaleByHrefSubstring('item/' + itemUUID);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
95
src/app/core/data/item-request-data.service.spec.ts
Normal file
95
src/app/core/data/item-request-data.service.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { ItemRequestDataService } from './item-request-data.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { ItemRequest } from '../shared/item-request.model';
|
||||||
|
import { PostRequest } from './request.models';
|
||||||
|
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
|
||||||
|
describe('ItemRequestDataService', () => {
|
||||||
|
let service: ItemRequestDataService;
|
||||||
|
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
|
||||||
|
const restApiEndpoint = 'rest/api/endpoint/';
|
||||||
|
const requestId = 'request-id';
|
||||||
|
let itemRequest: ItemRequest;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
itemRequest = Object.assign(new ItemRequest(), {
|
||||||
|
token: 'item-request-token',
|
||||||
|
});
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestId,
|
||||||
|
send: '',
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildFromRequestUUID: createSuccessfulRemoteDataObject$(itemRequest),
|
||||||
|
});
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: observableOf(restApiEndpoint),
|
||||||
|
});
|
||||||
|
|
||||||
|
service = new ItemRequestDataService(requestService, rdbService, null, null, halService, null, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('requestACopy', () => {
|
||||||
|
it('should send a POST request containing the provided item request', (done) => {
|
||||||
|
service.requestACopy(itemRequest).subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(new PostRequest(requestId, restApiEndpoint, itemRequest));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('grant', () => {
|
||||||
|
let email: RequestCopyEmail;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
email = new RequestCopyEmail('subject', 'message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a PUT request containing the correct properties', (done) => {
|
||||||
|
service.grant(itemRequest.token, email, true).subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.PUT,
|
||||||
|
body: JSON.stringify({
|
||||||
|
acceptRequest: true,
|
||||||
|
responseMessage: email.message,
|
||||||
|
subject: email.subject,
|
||||||
|
suggestOpenAccess: true,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deny', () => {
|
||||||
|
let email: RequestCopyEmail;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
email = new RequestCopyEmail('subject', 'message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a PUT request containing the correct properties', (done) => {
|
||||||
|
service.deny(itemRequest.token, email).subscribe(() => {
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
|
method: RestRequestMethod.PUT,
|
||||||
|
body: JSON.stringify({
|
||||||
|
acceptRequest: false,
|
||||||
|
responseMessage: email.message,
|
||||||
|
subject: email.subject,
|
||||||
|
suggestOpenAccess: false,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
131
src/app/core/data/item-request-data.service.ts
Normal file
131
src/app/core/data/item-request-data.service.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, filter, find, map } from 'rxjs/operators';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { getFirstCompletedRemoteData, sendRequest } from '../shared/operators';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { PostRequest, PutRequest } from './request.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { ItemRequest } from '../shared/item-request.model';
|
||||||
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { DataService } from './data.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service responsible for fetching/sending data from/to the REST API on the itemrequests endpoint
|
||||||
|
*/
|
||||||
|
@Injectable(
|
||||||
|
{
|
||||||
|
providedIn: 'root',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
export class ItemRequestDataService extends DataService<ItemRequest> {
|
||||||
|
|
||||||
|
protected linkPath = 'itemrequests';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<ItemRequest>,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemRequestEndpoint(): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for an {@link ItemRequest} by their token
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
getItemRequestEndpointByToken(token: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
map((href: string) => `${href}/${token}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a copy of an item
|
||||||
|
* @param itemRequest
|
||||||
|
*/
|
||||||
|
requestACopy(itemRequest: ItemRequest): Observable<RemoteData<ItemRequest>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const href$ = this.getItemRequestEndpoint();
|
||||||
|
|
||||||
|
href$.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new PostRequest(requestId, href, itemRequest);
|
||||||
|
this.requestService.send(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUID<ItemRequest>(requestId).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deny the request of an item
|
||||||
|
* @param token Token of the {@link ItemRequest}
|
||||||
|
* @param email Email to send back to the user requesting the item
|
||||||
|
*/
|
||||||
|
deny(token: string, email: RequestCopyEmail): Observable<RemoteData<ItemRequest>> {
|
||||||
|
return this.process(token, email, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grant the request of an item
|
||||||
|
* @param token Token of the {@link ItemRequest}
|
||||||
|
* @param email Email to send back to the user requesting the item
|
||||||
|
* @param suggestOpenAccess Whether or not to suggest the item to become open access
|
||||||
|
*/
|
||||||
|
grant(token: string, email: RequestCopyEmail, suggestOpenAccess = false): Observable<RemoteData<ItemRequest>> {
|
||||||
|
return this.process(token, email, true, suggestOpenAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the request of an item
|
||||||
|
* @param token Token of the {@link ItemRequest}
|
||||||
|
* @param email Email to send back to the user requesting the item
|
||||||
|
* @param grant Grant or deny the request (true = grant, false = deny)
|
||||||
|
* @param suggestOpenAccess Whether or not to suggest the item to become open access
|
||||||
|
*/
|
||||||
|
process(token: string, email: RequestCopyEmail, grant: boolean, suggestOpenAccess = false): Observable<RemoteData<ItemRequest>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
this.getItemRequestEndpointByToken(token).pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'application/json');
|
||||||
|
options.headers = headers;
|
||||||
|
return new PutRequest(requestId, endpointURL, JSON.stringify({
|
||||||
|
acceptRequest: grant,
|
||||||
|
responseMessage: email.message,
|
||||||
|
subject: email.subject,
|
||||||
|
suggestOpenAccess,
|
||||||
|
}), options);
|
||||||
|
}),
|
||||||
|
sendRequest(this.requestService)).subscribe();
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
181
src/app/core/data/version-data.service.spec.ts
Normal file
181
src/app/core/data/version-data.service.spec.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { HrefOnlyDataService } from './href-only-data.service';
|
||||||
|
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { VersionDataService } from './version-data.service';
|
||||||
|
import { Version } from '../shared/version.model';
|
||||||
|
import { VersionHistory } from '../shared/version-history.model';
|
||||||
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
|
||||||
|
describe('VersionDataService test', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: VersionDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let hrefOnlyDataService: HrefOnlyDataService;
|
||||||
|
let responseCacheEntry: RequestEntry;
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
id: '1234-1234',
|
||||||
|
uuid: '1234-1234',
|
||||||
|
bundles: observableOf({}),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.type': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'Article'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.contributor.author': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Smith, Donald'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.date.issued': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const itemRD = createSuccessfulRemoteDataObject(item);
|
||||||
|
|
||||||
|
const versionHistory = Object.assign(new VersionHistory(), {
|
||||||
|
id: '1',
|
||||||
|
draftVersion: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockVersion: Version = Object.assign(new Version(), {
|
||||||
|
item: createSuccessfulRemoteDataObject$(item),
|
||||||
|
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||||
|
version: 1,
|
||||||
|
});
|
||||||
|
const mockVersionRD = createSuccessfulRemoteDataObject(mockVersion);
|
||||||
|
|
||||||
|
const endpointURL = `https://rest.api/rest/api/versioning/versions`;
|
||||||
|
const findByIdRequestURL = `https://rest.api/rest/api/versioning/versions/${mockVersion.id}`;
|
||||||
|
const findByIdRequestURL$ = observableOf(findByIdRequestURL);
|
||||||
|
|
||||||
|
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
|
||||||
|
objectCache = {} as ObjectCacheService;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
const comparatorEntry = {} as any;
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const pageInfo = new PageInfo();
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||||
|
return new VersionDataService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
store,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparatorEntry
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: cold('a', { a: endpointURL })
|
||||||
|
});
|
||||||
|
responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
send: true,
|
||||||
|
removeByHrefSubstring: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: observableOf(responseCacheEntry),
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: hot('(a|)', {
|
||||||
|
a: mockVersionRD
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
spyOn((service as any), 'findByHref').and.callThrough();
|
||||||
|
spyOn((service as any), 'getIDHrefObs').and.returnValue(findByIdRequestURL$);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
service = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getHistoryFromVersion', () => {
|
||||||
|
it('should proxy the call to DataService.findByHref', () => {
|
||||||
|
scheduler.schedule(() => service.getHistoryFromVersion(mockVersion, true, true));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).findByHref).toHaveBeenCalledWith(findByIdRequestURL$, true, true, followLink('versionhistory'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a VersionHistory', () => {
|
||||||
|
const result = service.getHistoryFromVersion(mockVersion, true, true);
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: versionHistory
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an EMPTY observable when version is not given', () => {
|
||||||
|
const result = service.getHistoryFromVersion(null);
|
||||||
|
const expected = cold('|');
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getHistoryIdFromVersion', () => {
|
||||||
|
it('should return the version history id', () => {
|
||||||
|
spyOn((service as any), 'getHistoryFromVersion').and.returnValue(observableOf(versionHistory));
|
||||||
|
|
||||||
|
const result = service.getHistoryIdFromVersion(mockVersion);
|
||||||
|
const expected = cold('(a|)', {
|
||||||
|
a: versionHistory.id
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -10,10 +10,14 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
import { FindListOptions } from './request.models';
|
import { EMPTY, Observable } from 'rxjs';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
import { VERSION } from '../shared/version.resource-type';
|
import { VERSION } from '../shared/version.resource-type';
|
||||||
|
import { VersionHistory } from '../shared/version-history.model';
|
||||||
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service responsible for handling requests related to the Version object
|
* Service responsible for handling requests related to the Version object
|
||||||
@@ -36,9 +40,29 @@ export class VersionDataService extends DataService<Version> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the endpoint for browsing versions
|
* Get the version history for the given version
|
||||||
|
* @param version
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
*/
|
*/
|
||||||
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
getHistoryFromVersion(version: Version, useCachedVersionIfAvailable = false, reRequestOnStale = true): Observable<VersionHistory> {
|
||||||
return this.halService.getEndpoint(this.linkPath);
|
return isNotEmpty(version) ? this.findById(version.id, useCachedVersionIfAvailable, reRequestOnStale, followLink('versionhistory')).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((res: Version) => res.versionhistory),
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
) : EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the version history for the given version
|
||||||
|
* @param version
|
||||||
|
*/
|
||||||
|
getHistoryIdFromVersion(version: Version): Observable<string> {
|
||||||
|
return this.getHistoryFromVersion(version).pipe(
|
||||||
|
map((versionHistory: VersionHistory) => versionHistory.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,14 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
|
|||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { VersionDataService } from './version-data.service';
|
import { VersionDataService } from './version-data.service';
|
||||||
|
import { fakeAsync, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { VersionHistory } from '../shared/version-history.model';
|
||||||
|
import { Version } from '../shared/version.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import SpyObj = jasmine.SpyObj;
|
||||||
|
|
||||||
const url = 'fake-url';
|
const url = 'fake-url';
|
||||||
|
|
||||||
@@ -16,9 +24,97 @@ describe('VersionHistoryDataService', () => {
|
|||||||
let notificationsService: any;
|
let notificationsService: any;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
let versionService: VersionDataService;
|
let versionService: SpyObj<VersionDataService>;
|
||||||
let halService: any;
|
let halService: any;
|
||||||
|
|
||||||
|
const versionHistoryId = 'version-history-id';
|
||||||
|
const versionHistoryDraftId = 'version-history-draft-id';
|
||||||
|
const version1Id = 'version-1-id';
|
||||||
|
const version2Id = 'version-1-id';
|
||||||
|
const item1Uuid = 'item-1-uuid';
|
||||||
|
const item2Uuid = 'item-2-uuid';
|
||||||
|
const versionHistory = Object.assign(new VersionHistory(), {
|
||||||
|
id: versionHistoryId,
|
||||||
|
draftVersion: false,
|
||||||
|
});
|
||||||
|
const versionHistoryDraft = Object.assign(new VersionHistory(), {
|
||||||
|
id: versionHistoryDraftId,
|
||||||
|
draftVersion: true,
|
||||||
|
});
|
||||||
|
const version1 = Object.assign(new Version(), {
|
||||||
|
id: version1Id,
|
||||||
|
version: 1,
|
||||||
|
created: new Date(2020, 1, 1),
|
||||||
|
summary: 'first version',
|
||||||
|
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'version1-url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const version2 = Object.assign(new Version(), {
|
||||||
|
id: version2Id,
|
||||||
|
version: 2,
|
||||||
|
summary: 'second version',
|
||||||
|
created: new Date(2020, 1, 2),
|
||||||
|
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'version2-url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const versions = [version1, version2];
|
||||||
|
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
|
||||||
|
const item1 = Object.assign(new Item(), {
|
||||||
|
uuid: item1Uuid,
|
||||||
|
handle: '123456789/1',
|
||||||
|
version: createSuccessfulRemoteDataObject$(version1),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '/items/' + item2Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const item2 = Object.assign(new Item(), {
|
||||||
|
uuid: item2Uuid,
|
||||||
|
handle: '123456789/2',
|
||||||
|
version: createSuccessfulRemoteDataObject$(version2),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '/items/' + item2Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const items = [item1, item2];
|
||||||
|
version1.item = createSuccessfulRemoteDataObject$(item1);
|
||||||
|
version2.item = createSuccessfulRemoteDataObject$(item2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a VersionHistoryDataService used for testing
|
||||||
|
* @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional)
|
||||||
|
*/
|
||||||
|
function createService(requestEntry$?) {
|
||||||
|
requestService = getMockRequestService(requestEntry$);
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildList: jasmine.createSpy('buildList'),
|
||||||
|
buildFromRequestUUID: jasmine.createSpy('buildFromRequestUUID'),
|
||||||
|
});
|
||||||
|
objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
|
remove: jasmine.createSpy('remove')
|
||||||
|
});
|
||||||
|
versionService = jasmine.createSpyObj('objectCache', {
|
||||||
|
findByHref: jasmine.createSpy('findByHref'),
|
||||||
|
findAllByHref: jasmine.createSpy('findAllByHref'),
|
||||||
|
getHistoryFromVersion: jasmine.createSpy('getHistoryFromVersion'),
|
||||||
|
});
|
||||||
|
halService = new HALEndpointServiceStub(url);
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, versionService, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createService();
|
createService();
|
||||||
});
|
});
|
||||||
@@ -35,24 +131,70 @@ describe('VersionHistoryDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
describe('when getVersions is called', () => {
|
||||||
* Create a VersionHistoryDataService used for testing
|
beforeEach(waitForAsync(() => {
|
||||||
* @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional)
|
service.getVersions(versionHistoryId);
|
||||||
*/
|
}));
|
||||||
function createService(requestEntry$?) {
|
it('findAllByHref should have been called', () => {
|
||||||
requestService = getMockRequestService(requestEntry$);
|
expect(versionService.findAllByHref).toHaveBeenCalled();
|
||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
|
||||||
buildList: jasmine.createSpy('buildList')
|
|
||||||
});
|
});
|
||||||
objectCache = jasmine.createSpyObj('objectCache', {
|
});
|
||||||
remove: jasmine.createSpy('remove')
|
|
||||||
});
|
describe('when getBrowseEndpoint is called', () => {
|
||||||
versionService = jasmine.createSpyObj('objectCache', {
|
it('should return the correct value', () => {
|
||||||
findAllByHref: jasmine.createSpy('findAllByHref')
|
service.getBrowseEndpoint().subscribe((res) => {
|
||||||
});
|
expect(res).toBe(url + '/versionhistories');
|
||||||
halService = new HALEndpointServiceStub(url);
|
});
|
||||||
notificationsService = new NotificationsServiceStub();
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getVersionsEndpoint is called', () => {
|
||||||
|
it('should return the correct value', () => {
|
||||||
|
service.getVersionsEndpoint(versionHistoryId).subscribe((res) => {
|
||||||
|
expect(res).toBe(url + '/versions');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when cache is invalidated', () => {
|
||||||
|
it('should call setStaleByHrefSubstring', () => {
|
||||||
|
service.invalidateVersionHistoryCache(versionHistoryId);
|
||||||
|
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('versioning/versionhistories/' + versionHistoryId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isLatest$', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
spyOn(service, 'getLatestVersion$').and.returnValue(of(version2));
|
||||||
|
}));
|
||||||
|
it('should return false for version1', () => {
|
||||||
|
service.isLatest$(version1).subscribe((res) => {
|
||||||
|
expect(res).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return true for version2', () => {
|
||||||
|
service.isLatest$(version2).subscribe((res) => {
|
||||||
|
expect(res).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasDraftVersion$', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$<Version>(version1));
|
||||||
|
}));
|
||||||
|
it('should return false if draftVersion is false', fakeAsync(() => {
|
||||||
|
versionService.getHistoryFromVersion.and.returnValue(of(versionHistory));
|
||||||
|
service.hasDraftVersion$('href').subscribe((res) => {
|
||||||
|
expect(res).toBeFalse();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
it('should return true if draftVersion is true', fakeAsync(() => {
|
||||||
|
versionService.getHistoryFromVersion.and.returnValue(of(versionHistoryDraft));
|
||||||
|
service.hasDraftVersion$('href').subscribe((res) => {
|
||||||
|
expect(res).toBeTrue();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, versionService, null, null);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
@@ -8,19 +8,30 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
import { FindListOptions } from './request.models';
|
import { FindListOptions, PostRequest, RestRequest } from './request.models';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { PaginatedList } from './paginated-list.model';
|
import { PaginatedList } from './paginated-list.model';
|
||||||
import { Version } from '../shared/version.model';
|
import { Version } from '../shared/version.model';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
import { VERSION_HISTORY } from '../shared/version-history.resource-type';
|
import { VERSION_HISTORY } from '../shared/version-history.resource-type';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { VersionDataService } from './version-data.service';
|
import { VersionDataService } from './version-data.service';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import {
|
||||||
|
getAllSucceededRemoteData,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getRemoteDataPayload,
|
||||||
|
sendRequest
|
||||||
|
} from '../shared/operators';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { hasValueOperator } from '../../shared/empty.util';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service responsible for handling requests related to the VersionHistory object
|
* Service responsible for handling requests related to the VersionHistory object
|
||||||
@@ -79,4 +90,129 @@ export class VersionHistoryDataService extends DataService<VersionHistory> {
|
|||||||
|
|
||||||
return this.versionDataService.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.versionDataService.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new version for an item
|
||||||
|
* @param itemHref the item for which create a new version
|
||||||
|
* @param summary the summary of the new version
|
||||||
|
*/
|
||||||
|
createVersion(itemHref: string, summary: string): Observable<RemoteData<Version>> {
|
||||||
|
const requestOptions: HttpOptions = Object.create({});
|
||||||
|
let requestHeaders = new HttpHeaders();
|
||||||
|
requestHeaders = requestHeaders.append('Content-Type', 'text/uri-list');
|
||||||
|
requestOptions.headers = requestHeaders;
|
||||||
|
|
||||||
|
return this.halService.getEndpoint(this.versionsEndpoint).pipe(
|
||||||
|
take(1),
|
||||||
|
map((endpointUrl: string) => (summary?.length > 0) ? `${endpointUrl}?summary=${summary}` : `${endpointUrl}`),
|
||||||
|
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, itemHref, requestOptions)),
|
||||||
|
sendRequest(this.requestService),
|
||||||
|
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
) as Observable<RemoteData<Version>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest version in a version history
|
||||||
|
* @param versionHistory
|
||||||
|
*/
|
||||||
|
getLatestVersionFromHistory$(versionHistory: VersionHistory): Observable<Version> {
|
||||||
|
|
||||||
|
// Pagination options to fetch a single version on the first page (this is the latest version in the history)
|
||||||
|
const latestVersionOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'item-newest-version-options',
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const latestVersionSearch = new PaginatedSearchOptions({pagination: latestVersionOptions});
|
||||||
|
|
||||||
|
return this.getVersions(versionHistory.id, latestVersionSearch, false, true, followLink('item')).pipe(
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
hasValueOperator(),
|
||||||
|
filter((versions) => versions.page.length > 0),
|
||||||
|
map((versions) => versions.page[0])
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest version (return null if the specified version is null)
|
||||||
|
* @param version
|
||||||
|
*/
|
||||||
|
getLatestVersion$(version: Version): Observable<Version> {
|
||||||
|
// retrieve again version, including with versionHistory
|
||||||
|
return version.id ? this.versionDataService.findById(version.id, false, true, followLink('versionhistory')).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((res) => res.versionhistory),
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((versionHistoryRD) => this.getLatestVersionFromHistory$(versionHistoryRD)),
|
||||||
|
) : of(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given version is the latest (return null if `version` is null)
|
||||||
|
* @param version
|
||||||
|
* @returns `true` if the specified version is the latest one, `false` otherwise, or `null` if the specified version is null
|
||||||
|
*/
|
||||||
|
isLatest$(version: Version): Observable<boolean> {
|
||||||
|
return version ? this.getLatestVersion$(version).pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((latestVersion) => of(version.version === latestVersion.version))
|
||||||
|
) : of(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a worskpace item exists in the version history (return null if there is no version history)
|
||||||
|
* @param versionHref the href of the version
|
||||||
|
* @returns `true` if a workspace item exists, `false` otherwise, or `null` if a version history does not exist
|
||||||
|
*/
|
||||||
|
hasDraftVersion$(versionHref: string): Observable<boolean> {
|
||||||
|
return this.versionDataService.findByHref(versionHref, true, true, followLink('versionhistory')).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((res) => {
|
||||||
|
if (res.hasSucceeded && !res.hasNoContent) {
|
||||||
|
return of(res).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((version) => this.versionDataService.getHistoryFromVersion(version)),
|
||||||
|
map((versionHistory) => versionHistory ? versionHistory.draftVersion : false),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the item of the latest version in a version history
|
||||||
|
* @param versionHistory
|
||||||
|
*/
|
||||||
|
getLatestVersionItemFromHistory$(versionHistory: VersionHistory): Observable<Item> {
|
||||||
|
return this.getLatestVersionFromHistory$(versionHistory).pipe(
|
||||||
|
switchMap((newLatestVersion) => newLatestVersion.item),
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the item of the latest version from any version in the version history
|
||||||
|
* @param version
|
||||||
|
*/
|
||||||
|
getVersionHistoryFromVersion$(version: Version): Observable<VersionHistory> {
|
||||||
|
return this.versionDataService.getHistoryIdFromVersion(version).pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((res) => this.findById(res)),
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the cache of the version history
|
||||||
|
* @param versionHistoryID
|
||||||
|
*/
|
||||||
|
invalidateVersionHistoryCache(versionHistoryID: string) {
|
||||||
|
this.requestService.setStaleByHrefSubstring('versioning/versionhistories/' + versionHistoryID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
90
src/app/core/shared/item-request.model.ts
Normal file
90
src/app/core/shared/item-request.model.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
import { typedObject } from '../cache/builders/build-decorators';
|
||||||
|
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
import { ITEM_REQUEST } from './item-request.resource-type';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
import { HALLink } from './hal-link.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model class for an ItemRequest
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class ItemRequest implements CacheableObject {
|
||||||
|
static type = ITEM_REQUEST;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* opaque string which uniquely identifies this request
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* true if the request is for all bitstreams of the item.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
allfiles: boolean;
|
||||||
|
/**
|
||||||
|
* email address of the person requesting the files.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
requestEmail: string;
|
||||||
|
/**
|
||||||
|
* Human-readable name of the person requesting the files.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
requestName: string;
|
||||||
|
/**
|
||||||
|
* arbitrary message provided by the person requesting the files.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
requestMessage: string;
|
||||||
|
/**
|
||||||
|
* date that the request was recorded.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
requestDate: string;
|
||||||
|
/**
|
||||||
|
* true if the request has been granted.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
acceptRequest: boolean;
|
||||||
|
/**
|
||||||
|
* date that the request was granted or denied.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
decisionDate: string;
|
||||||
|
/**
|
||||||
|
* date on which the request is considered expired.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
expires: string;
|
||||||
|
/**
|
||||||
|
* UUID of the requested Item.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
itemId: string;
|
||||||
|
/**
|
||||||
|
* UUID of the requested bitstream.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
bitstreamId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this ItemRequest
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink;
|
||||||
|
item: HALLink;
|
||||||
|
bitstream: HALLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
9
src/app/core/shared/item-request.resource-type.ts
Normal file
9
src/app/core/shared/item-request.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for ItemRequest.
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const ITEM_REQUEST = new ResourceType('itemrequest');
|
@@ -22,6 +22,7 @@ export class VersionHistory extends DSpaceObject {
|
|||||||
_links: {
|
_links: {
|
||||||
self: HALLink;
|
self: HALLink;
|
||||||
versions: HALLink;
|
versions: HALLink;
|
||||||
|
draftVersion: HALLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +31,24 @@ export class VersionHistory extends DSpaceObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The summary of this Version History
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
summary: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the submitter of this Version History
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
submitterName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether exist a workspace item
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
draftVersion: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of versions within this history
|
* The list of versions within this history
|
||||||
*/
|
*/
|
||||||
|
150
src/app/core/submission/workflowitem-data.service.spec.ts
Normal file
150
src/app/core/submission/workflowitem-data.service.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||||
|
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||||
|
import { WorkflowItem } from './models/workflowitem.model';
|
||||||
|
|
||||||
|
describe('WorkflowItemDataService test', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: WorkflowItemDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let hrefOnlyDataService: HrefOnlyDataService;
|
||||||
|
let responseCacheEntry: RequestEntry;
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
id: '1234-1234',
|
||||||
|
uuid: '1234-1234',
|
||||||
|
bundles: observableOf({}),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.type': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'Article'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.contributor.author': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Smith, Donald'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.date.issued': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const itemRD = createSuccessfulRemoteDataObject(item);
|
||||||
|
const wsi = Object.assign(new WorkflowItem(), { item: observableOf(itemRD), id: '1234', uuid: '1234' });
|
||||||
|
const wsiRD = createSuccessfulRemoteDataObject(wsi);
|
||||||
|
|
||||||
|
const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`;
|
||||||
|
const searchRequestURL = `https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234`;
|
||||||
|
const searchRequestURL$ = observableOf(searchRequestURL);
|
||||||
|
|
||||||
|
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
|
||||||
|
objectCache = {} as ObjectCacheService;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
const comparatorEntry = {} as any;
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const pageInfo = new PageInfo();
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||||
|
return new WorkflowItemDataService(
|
||||||
|
comparatorEntry,
|
||||||
|
halService,
|
||||||
|
http,
|
||||||
|
notificationsService,
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
store
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: cold('a', { a: endpointURL })
|
||||||
|
});
|
||||||
|
responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
send: true,
|
||||||
|
removeByHrefSubstring: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: observableOf(responseCacheEntry),
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: hot('a|', {
|
||||||
|
a: wsiRD
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
spyOn((service as any), 'findByHref').and.callThrough();
|
||||||
|
spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
service = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findByItem', () => {
|
||||||
|
it('should proxy the call to DataService.findByHref', () => {
|
||||||
|
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<WorkspaceItem> for the search', () => {
|
||||||
|
const result = service.findByItem('1234-1234', true, true, pageInfo);
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: wsiRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -9,7 +9,7 @@ import { DataService } from '../data/data.service';
|
|||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { WorkflowItem } from './models/workflowitem.model';
|
import { WorkflowItem } from './models/workflowitem.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { DeleteByIDRequest } from '../data/request.models';
|
import { DeleteByIDRequest, FindListOptions } from '../data/request.models';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
@@ -19,6 +19,9 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that provides methods to make REST requests with workflow items endpoint.
|
* A service that provides methods to make REST requests with workflow items endpoint.
|
||||||
@@ -27,6 +30,7 @@ import { getFirstCompletedRemoteData } from '../shared/operators';
|
|||||||
@dataService(WorkflowItem.type)
|
@dataService(WorkflowItem.type)
|
||||||
export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
||||||
protected linkPath = 'workflowitems';
|
protected linkPath = 'workflowitems';
|
||||||
|
protected searchByItemLinkPath = 'item';
|
||||||
protected responseMsToLive = 10 * 1000;
|
protected responseMsToLive = 10 * 1000;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -86,4 +90,23 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
|||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the WorkflowItem object found through the UUID of an item
|
||||||
|
*
|
||||||
|
* @param uuid The uuid of the item
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param options The {@link FindListOptions} object
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
||||||
|
const findListOptions = new FindListOptions();
|
||||||
|
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
|
||||||
|
const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
||||||
|
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
150
src/app/core/submission/workspaceitem-data.service.spec.ts
Normal file
150
src/app/core/submission/workspaceitem-data.service.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||||
|
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||||
|
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||||
|
|
||||||
|
describe('WorkspaceitemDataService test', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: WorkspaceitemDataService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let hrefOnlyDataService: HrefOnlyDataService;
|
||||||
|
let responseCacheEntry: RequestEntry;
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
id: '1234-1234',
|
||||||
|
uuid: '1234-1234',
|
||||||
|
bundles: observableOf({}),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.type': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'Article'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.contributor.author': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Smith, Donald'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.date.issued': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const itemRD = createSuccessfulRemoteDataObject(item);
|
||||||
|
const wsi = Object.assign(new WorkspaceItem(), { item: observableOf(itemRD), id: '1234', uuid: '1234' });
|
||||||
|
const wsiRD = createSuccessfulRemoteDataObject(wsi);
|
||||||
|
|
||||||
|
const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`;
|
||||||
|
const searchRequestURL = `https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234`;
|
||||||
|
const searchRequestURL$ = observableOf(searchRequestURL);
|
||||||
|
|
||||||
|
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
|
||||||
|
objectCache = {} as ObjectCacheService;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
const comparatorEntry = {} as any;
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const pageInfo = new PageInfo();
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||||
|
return new WorkspaceitemDataService(
|
||||||
|
comparatorEntry,
|
||||||
|
halService,
|
||||||
|
http,
|
||||||
|
notificationsService,
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
store
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: cold('a', { a: endpointURL })
|
||||||
|
});
|
||||||
|
responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
send: true,
|
||||||
|
removeByHrefSubstring: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: observableOf(responseCacheEntry),
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: hot('a|', {
|
||||||
|
a: wsiRD
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
spyOn((service as any), 'findByHref').and.callThrough();
|
||||||
|
spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
service = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findByItem', () => {
|
||||||
|
it('should proxy the call to DataService.findByHref', () => {
|
||||||
|
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<WorkspaceItem> for the search', () => {
|
||||||
|
const result = service.findByItem('1234-1234', true, true, pageInfo);
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: wsiRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -12,6 +12,11 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { FindListOptions } from '../data/request.models';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that provides methods to make REST requests with workspaceitems endpoint.
|
* A service that provides methods to make REST requests with workspaceitems endpoint.
|
||||||
@@ -20,6 +25,7 @@ import { WorkspaceItem } from './models/workspaceitem.model';
|
|||||||
@dataService(WorkspaceItem.type)
|
@dataService(WorkspaceItem.type)
|
||||||
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
||||||
protected linkPath = 'workspaceitems';
|
protected linkPath = 'workspaceitems';
|
||||||
|
protected searchByItemLinkPath = 'item';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
||||||
@@ -33,4 +39,22 @@ export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the WorkspaceItem object found through the UUID of an item
|
||||||
|
*
|
||||||
|
* @param uuid The uuid of the item
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
|
* @param options The {@link FindListOptions} object
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
||||||
|
const findListOptions = new FindListOptions();
|
||||||
|
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
|
||||||
|
const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
||||||
|
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,66 +1,69 @@
|
|||||||
<div class="item-metadata">
|
<div class="item-metadata">
|
||||||
<div class="button-row top d-flex mb-2">
|
<div class="button-row top d-flex mb-2">
|
||||||
<button class="mr-auto btn btn-success"
|
<button class="mr-auto btn btn-success"
|
||||||
(click)="add()"><i
|
(click)="add()"><i
|
||||||
class="fas fa-plus"></i>
|
class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.add-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.add-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||||
(click)="reinstate()"><i
|
(click)="reinstate()"><i
|
||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
|
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||||
[disabled]="!(hasChanges() | async)"
|
[disabled]="!(hasChanges() | async)"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-responsive table-striped table-bordered" *ngIf="((updates$ | async)| dsObjectValues).length > 0">
|
<table class="table table-responsive table-striped table-bordered"
|
||||||
<tbody>
|
*ngIf="((updates$ | async)| dsObjectValues).length > 0">
|
||||||
<tr>
|
<thead>
|
||||||
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
|
<tr>
|
||||||
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
|
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
|
||||||
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
|
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
|
||||||
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
|
||||||
</tr>
|
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
||||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
</tr>
|
||||||
ds-edit-in-place-field
|
</thead>
|
||||||
[fieldUpdate]="updateValue || {}"
|
<tbody>
|
||||||
[url]="url"
|
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
||||||
[ngClass]="{
|
ds-edit-in-place-field
|
||||||
|
[fieldUpdate]="updateValue || {}"
|
||||||
|
[url]="url"
|
||||||
|
[ngClass]="{
|
||||||
'table-warning': updateValue.changeType === 0,
|
'table-warning': updateValue.changeType === 0,
|
||||||
'table-danger': updateValue.changeType === 2,
|
'table-danger': updateValue.changeType === 2,
|
||||||
'table-success': updateValue.changeType === 1
|
'table-success': updateValue.changeType === 1
|
||||||
}">
|
}">
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
|
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
|
||||||
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
||||||
|
</div>
|
||||||
|
<div class="button-row bottom">
|
||||||
|
<div class="mt-2 float-right">
|
||||||
|
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||||
|
(click)="reinstate()"><i
|
||||||
|
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary mr-0" [disabled]="!(hasChanges() | async)"
|
||||||
|
(click)="submit()"><i
|
||||||
|
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||||
|
[disabled]="!(hasChanges() | async)"
|
||||||
|
(click)="discard()"><i
|
||||||
|
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row bottom">
|
</div>
|
||||||
<div class="mt-2 float-right">
|
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
|
||||||
(click)="reinstate()"><i
|
|
||||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary mr-0" [disabled]="!(hasChanges() | async)"
|
|
||||||
(click)="submit()"><i
|
|
||||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
|
||||||
[disabled]="!(hasChanges() | async)"
|
|
||||||
(click)="discard()"><i
|
|
||||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
<div class="mt-4">
|
|
||||||
<ds-alert [content]="'item.edit.tabs.versionhistory.under-construction'" [type]="AlertTypeEnum.Warning"></ds-alert>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
|
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
|
||||||
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"></ds-item-versions>
|
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"
|
||||||
|
[displayActions]="true"></ds-item-versions>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ItemVersionHistoryComponent } from './item-version-history.component';
|
import { ItemVersionHistoryComponent } from './item-version-history.component';
|
||||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -18,12 +18,20 @@ describe('ItemVersionHistoryComponent', () => {
|
|||||||
handle: '123456789/1',
|
handle: '123456789/1',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activatedRoute = {
|
||||||
|
parent: {
|
||||||
|
parent: {
|
||||||
|
data: observableOf({dso: createSuccessfulRemoteDataObject(item)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ItemVersionHistoryComponent, VarDirective],
|
declarations: [ItemVersionHistoryComponent, VarDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) } } }
|
{ provide: ActivatedRoute, useValue: activatedRoute }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -30,6 +30,6 @@ export class ItemVersionHistoryComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)).pipe(getFirstSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
this.itemRD$ = this.route.parent.parent.data.pipe(map((data) => data.dso)).pipe(getFirstSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,7 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<ds-file-download-link [bitstream]="file">
|
<ds-file-download-link [bitstream]="file" [item]="item">
|
||||||
{{"item.page.filesection.download" | translate}}
|
{{"item.page.filesection.download" | translate}}
|
||||||
</ds-file-download-link>
|
</ds-file-download-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<ds-file-download-link [bitstream]="file">
|
<ds-file-download-link [bitstream]="file" [item]="item">
|
||||||
{{"item.page.filesection.download" | translate}}
|
{{"item.page.filesection.download" | translate}}
|
||||||
</ds-file-download-link>
|
</ds-file-download-link>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -22,6 +22,10 @@ export function getItemEditRoute(item: Item) {
|
|||||||
return new URLCombiner(getItemPageRoute(item), ITEM_EDIT_PATH).toString();
|
return new URLCombiner(getItemPageRoute(item), ITEM_EDIT_PATH).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getItemEditVersionhistoryRoute(item: Item) {
|
||||||
|
return new URLCombiner(getItemPageRoute(item), ITEM_EDIT_PATH, ITEM_EDIT_VERSIONHISTORY_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
export function getEntityPageRoute(entityType: string, itemId: string) {
|
export function getEntityPageRoute(entityType: string, itemId: string) {
|
||||||
if (isNotEmpty(entityType)) {
|
if (isNotEmpty(entityType)) {
|
||||||
return new URLCombiner('/entities', encodeURIComponent(entityType.toLowerCase()), itemId).toString();
|
return new URLCombiner('/entities', encodeURIComponent(entityType.toLowerCase()), itemId).toString();
|
||||||
@@ -34,5 +38,15 @@ export function getEntityEditRoute(entityType: string, itemId: string) {
|
|||||||
return new URLCombiner(getEntityPageRoute(entityType, itemId), ITEM_EDIT_PATH).toString();
|
return new URLCombiner(getEntityPageRoute(entityType, itemId), ITEM_EDIT_PATH).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route to an item's version
|
||||||
|
* @param versionId the ID of the version for which the route will be retrieved
|
||||||
|
*/
|
||||||
|
export function getItemVersionRoute(versionId: string) {
|
||||||
|
return new URLCombiner(getItemModuleRoute(), ITEM_VERSION_PATH, versionId).toString();
|
||||||
|
}
|
||||||
|
|
||||||
export const ITEM_EDIT_PATH = 'edit';
|
export const ITEM_EDIT_PATH = 'edit';
|
||||||
|
export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory';
|
||||||
|
export const ITEM_VERSION_PATH = 'version';
|
||||||
export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new';
|
export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new';
|
||||||
|
@@ -3,6 +3,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { ItemPageResolver } from './item-page.resolver';
|
import { ItemPageResolver } from './item-page.resolver';
|
||||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
||||||
|
import { VersionResolver } from './version-page/version.resolver';
|
||||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
import { LinkService } from '../core/cache/builders/link.service';
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||||
@@ -12,6 +13,8 @@ import { MenuItemType } from '../shared/menu/initial-menus-state';
|
|||||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||||
|
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||||
|
import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -42,6 +45,10 @@ import { ThemedFullItemPageComponent } from './full/themed-full-item-page.compon
|
|||||||
path: UPLOAD_BITSTREAM_PATH,
|
path: UPLOAD_BITSTREAM_PATH,
|
||||||
component: UploadBitstreamComponent,
|
component: UploadBitstreamComponent,
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':request-a-copy',
|
||||||
|
component: BitstreamRequestACopyPageComponent,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
data: {
|
data: {
|
||||||
@@ -58,6 +65,18 @@ import { ThemedFullItemPageComponent } from './full/themed-full-item-page.compon
|
|||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'version',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: VersionPageComponent,
|
||||||
|
resolve: {
|
||||||
|
dso: VersionResolver,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
@@ -67,6 +86,7 @@ import { ThemedFullItemPageComponent } from './full/themed-full-item-page.compon
|
|||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService,
|
LinkService,
|
||||||
ItemPageAdministratorGuard,
|
ItemPageAdministratorGuard,
|
||||||
|
VersionResolver,
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@@ -31,6 +31,8 @@ import { MediaViewerComponent } from './media-viewer/media-viewer.component';
|
|||||||
import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
|
import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
|
||||||
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
|
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
|
||||||
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
||||||
|
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||||
|
import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component';
|
||||||
import { ThemedFileSectionComponent} from './simple/field-components/file-section/themed-file-section.component';
|
import { ThemedFileSectionComponent} from './simple/field-components/file-section/themed-file-section.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -62,7 +64,8 @@ const DECLARATIONS = [
|
|||||||
AbstractIncrementalListComponent,
|
AbstractIncrementalListComponent,
|
||||||
MediaViewerComponent,
|
MediaViewerComponent,
|
||||||
MediaViewerVideoComponent,
|
MediaViewerVideoComponent,
|
||||||
MediaViewerImageComponent
|
MediaViewerImageComponent,
|
||||||
|
VersionPageComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -74,10 +77,11 @@ const DECLARATIONS = [
|
|||||||
StatisticsModule.forRoot(),
|
StatisticsModule.forRoot(),
|
||||||
JournalEntitiesModule.withEntryComponents(),
|
JournalEntitiesModule.withEntryComponents(),
|
||||||
ResearchEntitiesModule.withEntryComponents(),
|
ResearchEntitiesModule.withEntryComponents(),
|
||||||
NgxGalleryModule,
|
NgxGalleryModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS
|
...DECLARATIONS,
|
||||||
|
VersionedItemComponent
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...DECLARATIONS
|
...DECLARATIONS
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
||||||
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
||||||
<div class="file-section">
|
<div class="file-section">
|
||||||
<ds-file-download-link *ngFor="let file of bitstreams; let last=last;" [bitstream]="file">
|
<ds-file-download-link *ngFor="let file of bitstreams; let last=last;" [bitstream]="file" [item]="item">
|
||||||
<span>{{file?.name}}</span>
|
<span>{{file?.name}}</span>
|
||||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||||
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
<ds-item-versions class="mt-2" [item]="item" [displayActions]="false"></ds-item-versions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||||
|
@@ -3,6 +3,9 @@
|
|||||||
<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="pl-2">
|
<div class="pl-2">
|
||||||
|
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||||
|
[tooltipMsgCreate]="'item.page.version.create'"
|
||||||
|
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -29,6 +29,12 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
|||||||
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
||||||
import { UntypedItemComponent } from './untyped-item.component';
|
import { UntypedItemComponent } from './untyped-item.component';
|
||||||
|
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||||
|
import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||||
|
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||||
|
import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service';
|
||||||
|
|
||||||
const mockItem: Item = Object.assign(new Item(), {
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
|
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
|
||||||
@@ -47,13 +53,16 @@ describe('UntypedItemComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [
|
||||||
loader: {
|
TranslateModule.forRoot({
|
||||||
provide: TranslateLoader,
|
loader: {
|
||||||
useClass: TranslateLoaderMock
|
provide: TranslateLoader,
|
||||||
}
|
useClass: TranslateLoaderMock
|
||||||
})],
|
}
|
||||||
declarations: [UntypedItemComponent, GenericItemPageFieldComponent, TruncatePipe],
|
}),
|
||||||
|
RouterTestingModule,
|
||||||
|
],
|
||||||
|
declarations: [UntypedItemComponent, GenericItemPageFieldComponent, TruncatePipe ],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ItemDataService, useValue: {} },
|
{ provide: ItemDataService, useValue: {} },
|
||||||
{ provide: TruncatableService, useValue: {} },
|
{ provide: TruncatableService, useValue: {} },
|
||||||
@@ -68,9 +77,14 @@ describe('UntypedItemComponent', () => {
|
|||||||
{ provide: HttpClient, useValue: {} },
|
{ provide: HttpClient, useValue: {} },
|
||||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||||
|
{ provide: VersionHistoryDataService, useValue: {} },
|
||||||
|
{ provide: VersionDataService, useValue: {} },
|
||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
|
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||||
|
{ provide: SearchService, useValue: {} },
|
||||||
|
{ provide: ItemDataService, useValue: {} },
|
||||||
|
{ provide: ItemVersionsSharedService, useValue: {} },
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(UntypedItemComponent, {
|
}).overrideComponent(UntypedItemComponent, {
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { ItemComponent } from '../shared/item.component';
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { VersionedItemComponent } from '../versioned-item/versioned-item.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that represents a publication Item page
|
* Component that represents a publication Item page
|
||||||
@@ -15,6 +15,6 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
|||||||
templateUrl: './untyped-item.component.html',
|
templateUrl: './untyped-item.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class UntypedItemComponent extends ItemComponent {
|
export class UntypedItemComponent extends VersionedItemComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,93 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { VersionedItemComponent } from './versioned-item.component';
|
||||||
|
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||||
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
|
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { MetadataMap } from '../../../../core/shared/metadata.models';
|
||||||
|
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||||
|
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { Version } from '../../../../core/shared/version.model';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: new MetadataMap(),
|
||||||
|
relationships: createRelationshipsObservable(),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'item-href'
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
href: 'version-href'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@Component({template: ''})
|
||||||
|
class DummyComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('VersionedItemComponent', () => {
|
||||||
|
let component: VersionedItemComponent;
|
||||||
|
let fixture: ComponentFixture<VersionedItemComponent>;
|
||||||
|
|
||||||
|
let versionService: VersionDataService;
|
||||||
|
let versionHistoryService: VersionHistoryDataService;
|
||||||
|
|
||||||
|
const versionServiceSpy = jasmine.createSpyObj('versionService', {
|
||||||
|
findByHref: createSuccessfulRemoteDataObject$<Version>(new Version()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService', {
|
||||||
|
createVersion: createSuccessfulRemoteDataObject$<Version>(new Version()),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [VersionedItemComponent, DummyComponent],
|
||||||
|
imports: [RouterTestingModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy },
|
||||||
|
{ provide: TranslateService, useValue: {} },
|
||||||
|
{ provide: VersionDataService, useValue: versionServiceSpy },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
{ provide: ItemVersionsSharedService, useValue: {} },
|
||||||
|
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||||
|
{ provide: SearchService, useValue: {} },
|
||||||
|
{ provide: ItemDataService, useValue: {} },
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
versionService = TestBed.inject(VersionDataService);
|
||||||
|
versionHistoryService = TestBed.inject(VersionHistoryDataService);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(VersionedItemComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = mockItem;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when onCreateNewVersion() is called', () => {
|
||||||
|
it('should call versionService.findByHref', () => {
|
||||||
|
component.onCreateNewVersion();
|
||||||
|
expect(versionService.findByHref).toHaveBeenCalledWith('version-href');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,78 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ItemComponent } from '../shared/item.component';
|
||||||
|
import { ItemVersionsSummaryModalComponent } from '../../../../shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component';
|
||||||
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { Version } from '../../../../core/shared/version.model';
|
||||||
|
import { switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||||
|
import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||||
|
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-versioned-item',
|
||||||
|
templateUrl: './versioned-item.component.html',
|
||||||
|
styleUrls: ['./versioned-item.component.scss']
|
||||||
|
})
|
||||||
|
export class VersionedItemComponent extends ItemComponent {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private versionHistoryService: VersionHistoryDataService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private versionService: VersionDataService,
|
||||||
|
private itemVersionShared: ItemVersionsSharedService,
|
||||||
|
private router: Router,
|
||||||
|
private workspaceItemDataService: WorkspaceitemDataService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
private itemService: ItemDataService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a modal that allows to create a new version starting from the specified item, with optional summary
|
||||||
|
*/
|
||||||
|
onCreateNewVersion(): void {
|
||||||
|
|
||||||
|
const item = this.object;
|
||||||
|
const versionHref = item._links.version.href;
|
||||||
|
|
||||||
|
// Open modal
|
||||||
|
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
|
||||||
|
|
||||||
|
// Show current version in modal
|
||||||
|
this.versionService.findByHref(versionHref).pipe(getFirstCompletedRemoteData()).subscribe((res: RemoteData<Version>) => {
|
||||||
|
// if res.hasNoContent then the item is unversioned
|
||||||
|
activeModal.componentInstance.firstVersion = res.hasNoContent;
|
||||||
|
activeModal.componentInstance.versionNumber = (res.hasNoContent ? undefined : res.payload.version);
|
||||||
|
});
|
||||||
|
|
||||||
|
// On createVersionEvent emitted create new version and notify
|
||||||
|
activeModal.componentInstance.createVersionEvent.pipe(
|
||||||
|
switchMap((summary: string) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
// show success/failure notification
|
||||||
|
tap((res: RemoteData<Version>) => { this.itemVersionShared.notifyCreateNewVersion(res); }),
|
||||||
|
// get workspace item
|
||||||
|
getFirstSucceededRemoteDataPayload<Version>(),
|
||||||
|
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
|
||||||
|
getFirstSucceededRemoteDataPayload<Item>(),
|
||||||
|
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
|
||||||
|
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
|
||||||
|
).subscribe((wsItem) => {
|
||||||
|
const wsiId = wsItem.id;
|
||||||
|
const route = 'workspaceitems/' + wsiId + '/edit';
|
||||||
|
this.router.navigateByUrl(route);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { VersionPageComponent } from './version-page.component';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { createRelationshipsObservable } from '../../simple/item-types/shared/item.component.spec';
|
||||||
|
import { VersionDataService } from '../../../core/data/version-data.service';
|
||||||
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
|
import { Version } from '../../../core/shared/version.model';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
metadata: [],
|
||||||
|
relationships: createRelationshipsObservable(),
|
||||||
|
uuid: 'item-uuid',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockVersion: Version = Object.assign(new Version(), {
|
||||||
|
item: createSuccessfulRemoteDataObject$(mockItem),
|
||||||
|
version: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({ template: '' })
|
||||||
|
class DummyComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('VersionPageComponent', () => {
|
||||||
|
let component: VersionPageComponent;
|
||||||
|
let fixture: ComponentFixture<VersionPageComponent>;
|
||||||
|
let authService: AuthService;
|
||||||
|
|
||||||
|
const mockRoute = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
data: observableOf({dso: createSuccessfulRemoteDataObject(mockVersion)})
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
setRedirectUrl: {}
|
||||||
|
});
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [VersionPageComponent, DummyComponent],
|
||||||
|
imports: [RouterTestingModule.withRoutes([{ path: 'items/item-uuid', component: DummyComponent, pathMatch: 'full' }])],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: mockRoute },
|
||||||
|
{ provide: VersionDataService, useValue: {} },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(VersionPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,56 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, redirectOn4xx } from '../../../core/shared/operators';
|
||||||
|
import { VersionDataService } from '../../../core/data/version-data.service';
|
||||||
|
import { Version } from '../../../core/shared/version.model';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
|
import { getPageNotFoundRoute } from '../../../app-routing-paths';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-version-page',
|
||||||
|
templateUrl: './version-page.component.html',
|
||||||
|
styleUrls: ['./version-page.component.scss']
|
||||||
|
})
|
||||||
|
export class VersionPageComponent implements OnInit {
|
||||||
|
|
||||||
|
versionRD$: Observable<RemoteData<Version>>;
|
||||||
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private versionService: VersionDataService,
|
||||||
|
private authService: AuthService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
/* Retrieve version from resolver or redirect on 4xx */
|
||||||
|
this.versionRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.dso as RemoteData<Version>),
|
||||||
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Retrieve item from version and reroute to item's page or handle missing item */
|
||||||
|
this.versionRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((version) => version.item),
|
||||||
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
).subscribe((itemRD) => {
|
||||||
|
if (itemRD.hasNoContent) {
|
||||||
|
this.router.navigateByUrl(getPageNotFoundRoute(), { skipLocationChange: true });
|
||||||
|
} else {
|
||||||
|
const itemUrl = getItemPageRoute(itemRD.payload);
|
||||||
|
this.router.navigateByUrl(itemUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
54
src/app/item-page/version-page/version.resolver.ts
Normal file
54
src/app/item-page/version-page/version.resolver.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { ResolvedAction } from '../../core/resolving/resolver.actions';
|
||||||
|
import { Version } from '../../core/shared/version.model';
|
||||||
|
import { VersionDataService } from '../../core/data/version-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*/
|
||||||
|
export const VERSION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Version>[] = [
|
||||||
|
followLink('item'),
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific version before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class VersionResolver implements Resolve<RemoteData<Version>> {
|
||||||
|
constructor(
|
||||||
|
protected versionService: VersionDataService,
|
||||||
|
protected store: Store<any>,
|
||||||
|
protected router: Router
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a version based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Version>> {
|
||||||
|
const versionRD$ = this.versionService.findById(route.params.id,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
...VERSION_PAGE_LINKS_TO_FOLLOW
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
|
||||||
|
versionRD$.subscribe((versionRD: RemoteData<Version>) => {
|
||||||
|
this.store.dispatch(new ResolvedAction(state.url, versionRD.payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
return versionRD$;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
||||||
|
<h3 class="mb-4">{{'deny-request-copy.header' | translate}}</h3>
|
||||||
|
<div *ngIf="itemRequestRD && itemRequestRD.hasSucceeded">
|
||||||
|
<p>{{'deny-request-copy.intro' | translate}}</p>
|
||||||
|
|
||||||
|
<ds-email-request-copy [subject]="subject$ | async" [message]="message$ | async" (send)="deny($event)"></ds-email-request-copy>
|
||||||
|
</div>
|
||||||
|
<ds-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-loading>
|
||||||
|
</div>
|
@@ -0,0 +1,177 @@
|
|||||||
|
import { DenyRequestCopyComponent } from './deny-request-copy.component';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import {
|
||||||
|
createFailedRemoteDataObject$,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model';
|
||||||
|
|
||||||
|
describe('DenyRequestCopyComponent', () => {
|
||||||
|
let component: DenyRequestCopyComponent;
|
||||||
|
let fixture: ComponentFixture<DenyRequestCopyComponent>;
|
||||||
|
|
||||||
|
let router: Router;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
let authService: AuthService;
|
||||||
|
let translateService: TranslateService;
|
||||||
|
let itemDataService: ItemDataService;
|
||||||
|
let nameService: DSONameService;
|
||||||
|
let itemRequestService: ItemRequestDataService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
|
||||||
|
let itemRequest: ItemRequest;
|
||||||
|
let user: EPerson;
|
||||||
|
let item: Item;
|
||||||
|
let itemName: string;
|
||||||
|
let itemUrl: string;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
itemRequest = Object.assign(new ItemRequest(), {
|
||||||
|
token: 'item-request-token',
|
||||||
|
requestName: 'requester name'
|
||||||
|
});
|
||||||
|
user = Object.assign(new EPerson(), {
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
value: 'first'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
value: 'last'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
email: 'user-email',
|
||||||
|
});
|
||||||
|
itemName = 'item-name';
|
||||||
|
itemUrl = 'item-url';
|
||||||
|
item = Object.assign(new Item(), {
|
||||||
|
id: 'item-id',
|
||||||
|
metadata: {
|
||||||
|
'dc.identifier.uri': [
|
||||||
|
{
|
||||||
|
value: itemUrl
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
value: itemName
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
||||||
|
});
|
||||||
|
route = jasmine.createSpyObj('route', {}, {
|
||||||
|
data: observableOf({
|
||||||
|
request: createSuccessfulRemoteDataObject(itemRequest),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
getAuthenticatedUserFromStore: observableOf(user),
|
||||||
|
});
|
||||||
|
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||||
|
findById: createSuccessfulRemoteDataObject$(item),
|
||||||
|
});
|
||||||
|
nameService = jasmine.createSpyObj('nameService', {
|
||||||
|
getName: itemName,
|
||||||
|
});
|
||||||
|
itemRequestService = jasmine.createSpyObj('itemRequestService', {
|
||||||
|
deny: createSuccessfulRemoteDataObject$(itemRequest),
|
||||||
|
});
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', ['success', 'error']);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [DenyRequestCopyComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: ItemDataService, useValue: itemDataService },
|
||||||
|
{ provide: DSONameService, useValue: nameService },
|
||||||
|
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DenyRequestCopyComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
translateService = (component as any).translateService;
|
||||||
|
spyOn(translateService, 'get').and.returnValue(observableOf('translated-message'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('message$ should be parameterized correctly', (done) => {
|
||||||
|
component.message$.subscribe(() => {
|
||||||
|
expect(translateService.get).toHaveBeenCalledWith(jasmine.anything(), Object.assign({
|
||||||
|
recipientName: itemRequest.requestName,
|
||||||
|
itemUrl: itemUrl,
|
||||||
|
itemName: itemName,
|
||||||
|
authorName: user.name,
|
||||||
|
authorEmail: user.email,
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deny', () => {
|
||||||
|
let email: RequestCopyEmail;
|
||||||
|
|
||||||
|
describe('when the request is successful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
email = new RequestCopyEmail('subject', 'message');
|
||||||
|
(itemRequestService.deny as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(itemRequest));
|
||||||
|
component.deny(email);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a success notification', () => {
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to the homepage', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the request is unsuccessful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
email = new RequestCopyEmail('subject', 'message');
|
||||||
|
(itemRequestService.deny as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
component.deny(email);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a success notification', () => {
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not navigate', () => {
|
||||||
|
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,112 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import {
|
||||||
|
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload,
|
||||||
|
redirectOn4xx
|
||||||
|
} from '../../core/shared/operators';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||||
|
import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-deny-request-copy',
|
||||||
|
styleUrls: ['./deny-request-copy.component.scss'],
|
||||||
|
templateUrl: './deny-request-copy.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for denying an item request
|
||||||
|
*/
|
||||||
|
export class DenyRequestCopyComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The item request to deny
|
||||||
|
*/
|
||||||
|
itemRequestRD$: Observable<RemoteData<ItemRequest>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default subject of the message to send to the user requesting the item
|
||||||
|
*/
|
||||||
|
subject$: Observable<string>;
|
||||||
|
/**
|
||||||
|
* The default contents of the message to send to the user requesting the item
|
||||||
|
*/
|
||||||
|
message$: Observable<string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private authService: AuthService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private itemDataService: ItemDataService,
|
||||||
|
private nameService: DSONameService,
|
||||||
|
private itemRequestService: ItemRequestDataService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.itemRequestRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.request as RemoteData<ItemRequest>),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
);
|
||||||
|
|
||||||
|
const msgParams$ = observableCombineLatest(
|
||||||
|
this.itemRequestRD$.pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
this.authService.getAuthenticatedUserFromStore(),
|
||||||
|
).pipe(
|
||||||
|
switchMap(([itemRequest, user]: [ItemRequest, EPerson]) => {
|
||||||
|
return this.itemDataService.findById(itemRequest.itemId).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((item: Item) => {
|
||||||
|
const uri = item.firstMetadataValue('dc.identifier.uri');
|
||||||
|
return Object.assign({
|
||||||
|
recipientName: itemRequest.requestName,
|
||||||
|
itemUrl: isNotEmpty(uri) ? uri : item.handle,
|
||||||
|
itemName: this.nameService.getName(item),
|
||||||
|
authorName: user.name,
|
||||||
|
authorEmail: user.email,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subject$ = this.translateService.get('deny-request-copy.email.subject');
|
||||||
|
this.message$ = msgParams$.pipe(
|
||||||
|
switchMap((params) => this.translateService.get('deny-request-copy.email.message', params)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deny the item request
|
||||||
|
* @param email Subject and contents of the message to send back to the user requesting the item
|
||||||
|
*/
|
||||||
|
deny(email: RequestCopyEmail) {
|
||||||
|
this.itemRequestRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((itemRequest: ItemRequest) => this.itemRequestService.deny(itemRequest.token, email)),
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((rd) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
this.notificationsService.success(this.translateService.get('deny-request-copy.success'));
|
||||||
|
this.router.navigateByUrl('/');
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('deny-request-copy.error'), rd.errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subject">{{ 'grant-deny-request-copy.email.subject' | translate }}</label>
|
||||||
|
<input type="text" class="form-control" id="subject" [ngClass]="{'is-invalid': !subject || subject.length === 0}" [(ngModel)]="subject" name="subject">
|
||||||
|
<div *ngIf="!subject || subject.length === 0" class="invalid-feedback">
|
||||||
|
{{ 'grant-deny-request-copy.email.subject.empty' | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="message">{{ 'grant-deny-request-copy.email.message' | translate }}</label>
|
||||||
|
<textarea class="form-control" id="message" rows="8" [ngClass]="{'is-invalid': !message || message.length === 0}" [(ngModel)]="message" name="message"></textarea>
|
||||||
|
<div *ngIf="!message || message.length === 0" class="invalid-feedback">
|
||||||
|
{{ 'grant-deny-request-copy.email.message.empty' | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
<div class="d-flex flex-row-reverse">
|
||||||
|
<button (click)="submit()"
|
||||||
|
[disabled]="!message || message.length === 0 || !subject || subject.length === 0"
|
||||||
|
class="btn btn-primary"
|
||||||
|
title="{{'grant-deny-request-copy.email.send' | translate }}">
|
||||||
|
<i class="fas fa-envelope"></i> {{'grant-deny-request-copy.email.send' | translate }}
|
||||||
|
</button>
|
||||||
|
<button (click)="return()"
|
||||||
|
class="btn btn-outline-secondary mr-1"
|
||||||
|
title="{{'grant-deny-request-copy.email.back' | translate }}">
|
||||||
|
<i class="fas fa-arrow-left"></i> {{'grant-deny-request-copy.email.back' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@@ -0,0 +1,47 @@
|
|||||||
|
import { EmailRequestCopyComponent } from './email-request-copy.component';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { RequestCopyEmail } from './request-copy-email.model';
|
||||||
|
|
||||||
|
describe('EmailRequestCopyComponent', () => {
|
||||||
|
let component: EmailRequestCopyComponent;
|
||||||
|
let fixture: ComponentFixture<EmailRequestCopyComponent>;
|
||||||
|
|
||||||
|
let location: Location;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
location = jasmine.createSpyObj('location', ['back']);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [EmailRequestCopyComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: Location, useValue: location },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EmailRequestCopyComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return should navigate to the previous page', () => {
|
||||||
|
component.return();
|
||||||
|
expect(location.back).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submit should emit an email object', () => {
|
||||||
|
spyOn(component.send, 'emit').and.stub();
|
||||||
|
component.subject = 'test-subject';
|
||||||
|
component.message = 'test-message';
|
||||||
|
component.submit();
|
||||||
|
expect(component.send.emit).toHaveBeenCalledWith(new RequestCopyEmail('test-subject', 'test-message'));
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,45 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { RequestCopyEmail } from './request-copy-email.model';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-email-request-copy',
|
||||||
|
styleUrls: ['./email-request-copy.component.scss'],
|
||||||
|
templateUrl: './email-request-copy.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A form component for an email to send back to the user requesting an item
|
||||||
|
*/
|
||||||
|
export class EmailRequestCopyComponent {
|
||||||
|
/**
|
||||||
|
* Event emitter for sending the email
|
||||||
|
*/
|
||||||
|
@Output() send: EventEmitter<RequestCopyEmail> = new EventEmitter<RequestCopyEmail>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The subject of the email
|
||||||
|
*/
|
||||||
|
@Input() subject: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contents of the email
|
||||||
|
*/
|
||||||
|
@Input() message: string;
|
||||||
|
|
||||||
|
constructor(protected location: Location) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the email
|
||||||
|
*/
|
||||||
|
submit() {
|
||||||
|
this.send.emit(new RequestCopyEmail(this.subject, this.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return to the previous page
|
||||||
|
*/
|
||||||
|
return() {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* A class representing an email to send back to the user requesting an item
|
||||||
|
*/
|
||||||
|
export class RequestCopyEmail {
|
||||||
|
constructor(public subject: string,
|
||||||
|
public message: string) {
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
||||||
|
<h3 class="mb-4">{{'grant-deny-request-copy.header' | translate}}</h3>
|
||||||
|
<div *ngIf="itemRequestRD && itemRequestRD.hasSucceeded">
|
||||||
|
<div *ngIf="!itemRequestRD.payload.decisionDate">
|
||||||
|
<p [innerHTML]="'grant-deny-request-copy.intro1' | translate:{ url: (itemUrl$ | async), name: (itemName$ | async) }"></p>
|
||||||
|
<p>{{'grant-deny-request-copy.intro2' | translate}}</p>
|
||||||
|
|
||||||
|
<div class="btn-group ">
|
||||||
|
<a [routerLink]="grantRoute$ | async"
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
title="{{'grant-deny-request-copy.grant' | translate }}">
|
||||||
|
{{'grant-deny-request-copy.grant' | translate }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a [routerLink]="denyRoute$ | async"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
title="{{'grant-deny-request-copy.deny' | translate }}">
|
||||||
|
{{'grant-deny-request-copy.deny' | translate }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="itemRequestRD.payload.decisionDate" class="processed-message">
|
||||||
|
<p>{{'grant-deny-request-copy.processed' | translate}}</p>
|
||||||
|
<p class="text-center">
|
||||||
|
<a routerLink="/home" class="btn btn-primary">{{'grant-deny-request-copy.home-page' | translate}}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-loading>
|
||||||
|
</div>
|
@@ -0,0 +1,141 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import {
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy.component';
|
||||||
|
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
|
||||||
|
import { getRequestCopyDenyRoute, getRequestCopyGrantRoute } from '../request-copy-routing-paths';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('GrantDenyRequestCopyComponent', () => {
|
||||||
|
let component: GrantDenyRequestCopyComponent;
|
||||||
|
let fixture: ComponentFixture<GrantDenyRequestCopyComponent>;
|
||||||
|
|
||||||
|
let router: Router;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
let authService: AuthService;
|
||||||
|
let itemDataService: ItemDataService;
|
||||||
|
let nameService: DSONameService;
|
||||||
|
|
||||||
|
let itemRequest: ItemRequest;
|
||||||
|
let item: Item;
|
||||||
|
let itemName: string;
|
||||||
|
let itemUrl: string;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
itemRequest = Object.assign(new ItemRequest(), {
|
||||||
|
token: 'item-request-token',
|
||||||
|
requestName: 'requester name'
|
||||||
|
});
|
||||||
|
itemName = 'item-name';
|
||||||
|
item = Object.assign(new Item(), {
|
||||||
|
id: 'item-id',
|
||||||
|
metadata: {
|
||||||
|
'dc.identifier.uri': [
|
||||||
|
{
|
||||||
|
value: itemUrl
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
value: itemName
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
itemUrl = getItemPageRoute(item);
|
||||||
|
|
||||||
|
route = jasmine.createSpyObj('route', {}, {
|
||||||
|
data: observableOf({
|
||||||
|
request: createSuccessfulRemoteDataObject(itemRequest),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
});
|
||||||
|
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||||
|
findById: createSuccessfulRemoteDataObject$(item),
|
||||||
|
});
|
||||||
|
nameService = jasmine.createSpyObj('nameService', {
|
||||||
|
getName: itemName,
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [GrantDenyRequestCopyComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: ItemDataService, useValue: itemDataService },
|
||||||
|
{ provide: DSONameService, useValue: nameService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(GrantDenyRequestCopyComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
router = (component as any).router;
|
||||||
|
spyOn(router, 'navigateByUrl').and.stub();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialise itemName$', (done) => {
|
||||||
|
component.itemName$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(itemName);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialise itemUrl$', (done) => {
|
||||||
|
component.itemUrl$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(itemUrl);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialise denyRoute$', (done) => {
|
||||||
|
component.denyRoute$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(getRequestCopyDenyRoute(itemRequest.token));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialise grantRoute$', (done) => {
|
||||||
|
component.grantRoute$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(getRequestCopyGrantRoute(itemRequest.token));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('processed message', () => {
|
||||||
|
it('should not be displayed when decisionDate is undefined', () => {
|
||||||
|
const message = fixture.debugElement.query(By.css('.processed-message'));
|
||||||
|
expect(message).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be displayed when decisionDate is defined', () => {
|
||||||
|
component.itemRequestRD$ = createSuccessfulRemoteDataObject$(Object.assign(new ItemRequest(), itemRequest, {
|
||||||
|
decisionDate: 'defined-date'
|
||||||
|
}));
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const message = fixture.debugElement.query(By.css('.processed-message'));
|
||||||
|
expect(message).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,97 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import {
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
redirectOn4xx
|
||||||
|
} from '../../core/shared/operators';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { getRequestCopyDenyRoute, getRequestCopyGrantRoute } from '../request-copy-routing-paths';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-grant-deny-request-copy',
|
||||||
|
styleUrls: ['./grant-deny-request-copy.component.scss'],
|
||||||
|
templateUrl: './grant-deny-request-copy.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for an author to decide to grant or deny an item request
|
||||||
|
*/
|
||||||
|
export class GrantDenyRequestCopyComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The item request to grant or deny
|
||||||
|
*/
|
||||||
|
itemRequestRD$: Observable<RemoteData<ItemRequest>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item the request is requesting access to
|
||||||
|
*/
|
||||||
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the item
|
||||||
|
*/
|
||||||
|
itemName$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The url of the item
|
||||||
|
*/
|
||||||
|
itemUrl$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The route to the page for denying access to the item
|
||||||
|
*/
|
||||||
|
denyRoute$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The route to the page for granting access to the item
|
||||||
|
*/
|
||||||
|
grantRoute$: Observable<string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private authService: AuthService,
|
||||||
|
private itemDataService: ItemDataService,
|
||||||
|
private nameService: DSONameService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.itemRequestRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.request as RemoteData<ItemRequest>),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
);
|
||||||
|
this.itemRD$ = this.itemRequestRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((itemRequest: ItemRequest) => this.itemDataService.findById(itemRequest.itemId)),
|
||||||
|
);
|
||||||
|
this.itemName$ = this.itemRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((item) => this.nameService.getName(item)),
|
||||||
|
);
|
||||||
|
this.itemUrl$ = this.itemRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((item) => getItemPageRoute(item)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.denyRoute$ = this.itemRequestRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((itemRequest: ItemRequest) => getRequestCopyDenyRoute(itemRequest.token))
|
||||||
|
);
|
||||||
|
this.grantRoute$ = this.itemRequestRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((itemRequest: ItemRequest) => getRequestCopyGrantRoute(itemRequest.token))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
<div class="container" *ngVar="(itemRequestRD$ | async) as itemRequestRD">
|
||||||
|
<h3 class="mb-4">{{'grant-request-copy.header' | translate}}</h3>
|
||||||
|
<div *ngIf="itemRequestRD && itemRequestRD.hasSucceeded">
|
||||||
|
<p>{{'grant-request-copy.intro' | translate}}</p>
|
||||||
|
|
||||||
|
<ds-email-request-copy [subject]="subject$ | async" [message]="message$ | async" (send)="grant($event)">
|
||||||
|
<p>{{ 'grant-deny-request-copy.email.permissions.info' | translate }}</p>
|
||||||
|
<form class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" value="" id="permissions" [(ngModel)]="suggestOpenAccess" name="permissions">
|
||||||
|
<label class="form-check-label" for="permissions">{{ 'grant-deny-request-copy.email.permissions.label' | translate }}</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ds-email-request-copy>
|
||||||
|
</div>
|
||||||
|
<ds-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-loading>
|
||||||
|
</div>
|
@@ -0,0 +1,177 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import {
|
||||||
|
createFailedRemoteDataObject$,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model';
|
||||||
|
import { GrantRequestCopyComponent } from './grant-request-copy.component';
|
||||||
|
|
||||||
|
describe('GrantRequestCopyComponent', () => {
|
||||||
|
let component: GrantRequestCopyComponent;
|
||||||
|
let fixture: ComponentFixture<GrantRequestCopyComponent>;
|
||||||
|
|
||||||
|
let router: Router;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
let authService: AuthService;
|
||||||
|
let translateService: TranslateService;
|
||||||
|
let itemDataService: ItemDataService;
|
||||||
|
let nameService: DSONameService;
|
||||||
|
let itemRequestService: ItemRequestDataService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
|
||||||
|
let itemRequest: ItemRequest;
|
||||||
|
let user: EPerson;
|
||||||
|
let item: Item;
|
||||||
|
let itemName: string;
|
||||||
|
let itemUrl: string;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
itemRequest = Object.assign(new ItemRequest(), {
|
||||||
|
token: 'item-request-token',
|
||||||
|
requestName: 'requester name'
|
||||||
|
});
|
||||||
|
user = Object.assign(new EPerson(), {
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
value: 'first'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
value: 'last'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
email: 'user-email',
|
||||||
|
});
|
||||||
|
itemName = 'item-name';
|
||||||
|
itemUrl = 'item-url';
|
||||||
|
item = Object.assign(new Item(), {
|
||||||
|
id: 'item-id',
|
||||||
|
metadata: {
|
||||||
|
'dc.identifier.uri': [
|
||||||
|
{
|
||||||
|
value: itemUrl
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
value: itemName
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
||||||
|
});
|
||||||
|
route = jasmine.createSpyObj('route', {}, {
|
||||||
|
data: observableOf({
|
||||||
|
request: createSuccessfulRemoteDataObject(itemRequest),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
getAuthenticatedUserFromStore: observableOf(user),
|
||||||
|
});
|
||||||
|
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||||
|
findById: createSuccessfulRemoteDataObject$(item),
|
||||||
|
});
|
||||||
|
nameService = jasmine.createSpyObj('nameService', {
|
||||||
|
getName: itemName,
|
||||||
|
});
|
||||||
|
itemRequestService = jasmine.createSpyObj('itemRequestService', {
|
||||||
|
grant: createSuccessfulRemoteDataObject$(itemRequest),
|
||||||
|
});
|
||||||
|
notificationsService = jasmine.createSpyObj('notificationsService', ['success', 'error']);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [GrantRequestCopyComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: ItemDataService, useValue: itemDataService },
|
||||||
|
{ provide: DSONameService, useValue: nameService },
|
||||||
|
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(GrantRequestCopyComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
translateService = (component as any).translateService;
|
||||||
|
spyOn(translateService, 'get').and.returnValue(observableOf('translated-message'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('message$ should be parameterized correctly', (done) => {
|
||||||
|
component.message$.subscribe(() => {
|
||||||
|
expect(translateService.get).toHaveBeenCalledWith(jasmine.anything(), Object.assign({
|
||||||
|
recipientName: itemRequest.requestName,
|
||||||
|
itemUrl: itemUrl,
|
||||||
|
itemName: itemName,
|
||||||
|
authorName: user.name,
|
||||||
|
authorEmail: user.email,
|
||||||
|
}));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('grant', () => {
|
||||||
|
let email: RequestCopyEmail;
|
||||||
|
|
||||||
|
describe('when the request is successful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
email = new RequestCopyEmail('subject', 'message');
|
||||||
|
(itemRequestService.grant as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(itemRequest));
|
||||||
|
component.grant(email);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a success notification', () => {
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to the homepage', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the request is unsuccessful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
email = new RequestCopyEmail('subject', 'message');
|
||||||
|
(itemRequestService.grant as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
component.grant(email);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a success notification', () => {
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not navigate', () => {
|
||||||
|
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,118 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import {
|
||||||
|
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload,
|
||||||
|
redirectOn4xx
|
||||||
|
} from '../../core/shared/operators';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||||
|
import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-grant-request-copy',
|
||||||
|
styleUrls: ['./grant-request-copy.component.scss'],
|
||||||
|
templateUrl: './grant-request-copy.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for granting an item request
|
||||||
|
*/
|
||||||
|
export class GrantRequestCopyComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The item request to accept
|
||||||
|
*/
|
||||||
|
itemRequestRD$: Observable<RemoteData<ItemRequest>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default subject of the message to send to the user requesting the item
|
||||||
|
*/
|
||||||
|
subject$: Observable<string>;
|
||||||
|
/**
|
||||||
|
* The default contents of the message to send to the user requesting the item
|
||||||
|
*/
|
||||||
|
message$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the item should be open access, to avoid future requests
|
||||||
|
* Defaults to false
|
||||||
|
*/
|
||||||
|
suggestOpenAccess = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private authService: AuthService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private itemDataService: ItemDataService,
|
||||||
|
private nameService: DSONameService,
|
||||||
|
private itemRequestService: ItemRequestDataService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.itemRequestRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.request as RemoteData<ItemRequest>),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
);
|
||||||
|
|
||||||
|
const msgParams$ = observableCombineLatest(
|
||||||
|
this.itemRequestRD$.pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
this.authService.getAuthenticatedUserFromStore(),
|
||||||
|
).pipe(
|
||||||
|
switchMap(([itemRequest, user]: [ItemRequest, EPerson]) => {
|
||||||
|
return this.itemDataService.findById(itemRequest.itemId).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((item: Item) => {
|
||||||
|
const uri = item.firstMetadataValue('dc.identifier.uri');
|
||||||
|
return Object.assign({
|
||||||
|
recipientName: itemRequest.requestName,
|
||||||
|
itemUrl: isNotEmpty(uri) ? uri : item.handle,
|
||||||
|
itemName: this.nameService.getName(item),
|
||||||
|
authorName: user.name,
|
||||||
|
authorEmail: user.email,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subject$ = this.translateService.get('grant-request-copy.email.subject');
|
||||||
|
this.message$ = msgParams$.pipe(
|
||||||
|
switchMap((params) => this.translateService.get('grant-request-copy.email.message', params)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grant the item request
|
||||||
|
* @param email Subject and contents of the message to send back to the user requesting the item
|
||||||
|
*/
|
||||||
|
grant(email: RequestCopyEmail) {
|
||||||
|
this.itemRequestRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((itemRequest: ItemRequest) => this.itemRequestService.grant(itemRequest.token, email, this.suggestOpenAccess)),
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((rd) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
this.notificationsService.success(this.translateService.get('grant-request-copy.success'));
|
||||||
|
this.router.navigateByUrl('/');
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('grant-request-copy.error'), rd.errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
src/app/request-copy/request-copy-routing-paths.ts
Normal file
18
src/app/request-copy/request-copy-routing-paths.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
|
import { getRequestCopyModulePath } from '../app-routing-paths';
|
||||||
|
|
||||||
|
export function getRequestCopyRoute(token: string) {
|
||||||
|
return new URLCombiner(getRequestCopyModulePath(), token).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REQUEST_COPY_DENY_PATH = 'deny';
|
||||||
|
|
||||||
|
export function getRequestCopyDenyRoute(token: string) {
|
||||||
|
return new URLCombiner(getRequestCopyRoute(token), REQUEST_COPY_DENY_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REQUEST_COPY_GRANT_PATH = 'grant';
|
||||||
|
|
||||||
|
export function getRequestCopyGrantRoute(token: string) {
|
||||||
|
return new URLCombiner(getRequestCopyRoute(token), REQUEST_COPY_GRANT_PATH).toString();
|
||||||
|
}
|
40
src/app/request-copy/request-copy-routing.module.ts
Normal file
40
src/app/request-copy/request-copy-routing.module.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { RequestCopyResolver } from './request-copy.resolver';
|
||||||
|
import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-deny-request-copy.component';
|
||||||
|
import { REQUEST_COPY_DENY_PATH, REQUEST_COPY_GRANT_PATH } from './request-copy-routing-paths';
|
||||||
|
import { DenyRequestCopyComponent } from './deny-request-copy/deny-request-copy.component';
|
||||||
|
import { GrantRequestCopyComponent } from './grant-request-copy/grant-request-copy.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: ':token',
|
||||||
|
resolve: {
|
||||||
|
request: RequestCopyResolver
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: GrantDenyRequestCopyComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: REQUEST_COPY_DENY_PATH,
|
||||||
|
component: DenyRequestCopyComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: REQUEST_COPY_GRANT_PATH,
|
||||||
|
component: GrantRequestCopyComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
RequestCopyResolver,
|
||||||
|
GrantDenyRequestCopyComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class RequestCopyRoutingModule {
|
||||||
|
}
|
30
src/app/request-copy/request-copy.module.ts
Normal file
30
src/app/request-copy/request-copy.module.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-deny-request-copy.component';
|
||||||
|
import { RequestCopyRoutingModule } from './request-copy-routing.module';
|
||||||
|
import { DenyRequestCopyComponent } from './deny-request-copy/deny-request-copy.component';
|
||||||
|
import { EmailRequestCopyComponent } from './email-request-copy/email-request-copy.component';
|
||||||
|
import { GrantRequestCopyComponent } from './grant-request-copy/grant-request-copy.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
RequestCopyRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
GrantDenyRequestCopyComponent,
|
||||||
|
DenyRequestCopyComponent,
|
||||||
|
EmailRequestCopyComponent,
|
||||||
|
GrantRequestCopyComponent,
|
||||||
|
],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module related to components used to grant or deny an item request
|
||||||
|
*/
|
||||||
|
export class RequestCopyModule {
|
||||||
|
|
||||||
|
}
|
26
src/app/request-copy/request-copy.resolver.ts
Normal file
26
src/app/request-copy/request-copy.resolver.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { ItemRequest } from '../core/shared/item-request.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { ItemRequestDataService } from '../core/data/item-request-data.service';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves an {@link ItemRequest} from the token found in the route's parameters
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class RequestCopyResolver implements Resolve<RemoteData<ItemRequest>> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private itemRequestDataService: ItemRequestDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<ItemRequest>> | Promise<RemoteData<ItemRequest>> | RemoteData<ItemRequest> {
|
||||||
|
return this.itemRequestDataService.findById(route.params.token).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -55,10 +55,4 @@ describe('ConfigurationSearchPageComponent', () => {
|
|||||||
expect(routeService.setParameter).toHaveBeenCalledWith('fixedFilterQuery', QUERY);
|
expect(routeService.setParameter).toHaveBeenCalledWith('fixedFilterQuery', QUERY);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reset route parameters on destroy', () => {
|
|
||||||
fixture.destroy();
|
|
||||||
|
|
||||||
expect(routeService.setParameter).toHaveBeenCalledWith('configuration', undefined);
|
|
||||||
expect(routeService.setParameter).toHaveBeenCalledWith('fixedFilterQuery', undefined);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -6,7 +6,6 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
@@ -34,7 +33,7 @@ import { Router } from '@angular/router';
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit, OnDestroy {
|
export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* The configuration to use for the search options
|
* The configuration to use for the search options
|
||||||
* If empty, the configuration will be determined by the route parameter called 'configuration'
|
* If empty, the configuration will be determined by the route parameter called 'configuration'
|
||||||
@@ -72,17 +71,4 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements
|
|||||||
this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery);
|
this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the updated query/configuration set in ngOnInit()
|
|
||||||
*/
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
super.ngOnDestroy();
|
|
||||||
if (hasValue(this.configuration)) {
|
|
||||||
this.routeService.setParameter('configuration', undefined);
|
|
||||||
}
|
|
||||||
if (hasValue(this.fixedFilterQuery)) {
|
|
||||||
this.routeService.setParameter('fixedFilterQuery', undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -52,7 +52,7 @@
|
|||||||
[searchPlaceholder]="'search.search-form.placeholder' | translate">
|
[searchPlaceholder]="'search.search-form.placeholder' | translate">
|
||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
<div class="row mb-3 mb-md-1">
|
<div class="row mb-3 mb-md-1">
|
||||||
<div class="labels col-sm-9 offset-sm-3">
|
<div class="labels col-sm-9">
|
||||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,87 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h3 class="mb-4">{{'bitstream-request-a-copy.header' | translate}}</h3>
|
||||||
|
<div *ngIf="canDownload$|async" class="alert alert-success">
|
||||||
|
<span>{{'bitstream-request-a-copy.alert.canDownload1' | translate}}</span>
|
||||||
|
<a [routerLink]="getBitstreamLink()">{{'bitstream-request-a-copy.alert.canDownload2'| translate}}</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>{{'bitstream-request-a-copy.intro' | translate}} <a [routerLink]="getItemPath()">{{itemName}}</a></p>
|
||||||
|
<p *ngIf="bitstream != undefined && allfiles.value === 'false'">{{'bitstream-request-a-copy.intro.bitstream.one' | translate}} {{bitstreamName}}</p>
|
||||||
|
<p *ngIf="allfiles.value === 'true'">{{'bitstream-request-a-copy.intro.bitstream.all' | translate}}</p>
|
||||||
|
</div>
|
||||||
|
<form [class]="'ng-invalid'" [formGroup]="requestCopyForm" (ngSubmit)="onSubmit()">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="name">{{'bitstream-request-a-copy.name.label' | translate}}</label>
|
||||||
|
<input [className]="(name.invalid) && (name.dirty || name.touched) ? 'form-control is-invalid' :'form-control'"
|
||||||
|
type="text" id="name" formControlName="name"/>
|
||||||
|
<div *ngIf="name.invalid && (name.dirty || name.touched)"
|
||||||
|
class="invalid-feedback show-feedback">
|
||||||
|
<span *ngIf="name.errors && name.errors.required">
|
||||||
|
{{ 'bitstream-request-a-copy.name.error' | translate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<label
|
||||||
|
for="email">{{'bitstream-request-a-copy.email.label' | translate}}</label>
|
||||||
|
<input
|
||||||
|
[className]="(email.invalid) && (email.dirty || email.touched) ? 'form-control is-invalid' :'form-control'"
|
||||||
|
id="email" formControlName="email">
|
||||||
|
<div *ngIf="email.invalid && (email.dirty || email.touched)"
|
||||||
|
class="invalid-feedback show-feedback">
|
||||||
|
<span *ngIf="email.errors">
|
||||||
|
{{ 'bitstream-request-a-copy.email.error' | translate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted ds-hint">{{'bitstream-request-a-copy.email.hint' |translate}}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div>{{'bitstream-request-a-copy.allfiles.label' |translate}}</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<input [className]="'form-check-input'" type="radio"
|
||||||
|
id="allfiles-true" formControlName="allfiles" value="true">
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="allfiles-true">{{'bitstream-request-a-copy.files-all-true.label' | translate}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<input [className]="'form-check-input'" type="radio"
|
||||||
|
id="allfiles-false" formControlName="allfiles" value="false" [attr.disabled]="bitstream === undefined ? true : null ">
|
||||||
|
<label class="form-check-label"
|
||||||
|
for="allfiles-false">{{'bitstream-request-a-copy.files-all-false.label' | translate}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<label
|
||||||
|
for="message">{{'bitstream-request-a-copy.message.label' | translate}}</label>
|
||||||
|
<textarea rows="5"
|
||||||
|
[className]="'form-control'"
|
||||||
|
id="message" formControlName="message"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-right">
|
||||||
|
|
||||||
|
<a (click)="navigateBack()" role="button" class="btn btn-outline-secondary mr-1">
|
||||||
|
<i class="fas fa-arrow-left"></i> {{'bitstream-request-a-copy.return' | translate}}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button
|
||||||
|
[disabled]="requestCopyForm.invalid"
|
||||||
|
class="btn btn-default btn-primary"
|
||||||
|
(click)="onSubmit()">{{'bitstream-request-a-copy.submit' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,289 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import {
|
||||||
|
createFailedRemoteDataObject$,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../remote-data.utils';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page.component';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterStub } from '../testing/router.stub';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { NotificationsServiceStub } from '../testing/notifications-service.stub';
|
||||||
|
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||||
|
import { NotificationsService } from '../notifications/notifications.service';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { DSONameServiceMock } from '../mocks/dso-name.service.mock';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||||
|
|
||||||
|
|
||||||
|
describe('BitstreamRequestACopyPageComponent', () => {
|
||||||
|
let component: BitstreamRequestACopyPageComponent;
|
||||||
|
let fixture: ComponentFixture<BitstreamRequestACopyPageComponent>;
|
||||||
|
|
||||||
|
let authService: AuthService;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let activatedRoute;
|
||||||
|
let router;
|
||||||
|
let itemRequestDataService;
|
||||||
|
let notificationsService;
|
||||||
|
let location;
|
||||||
|
let bitstreamDataService;
|
||||||
|
|
||||||
|
let item: Item;
|
||||||
|
let bitstream: Bitstream;
|
||||||
|
let eperson;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
eperson = Object.assign(new EPerson(), {
|
||||||
|
email: 'test@mail.org',
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [{value: 'Test'}],
|
||||||
|
'eperson.lastname': [{value: 'User'}],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(false),
|
||||||
|
getAuthenticatedUserFromStore: observableOf(eperson)
|
||||||
|
});
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationSerivice', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
itemRequestDataService = jasmine.createSpyObj('itemRequestDataService', {
|
||||||
|
requestACopy: createSuccessfulRemoteDataObject$({})
|
||||||
|
});
|
||||||
|
|
||||||
|
location = jasmine.createSpyObj('location', {
|
||||||
|
back: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
item = Object.assign(new Item(), {uuid: 'item-uuid'});
|
||||||
|
|
||||||
|
bitstream = Object.assign(new Bitstream(), {
|
||||||
|
uuid: 'bitstreamUuid',
|
||||||
|
_links: {
|
||||||
|
content: {href: 'bitstream-content-link'},
|
||||||
|
self: {href: 'bitstream-self-link'},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
activatedRoute = {
|
||||||
|
data: observableOf({
|
||||||
|
dso: createSuccessfulRemoteDataObject(
|
||||||
|
item
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
queryParams: observableOf({
|
||||||
|
bitstream : bitstream.uuid
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||||
|
findById: createSuccessfulRemoteDataObject$(bitstream)
|
||||||
|
});
|
||||||
|
|
||||||
|
router = new RouterStub();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTestbed() {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, TranslateModule.forRoot(), FormsModule, ReactiveFormsModule],
|
||||||
|
declarations: [BitstreamRequestACopyPageComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: Location, useValue: location},
|
||||||
|
{provide: ActivatedRoute, useValue: activatedRoute},
|
||||||
|
{provide: Router, useValue: router},
|
||||||
|
{provide: AuthorizationDataService, useValue: authorizationService},
|
||||||
|
{provide: AuthService, useValue: authService},
|
||||||
|
{provide: ItemRequestDataService, useValue: itemRequestDataService},
|
||||||
|
{provide: NotificationsService, useValue: notificationsService},
|
||||||
|
{provide: DSONameService, useValue: new DSONameServiceMock()},
|
||||||
|
{provide: BitstreamDataService, useValue: bitstreamDataService},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('init', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should init the comp', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should show a form to request a copy', () => {
|
||||||
|
describe('when the user is not logged in', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('show the form with no values filled in based on the user', () => {
|
||||||
|
expect(component.name.value).toEqual('');
|
||||||
|
expect(component.email.value).toEqual('');
|
||||||
|
expect(component.allfiles.value).toEqual('false');
|
||||||
|
expect(component.message.value).toEqual('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is logged in', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('show the form with values filled in based on the user', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component.name.value).toEqual(eperson.name);
|
||||||
|
expect(component.email.value).toEqual(eperson.email);
|
||||||
|
expect(component.allfiles.value).toEqual('false');
|
||||||
|
expect(component.message.value).toEqual('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when no bitstream was provided', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
activatedRoute = {
|
||||||
|
data: observableOf({
|
||||||
|
dso: createSuccessfulRemoteDataObject(
|
||||||
|
item
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
queryParams: observableOf({
|
||||||
|
})
|
||||||
|
};
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should set the all files value to true and disable the false value', () => {
|
||||||
|
expect(component.name.value).toEqual('');
|
||||||
|
expect(component.email.value).toEqual('');
|
||||||
|
expect(component.allfiles.value).toEqual('true');
|
||||||
|
expect(component.message.value).toEqual('');
|
||||||
|
|
||||||
|
const allFilesFalse = fixture.debugElement.query(By.css('#allfiles-false')).nativeElement;
|
||||||
|
expect(allFilesFalse.getAttribute('disabled')).toBeTruthy();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when the user has authorization to download the file', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should show an alert indicating the user can download the file', () => {
|
||||||
|
const alert = fixture.debugElement.query(By.css('.alert')).nativeElement;
|
||||||
|
expect(alert.innerHTML).toContain('bitstream-request-a-copy.alert.canDownload');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onSubmit', () => {
|
||||||
|
describe('onSuccess', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should take the current form information and submit it', () => {
|
||||||
|
component.name.patchValue('User Name');
|
||||||
|
component.email.patchValue('user@name.org');
|
||||||
|
component.allfiles.patchValue('false');
|
||||||
|
component.message.patchValue('I would like to request a copy');
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
const itemRequest = Object.assign(new ItemRequest(),
|
||||||
|
{
|
||||||
|
itemId: item.uuid,
|
||||||
|
bitstreamId: bitstream.uuid,
|
||||||
|
allfiles: 'false',
|
||||||
|
requestEmail: 'user@name.org',
|
||||||
|
requestName: 'User Name',
|
||||||
|
requestMessage: 'I would like to request a copy'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest);
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(location.back).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onFail', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
(itemRequestDataService.requestACopy as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BitstreamRequestACopyPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should take the current form information and submit it', () => {
|
||||||
|
component.name.patchValue('User Name');
|
||||||
|
component.email.patchValue('user@name.org');
|
||||||
|
component.allfiles.patchValue('false');
|
||||||
|
component.message.patchValue('I would like to request a copy');
|
||||||
|
|
||||||
|
component.onSubmit();
|
||||||
|
const itemRequest = Object.assign(new ItemRequest(),
|
||||||
|
{
|
||||||
|
itemId: item.uuid,
|
||||||
|
bitstreamId: bitstream.uuid,
|
||||||
|
allfiles: 'false',
|
||||||
|
requestEmail: 'user@name.org',
|
||||||
|
requestName: 'User Name',
|
||||||
|
requestMessage: 'I would like to request a copy'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(itemRequestDataService.requestACopy).toHaveBeenCalledWith(itemRequest);
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(location.back).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,213 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { getBitstreamDownloadRoute, getForbiddenRoute } from '../../app-routing-paths';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||||
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { NotificationsService } from '../notifications/notifications.service';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||||
|
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-bitstream-request-a-copy-page',
|
||||||
|
templateUrl: './bitstream-request-a-copy-page.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Page component for requesting a copy for a bitstream
|
||||||
|
*/
|
||||||
|
export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
item$: Observable<Item>;
|
||||||
|
|
||||||
|
canDownload$: Observable<boolean>;
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
requestCopyForm: FormGroup;
|
||||||
|
|
||||||
|
item: Item;
|
||||||
|
itemName: string;
|
||||||
|
|
||||||
|
bitstream$: Observable<Bitstream>;
|
||||||
|
bitstream: Bitstream;
|
||||||
|
bitstreamName: string;
|
||||||
|
|
||||||
|
constructor(private location: Location,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
|
private authorizationService: AuthorizationDataService,
|
||||||
|
private auth: AuthService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private itemRequestDataService: ItemRequestDataService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private dsoNameService: DSONameService,
|
||||||
|
private bitstreamService: BitstreamDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.requestCopyForm = this.formBuilder.group({
|
||||||
|
name: new FormControl('', {
|
||||||
|
validators: [Validators.required],
|
||||||
|
}),
|
||||||
|
email: new FormControl('', {
|
||||||
|
validators: [Validators.required,
|
||||||
|
Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$')]
|
||||||
|
}),
|
||||||
|
allfiles: new FormControl(''),
|
||||||
|
message: new FormControl(''),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.item$ = this.route.data.pipe(
|
||||||
|
map((data) => data.dso),
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subs.push(this.item$.subscribe((item) => {
|
||||||
|
this.item = item;
|
||||||
|
this.itemName = this.dsoNameService.getName(item);
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.bitstream$ = this.route.queryParams.pipe(
|
||||||
|
filter((params) => hasValue(params) && hasValue(params.bitstream)),
|
||||||
|
switchMap((params) => this.bitstreamService.findById(params.bitstream)),
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subs.push(this.bitstream$.subscribe((bitstream) => {
|
||||||
|
this.bitstream = bitstream;
|
||||||
|
this.bitstreamName = this.dsoNameService.getName(bitstream);
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.canDownload$ = this.bitstream$.pipe(
|
||||||
|
switchMap((bitstream) => this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(bitstream) ? bitstream.self : undefined))
|
||||||
|
);
|
||||||
|
const canRequestCopy$ = this.bitstream$.pipe(
|
||||||
|
switchMap((bitstream) => this.authorizationService.isAuthorized(FeatureID.CanRequestACopy, isNotEmpty(bitstream) ? bitstream.self : undefined)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subs.push(observableCombineLatest([this.canDownload$, canRequestCopy$]).subscribe(([canDownload, canRequestCopy]) => {
|
||||||
|
if (!canDownload && !canRequestCopy) {
|
||||||
|
this.router.navigateByUrl(getForbiddenRoute(), {skipLocationChange: true});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this.initValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.requestCopyForm.get('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
get email() {
|
||||||
|
return this.requestCopyForm.get('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
get message() {
|
||||||
|
return this.requestCopyForm.get('message');
|
||||||
|
}
|
||||||
|
|
||||||
|
get allfiles() {
|
||||||
|
return this.requestCopyForm.get('allfiles');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the form values based on the current user.
|
||||||
|
*/
|
||||||
|
private initValues() {
|
||||||
|
this.getCurrentUser().pipe(take(1)).subscribe((user) => {
|
||||||
|
this.requestCopyForm.patchValue({allfiles: 'true'});
|
||||||
|
if (hasValue(user)) {
|
||||||
|
this.requestCopyForm.patchValue({name: user.name, email: user.email});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.bitstream$.pipe(take(1)).subscribe((bitstream) => {
|
||||||
|
this.requestCopyForm.patchValue({allfiles: 'false'});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the current user
|
||||||
|
*/
|
||||||
|
private getCurrentUser(): Observable<EPerson> {
|
||||||
|
return this.auth.isAuthenticated().pipe(
|
||||||
|
switchMap((authenticated) => {
|
||||||
|
if (authenticated) {
|
||||||
|
return this.auth.getAuthenticatedUserFromStore();
|
||||||
|
} else {
|
||||||
|
return observableOf(undefined);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the the form values as an item request to the server.
|
||||||
|
* When the submission is successful, the user will be redirected to the item page and a success notification will be shown.
|
||||||
|
* When the submission fails, the user will stay on the page and an error notification will be shown
|
||||||
|
*/
|
||||||
|
onSubmit() {
|
||||||
|
const itemRequest = new ItemRequest();
|
||||||
|
if (hasValue(this.bitstream)) {
|
||||||
|
itemRequest.bitstreamId = this.bitstream.uuid;
|
||||||
|
}
|
||||||
|
itemRequest.itemId = this.item.uuid;
|
||||||
|
itemRequest.allfiles = this.allfiles.value;
|
||||||
|
itemRequest.requestEmail = this.email.value;
|
||||||
|
itemRequest.requestName = this.name.value;
|
||||||
|
itemRequest.requestMessage = this.message.value;
|
||||||
|
|
||||||
|
this.itemRequestDataService.requestACopy(itemRequest).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((rd) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
this.notificationsService.success(this.translateService.get('bitstream-request-a-copy.submit.success'));
|
||||||
|
this.navigateBack();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('bitstream-request-a-copy.submit.error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.subs)) {
|
||||||
|
this.subs.forEach((sub) => {
|
||||||
|
if (hasValue(sub)) {
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates back to the user's previous location
|
||||||
|
*/
|
||||||
|
navigateBack() {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemPath() {
|
||||||
|
return [getItemPageRoute(this.item)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the link to the bistream download page
|
||||||
|
*/
|
||||||
|
getBitstreamLink() {
|
||||||
|
return [getBitstreamDownloadRoute(this.bitstream)];
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
<button *ngIf="isAuthorized$ | async"
|
||||||
|
class="edit-button btn btn-dark btn-sm"
|
||||||
|
(click)="createNewVersion()"
|
||||||
|
[disabled]="disableNewVersionButton$ | async"
|
||||||
|
[ngbTooltip]="tooltipMsg$ | async | translate"
|
||||||
|
role="button" [title]="tooltipMsg$ | async |translate" [attr.aria-label]="tooltipMsg$ | async | translate">
|
||||||
|
<i class="fas fa-code-branch fa-fw"></i>
|
||||||
|
</button>
|
@@ -0,0 +1,3 @@
|
|||||||
|
.btn-dark {
|
||||||
|
background-color: var(--ds-admin-sidebar-bg);
|
||||||
|
}
|
@@ -0,0 +1,96 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { DsoPageVersionButtonComponent } from './dso-page-version-button.component';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { Observable, of, of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||||
|
|
||||||
|
describe('DsoPageVersionButtonComponent', () => {
|
||||||
|
let component: DsoPageVersionButtonComponent;
|
||||||
|
let fixture: ComponentFixture<DsoPageVersionButtonComponent>;
|
||||||
|
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let versionHistoryService: VersionHistoryDataService;
|
||||||
|
|
||||||
|
let dso: Item;
|
||||||
|
let tooltipMsg: Observable<string>;
|
||||||
|
|
||||||
|
const authorizationServiceSpy = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
|
||||||
|
|
||||||
|
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService',
|
||||||
|
['getVersions', 'getLatestVersionFromHistory$', 'isLatest$', 'hasDraftVersion$']
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
dso = Object.assign(new Item(), {
|
||||||
|
id: 'test-item',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'test-item-selflink' },
|
||||||
|
version: { href: 'test-item-version-selflink' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tooltipMsg = of('tooltip-msg');
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [DsoPageVersionButtonComponent],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationServiceSpy },
|
||||||
|
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy },
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
authorizationService = TestBed.inject(AuthorizationDataService);
|
||||||
|
versionHistoryService = TestBed.inject(VersionHistoryDataService);
|
||||||
|
|
||||||
|
versionHistoryServiceSpy.hasDraftVersion$.and.returnValue(observableOf(true));
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DsoPageVersionButtonComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.dso = dso;
|
||||||
|
component.tooltipMsg$ = tooltipMsg;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check the authorization of the current user', () => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.CanCreateVersion, dso.self);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if the item has a draft version', () => {
|
||||||
|
expect(versionHistoryServiceSpy.hasDraftVersion$).toHaveBeenCalledWith(dso._links.version.href);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is authorized', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationServiceSpy.isAuthorized.and.returnValue(observableOf(true));
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a button', () => {
|
||||||
|
const button = fixture.debugElement.query(By.css('button'));
|
||||||
|
expect(button).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is not authorized', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationServiceSpy.isAuthorized.and.returnValue(observableOf(false));
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a button', () => {
|
||||||
|
const button = fixture.debugElement.query(By.css('button'));
|
||||||
|
expect(button).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,78 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-dso-page-version-button',
|
||||||
|
templateUrl: './dso-page-version-button.component.html',
|
||||||
|
styleUrls: ['./dso-page-version-button.component.scss']
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Display a button linking to the edit page of a DSpaceObject
|
||||||
|
*/
|
||||||
|
export class DsoPageVersionButtonComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The item for which display a button to create a new version
|
||||||
|
*/
|
||||||
|
@Input() dso: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message for the tooltip on the button
|
||||||
|
* Supports i18n keys
|
||||||
|
*/
|
||||||
|
@Input() tooltipMsgCreate: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message for the tooltip on the button (when is disabled)
|
||||||
|
* Supports i18n keys
|
||||||
|
*/
|
||||||
|
@Input() tooltipMsgHasDraft: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event that triggers the creation of the new version
|
||||||
|
*/
|
||||||
|
@Output() newVersionEvent = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the current user is authorized to create a new version of the DSpaceObject
|
||||||
|
*/
|
||||||
|
isAuthorized$: Observable<boolean>;
|
||||||
|
|
||||||
|
disableNewVersionButton$: Observable<boolean>;
|
||||||
|
|
||||||
|
tooltipMsg$: Observable<string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected versionHistoryService: VersionHistoryDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new version for the current item
|
||||||
|
*/
|
||||||
|
createNewVersion() {
|
||||||
|
this.newVersionEvent.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.isAuthorized$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.dso.self);
|
||||||
|
|
||||||
|
this.disableNewVersionButton$ = this.versionHistoryService.hasDraftVersion$(this.dso._links.version.href).pipe(
|
||||||
|
// button is disabled if hasDraftVersion = true, and enabled if hasDraftVersion = false or null
|
||||||
|
// (hasDraftVersion is null when a version history does not exist)
|
||||||
|
map((res) => Boolean(res)),
|
||||||
|
startWith(true),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.tooltipMsg$ = this.disableNewVersionButton$.pipe(
|
||||||
|
switchMap((hasDraftVersion) => of(hasDraftVersion ? this.tooltipMsgHasDraft : this.tooltipMsgCreate)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
<a [href]="bitstreamPath" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
|
<a [routerLink]="(bitstreamPath$| async)?.routerLink" [queryParams]="(bitstreamPath$| async)?.queryParams" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
|
||||||
|
<span *ngIf="!(canDownload$ |async)"><i class="fas fa-lock"></i></span>
|
||||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@@ -1,62 +1,145 @@
|
|||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { FileDownloadLinkComponent } from './file-download-link.component';
|
import { FileDownloadLinkComponent } from './file-download-link.component';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
|
||||||
import { FileService } from '../../core/shared/file.service';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { getBitstreamModuleRoute } from '../../app-routing-paths';
|
import { getBitstreamModuleRoute } from '../../app-routing-paths';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { getItemModuleRoute } from '../../item-page/item-page-routing-paths';
|
||||||
|
import { RouterLinkDirectiveStub } from '../testing/router-link-directive.stub';
|
||||||
|
|
||||||
describe('FileDownloadLinkComponent', () => {
|
describe('FileDownloadLinkComponent', () => {
|
||||||
let component: FileDownloadLinkComponent;
|
let component: FileDownloadLinkComponent;
|
||||||
let fixture: ComponentFixture<FileDownloadLinkComponent>;
|
let fixture: ComponentFixture<FileDownloadLinkComponent>;
|
||||||
|
|
||||||
let authService: AuthService;
|
let scheduler;
|
||||||
let fileService: FileService;
|
let authorizationService: AuthorizationDataService;
|
||||||
|
|
||||||
let bitstream: Bitstream;
|
let bitstream: Bitstream;
|
||||||
|
let item: Item;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
authService = jasmine.createSpyObj('authService', {
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
isAuthenticated: observableOf(true)
|
isAuthorized: cold('-a', {a: true})
|
||||||
});
|
});
|
||||||
fileService = jasmine.createSpyObj('fileService', ['downloadFile']);
|
|
||||||
bitstream = Object.assign(new Bitstream(), {
|
bitstream = Object.assign(new Bitstream(), {
|
||||||
uuid: 'bitstreamUuid',
|
uuid: 'bitstreamUuid',
|
||||||
|
_links: {
|
||||||
|
self: {href: 'obj-selflink'}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
item = Object.assign(new Item(), {
|
||||||
|
uuid: 'itemUuid',
|
||||||
|
_links: {
|
||||||
|
self: {href: 'obj-selflink'}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
function initTestbed() {
|
||||||
init();
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [FileDownloadLinkComponent],
|
declarations: [FileDownloadLinkComponent, RouterLinkDirectiveStub],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: AuthService, useValue: authService },
|
{provide: AuthorizationDataService, useValue: authorizationService},
|
||||||
{ provide: FileService, useValue: fileService },
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
}));
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
component.bitstream = bitstream;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('init', () => {
|
describe('init', () => {
|
||||||
|
|
||||||
describe('getBitstreamPath', () => {
|
describe('getBitstreamPath', () => {
|
||||||
it('should set the bitstreamPath based on the input bitstream', () => {
|
describe('when the user has download rights', () => {
|
||||||
expect(component.bitstreamPath).toEqual(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
beforeEach(waitForAsync(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
init();
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.bitstream = bitstream;
|
||||||
|
component.item = item;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should return the bitstreamPath based on the input bitstream', () => {
|
||||||
|
expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: { routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(), queryParams: {} }}));
|
||||||
|
expect(component.canDownload$).toBeObservable(cold('--a', {a: true}));
|
||||||
|
|
||||||
|
});
|
||||||
|
it('should init the component', () => {
|
||||||
|
scheduler.flush();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const link = fixture.debugElement.query(By.css('a'));
|
||||||
|
expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
||||||
|
const lock = fixture.debugElement.query(By.css('.fa-lock'));
|
||||||
|
expect(lock).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when the user has no download rights but has the right to request a copy', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
init();
|
||||||
|
(authorizationService.isAuthorized as jasmine.Spy).and.callFake((featureId, object) => {
|
||||||
|
if (featureId === FeatureID.CanDownload) {
|
||||||
|
return cold('-a', {a: false});
|
||||||
|
}
|
||||||
|
return cold('-a', {a: true});
|
||||||
|
});
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.item = item;
|
||||||
|
component.bitstream = bitstream;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should return the bitstreamPath based on the input bitstream', () => {
|
||||||
|
expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: { routerLink: new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString(), queryParams: { bitstream: bitstream.uuid } }}));
|
||||||
|
expect(component.canDownload$).toBeObservable(cold('--a', {a: false}));
|
||||||
|
|
||||||
|
});
|
||||||
|
it('should init the component', () => {
|
||||||
|
scheduler.flush();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const link = fixture.debugElement.query(By.css('a'));
|
||||||
|
expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString());
|
||||||
|
const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement;
|
||||||
|
expect(lock).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when the user has no download rights and no request a copy rights', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
init();
|
||||||
|
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(cold('-a', {a: false}));
|
||||||
|
initTestbed();
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.bitstream = bitstream;
|
||||||
|
component.item = item;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('should return the bitstreamPath based on the input bitstream', () => {
|
||||||
|
expect(component.bitstreamPath$).toBeObservable(cold('-a', {a: { routerLink: new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString(), queryParams: {} }}));
|
||||||
|
expect(component.canDownload$).toBeObservable(cold('--a', {a: false}));
|
||||||
|
|
||||||
|
});
|
||||||
|
it('should init the component', () => {
|
||||||
|
scheduler.flush();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const link = fixture.debugElement.query(By.css('a'));
|
||||||
|
expect(link.injector.get(RouterLinkDirectiveStub).routerLink).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
||||||
|
const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement;
|
||||||
|
expect(lock).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init the component', () => {
|
|
||||||
const link = fixture.debugElement.query(By.css('a')).nativeElement;
|
|
||||||
expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,12 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
|
import { getBitstreamDownloadRoute, getBitstreamRequestACopyRoute } from '../../app-routing-paths';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-file-download-link',
|
selector: 'ds-file-download-link',
|
||||||
@@ -19,6 +25,8 @@ export class FileDownloadLinkComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() bitstream: Bitstream;
|
@Input() bitstream: Bitstream;
|
||||||
|
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional css classes to apply to link
|
* Additional css classes to apply to link
|
||||||
*/
|
*/
|
||||||
@@ -29,13 +37,44 @@ export class FileDownloadLinkComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() isBlank = false;
|
@Input() isBlank = false;
|
||||||
|
|
||||||
bitstreamPath: string;
|
@Input() enableRequestACopy = true;
|
||||||
|
|
||||||
|
bitstreamPath$: Observable<{
|
||||||
|
routerLink: string,
|
||||||
|
queryParams: any,
|
||||||
|
}>;
|
||||||
|
|
||||||
|
canDownload$: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authorizationService: AuthorizationDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.bitstreamPath = this.getBitstreamPath();
|
if (this.enableRequestACopy) {
|
||||||
|
this.canDownload$ = this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
||||||
|
const canRequestACopy$ = this.authorizationService.isAuthorized(FeatureID.CanRequestACopy, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
||||||
|
this.bitstreamPath$ = observableCombineLatest([this.canDownload$, canRequestACopy$]).pipe(
|
||||||
|
map(([canDownload, canRequestACopy]) => this.getBitstreamPath(canDownload, canRequestACopy))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.bitstreamPath$ = observableOf(this.getBitstreamDownloadPath());
|
||||||
|
this.canDownload$ = observableOf(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getBitstreamPath() {
|
getBitstreamPath(canDownload: boolean, canRequestACopy: boolean) {
|
||||||
return getBitstreamDownloadRoute(this.bitstream);
|
if (!canDownload && canRequestACopy && hasValue(this.item)) {
|
||||||
|
return getBitstreamRequestACopyRoute(this.item, this.bitstream);
|
||||||
|
}
|
||||||
|
return this.getBitstreamDownloadPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBitstreamDownloadPath() {
|
||||||
|
return {
|
||||||
|
routerLink: getBitstreamDownloadRoute(this.bitstream),
|
||||||
|
queryParams: {}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user