mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'DSpace:main' into DA-1767
This commit is contained in:
16
.github/workflows/issue_opened.yml
vendored
16
.github/workflows/issue_opened.yml
vendored
@@ -10,20 +10,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Add the new issue to a project board, if it needs triage
|
# Add the new issue to a project board, if it needs triage
|
||||||
# See https://github.com/marketplace/actions/create-project-card-action
|
# See https://github.com/actions/add-to-project
|
||||||
- name: Add issue to project board
|
- name: Add issue to triage board
|
||||||
# Only add to project board if issue is flagged as "needs triage" or has no labels
|
# Only add to project board if issue is flagged as "needs triage" or has no labels
|
||||||
# NOTE: By default we flag new issues as "needs triage" in our issue template
|
# NOTE: By default we flag new issues as "needs triage" in our issue template
|
||||||
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
|
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
|
||||||
uses: technote-space/create-project-card-action@v1
|
uses: actions/add-to-project@v0.3.0
|
||||||
# Note, the authentication token below is an ORG level Secret.
|
# Note, the authentication token below is an ORG level Secret.
|
||||||
# It must be created/recreated manually via a personal access token with "public_repo" and "admin:org" permissions
|
# It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions
|
||||||
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
|
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
|
||||||
# This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific)
|
# This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific)
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.ORG_PROJECT_TOKEN }}
|
github-token: ${{ secrets.TRIAGE_PROJECT_TOKEN }}
|
||||||
PROJECT: DSpace Backlog
|
project-url: https://github.com/orgs/DSpace/projects/24
|
||||||
COLUMN: Triage
|
|
||||||
CHECK_ORG_PROJECT: true
|
|
||||||
# Ignore errors
|
|
||||||
continue-on-error: true
|
|
||||||
|
@@ -351,7 +351,7 @@ Documentation
|
|||||||
|
|
||||||
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/
|
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/
|
||||||
|
|
||||||
Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of htis codebase.
|
Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of this codebase.
|
||||||
|
|
||||||
### Building code documentation
|
### Building code documentation
|
||||||
|
|
||||||
|
@@ -14,6 +14,8 @@ ui:
|
|||||||
rateLimiter:
|
rateLimiter:
|
||||||
windowMs: 60000 # 1 minute
|
windowMs: 60000 # 1 minute
|
||||||
max: 500 # limit each IP to 500 requests per windowMs
|
max: 500 # limit each IP to 500 requests per windowMs
|
||||||
|
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||||
|
useProxies: true
|
||||||
|
|
||||||
# The REST API server settings
|
# The REST API server settings
|
||||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||||
@@ -135,9 +137,6 @@ languages:
|
|||||||
- code: lv
|
- code: lv
|
||||||
label: Latviešu
|
label: Latviešu
|
||||||
active: true
|
active: true
|
||||||
- code: hi
|
|
||||||
label: Hindi
|
|
||||||
active: true
|
|
||||||
- code: hu
|
- code: hu
|
||||||
label: Magyar
|
label: Magyar
|
||||||
active: true
|
active: true
|
||||||
@@ -165,6 +164,9 @@ languages:
|
|||||||
- code: bn
|
- code: bn
|
||||||
label: বাংলা
|
label: বাংলা
|
||||||
active: true
|
active: true
|
||||||
|
- code: hi
|
||||||
|
label: हिंदी
|
||||||
|
active: true
|
||||||
- code: el
|
- code: el
|
||||||
label: Ελληνικά
|
label: Ελληνικά
|
||||||
active: true
|
active: true
|
||||||
@@ -288,3 +290,9 @@ mediaViewer:
|
|||||||
info:
|
info:
|
||||||
enableEndUserAgreement: true
|
enableEndUserAgreement: true
|
||||||
enablePrivacyStatement: true
|
enablePrivacyStatement: true
|
||||||
|
|
||||||
|
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
||||||
|
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
||||||
|
markdown:
|
||||||
|
enabled: false
|
||||||
|
mathjax: false
|
@@ -24,7 +24,7 @@ import 'cypress-axe';
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
|
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
|
||||||
// This just ensures it doesn't get in the way of matching other objects in the page.
|
// This just ensures it doesn't get in the way of matching other objects in the page.
|
||||||
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}');
|
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}');
|
||||||
});
|
});
|
||||||
|
|
||||||
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
|
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular",
|
"name": "dspace-angular",
|
||||||
"version": "7.4.0-next",
|
"version": "7.5.0-next",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:watch": "nodemon",
|
"config:watch": "nodemon",
|
||||||
@@ -78,6 +78,7 @@
|
|||||||
"@nguniversal/express-engine": "^13.0.2",
|
"@nguniversal/express-engine": "^13.0.2",
|
||||||
"@ngx-translate/core": "^13.0.0",
|
"@ngx-translate/core": "^13.0.0",
|
||||||
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
|
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
|
||||||
|
"@types/grecaptcha": "^3.0.4",
|
||||||
"angular-idle-preload": "3.0.0",
|
"angular-idle-preload": "3.0.0",
|
||||||
"angulartics2": "^12.0.0",
|
"angulartics2": "^12.0.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
@@ -209,7 +209,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
||||||
this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup);
|
this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup);
|
||||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||||
}
|
}
|
||||||
@@ -315,7 +314,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<any>) => {
|
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<any>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
|
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
|
||||||
this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href);
|
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
|
||||||
}
|
}
|
||||||
|
@@ -20,14 +20,12 @@ import { TestScheduler } from 'rxjs/testing';
|
|||||||
import {
|
import {
|
||||||
createNoContentRemoteDataObject$,
|
createNoContentRemoteDataObject$,
|
||||||
createSuccessfulRemoteDataObject,
|
createSuccessfulRemoteDataObject,
|
||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$,
|
||||||
|
createFailedRemoteDataObject$
|
||||||
} from '../../../shared/remote-data.utils';
|
} from '../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
|
||||||
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 { FindListOptions } from '../../../core/data/find-list-options.model';
|
|
||||||
|
|
||||||
describe('BitstreamFormatsComponent', () => {
|
describe('BitstreamFormatsComponent', () => {
|
||||||
let comp: BitstreamFormatsComponent;
|
let comp: BitstreamFormatsComponent;
|
||||||
@@ -85,10 +83,6 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
];
|
];
|
||||||
const mockFormatsRD = createSuccessfulRemoteDataObject(createPaginatedList(mockFormatsList));
|
const mockFormatsRD = createSuccessfulRemoteDataObject(createPaginatedList(mockFormatsList));
|
||||||
|
|
||||||
const pagination = Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 });
|
|
||||||
const sort = new SortOptions('score', SortDirection.DESC);
|
|
||||||
const findlistOptions = Object.assign(new FindListOptions(), { currentPage: 1, elementsPerPage: 20 });
|
|
||||||
|
|
||||||
const initAsync = () => {
|
const initAsync = () => {
|
||||||
notificationsServiceStub = new NotificationsServiceStub();
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
@@ -246,7 +240,7 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
beforeEach(initBeforeEach);
|
beforeEach(initBeforeEach);
|
||||||
it('should clear bitstream formats ', () => {
|
it('should clear bitstream formats and show a success notification', () => {
|
||||||
comp.deleteFormats();
|
comp.deleteFormats();
|
||||||
|
|
||||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
@@ -275,7 +269,7 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
selectBitstreamFormat: {},
|
selectBitstreamFormat: {},
|
||||||
deselectBitstreamFormat: {},
|
deselectBitstreamFormat: {},
|
||||||
deselectAllBitstreamFormats: {},
|
deselectAllBitstreamFormats: {},
|
||||||
delete: observableOf(false),
|
delete: createFailedRemoteDataObject$(),
|
||||||
clearBitStreamFormatRequests: observableOf('cleared')
|
clearBitStreamFormatRequests: observableOf('cleared')
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,7 +289,7 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
beforeEach(initBeforeEach);
|
beforeEach(initBeforeEach);
|
||||||
it('should clear bitstream formats ', () => {
|
it('should clear bitstream formats and show an error notification', () => {
|
||||||
comp.deleteFormats();
|
comp.deleteFormats();
|
||||||
|
|
||||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
|
@@ -5,7 +5,7 @@ import { PaginatedList } from '../../../core/data/paginated-list.model';
|
|||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -13,6 +13,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
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 { FindListOptions } from '../../../core/data/find-list-options.model';
|
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a list of bitstream formats
|
* This component renders a list of bitstream formats
|
||||||
@@ -59,31 +60,39 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
* Deletes the currently selected formats from the registry and updates the presented list
|
* Deletes the currently selected formats from the registry and updates the presented list
|
||||||
*/
|
*/
|
||||||
deleteFormats() {
|
deleteFormats() {
|
||||||
this.bitstreamFormatService.clearBitStreamFormatRequests().subscribe();
|
this.bitstreamFormatService.clearBitStreamFormatRequests();
|
||||||
this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(take(1)).subscribe(
|
this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||||
(formats) => {
|
take(1),
|
||||||
const tasks$ = [];
|
// emit all formats in the array one at a time
|
||||||
for (const format of formats) {
|
mergeMap((formats: BitstreamFormat[]) => formats),
|
||||||
if (hasValue(format.id)) {
|
// delete each format
|
||||||
tasks$.push(this.bitstreamFormatService.delete(format.id).pipe(map((response: RemoteData<NoContent>) => response.hasSucceeded)));
|
mergeMap((format: BitstreamFormat) => this.bitstreamFormatService.delete(format.id).pipe(
|
||||||
}
|
// wait for each response to come back
|
||||||
}
|
getFirstCompletedRemoteData(),
|
||||||
zip(...tasks$).subscribe((results: boolean[]) => {
|
// return a boolean to indicate whether a response succeeded
|
||||||
const successResponses = results.filter((result: boolean) => result);
|
map((response: RemoteData<NoContent>) => response.hasSucceeded),
|
||||||
const failedResponses = results.filter((result: boolean) => !result);
|
)),
|
||||||
if (successResponses.length > 0) {
|
// wait for all responses to come in and return them as a single array
|
||||||
this.showNotification(true, successResponses.length);
|
toArray()
|
||||||
}
|
).subscribe((results: boolean[]) => {
|
||||||
if (failedResponses.length > 0) {
|
// Count the number of succeeded and failed deletions
|
||||||
this.showNotification(false, failedResponses.length);
|
const successResponses = results.filter((result: boolean) => result);
|
||||||
}
|
const failedResponses = results.filter((result: boolean) => !result);
|
||||||
|
|
||||||
this.deselectAll();
|
// Show a notification indicating the number of succeeded and failed deletions
|
||||||
|
if (successResponses.length > 0) {
|
||||||
this.paginationService.resetPage(this.pageConfig.id);
|
this.showNotification(true, successResponses.length);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
if (failedResponses.length > 0) {
|
||||||
|
this.showNotification(false, failedResponses.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the selection
|
||||||
|
this.deselectAll();
|
||||||
|
|
||||||
|
// reload the page
|
||||||
|
this.paginationService.resetPage(this.pageConfig.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { MenuService } from '../../../shared/menu/menu.service';
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
||||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
@@ -6,7 +6,7 @@ import { ScriptDataService } from '../../core/data/processes/script-data.service
|
|||||||
import { AdminSidebarComponent } from './admin-sidebar.component';
|
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service.stub';
|
||||||
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
@@ -5,7 +5,7 @@ import { AuthService } from '../../core/auth/auth.service';
|
|||||||
import { slideSidebar } from '../../shared/animations/slide';
|
import { slideSidebar } from '../../shared/animations/slide';
|
||||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
@@ -69,7 +69,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
this.sidebarWidth = this.variableService.getVariable('--ds-sidebar-items-width');
|
||||||
this.authService.isAuthenticated()
|
this.authService.isAuthenticated()
|
||||||
.subscribe((loggedIn: boolean) => {
|
.subscribe((loggedIn: boolean) => {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
|
@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
|
import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
|
||||||
import { MenuService } from '../../../shared/menu/menu.service';
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
||||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
@@ -2,7 +2,7 @@ import { Component, Inject, Injector, OnInit } from '@angular/core';
|
|||||||
import { rotate } from '../../../shared/animations/rotate';
|
import { rotate } from '../../../shared/animations/rotate';
|
||||||
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
||||||
import { slide } from '../../../shared/animations/slide';
|
import { slide } from '../../../shared/animations/slide';
|
||||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||||
import { bgColor } from '../../../shared/animations/bgColor';
|
import { bgColor } from '../../../shared/animations/bgColor';
|
||||||
import { MenuService } from '../../../shared/menu/menu.service';
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||||
@@ -65,7 +65,7 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.sidebarActiveBg = this.variableService.getVariable('adminSidebarActiveBg');
|
this.sidebarActiveBg = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
||||||
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||||
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||||
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
|
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
|
||||||
|
@@ -18,7 +18,7 @@ import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.ser
|
|||||||
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
|
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { MenuService } from './shared/menu/menu.service';
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from './shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from './shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from './shared/testing/css-variable-service.stub';
|
||||||
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
||||||
import { HostWindowService } from './shared/host-window.service';
|
import { HostWindowService } from './shared/host-window.service';
|
||||||
|
@@ -25,7 +25,7 @@ import { HostWindowState } from './shared/search/host-window.reducer';
|
|||||||
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
||||||
import { isAuthenticationBlocking } from './core/auth/selectors';
|
import { isAuthenticationBlocking } from './core/auth/selectors';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from './shared/sass-helper/css-variable.service';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { models } from './core/core.module';
|
import { models } from './core/core.module';
|
||||||
import { ThemeService } from './shared/theme-support/theme.service';
|
import { ThemeService } from './shared/theme-support/theme.service';
|
||||||
@@ -110,18 +110,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private storeCSSVariables() {
|
private storeCSSVariables() {
|
||||||
this.cssService.addCSSVariable('xlMin', '1200px');
|
this.cssService.clearCSSVariables();
|
||||||
this.cssService.addCSSVariable('mdMin', '768px');
|
this.cssService.addCSSVariables(this.cssService.getCSSVariablesFromStylesheets(this.document));
|
||||||
this.cssService.addCSSVariable('lgMin', '576px');
|
|
||||||
this.cssService.addCSSVariable('smMin', '0');
|
|
||||||
this.cssService.addCSSVariable('adminSidebarActiveBg', '#0f1b28');
|
|
||||||
this.cssService.addCSSVariable('sidebarItemsWidth', '250px');
|
|
||||||
this.cssService.addCSSVariable('collapsedSidebarWidth', '53.234px');
|
|
||||||
this.cssService.addCSSVariable('totalSidebarWidth', '303.234px');
|
|
||||||
// const vars = variables.locals || {};
|
|
||||||
// Object.keys(vars).forEach((name: string) => {
|
|
||||||
// this.cssService.addCSSVariable(name, vars[name]);
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
|
@@ -35,7 +35,7 @@ import {
|
|||||||
ObjectSelectionListState,
|
ObjectSelectionListState,
|
||||||
objectSelectionReducer
|
objectSelectionReducer
|
||||||
} from './shared/object-select/object-select.reducer';
|
} from './shared/object-select/object-select.reducer';
|
||||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/css-variable.reducer';
|
||||||
|
|
||||||
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
||||||
import {
|
import {
|
||||||
|
@@ -23,7 +23,6 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
|
|||||||
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
||||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
|
||||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||||
import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service';
|
import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service';
|
||||||
@@ -257,7 +256,6 @@ const PROVIDERS = [
|
|||||||
DefaultChangeAnalyzer,
|
DefaultChangeAnalyzer,
|
||||||
ArrayMoveChangeAnalyzer,
|
ArrayMoveChangeAnalyzer,
|
||||||
ObjectSelectService,
|
ObjectSelectService,
|
||||||
CSSVariableService,
|
|
||||||
MenuService,
|
MenuService,
|
||||||
ObjectUpdatesService,
|
ObjectUpdatesService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@@ -10,6 +10,7 @@ import { ResourceType } from '../../shared/resource-type';
|
|||||||
import { BaseDataService } from './base-data.service';
|
import { BaseDataService } from './base-data.service';
|
||||||
import { HALDataService } from './hal-data-service.interface';
|
import { HALDataService } from './hal-data-service.interface';
|
||||||
import { dataService, getDataServiceFor } from './data-service.decorator';
|
import { dataService, getDataServiceFor } from './data-service.decorator';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
class TestService extends BaseDataService<any> {
|
class TestService extends BaseDataService<any> {
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,7 @@ let testType;
|
|||||||
|
|
||||||
describe('@dataService/getDataServiceFor', () => {
|
describe('@dataService/getDataServiceFor', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testType = new ResourceType('testType-' + new Date().getTime());
|
testType = new ResourceType(`testType-${uuidv4()}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should register a resourcetype for a dataservice', () => {
|
it('should register a resourcetype for a dataservice', () => {
|
||||||
|
@@ -9,6 +9,8 @@ import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { RequestEntry } from './request-entry.model';
|
import { RequestEntry } from './request-entry.model';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
|
||||||
describe('EpersonRegistrationService', () => {
|
describe('EpersonRegistrationService', () => {
|
||||||
let testScheduler;
|
let testScheduler;
|
||||||
@@ -79,8 +81,23 @@ describe('EpersonRegistrationService', () => {
|
|||||||
it('should send an email registration', () => {
|
it('should send an email registration', () => {
|
||||||
|
|
||||||
const expected = service.registerEmail('test@mail.org');
|
const expected = service.registerEmail('test@mail.org');
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
options.headers = headers;
|
||||||
|
|
||||||
expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration));
|
expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration, options));
|
||||||
|
expect(expected).toBeObservable(cold('(a|)', { a: rd }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send an email registration with captcha', () => {
|
||||||
|
|
||||||
|
const expected = service.registerEmail('test@mail.org', 'afreshcaptchatoken');
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
headers = headers.append('x-recaptcha-token', 'afreshcaptchatoken');
|
||||||
|
options.headers = headers;
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration, options));
|
||||||
expect(expected).toBeObservable(cold('(a|)', { a: rd }));
|
expect(expected).toBeObservable(cold('(a|)', { a: rd }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -3,15 +3,17 @@ import { RequestService } from './request.service';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { GetRequest, PostRequest } from './request.models';
|
import { GetRequest, PostRequest } from './request.models';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, find, map, skipWhile } from 'rxjs/operators';
|
import { filter, find, map } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { Registration } from '../shared/registration.model';
|
import { Registration } from '../shared/registration.model';
|
||||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators';
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
import { RegistrationResponseParsingService } from './registration-response-parsing.service';
|
import { RegistrationResponseParsingService } from './registration-response-parsing.service';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -51,8 +53,9 @@ export class EpersonRegistrationService {
|
|||||||
/**
|
/**
|
||||||
* Register a new email address
|
* Register a new email address
|
||||||
* @param email
|
* @param email
|
||||||
|
* @param captchaToken the value of x-recaptcha-token header
|
||||||
*/
|
*/
|
||||||
registerEmail(email: string): Observable<RemoteData<Registration>> {
|
registerEmail(email: string, captchaToken: string = null): Observable<RemoteData<Registration>> {
|
||||||
const registration = new Registration();
|
const registration = new Registration();
|
||||||
registration.email = email;
|
registration.email = email;
|
||||||
|
|
||||||
@@ -60,10 +63,17 @@ export class EpersonRegistrationService {
|
|||||||
|
|
||||||
const href$ = this.getRegistrationEndpoint();
|
const href$ = this.getRegistrationEndpoint();
|
||||||
|
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
if (captchaToken) {
|
||||||
|
headers = headers.append('x-recaptcha-token', captchaToken);
|
||||||
|
}
|
||||||
|
options.headers = headers;
|
||||||
|
|
||||||
href$.pipe(
|
href$.pipe(
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const request = new PostRequest(requestId, href, registration);
|
const request = new PostRequest(requestId, href, registration, options);
|
||||||
this.requestService.send(request);
|
this.requestService.send(request);
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe();
|
||||||
|
@@ -50,7 +50,6 @@ describe('EndUserAgreementGuard', () => {
|
|||||||
it('should return true', (done) => {
|
it('should return true', (done) => {
|
||||||
environment.info.enableEndUserAgreement = false;
|
environment.info.enableEndUserAgreement = false;
|
||||||
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
||||||
console.log(result);
|
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@@ -191,9 +191,7 @@ describe('GroupDataService', () => {
|
|||||||
callback();
|
callback();
|
||||||
|
|
||||||
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock2._links.self.href);
|
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock2._links.self.href);
|
||||||
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
|
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
|
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -218,9 +216,7 @@ describe('GroupDataService', () => {
|
|||||||
callback();
|
callback();
|
||||||
|
|
||||||
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock._links.self.href);
|
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock._links.self.href);
|
||||||
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
|
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
|
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
|
||||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -179,7 +179,6 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
|
|
||||||
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
||||||
this.invalidateByHref(ePerson._links.self.href),
|
this.invalidateByHref(ePerson._links.self.href),
|
||||||
this.invalidateByHref(activeGroup._links.self.href),
|
|
||||||
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
|
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
|
||||||
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
|
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
|
||||||
));
|
));
|
||||||
@@ -198,7 +197,6 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
|
|
||||||
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
||||||
this.invalidateByHref(ePerson._links.self.href),
|
this.invalidateByHref(ePerson._links.self.href),
|
||||||
this.invalidateByHref(activeGroup._links.self.href),
|
|
||||||
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
|
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
|
||||||
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
|
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
|
||||||
));
|
));
|
||||||
|
25
src/app/core/google-recaptcha/google-recaptcha.module.ts
Normal file
25
src/app/core/google-recaptcha/google-recaptcha.module.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { GoogleRecaptchaComponent } from '../../shared/google-recaptcha/google-recaptcha.component';
|
||||||
|
|
||||||
|
import { GoogleRecaptchaService } from './google-recaptcha.service';
|
||||||
|
|
||||||
|
const PROVIDERS = [
|
||||||
|
GoogleRecaptchaService
|
||||||
|
];
|
||||||
|
|
||||||
|
const COMPONENTS = [
|
||||||
|
GoogleRecaptchaComponent
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule ],
|
||||||
|
providers: [...PROVIDERS],
|
||||||
|
declarations: [...COMPONENTS],
|
||||||
|
exports: [...COMPONENTS]
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module handles google recaptcha functionalities
|
||||||
|
*/
|
||||||
|
export class GoogleRecaptchaModule {}
|
@@ -0,0 +1,57 @@
|
|||||||
|
import { GoogleRecaptchaService } from './google-recaptcha.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { NativeWindowRef } from '../services/window.service';
|
||||||
|
|
||||||
|
describe('GoogleRecaptchaService', () => {
|
||||||
|
let service: GoogleRecaptchaService;
|
||||||
|
|
||||||
|
let rendererFactory2;
|
||||||
|
let configurationDataService;
|
||||||
|
let spy: jasmine.Spy;
|
||||||
|
let scriptElementMock: any;
|
||||||
|
let cookieService;
|
||||||
|
let window;
|
||||||
|
const innerHTMLTestValue = 'mock-script-inner-html';
|
||||||
|
const document = { documentElement: { lang: 'en' } } as Document;
|
||||||
|
scriptElementMock = {
|
||||||
|
set innerHTML(newVal) { /* noop */ },
|
||||||
|
get innerHTML() { return innerHTMLTestValue; }
|
||||||
|
};
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
window = new NativeWindowRef();
|
||||||
|
rendererFactory2 = jasmine.createSpyObj('rendererFactory2', {
|
||||||
|
createRenderer: observableOf('googleRecaptchaToken'),
|
||||||
|
createElement: scriptElementMock
|
||||||
|
});
|
||||||
|
configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
|
findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['googleRecaptchaToken'] })
|
||||||
|
});
|
||||||
|
cookieService = jasmine.createSpyObj('cookieService', {
|
||||||
|
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
|
||||||
|
set: () => {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
service = new GoogleRecaptchaService(cookieService, document, window, rendererFactory2, configurationDataService);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRecaptchaToken', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spy = spyOn(service, 'getRecaptchaToken').and.stub();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a Request with action', () => {
|
||||||
|
service.getRecaptchaToken('test');
|
||||||
|
expect(spy).toHaveBeenCalledWith('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
176
src/app/core/google-recaptcha/google-recaptcha.service.ts
Normal file
176
src/app/core/google-recaptcha/google-recaptcha.service.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
|
||||||
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
|
|
||||||
|
export const CAPTCHA_COOKIE = '_GRECAPTCHA';
|
||||||
|
export const CAPTCHA_NAME = 'google-recaptcha';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A GoogleRecaptchaService used to send action and get a token from REST
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class GoogleRecaptchaService {
|
||||||
|
|
||||||
|
private renderer: Renderer2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Google Recaptcha version
|
||||||
|
*/
|
||||||
|
private captchaVersionSubject$ = new BehaviorSubject<string>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Google Recaptcha Key
|
||||||
|
*/
|
||||||
|
private captchaKeySubject$ = new BehaviorSubject<string>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Google Recaptcha mode
|
||||||
|
*/
|
||||||
|
private captchaModeSubject$ = new BehaviorSubject<string>(null);
|
||||||
|
|
||||||
|
captchaKey(): Observable<string> {
|
||||||
|
return this.captchaKeySubject$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaMode(): Observable<string> {
|
||||||
|
return this.captchaModeSubject$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaVersion(): Observable<string> {
|
||||||
|
return this.captchaVersionSubject$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cookieService: CookieService,
|
||||||
|
@Inject(DOCUMENT) private _document: Document,
|
||||||
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
|
rendererFactory: RendererFactory2,
|
||||||
|
private configService: ConfigurationDataService,
|
||||||
|
) {
|
||||||
|
if (this._window.nativeWindow) {
|
||||||
|
this._window.nativeWindow.refreshCaptchaScript = this.refreshCaptchaScript;
|
||||||
|
}
|
||||||
|
this.renderer = rendererFactory.createRenderer(null, null);
|
||||||
|
const registrationVerification$ = this.configService.findByPropertyName('registration.verification.enabled').pipe(
|
||||||
|
take(1),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((res: RemoteData<ConfigurationProperty>) => {
|
||||||
|
return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0].toLowerCase() === 'true';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
registrationVerification$.subscribe(registrationVerification => {
|
||||||
|
if (registrationVerification) {
|
||||||
|
this.loadRecaptchaProperties();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRecaptchaProperties() {
|
||||||
|
const recaptchaKeyRD$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
const recaptchaVersionRD$ = this.configService.findByPropertyName('google.recaptcha.version').pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
const recaptchaModeRD$ = this.configService.findByPropertyName('google.recaptcha.mode').pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
combineLatest([recaptchaVersionRD$, recaptchaModeRD$, recaptchaKeyRD$]).subscribe(([recaptchaVersionRD, recaptchaModeRD, recaptchaKeyRD]) => {
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.cookieService.get('klaro-anonymous') && this.cookieService.get('klaro-anonymous')[CAPTCHA_NAME] &&
|
||||||
|
recaptchaKeyRD.hasSucceeded && recaptchaVersionRD.hasSucceeded &&
|
||||||
|
isNotEmpty(recaptchaVersionRD.payload?.values) && isNotEmpty(recaptchaKeyRD.payload?.values)
|
||||||
|
) {
|
||||||
|
const key = recaptchaKeyRD.payload?.values[0];
|
||||||
|
const version = recaptchaVersionRD.payload?.values[0];
|
||||||
|
this.captchaKeySubject$.next(key);
|
||||||
|
this.captchaVersionSubject$.next(version);
|
||||||
|
|
||||||
|
let captchaUrl;
|
||||||
|
switch (version) {
|
||||||
|
case 'v3':
|
||||||
|
if (recaptchaKeyRD.hasSucceeded && isNotEmpty(recaptchaKeyRD.payload?.values)) {
|
||||||
|
captchaUrl = this.buildCaptchaUrl(key);
|
||||||
|
this.captchaModeSubject$.next('invisible');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'v2':
|
||||||
|
if (recaptchaModeRD.hasSucceeded && isNotEmpty(recaptchaModeRD.payload?.values)) {
|
||||||
|
captchaUrl = this.buildCaptchaUrl();
|
||||||
|
this.captchaModeSubject$.next(recaptchaModeRD.payload?.values[0]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// TODO handle error
|
||||||
|
}
|
||||||
|
if (captchaUrl) {
|
||||||
|
this.loadScript(captchaUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of string
|
||||||
|
* @param action action is the process type in which used to protect multiple spam REST calls
|
||||||
|
*/
|
||||||
|
public getRecaptchaToken(action) {
|
||||||
|
return this.captchaKey().pipe(
|
||||||
|
switchMap((key) => grecaptcha.execute(key, {action: action}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of string
|
||||||
|
*/
|
||||||
|
public executeRecaptcha() {
|
||||||
|
return of(grecaptcha.execute());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRecaptchaTokenResponse() {
|
||||||
|
return grecaptcha.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the google captcha ur with google captchas api key
|
||||||
|
*
|
||||||
|
* @param key contains a secret key of a google captchas
|
||||||
|
* @returns string which has google captcha url with google captchas key
|
||||||
|
*/
|
||||||
|
buildCaptchaUrl(key?: string) {
|
||||||
|
const apiUrl = 'https://www.google.com/recaptcha/api.js';
|
||||||
|
return key ? `${apiUrl}?render=${key}` : apiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append the google captchas script to the document
|
||||||
|
*
|
||||||
|
* @param url contains a script url which will be loaded into page
|
||||||
|
* @returns A promise
|
||||||
|
*/
|
||||||
|
private loadScript(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = this.renderer.createElement('script');
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.src = url;
|
||||||
|
script.text = ``;
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = reject;
|
||||||
|
this.renderer.appendChild(this._document.head, script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshCaptchaScript = () => {
|
||||||
|
this.loadRecaptchaProperties();
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@@ -111,7 +111,6 @@ export class OrcidAuthService {
|
|||||||
).pipe(
|
).pipe(
|
||||||
map(([authorizeUrl, clientId, scopes]) => {
|
map(([authorizeUrl, clientId, scopes]) => {
|
||||||
const redirectUri = new URLCombiner(this._window.nativeWindow.origin, encodeURIComponent(this.router.url.split('?')[0]));
|
const redirectUri = new URLCombiner(this._window.nativeWindow.origin, encodeURIComponent(this.router.url.split('?')[0]));
|
||||||
console.log(redirectUri.toString());
|
|
||||||
return authorizeUrl.values[0] + '?client_id=' + clientId.values[0] + '&redirect_uri=' + redirectUri + '&response_type=code&scope='
|
return authorizeUrl.values[0] + '?client_id=' + clientId.values[0] + '&redirect_uri=' + redirectUri + '&response_type=code&scope='
|
||||||
+ scopes.values.join(' ');
|
+ scopes.values.join(' ');
|
||||||
}));
|
}));
|
||||||
|
@@ -25,5 +25,12 @@ export class Registration implements UnCacheableObject {
|
|||||||
* The token linked to the registration
|
* The token linked to the registration
|
||||||
*/
|
*/
|
||||||
token: string;
|
token: string;
|
||||||
|
/**
|
||||||
|
* The token linked to the registration
|
||||||
|
*/
|
||||||
|
groupNames: string[];
|
||||||
|
/**
|
||||||
|
* The token linked to the registration
|
||||||
|
*/
|
||||||
|
groups: string[];
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||||
import { Injectable, InjectionToken } from '@angular/core';
|
import { Injectable, InjectionToken } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +26,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
|||||||
|
|
||||||
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
||||||
export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch');
|
export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch');
|
||||||
|
export const REFRESH_FILTER: InjectionToken<BehaviorSubject<any>> = new InjectionToken<boolean>('refreshFilters');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that performs all actions that have to do with search filters and facets
|
* Service that performs all actions that have to do with search filters and facets
|
||||||
|
@@ -13,7 +13,6 @@ import { GenericConstructor } from '../generic-constructor';
|
|||||||
import { HALEndpointService } from '../hal-endpoint.service';
|
import { HALEndpointService } from '../hal-endpoint.service';
|
||||||
import { URLCombiner } from '../../url-combiner/url-combiner';
|
import { URLCombiner } from '../../url-combiner/url-combiner';
|
||||||
import { hasValue, hasValueOperator, isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue, hasValueOperator, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { SearchOptions } from '../../../shared/search/models/search-options.model';
|
|
||||||
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
|
||||||
import { SearchResponseParsingService } from '../../data/search-response-parsing.service';
|
import { SearchResponseParsingService } from '../../data/search-response-parsing.service';
|
||||||
import { SearchObjects } from '../../../shared/search/models/search-objects.model';
|
import { SearchObjects } from '../../../shared/search/models/search-objects.model';
|
||||||
@@ -264,17 +263,26 @@ export class SearchService implements OnDestroy {
|
|||||||
* @param {number} valuePage The page number of the filter values
|
* @param {number} valuePage The page number of the filter values
|
||||||
* @param {SearchOptions} searchOptions The search configuration for the current search
|
* @param {SearchOptions} searchOptions The search configuration for the current search
|
||||||
* @param {string} filterQuery The optional query used to filter out filter values
|
* @param {string} filterQuery The optional query used to filter out filter values
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
* @returns {Observable<RemoteData<PaginatedList<FacetValue>>>} Emits the given page of facet values
|
* @returns {Observable<RemoteData<PaginatedList<FacetValue>>>} Emits the given page of facet values
|
||||||
*/
|
*/
|
||||||
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions, filterQuery?: string): Observable<RemoteData<FacetValues>> {
|
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: PaginatedSearchOptions, filterQuery?: string, useCachedVersionIfAvailable = true): Observable<RemoteData<FacetValues>> {
|
||||||
let href;
|
let href;
|
||||||
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
|
let args: string[] = [];
|
||||||
if (hasValue(filterQuery)) {
|
if (hasValue(filterQuery)) {
|
||||||
args.push(`prefix=${filterQuery}`);
|
args.push(`prefix=${filterQuery}`);
|
||||||
}
|
}
|
||||||
if (hasValue(searchOptions)) {
|
if (hasValue(searchOptions)) {
|
||||||
|
searchOptions = Object.assign(new PaginatedSearchOptions({}), searchOptions, {
|
||||||
|
pagination: Object.assign({}, searchOptions.pagination, {
|
||||||
|
currentPage: valuePage,
|
||||||
|
pageSize: filterConfig.pageSize
|
||||||
|
})
|
||||||
|
});
|
||||||
href = searchOptions.toRestUrl(filterConfig._links.self.href, args);
|
href = searchOptions.toRestUrl(filterConfig._links.self.href, args);
|
||||||
} else {
|
} else {
|
||||||
|
args = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`, ...args];
|
||||||
href = new URLCombiner(filterConfig._links.self.href, `?${args.join('&')}`).toString();
|
href = new URLCombiner(filterConfig._links.self.href, `?${args.join('&')}`).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +292,7 @@ export class SearchService implements OnDestroy {
|
|||||||
return FacetValueResponseParsingService;
|
return FacetValueResponseParsingService;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.requestService.send(request, true);
|
this.requestService.send(request, useCachedVersionIfAvailable);
|
||||||
|
|
||||||
return this.rdb.buildFromHref(href);
|
return this.rdb.buildFromHref(href);
|
||||||
}
|
}
|
||||||
|
@@ -6,8 +6,7 @@
|
|||||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
|
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
|
||||||
</ds-listable-object-component-loader>
|
</ds-listable-object-component-loader>
|
||||||
</div>
|
</div>
|
||||||
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-left ng-tns-c290-40">Load
|
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-left ng-tns-c290-40"> {{'vocabulary-treeview.load-more' | translate }} ...</button>
|
||||||
more...</button>
|
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}">
|
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}">
|
||||||
|
@@ -22,7 +22,6 @@ describe('ItemPageComponent', () => {
|
|||||||
|
|
||||||
class AcceptNoneGuard implements CanActivate {
|
class AcceptNoneGuard implements CanActivate {
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||||
console.log('BLA');
|
|
||||||
return observableOf(false);
|
return observableOf(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div>
|
<div [ngClass]="showThumbnails ? 'hide-modal-thumbnail-column' : ''">
|
||||||
<div class="modal-header">{{'virtual-metadata.delete-relationship.modal-head' | translate}}
|
<div class="modal-header">{{'virtual-metadata.delete-relationship.modal-head' | translate}}
|
||||||
<button type="button" class="close" (click)="close.emit()" aria-label="Close">
|
<button type="button" class="close" (click)="close.emit()" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
|
@@ -7,6 +7,8 @@ import { VirtualMetadataComponent } from './virtual-metadata.component';
|
|||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
import { APP_CONFIG } from '../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
describe('VirtualMetadataComponent', () => {
|
describe('VirtualMetadataComponent', () => {
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ describe('VirtualMetadataComponent', () => {
|
|||||||
declarations: [VirtualMetadataComponent, VarDirective],
|
declarations: [VirtualMetadataComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }
|
||||||
], schemas: [
|
], schemas: [
|
||||||
NO_ERRORS_SCHEMA
|
NO_ERRORS_SCHEMA
|
||||||
]
|
]
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
|
||||||
import {Observable} from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import {Item} from '../../../core/shared/item.model';
|
||||||
import {MetadataValue} from '../../../core/shared/metadata.models';
|
import {MetadataValue} from '../../../core/shared/metadata.models';
|
||||||
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-virtual-metadata',
|
selector: 'ds-virtual-metadata',
|
||||||
@@ -45,6 +46,12 @@ export class VirtualMetadataComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Output() save = new EventEmitter();
|
@Output() save = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates when thumbnails are required by configuration and therefore
|
||||||
|
* need to be hidden in the modal layout.
|
||||||
|
*/
|
||||||
|
showThumbnails: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an array of the left and the right item of the relationship to be deleted.
|
* Get an array of the left and the right item of the relationship to be deleted.
|
||||||
*/
|
*/
|
||||||
@@ -56,7 +63,9 @@ export class VirtualMetadataComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected objectUpdatesService: ObjectUpdatesService,
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
) {
|
) {
|
||||||
|
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<h2>{{MESSAGE_PREFIX + '.header'|translate}}</h2>
|
<h2>{{MESSAGE_PREFIX + '.header'|translate}}</h2>
|
||||||
<p>{{MESSAGE_PREFIX + '.info' | translate}}</p>
|
<p>{{MESSAGE_PREFIX + '.info' | translate}}</p>
|
||||||
|
|
||||||
<form [class]="'ng-invalid'" [formGroup]="form" (ngSubmit)="register()">
|
<form [class]="'ng-invalid'" [formGroup]="form">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -28,9 +28,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ds-alert [type]="AlertTypeEnum.Warning" *ngIf="registrationVerification && !isRecaptchaCookieAccepted()">
|
||||||
|
<p class="m-0" [innerHTML]="MESSAGE_PREFIX + '.google-recaptcha.must-accept-cookies' | translate"></p>
|
||||||
|
<p class="m-0"><a href="javascript:void(0);" (click)="this.klaroService.showSettings()">{{ MESSAGE_PREFIX + '.google-recaptcha.open-cookie-settings' | translate }}</a></p>
|
||||||
|
</ds-alert>
|
||||||
|
|
||||||
|
<div class="my-3" *ngIf="isRecaptchaCookieAccepted() && (googleRecaptchaService.captchaVersion() | async) === 'v2'">
|
||||||
|
<ds-google-recaptcha [captchaMode]="(googleRecaptchaService.captchaMode() | async)"
|
||||||
|
(executeRecaptcha)="register($event)" (checkboxChecked)="onCheckboxChecked($event)"
|
||||||
|
(showNotification)="showNotification($event)"></ds-google-recaptcha>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!((googleRecaptchaService.captchaVersion() | async) === 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'); else v2Invisible">
|
||||||
|
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
|
||||||
|
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #v2Invisible>
|
||||||
|
<button class="btn btn-primary" [disabled]="form.invalid" (click)="executeRecaptcha()">
|
||||||
|
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<button class="btn btn-primary"
|
|
||||||
[disabled]="form.invalid"
|
|
||||||
(click)="register()">{{MESSAGE_PREFIX + '.submit'| translate}}</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { waitForAsync, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf, of } from 'rxjs';
|
||||||
import { RestResponse } from '../core/cache/response.models';
|
import { RestResponse } from '../core/cache/response.models';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
@@ -14,6 +14,10 @@ import { RouterStub } from '../shared/testing/router.stub';
|
|||||||
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
|
||||||
import { RegisterEmailFormComponent } from './register-email-form.component';
|
import { RegisterEmailFormComponent } from './register-email-form.component';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||||
|
import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service';
|
||||||
|
import { CookieService } from '../core/services/cookie.service';
|
||||||
|
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
||||||
|
|
||||||
describe('RegisterEmailComponent', () => {
|
describe('RegisterEmailComponent', () => {
|
||||||
|
|
||||||
@@ -24,6 +28,22 @@ describe('RegisterEmailComponent', () => {
|
|||||||
let epersonRegistrationService: EpersonRegistrationService;
|
let epersonRegistrationService: EpersonRegistrationService;
|
||||||
let notificationsService;
|
let notificationsService;
|
||||||
|
|
||||||
|
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
|
findByPropertyName: jasmine.createSpy('findByPropertyName')
|
||||||
|
});
|
||||||
|
|
||||||
|
const captchaVersion$ = of('v3');
|
||||||
|
const captchaMode$ = of('invisible');
|
||||||
|
const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['true'] });
|
||||||
|
const confResponseDisabled$ = createSuccessfulRemoteDataObject$({ values: ['false'] });
|
||||||
|
|
||||||
|
const googleRecaptchaService = jasmine.createSpyObj('googleRecaptchaService', {
|
||||||
|
getRecaptchaToken: Promise.resolve('googleRecaptchaToken'),
|
||||||
|
executeRecaptcha: Promise.resolve('googleRecaptchaToken'),
|
||||||
|
getRecaptchaTokenResponse: Promise.resolve('googleRecaptchaToken'),
|
||||||
|
captchaVersion: captchaVersion$,
|
||||||
|
captchaMode: captchaMode$,
|
||||||
|
});
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
router = new RouterStub();
|
router = new RouterStub();
|
||||||
@@ -39,8 +59,11 @@ describe('RegisterEmailComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{provide: Router, useValue: router},
|
{provide: Router, useValue: router},
|
||||||
{provide: EpersonRegistrationService, useValue: epersonRegistrationService},
|
{provide: EpersonRegistrationService, useValue: epersonRegistrationService},
|
||||||
|
{provide: ConfigurationDataService, useValue: configurationDataService},
|
||||||
{provide: FormBuilder, useValue: new FormBuilder()},
|
{provide: FormBuilder, useValue: new FormBuilder()},
|
||||||
{provide: NotificationsService, useValue: notificationsService},
|
{provide: NotificationsService, useValue: notificationsService},
|
||||||
|
{provide: CookieService, useValue: new CookieServiceMock()},
|
||||||
|
{provide: GoogleRecaptchaService, useValue: googleRecaptchaService},
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -48,6 +71,9 @@ describe('RegisterEmailComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(RegisterEmailFormComponent);
|
fixture = TestBed.createComponent(RegisterEmailFormComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
googleRecaptchaService.captchaVersion$ = captchaVersion$;
|
||||||
|
googleRecaptchaService.captchaMode$ = captchaMode$;
|
||||||
|
configurationDataService.findByPropertyName.and.returnValues(confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -90,4 +116,33 @@ describe('RegisterEmailComponent', () => {
|
|||||||
expect(router.navigate).not.toHaveBeenCalled();
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('register with google recaptcha', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
configurationDataService.findByPropertyName.and.returnValues(confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$);
|
||||||
|
googleRecaptchaService.captchaVersion$ = captchaVersion$;
|
||||||
|
googleRecaptchaService.captchaMode$ = captchaMode$;
|
||||||
|
comp.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should send a registration to the service and on success display a message and return to home', fakeAsync(() => {
|
||||||
|
comp.form.patchValue({email: 'valid@email.org'});
|
||||||
|
comp.register();
|
||||||
|
tick();
|
||||||
|
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken');
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/home']);
|
||||||
|
}));
|
||||||
|
it('should send a registration to the service and on error display a message', fakeAsync(() => {
|
||||||
|
(epersonRegistrationService.registerEmail as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 400, 'Bad Request')));
|
||||||
|
|
||||||
|
comp.form.patchValue({email: 'valid@email.org'});
|
||||||
|
|
||||||
|
comp.register();
|
||||||
|
tick();
|
||||||
|
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken');
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, Input, OnInit, Optional } from '@angular/core';
|
||||||
import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
|
import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
|
||||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
@@ -6,6 +6,16 @@ import { Router } from '@angular/router';
|
|||||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { Registration } from '../core/shared/registration.model';
|
import { Registration } from '../core/shared/registration.model';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
|
||||||
|
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||||
|
import { isNotEmpty } from '../shared/empty.util';
|
||||||
|
import { BehaviorSubject, combineLatest, Observable, of, switchMap } from 'rxjs';
|
||||||
|
import { map, startWith, take } from 'rxjs/operators';
|
||||||
|
import { CAPTCHA_NAME, GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service';
|
||||||
|
import { AlertType } from '../shared/alert/aletr-type';
|
||||||
|
import { KlaroService } from '../shared/cookies/klaro.service';
|
||||||
|
import { CookieService } from '../core/services/cookie.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-register-email-form',
|
selector: 'ds-register-email-form',
|
||||||
@@ -27,12 +37,40 @@ export class RegisterEmailFormComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
MESSAGE_PREFIX: string;
|
MESSAGE_PREFIX: string;
|
||||||
|
|
||||||
|
public AlertTypeEnum = AlertType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* registration verification configuration
|
||||||
|
*/
|
||||||
|
registrationVerification = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the user completed the reCaptcha verification (checkbox mode)
|
||||||
|
*/
|
||||||
|
checkboxCheckedSubject$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
disableUntilChecked = true;
|
||||||
|
|
||||||
|
captchaVersion(): Observable<string> {
|
||||||
|
return this.googleRecaptchaService.captchaVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaMode(): Observable<string> {
|
||||||
|
return this.googleRecaptchaService.captchaMode();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private epersonRegistrationService: EpersonRegistrationService,
|
private epersonRegistrationService: EpersonRegistrationService,
|
||||||
private notificationService: NotificationsService,
|
private notificationService: NotificationsService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private formBuilder: FormBuilder
|
private formBuilder: FormBuilder,
|
||||||
|
private configService: ConfigurationDataService,
|
||||||
|
public googleRecaptchaService: GoogleRecaptchaService,
|
||||||
|
public cookieService: CookieService,
|
||||||
|
@Optional() public klaroService: KlaroService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -45,30 +83,127 @@ export class RegisterEmailFormComponent implements OnInit {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
this.configService.findByPropertyName('registration.verification.enabled').pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((res: ConfigurationProperty) => res?.values[0].toLowerCase() === 'true')
|
||||||
|
).subscribe((res: boolean) => {
|
||||||
|
this.registrationVerification = res;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.disableUntilCheckedFcn().subscribe((res) => {
|
||||||
|
this.disableUntilChecked = res;
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* execute the captcha function for v2 invisible
|
||||||
|
*/
|
||||||
|
executeRecaptcha() {
|
||||||
|
this.googleRecaptchaService.executeRecaptcha();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an email address
|
* Register an email address
|
||||||
*/
|
*/
|
||||||
register() {
|
register(tokenV2?) {
|
||||||
if (!this.form.invalid) {
|
if (!this.form.invalid) {
|
||||||
this.epersonRegistrationService.registerEmail(this.email.value).subscribe((response: RemoteData<Registration>) => {
|
if (this.registrationVerification) {
|
||||||
if (response.hasSucceeded) {
|
combineLatest([this.captchaVersion(), this.captchaMode()]).pipe(
|
||||||
this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`),
|
switchMap(([captchaVersion, captchaMode]) => {
|
||||||
this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value}));
|
if (captchaVersion === 'v3') {
|
||||||
this.router.navigate(['/home']);
|
return this.googleRecaptchaService.getRecaptchaToken('register_email');
|
||||||
} else {
|
} else if (captchaVersion === 'v2' && captchaMode === 'checkbox') {
|
||||||
this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`),
|
return of(this.googleRecaptchaService.getRecaptchaTokenResponse());
|
||||||
this.translateService.get(`${this.MESSAGE_PREFIX}.error.content`, {email: this.email.value}));
|
} else if (captchaVersion === 'v2' && captchaMode === 'invisible') {
|
||||||
|
return of(tokenV2);
|
||||||
|
} else {
|
||||||
|
console.error(`Invalid reCaptcha configuration: version = ${captchaVersion}, mode = ${captchaMode}`);
|
||||||
|
this.showNotification('error');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
take(1),
|
||||||
|
).subscribe((token) => {
|
||||||
|
if (isNotEmpty(token)) {
|
||||||
|
this.registration(token);
|
||||||
|
} else {
|
||||||
|
console.error('reCaptcha error');
|
||||||
|
this.showNotification('error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
} else {
|
||||||
|
this.registration();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration of an email address
|
||||||
|
*/
|
||||||
|
registration(captchaToken = null) {
|
||||||
|
let registerEmail$ = captchaToken ?
|
||||||
|
this.epersonRegistrationService.registerEmail(this.email.value, captchaToken) :
|
||||||
|
this.epersonRegistrationService.registerEmail(this.email.value);
|
||||||
|
registerEmail$.subscribe((response: RemoteData<Registration>) => {
|
||||||
|
if (response.hasSucceeded) {
|
||||||
|
this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`),
|
||||||
|
this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value}));
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
} else {
|
||||||
|
this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`),
|
||||||
|
this.translateService.get(`${this.MESSAGE_PREFIX}.error.content`, {email: this.email.value}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the user has accepted the required cookies for reCaptcha
|
||||||
|
*/
|
||||||
|
isRecaptchaCookieAccepted(): boolean {
|
||||||
|
const klaroAnonymousCookie = this.cookieService.get('klaro-anonymous');
|
||||||
|
return isNotEmpty(klaroAnonymousCookie) ? klaroAnonymousCookie[CAPTCHA_NAME] : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the user has not completed the reCaptcha verification (checkbox mode)
|
||||||
|
*/
|
||||||
|
disableUntilCheckedFcn(): Observable<boolean> {
|
||||||
|
const checked$ = this.checkboxCheckedSubject$.asObservable();
|
||||||
|
return combineLatest([this.captchaVersion(), this.captchaMode(), checked$]).pipe(
|
||||||
|
// disable if checkbox is not checked or if reCaptcha is not in v2 checkbox mode
|
||||||
|
switchMap(([captchaVersion, captchaMode, checked]) => captchaVersion === 'v2' && captchaMode === 'checkbox' ? of(!checked) : of(false)),
|
||||||
|
startWith(true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get email() {
|
get email() {
|
||||||
return this.form.get('email');
|
return this.form.get('email');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCheckboxChecked(checked: boolean) {
|
||||||
|
this.checkboxCheckedSubject$.next(checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a notification to the user
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
showNotification(key) {
|
||||||
|
const notificationTitle = this.translateService.get(this.MESSAGE_PREFIX + '.google-recaptcha.notification.title');
|
||||||
|
const notificationErrorMsg = this.translateService.get(this.MESSAGE_PREFIX + '.google-recaptcha.notification.message.error');
|
||||||
|
const notificationExpiredMsg = this.translateService.get(this.MESSAGE_PREFIX + '.google-recaptcha.notification.message.expired');
|
||||||
|
switch (key) {
|
||||||
|
case 'expired':
|
||||||
|
this.notificationsService.warning(notificationTitle, notificationExpiredMsg);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
this.notificationsService.error(notificationTitle, notificationErrorMsg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`Unimplemented notification '${key}' from reCaptcha service`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import { RegisterEmailFormComponent } from './register-email-form.component';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
RegisterEmailFormComponent,
|
RegisterEmailFormComponent,
|
||||||
|
@@ -17,7 +17,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { RouterMock } from '../shared/mocks/router.mock';
|
import { RouterMock } from '../shared/mocks/router.mock';
|
||||||
import { MockActivatedRoute } from '../shared/mocks/active-router.mock';
|
import { MockActivatedRoute } from '../shared/mocks/active-router.mock';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from '../shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from '../shared/testing/css-variable-service.stub';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
|
||||||
|
@@ -10,7 +10,7 @@ import { MetadataService } from '../core/metadata/metadata.service';
|
|||||||
import { HostWindowState } from '../shared/search/host-window.reducer';
|
import { HostWindowState } from '../shared/search/host-window.reducer';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../core/services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../core/services/window.service';
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../shared/sass-helper/css-variable.service';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { ThemeConfig } from '../../config/theme.model';
|
import { ThemeConfig } from '../../config/theme.model';
|
||||||
@@ -63,8 +63,8 @@ export class RootComponent implements OnInit {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
||||||
|
|
||||||
this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth');
|
this.collapsedSidebarWidth = this.cssService.getVariable('--ds-collapsed-sidebar-width');
|
||||||
this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth');
|
this.totalSidebarWidth = this.cssService.getVariable('--ds-total-sidebar-width');
|
||||||
|
|
||||||
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
||||||
this.slideSidebarOver = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()])
|
this.slideSidebarOver = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()])
|
||||||
|
@@ -21,6 +21,8 @@ describe('BrowserKlaroService', () => {
|
|||||||
const trackingIdProp = 'google.analytics.key';
|
const trackingIdProp = 'google.analytics.key';
|
||||||
const trackingIdTestValue = 'mock-tracking-id';
|
const trackingIdTestValue = 'mock-tracking-id';
|
||||||
const googleAnalytics = 'google-analytics';
|
const googleAnalytics = 'google-analytics';
|
||||||
|
const recaptchaProp = 'registration.verification.enabled';
|
||||||
|
const recaptchaValue = 'true';
|
||||||
let translateService;
|
let translateService;
|
||||||
let ePersonService;
|
let ePersonService;
|
||||||
let authService;
|
let authService;
|
||||||
@@ -31,8 +33,8 @@ describe('BrowserKlaroService', () => {
|
|||||||
let configurationDataService: ConfigurationDataService;
|
let configurationDataService: ConfigurationDataService;
|
||||||
const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', {
|
const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', {
|
||||||
findByPropertyName: createSuccessfulRemoteDataObject$({
|
findByPropertyName: createSuccessfulRemoteDataObject$({
|
||||||
...new ConfigurationProperty(),
|
... new ConfigurationProperty(),
|
||||||
name: trackingIdProp,
|
name: recaptchaProp,
|
||||||
values: values,
|
values: values,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -57,7 +59,7 @@ describe('BrowserKlaroService', () => {
|
|||||||
isAuthenticated: observableOf(true),
|
isAuthenticated: observableOf(true),
|
||||||
getAuthenticatedUserFromStore: observableOf(user)
|
getAuthenticatedUserFromStore: observableOf(user)
|
||||||
});
|
});
|
||||||
configurationDataService = createConfigSuccessSpy(trackingIdTestValue);
|
configurationDataService = createConfigSuccessSpy(recaptchaValue);
|
||||||
findByPropertyName = configurationDataService.findByPropertyName;
|
findByPropertyName = configurationDataService.findByPropertyName;
|
||||||
cookieService = jasmine.createSpyObj('cookieService', {
|
cookieService = jasmine.createSpyObj('cookieService', {
|
||||||
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
|
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
|
||||||
@@ -298,15 +300,18 @@ describe('BrowserKlaroService', () => {
|
|||||||
|
|
||||||
describe('initialize google analytics configuration', () => {
|
describe('initialize google analytics configuration', () => {
|
||||||
let GOOGLE_ANALYTICS_KEY;
|
let GOOGLE_ANALYTICS_KEY;
|
||||||
|
let REGISTRATION_VERIFICATION_ENABLED_KEY;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY);
|
GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY);
|
||||||
configurationDataService.findByPropertyName = findByPropertyName;
|
REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY);
|
||||||
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
|
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
|
||||||
translateService.get.and.returnValue(observableOf('loading...'));
|
translateService.get.and.returnValue(observableOf('loading...'));
|
||||||
spyOn(service, 'addAppMessages');
|
spyOn(service, 'addAppMessages');
|
||||||
spyOn((service as any), 'initializeUser');
|
spyOn((service as any), 'initializeUser');
|
||||||
spyOn(service, 'translateConfiguration');
|
spyOn(service, 'translateConfiguration');
|
||||||
|
configurationDataService.findByPropertyName = findByPropertyName;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not filter googleAnalytics when servicesToHide are empty', () => {
|
it('should not filter googleAnalytics when servicesToHide are empty', () => {
|
||||||
const filteredConfig = (service as any).filterConfigServices([]);
|
const filteredConfig = (service as any).filterConfigServices([]);
|
||||||
expect(filteredConfig).toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
expect(filteredConfig).toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||||
@@ -316,31 +321,75 @@ describe('BrowserKlaroService', () => {
|
|||||||
expect(filteredConfig).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
expect(filteredConfig).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||||
});
|
});
|
||||||
it('should have been initialized with googleAnalytics', () => {
|
it('should have been initialized with googleAnalytics', () => {
|
||||||
|
configurationDataService.findByPropertyName = jasmine.createSpy('configurationDataService').and.returnValue(
|
||||||
|
createSuccessfulRemoteDataObject$({
|
||||||
|
...new ConfigurationProperty(),
|
||||||
|
name: trackingIdProp,
|
||||||
|
values: [googleAnalytics],
|
||||||
|
})
|
||||||
|
);
|
||||||
service.initialize();
|
service.initialize();
|
||||||
expect(service.klaroConfig.services).toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
expect(service.klaroConfig.services).toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||||
});
|
});
|
||||||
it('should filter googleAnalytics when empty configuration is retrieved', () => {
|
it('should filter googleAnalytics when empty configuration is retrieved', () => {
|
||||||
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
configurationDataService.findByPropertyName =
|
||||||
createSuccessfulRemoteDataObject$({
|
jasmine.createSpy()
|
||||||
...new ConfigurationProperty(),
|
.withArgs(GOOGLE_ANALYTICS_KEY)
|
||||||
name: googleAnalytics,
|
.and
|
||||||
values: [],
|
.returnValue(
|
||||||
}));
|
createSuccessfulRemoteDataObject$({
|
||||||
|
... new ConfigurationProperty(),
|
||||||
|
name: googleAnalytics,
|
||||||
|
values: [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
|
||||||
|
.and
|
||||||
|
.returnValue(
|
||||||
|
createSuccessfulRemoteDataObject$({
|
||||||
|
... new ConfigurationProperty(),
|
||||||
|
name: trackingIdTestValue,
|
||||||
|
values: ['false'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
service.initialize();
|
service.initialize();
|
||||||
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||||
});
|
});
|
||||||
it('should filter googleAnalytics when an error occurs', () => {
|
it('should filter googleAnalytics when an error occurs', () => {
|
||||||
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
configurationDataService.findByPropertyName =
|
||||||
createFailedRemoteDataObject$('Erro while loading GA')
|
jasmine.createSpy()
|
||||||
);
|
.withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||||
|
createFailedRemoteDataObject$('Error while loading GA')
|
||||||
|
)
|
||||||
|
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
|
||||||
|
.and
|
||||||
|
.returnValue(
|
||||||
|
createSuccessfulRemoteDataObject$({
|
||||||
|
... new ConfigurationProperty(),
|
||||||
|
name: trackingIdTestValue,
|
||||||
|
values: ['false'],
|
||||||
|
})
|
||||||
|
);
|
||||||
service.initialize();
|
service.initialize();
|
||||||
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||||
});
|
});
|
||||||
it('should filter googleAnalytics when an invalid payload is retrieved', () => {
|
it('should filter googleAnalytics when an invalid payload is retrieved', () => {
|
||||||
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
configurationDataService.findByPropertyName =
|
||||||
createSuccessfulRemoteDataObject$(null)
|
jasmine.createSpy()
|
||||||
);
|
.withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||||
|
createSuccessfulRemoteDataObject$(null)
|
||||||
|
)
|
||||||
|
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
|
||||||
|
.and
|
||||||
|
.returnValue(
|
||||||
|
createSuccessfulRemoteDataObject$({
|
||||||
|
... new ConfigurationProperty(),
|
||||||
|
name: trackingIdTestValue,
|
||||||
|
values: ['false'],
|
||||||
|
})
|
||||||
|
);
|
||||||
service.initialize();
|
service.initialize();
|
||||||
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||||
});
|
});
|
||||||
|
@@ -15,6 +15,7 @@ import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-config
|
|||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||||
|
import { CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata field to store a user's cookie consent preferences in
|
* Metadata field to store a user's cookie consent preferences in
|
||||||
@@ -49,6 +50,8 @@ export class BrowserKlaroService extends KlaroService {
|
|||||||
|
|
||||||
private readonly GOOGLE_ANALYTICS_KEY = 'google.analytics.key';
|
private readonly GOOGLE_ANALYTICS_KEY = 'google.analytics.key';
|
||||||
|
|
||||||
|
private readonly REGISTRATION_VERIFICATION_ENABLED_KEY = 'registration.verification.enabled';
|
||||||
|
|
||||||
private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics';
|
private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,15 +81,30 @@ export class BrowserKlaroService extends KlaroService {
|
|||||||
this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
|
this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
|
||||||
}
|
}
|
||||||
|
|
||||||
const servicesToHide$: Observable<string[]> = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
|
const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
map(remoteData => {
|
map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)),
|
||||||
if (!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)) {
|
);
|
||||||
return [this.GOOGLE_ANALYTICS_SERVICE_NAME];
|
|
||||||
} else {
|
const hideRegistrationVerification$ = this.configService.findByPropertyName(this.REGISTRATION_VERIFICATION_ENABLED_KEY).pipe(
|
||||||
return [];
|
getFirstCompletedRemoteData(),
|
||||||
|
map((remoteData) =>
|
||||||
|
!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() !== 'true'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const servicesToHide$: Observable<string[]> = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$]).pipe(
|
||||||
|
map(([hideGoogleAnalytics, hideRegistrationVerification]) => {
|
||||||
|
let servicesToHideArray: string[] = [];
|
||||||
|
if (hideGoogleAnalytics) {
|
||||||
|
servicesToHideArray.push(this.GOOGLE_ANALYTICS_SERVICE_NAME);
|
||||||
}
|
}
|
||||||
}),
|
if (hideRegistrationVerification) {
|
||||||
|
servicesToHideArray.push(CAPTCHA_NAME);
|
||||||
|
}
|
||||||
|
console.log(servicesToHideArray);
|
||||||
|
return servicesToHideArray;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.translateService.setDefaultLang(environment.defaultLanguage);
|
this.translateService.setDefaultLang(environment.defaultLanguage);
|
||||||
@@ -308,4 +326,5 @@ export class BrowserKlaroService extends KlaroService {
|
|||||||
private filterConfigServices(servicesToHide: string[]): Pick<typeof klaroConfiguration, 'services'>[] {
|
private filterConfigServices(servicesToHide: string[]): Pick<typeof klaroConfiguration, 'services'>[] {
|
||||||
return this.klaroConfig.services.filter(service => !servicesToHide.some(name => name === service.name));
|
return this.klaroConfig.services.filter(service => !servicesToHide.some(name => name === service.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { TOKENITEM } from '../../core/auth/models/auth-token-info.model';
|
import { TOKENITEM } from '../../core/auth/models/auth-token-info.model';
|
||||||
import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service';
|
import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service';
|
||||||
import { LANG_COOKIE } from '../../core/locale/locale.service';
|
import { LANG_COOKIE } from '../../core/locale/locale.service';
|
||||||
|
import { CAPTCHA_COOKIE, CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cookie for has_agreed_end_user
|
* Cookie for has_agreed_end_user
|
||||||
@@ -157,5 +158,17 @@ export const klaroConfiguration: any = {
|
|||||||
*/
|
*/
|
||||||
onlyOnce: true,
|
onlyOnce: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: CAPTCHA_NAME,
|
||||||
|
purposes: ['registration-password-recovery'],
|
||||||
|
required: false,
|
||||||
|
cookies: [
|
||||||
|
[/^klaro-.+$/],
|
||||||
|
CAPTCHA_COOKIE
|
||||||
|
],
|
||||||
|
onAccept: `window.refreshCaptchaScript?.call()`,
|
||||||
|
onDecline: `window.refreshCaptchaScript?.call()`,
|
||||||
|
onlyOnce: true,
|
||||||
|
}
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@@ -1,4 +1,10 @@
|
|||||||
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core';
|
import {
|
||||||
|
DynamicFormControlLayout,
|
||||||
|
DynamicFormControlRelation,
|
||||||
|
DynamicFormGroupModel,
|
||||||
|
DynamicFormGroupModelConfig,
|
||||||
|
serializable
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@@ -16,6 +22,7 @@ export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig {
|
|||||||
separator: string;
|
separator: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
|
typeBindRelations?: DynamicFormControlRelation[];
|
||||||
relationship?: RelationshipOptions;
|
relationship?: RelationshipOptions;
|
||||||
repeatable: boolean;
|
repeatable: boolean;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
@@ -29,6 +36,8 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
|
|||||||
|
|
||||||
@serializable() separator: string;
|
@serializable() separator: string;
|
||||||
@serializable() hasLanguages = false;
|
@serializable() hasLanguages = false;
|
||||||
|
@serializable() typeBindRelations: DynamicFormControlRelation[];
|
||||||
|
@serializable() typeBindHidden = false;
|
||||||
@serializable() relationship?: RelationshipOptions;
|
@serializable() relationship?: RelationshipOptions;
|
||||||
@serializable() repeatable?: boolean;
|
@serializable() repeatable?: boolean;
|
||||||
@serializable() required?: boolean;
|
@serializable() required?: boolean;
|
||||||
@@ -55,6 +64,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
|
|||||||
this.metadataValue = config.metadataValue;
|
this.metadataValue = config.metadataValue;
|
||||||
this.valueUpdates = new Subject<string>();
|
this.valueUpdates = new Subject<string>();
|
||||||
this.valueUpdates.subscribe((value: string) => this.value = value);
|
this.valueUpdates.subscribe((value: string) => this.value = value);
|
||||||
|
this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="g-recaptcha"
|
||||||
|
[attr.data-callback]="'dataCallback'"
|
||||||
|
[attr.data-expired-callback]="'expiredCallback'"
|
||||||
|
[attr.data-error-callback]="'errorCallback'"
|
||||||
|
[attr.data-sitekey]="(recaptchaKey$ | async)?.values[0]"
|
||||||
|
[attr.data-size]="captchaMode === 'invisible' ? 'invisible' : null"></div>
|
@@ -0,0 +1,50 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NativeWindowService } from '../../core/services/window.service';
|
||||||
|
|
||||||
|
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||||
|
import { NativeWindowMockFactory } from '../mocks/mock-native-window-ref';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
|
import { GoogleRecaptchaComponent } from './google-recaptcha.component';
|
||||||
|
|
||||||
|
describe('GoogleRecaptchaComponent', () => {
|
||||||
|
|
||||||
|
let component: GoogleRecaptchaComponent;
|
||||||
|
|
||||||
|
let fixture: ComponentFixture<GoogleRecaptchaComponent>;
|
||||||
|
|
||||||
|
|
||||||
|
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
|
findByPropertyName: jasmine.createSpy('findByPropertyName')
|
||||||
|
});
|
||||||
|
|
||||||
|
const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['valid-google-recaptcha-key'] });
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ GoogleRecaptchaComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||||
|
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(GoogleRecaptchaComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
configurationDataService.findByPropertyName.and.returnValues(confResponse$);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rendered google recaptcha.',() => {
|
||||||
|
const container = fixture.debugElement.query(By.css('.g-recaptcha'));
|
||||||
|
expect(container).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
|
||||||
|
|
||||||
|
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from 'src/app/core/services/window.service';
|
||||||
|
import { isNotEmpty } from '../empty.util';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-google-recaptcha',
|
||||||
|
templateUrl: './google-recaptcha.component.html',
|
||||||
|
styleUrls: ['./google-recaptcha.component.scss'],
|
||||||
|
})
|
||||||
|
export class GoogleRecaptchaComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() captchaMode: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An EventEmitter that's fired whenever the form is being submitted
|
||||||
|
*/
|
||||||
|
@Output() executeRecaptcha: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
@Output() checkboxChecked: EventEmitter<boolean> = new EventEmitter();
|
||||||
|
|
||||||
|
@Output() showNotification: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
recaptchaKey$: Observable<any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
|
private configService: ConfigurationDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the google recaptcha site key
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
this._window.nativeWindow.dataCallback = this.dataCallbackFcn;
|
||||||
|
this._window.nativeWindow.expiredCallback = this.expiredCallbackFcn;
|
||||||
|
this._window.nativeWindow.errorCallback = this.errorCallbackFcn;
|
||||||
|
}
|
||||||
|
|
||||||
|
dataCallbackFcn = ($event) => {
|
||||||
|
switch (this.captchaMode) {
|
||||||
|
case 'invisible':
|
||||||
|
this.executeRecaptcha.emit($event);
|
||||||
|
break;
|
||||||
|
case 'checkbox':
|
||||||
|
this.checkboxChecked.emit(isNotEmpty($event));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Invalid reCaptcha mode '${this.captchaMode}`);
|
||||||
|
this.showNotification.emit('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expiredCallbackFcn = () => {
|
||||||
|
this.checkboxChecked.emit(false);
|
||||||
|
this.showNotification.emit('expired');
|
||||||
|
};
|
||||||
|
|
||||||
|
errorCallbackFcn = () => {
|
||||||
|
this.showNotification.emit('error');
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@@ -7,7 +7,7 @@ import { createSelector, select, Store } from '@ngrx/store';
|
|||||||
|
|
||||||
import { hasValue } from './empty.util';
|
import { hasValue } from './empty.util';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
import { CSSVariableService } from './sass-helper/sass-helper.service';
|
import { CSSVariableService } from './sass-helper/css-variable.service';
|
||||||
|
|
||||||
export enum WidthCategory {
|
export enum WidthCategory {
|
||||||
XS,
|
XS,
|
||||||
@@ -31,10 +31,10 @@ export class HostWindowService {
|
|||||||
/* See _exposed_variables.scss */
|
/* See _exposed_variables.scss */
|
||||||
variableService.getAllVariables()
|
variableService.getAllVariables()
|
||||||
.subscribe((variables) => {
|
.subscribe((variables) => {
|
||||||
this.breakPoints.XL_MIN = parseInt(variables.xlMin, 10);
|
this.breakPoints.XL_MIN = parseInt(variables['--bs-xl-min'], 10);
|
||||||
this.breakPoints.LG_MIN = parseInt(variables.lgMin, 10);
|
this.breakPoints.LG_MIN = parseInt(variables['--bs-lg-min'], 10);
|
||||||
this.breakPoints.MD_MIN = parseInt(variables.mdMin, 10);
|
this.breakPoints.MD_MIN = parseInt(variables['--bs-md-min'], 10);
|
||||||
this.breakPoints.SM_MIN = parseInt(variables.smMin, 10);
|
this.breakPoints.SM_MIN = parseInt(variables['--bs-sm-min'], 10);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
src/app/shared/key-value-pair.model.ts
Normal file
4
src/app/shared/key-value-pair.model.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface KeyValuePair<K, V> {
|
||||||
|
key: K;
|
||||||
|
value: V;
|
||||||
|
}
|
@@ -10,11 +10,11 @@ import { RequestService } from '../../../../core/data/request.service';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { switchMap, take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { CLAIMED_TASK } from '../../../../core/tasks/models/claimed-task-object.resource-type';
|
import { CLAIMED_TASK } from '../../../../core/tasks/models/claimed-task-object.resource-type';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { MyDSpaceReloadableActionsComponent } from '../../mydspace-reloadable-actions';
|
import { MyDSpaceReloadableActionsComponent } from '../../mydspace-reloadable-actions';
|
||||||
|
import { isEmpty } from '../../../empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract component for rendering a claimed task's action
|
* Abstract component for rendering a claimed task's action
|
||||||
@@ -36,6 +36,11 @@ export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReload
|
|||||||
|
|
||||||
object: ClaimedTask;
|
object: ClaimedTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item object that belonging to the ClaimedTask object
|
||||||
|
*/
|
||||||
|
item: Item;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anchor used to reload the pool task.
|
* Anchor used to reload the pool task.
|
||||||
*/
|
*/
|
||||||
@@ -43,6 +48,11 @@ export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReload
|
|||||||
|
|
||||||
subs = [];
|
subs = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The workflowitem object that belonging to the ClaimedTask object
|
||||||
|
*/
|
||||||
|
workflowitem: WorkflowItem;
|
||||||
|
|
||||||
protected constructor(protected injector: Injector,
|
protected constructor(protected injector: Injector,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
@@ -85,16 +95,10 @@ export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReload
|
|||||||
* Retrieve the itemUuid.
|
* Retrieve the itemUuid.
|
||||||
*/
|
*/
|
||||||
initReloadAnchor() {
|
initReloadAnchor() {
|
||||||
if (!(this.object as any).workflowitem) {
|
if (isEmpty(this.item)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.subs.push(this.object.workflowitem.pipe(
|
this.itemUuid = this.item.uuid;
|
||||||
getFirstSucceededRemoteDataPayload(),
|
|
||||||
switchMap((workflowItem: WorkflowItem) => workflowItem.item.pipe(getFirstSucceededRemoteDataPayload())
|
|
||||||
))
|
|
||||||
.subscribe((item: Item) => {
|
|
||||||
this.itemUuid = item.uuid;
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@@ -1,16 +1,23 @@
|
|||||||
<ng-container *ngVar="(actionRD$ | async)?.payload as workflowAction">
|
<ng-container *ngVar="(actionRD$ | async)?.payload as workflowAction">
|
||||||
<div class="mt-1 mb-3 space-children-mr">
|
<div class="mt-1 mb-3 space-children-mr">
|
||||||
<ds-claimed-task-actions-loader *ngFor="let option of workflowAction?.options" [option]="option" [object]="object"
|
<ds-claimed-task-actions-loader *ngFor="let option of workflowAction?.options"
|
||||||
(processCompleted)="this.processCompleted.emit($event)">
|
[item]="item"
|
||||||
|
[option]="option"
|
||||||
|
[object]="object"
|
||||||
|
[workflowitem]="workflowitem"
|
||||||
|
(processCompleted)="this.processCompleted.emit($event)">
|
||||||
</ds-claimed-task-actions-loader>
|
</ds-claimed-task-actions-loader>
|
||||||
|
|
||||||
<button class="btn btn-primary workflow-view" ngbTooltip="{{'submission.workflow.generic.view-help' | translate}}"
|
<button class="btn btn-primary workflow-view" ngbTooltip="{{'submission.workflow.generic.view-help' | translate}}"
|
||||||
[routerLink]="[getWorkflowItemViewRoute((workflowitem$ | async))]">
|
[routerLink]="[getWorkflowItemViewRoute(workflowitem)]">
|
||||||
<i class="fa fa-info-circle"></i> {{"submission.workflow.generic.view" | translate}}
|
<i class="fa fa-info-circle"></i> {{"submission.workflow.generic.view" | translate}}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ds-claimed-task-actions-loader [option]="returnToPoolOption" [object]="object"
|
<ds-claimed-task-actions-loader [item]="item"
|
||||||
(processCompleted)="this.processCompleted.emit($event)">
|
[option]="returnToPoolOption"
|
||||||
|
[object]="object"
|
||||||
|
[workflowitem]="workflowitem"
|
||||||
|
(processCompleted)="this.processCompleted.emit($event)">
|
||||||
</ds-claimed-task-actions-loader>
|
</ds-claimed-task-actions-loader>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -3,7 +3,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { cold } from 'jasmine-marbles';
|
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||||
@@ -123,7 +122,9 @@ describe('ClaimedTaskActionsComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ClaimedTaskActionsComponent);
|
fixture = TestBed.createComponent(ClaimedTaskActionsComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
component.item = item;
|
||||||
component.object = mockObject;
|
component.object = mockObject;
|
||||||
|
component.workflowitem = workflowitem;
|
||||||
notificationsServiceStub = TestBed.inject(NotificationsService as any);
|
notificationsServiceStub = TestBed.inject(NotificationsService as any);
|
||||||
router = TestBed.inject(Router as any);
|
router = TestBed.inject(Router as any);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -133,11 +134,11 @@ describe('ClaimedTaskActionsComponent', () => {
|
|||||||
component.object = null;
|
component.object = null;
|
||||||
component.initObjects(mockObject);
|
component.initObjects(mockObject);
|
||||||
|
|
||||||
|
expect(component.item).toEqual(item);
|
||||||
|
|
||||||
expect(component.object).toEqual(mockObject);
|
expect(component.object).toEqual(mockObject);
|
||||||
|
|
||||||
expect(component.workflowitem$).toBeObservable(cold('(b|)', {
|
expect(component.workflowitem).toEqual(workflowitem);
|
||||||
b: rdWorkflowitem.payload
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reload page on process completed', waitForAsync(() => {
|
it('should reload page on process completed', waitForAsync(() => {
|
||||||
|
@@ -2,12 +2,10 @@ import { Component, Injector, Input, OnInit } from '@angular/core';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, take } from 'rxjs/operators';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
|
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
|
||||||
import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model';
|
import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model';
|
||||||
import { isNotUndefined } from '../../empty.util';
|
|
||||||
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { MyDSpaceActionsComponent } from '../mydspace-actions';
|
import { MyDSpaceActionsComponent } from '../mydspace-actions';
|
||||||
@@ -18,6 +16,7 @@ import { WorkflowAction } from '../../../core/tasks/models/workflow-action-objec
|
|||||||
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
|
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
|
||||||
import { WORKFLOW_TASK_OPTION_RETURN_TO_POOL } from './return-to-pool/claimed-task-actions-return-to-pool.component';
|
import { WORKFLOW_TASK_OPTION_RETURN_TO_POOL } from './return-to-pool/claimed-task-actions-return-to-pool.component';
|
||||||
import { getWorkflowItemViewRoute } from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
import { getWorkflowItemViewRoute } from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents actions related to ClaimedTask object.
|
* This component represents actions related to ClaimedTask object.
|
||||||
@@ -34,10 +33,15 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
|
|||||||
*/
|
*/
|
||||||
@Input() object: ClaimedTask;
|
@Input() object: ClaimedTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item object that belonging to the ClaimedTask object
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the ClaimedTask object
|
* The workflowitem object that belonging to the ClaimedTask object
|
||||||
*/
|
*/
|
||||||
public workflowitem$: Observable<WorkflowItem>;
|
@Input() workflowitem: WorkflowItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The workflow action available for this task
|
* The workflow action available for this task
|
||||||
@@ -87,11 +91,6 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
|
|||||||
*/
|
*/
|
||||||
initObjects(object: ClaimedTask) {
|
initObjects(object: ClaimedTask) {
|
||||||
this.object = object;
|
this.object = object;
|
||||||
|
|
||||||
this.workflowitem$ = (this.object.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
|
||||||
filter((rd: RemoteData<WorkflowItem>) => ((!rd.isRequestPending) && isNotUndefined(rd.payload))),
|
|
||||||
map((rd: RemoteData<WorkflowItem>) => rd.payload),
|
|
||||||
take(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ClaimedTaskActionsLoaderComponent } from './claimed-task-actions-loader.component';
|
import { ClaimedTaskActionsLoaderComponent } from './claimed-task-actions-loader.component';
|
||||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive';
|
import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive';
|
||||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||||
@@ -15,6 +15,8 @@ import { RequestService } from '../../../../core/data/request.service';
|
|||||||
import { PoolTaskDataService } from '../../../../core/tasks/pool-task-data.service';
|
import { PoolTaskDataService } from '../../../../core/tasks/pool-task-data.service';
|
||||||
import { getMockSearchService } from '../../../mocks/search-service.mock';
|
import { getMockSearchService } from '../../../mocks/search-service.mock';
|
||||||
import { getMockRequestService } from '../../../mocks/request.service.mock';
|
import { getMockRequestService } from '../../../mocks/request.service.mock';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
|
|
||||||
const searchService = getMockSearchService();
|
const searchService = getMockSearchService();
|
||||||
|
|
||||||
@@ -27,6 +29,37 @@ describe('ClaimedTaskActionsLoaderComponent', () => {
|
|||||||
const option = 'test_option';
|
const option = 'test_option';
|
||||||
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
|
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
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 workflowitem = Object.assign(new WorkflowItem(), { id: '333' });
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
@@ -52,8 +85,10 @@ describe('ClaimedTaskActionsLoaderComponent', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
fixture = TestBed.createComponent(ClaimedTaskActionsLoaderComponent);
|
fixture = TestBed.createComponent(ClaimedTaskActionsLoaderComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = item;
|
||||||
comp.object = object;
|
comp.object = object;
|
||||||
comp.option = option;
|
comp.option = option;
|
||||||
|
comp.workflowitem = workflowitem;
|
||||||
spyOn(comp, 'getComponentByWorkflowTaskOption').and.returnValue(ClaimedTaskActionsEditMetadataComponent);
|
spyOn(comp, 'getComponentByWorkflowTaskOption').and.returnValue(ClaimedTaskActionsEditMetadataComponent);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@@ -15,6 +15,8 @@ import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-ac
|
|||||||
import { hasValue } from '../../../empty.util';
|
import { hasValue } from '../../../empty.util';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { MyDSpaceActionsResult } from '../../mydspace-actions';
|
import { MyDSpaceActionsResult } from '../../mydspace-actions';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-claimed-task-actions-loader',
|
selector: 'ds-claimed-task-actions-loader',
|
||||||
@@ -25,6 +27,11 @@ import { MyDSpaceActionsResult } from '../../mydspace-actions';
|
|||||||
* Passes on the ClaimedTask to the component and subscribes to the processCompleted output
|
* Passes on the ClaimedTask to the component and subscribes to the processCompleted output
|
||||||
*/
|
*/
|
||||||
export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
|
export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* The item object that belonging to the ClaimedTask object
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ClaimedTask object
|
* The ClaimedTask object
|
||||||
*/
|
*/
|
||||||
@@ -36,6 +43,11 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Input() option: string;
|
@Input() option: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The workflowitem object that belonging to the ClaimedTask object
|
||||||
|
*/
|
||||||
|
@Input() workflowitem: WorkflowItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the success or failure of a processed action
|
* Emits the success or failure of a processed action
|
||||||
*/
|
*/
|
||||||
@@ -69,7 +81,9 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const componentRef = viewContainerRef.createComponent(componentFactory);
|
const componentRef = viewContainerRef.createComponent(componentFactory);
|
||||||
const componentInstance = (componentRef.instance as ClaimedTaskActionsAbstractComponent);
|
const componentInstance = (componentRef.instance as ClaimedTaskActionsAbstractComponent);
|
||||||
|
componentInstance.item = this.item;
|
||||||
componentInstance.object = this.object;
|
componentInstance.object = this.object;
|
||||||
|
componentInstance.workflowitem = this.workflowitem;
|
||||||
if (hasValue(componentInstance.processCompleted)) {
|
if (hasValue(componentInstance.processCompleted)) {
|
||||||
this.subs.push(componentInstance.processCompleted.subscribe((result) => this.processCompleted.emit(result)));
|
this.subs.push(componentInstance.processCompleted.subscribe((result) => this.processCompleted.emit(result)));
|
||||||
}
|
}
|
||||||
|
@@ -12,10 +12,7 @@ import { RouterStub } from '../testing/router.stub';
|
|||||||
import { getMockSearchService } from '../mocks/search-service.mock';
|
import { getMockSearchService } from '../mocks/search-service.mock';
|
||||||
import { getMockRequestService } from '../mocks/request.service.mock';
|
import { getMockRequestService } from '../mocks/request.service.mock';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import {
|
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||||
createFailedRemoteDataObject,
|
|
||||||
createSuccessfulRemoteDataObject
|
|
||||||
} from '../remote-data.utils';
|
|
||||||
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
|
||||||
import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
|
||||||
import { NotificationsService } from '../notifications/notifications.service';
|
import { NotificationsService } from '../notifications/notifications.service';
|
||||||
@@ -103,7 +100,9 @@ describe('MyDSpaceReloadableActionsComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(PoolTaskActionsComponent);
|
fixture = TestBed.createComponent(PoolTaskActionsComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
component.item = item;
|
||||||
component.object = mockObject;
|
component.object = mockObject;
|
||||||
|
component.workflowitem = workflowitem;
|
||||||
notificationsServiceStub = TestBed.get(NotificationsService);
|
notificationsServiceStub = TestBed.get(NotificationsService);
|
||||||
router = TestBed.get(Router);
|
router = TestBed.get(Router);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@@ -8,6 +8,6 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary workflow-view ml-1 mt-1 mb-3" data-test="view-btn"
|
<button class="btn btn-primary workflow-view ml-1 mt-1 mb-3" data-test="view-btn"
|
||||||
ngbTooltip="{{'submission.workflow.generic.view-help' | translate}}"
|
ngbTooltip="{{'submission.workflow.generic.view-help' | translate}}"
|
||||||
[routerLink]="[getWorkflowItemViewRoute((workflowitem$ | async))]">
|
[routerLink]="[getWorkflowItemViewRoute(workflowitem)]">
|
||||||
<i class="fa fa-info-circle"></i> {{"submission.workflow.generic.view" | translate}}
|
<i class="fa fa-info-circle"></i> {{"submission.workflow.generic.view" | translate}}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -4,7 +4,6 @@ import { Router } from '@angular/router';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { cold } from 'jasmine-marbles';
|
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||||
@@ -105,7 +104,9 @@ describe('PoolTaskActionsComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(PoolTaskActionsComponent);
|
fixture = TestBed.createComponent(PoolTaskActionsComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
component.item = item;
|
||||||
component.object = mockObject;
|
component.object = mockObject;
|
||||||
|
component.workflowitem = workflowitem;
|
||||||
notificationsServiceStub = TestBed.inject(NotificationsService as any);
|
notificationsServiceStub = TestBed.inject(NotificationsService as any);
|
||||||
router = TestBed.inject(Router as any);
|
router = TestBed.inject(Router as any);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -120,11 +121,11 @@ describe('PoolTaskActionsComponent', () => {
|
|||||||
component.object = null;
|
component.object = null;
|
||||||
component.initObjects(mockObject);
|
component.initObjects(mockObject);
|
||||||
|
|
||||||
|
expect(component.item).toEqual(item);
|
||||||
|
|
||||||
expect(component.object).toEqual(mockObject);
|
expect(component.object).toEqual(mockObject);
|
||||||
|
|
||||||
expect(component.workflowitem$).toBeObservable(cold('(b|)', {
|
expect(component.workflowitem).toEqual(workflowitem);
|
||||||
b: rdWorkflowitem.payload
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display claim task button', () => {
|
it('should display claim task button', () => {
|
||||||
|
@@ -2,19 +2,17 @@ import { Component, Injector, Input, OnDestroy } from '@angular/core';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { switchMap, take } from 'rxjs/operators';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PoolTask } from '../../../core/tasks/models/pool-task-object.model';
|
import { PoolTask } from '../../../core/tasks/models/pool-task-object.model';
|
||||||
import { PoolTaskDataService } from '../../../core/tasks/pool-task-data.service';
|
import { PoolTaskDataService } from '../../../core/tasks/pool-task-data.service';
|
||||||
import { isNotUndefined } from '../../empty.util';
|
|
||||||
import { NotificationsService } from '../../notifications/notifications.service';
|
import { NotificationsService } from '../../notifications/notifications.service';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
|
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { MyDSpaceReloadableActionsComponent } from '../mydspace-reloadable-actions';
|
import { MyDSpaceReloadableActionsComponent } from '../mydspace-reloadable-actions';
|
||||||
@@ -36,10 +34,15 @@ export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent
|
|||||||
*/
|
*/
|
||||||
@Input() object: PoolTask;
|
@Input() object: PoolTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item object that belonging to the PoolTask object
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the PoolTask object
|
* The workflowitem object that belonging to the PoolTask object
|
||||||
*/
|
*/
|
||||||
public workflowitem$: Observable<WorkflowItem>;
|
@Input() workflowitem: WorkflowItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anchor used to reload the pool task.
|
* Anchor used to reload the pool task.
|
||||||
@@ -83,10 +86,6 @@ export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent
|
|||||||
*/
|
*/
|
||||||
initObjects(object: PoolTask) {
|
initObjects(object: PoolTask) {
|
||||||
this.object = object;
|
this.object = object;
|
||||||
this.workflowitem$ = (this.object.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
|
||||||
filter((rd: RemoteData<WorkflowItem>) => ((!rd.isRequestPending) && isNotUndefined(rd.payload))),
|
|
||||||
map((rd: RemoteData<WorkflowItem>) => rd.payload),
|
|
||||||
take(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actionExecution(): Observable<ProcessTaskResponse> {
|
actionExecution(): Observable<ProcessTaskResponse> {
|
||||||
@@ -104,13 +103,7 @@ export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent
|
|||||||
* Retrieve the itemUuid.
|
* Retrieve the itemUuid.
|
||||||
*/
|
*/
|
||||||
initReloadAnchor() {
|
initReloadAnchor() {
|
||||||
(this.object as any).workflowitem.pipe(
|
this.itemUuid = this.item.uuid;
|
||||||
getFirstSucceededRemoteDataPayload(),
|
|
||||||
switchMap((workflowItem: WorkflowItem) => workflowItem.item.pipe(getFirstSucceededRemoteDataPayload())
|
|
||||||
))
|
|
||||||
.subscribe((item: Item) => {
|
|
||||||
this.itemUuid = item.uuid;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@@ -20,7 +20,7 @@
|
|||||||
[importable]="importable"
|
[importable]="importable"
|
||||||
[importConfig]="importConfig"
|
[importConfig]="importConfig"
|
||||||
(importObject)="importObject.emit($event)"
|
(importObject)="importObject.emit($event)"
|
||||||
(contentChange)="contentChange.emit()"
|
(contentChange)="contentChange.emit($event)"
|
||||||
(prev)="goPrev()"
|
(prev)="goPrev()"
|
||||||
(next)="goNext()"
|
(next)="goNext()"
|
||||||
*ngIf="(currentMode$ | async) === viewModeEnum.ListElement">
|
*ngIf="(currentMode$ | async) === viewModeEnum.ListElement">
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
[context]="context"
|
[context]="context"
|
||||||
[hidePaginationDetail]="hidePaginationDetail"
|
[hidePaginationDetail]="hidePaginationDetail"
|
||||||
[showPaginator]="showPaginator"
|
[showPaginator]="showPaginator"
|
||||||
|
(contentChange)="contentChange.emit($event)"
|
||||||
*ngIf="(currentMode$ | async) === viewModeEnum.DetailedListElement">
|
*ngIf="(currentMode$ | async) === viewModeEnum.DetailedListElement">
|
||||||
</ds-object-detail>
|
</ds-object-detail>
|
||||||
|
|
||||||
|
@@ -59,6 +59,11 @@ export class ObjectCollectionComponent implements OnInit {
|
|||||||
@Input() hideGear = false;
|
@Input() hideGear = false;
|
||||||
@Input() selectable = false;
|
@Input() selectable = false;
|
||||||
@Input() selectionConfig: {repeatable: boolean, listId: string};
|
@Input() selectionConfig: {repeatable: boolean, listId: string};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit custom event for listable object custom actions.
|
||||||
|
*/
|
||||||
|
@Output() customEvent = new EventEmitter<any>();
|
||||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
|
||||||
|
@@ -5,7 +5,9 @@ import { ListableObject } from '../listable-object.model';
|
|||||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
import { Context } from '../../../../core/shared/context.model';
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { ItemListElementComponent } from '../../../object-list/item-list-element/item-types/item/item-list-element.component';
|
import {
|
||||||
|
ItemListElementComponent
|
||||||
|
} from '../../../object-list/item-list-element/item-types/item/item-list-element.component';
|
||||||
import { ListableObjectDirective } from './listable-object.directive';
|
import { ListableObjectDirective } from './listable-object.directive';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
@@ -146,7 +148,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
|||||||
expect((comp as any).instantiateComponent).not.toHaveBeenCalled();
|
expect((comp as any).instantiateComponent).not.toHaveBeenCalled();
|
||||||
|
|
||||||
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
||||||
tick();
|
tick(200);
|
||||||
|
|
||||||
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject);
|
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject);
|
||||||
}));
|
}));
|
||||||
@@ -155,7 +157,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
|||||||
expect((comp as any).contentChange.emit).not.toHaveBeenCalled();
|
expect((comp as any).contentChange.emit).not.toHaveBeenCalled();
|
||||||
|
|
||||||
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
||||||
tick();
|
tick(200);
|
||||||
|
|
||||||
expect((comp as any).contentChange.emit).toHaveBeenCalledWith(reloadedObject);
|
expect((comp as any).contentChange.emit).toHaveBeenCalledWith(reloadedObject);
|
||||||
}));
|
}));
|
||||||
|
@@ -1,17 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ComponentFactoryResolver,
|
ComponentRef,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
ViewChild,
|
|
||||||
EventEmitter,
|
|
||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
OnChanges,
|
ViewChild
|
||||||
ComponentRef
|
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ListableObject } from '../listable-object.model';
|
import { ListableObject } from '../listable-object.model';
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { Context } from '../../../../core/shared/context.model';
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
@@ -20,9 +24,7 @@ import { GenericConstructor } from '../../../../core/shared/generic-constructor'
|
|||||||
import { ListableObjectDirective } from './listable-object.directive';
|
import { ListableObjectDirective } from './listable-object.directive';
|
||||||
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
||||||
import { hasValue, isNotEmpty } from '../../../empty.util';
|
import { hasValue, isNotEmpty } from '../../../empty.util';
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { ThemeService } from '../../../theme-support/theme.service';
|
import { ThemeService } from '../../../theme-support/theme.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -82,7 +84,7 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
|
|||||||
/**
|
/**
|
||||||
* Directive hook used to place the dynamic child component
|
* Directive hook used to place the dynamic child component
|
||||||
*/
|
*/
|
||||||
@ViewChild(ListableObjectDirective, {static: true}) listableObjectDirective: ListableObjectDirective;
|
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View on the badges template, to be passed on to the loaded component (which will place the badges in the desired
|
* View on the badges template, to be passed on to the loaded component (which will place the badges in the desired
|
||||||
@@ -120,22 +122,19 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
|
|||||||
* The list of input and output names for the dynamic component
|
* The list of input and output names for the dynamic component
|
||||||
*/
|
*/
|
||||||
protected inAndOutputNames: string[] = [
|
protected inAndOutputNames: string[] = [
|
||||||
'object',
|
'object',
|
||||||
'index',
|
'index',
|
||||||
'linkType',
|
'linkType',
|
||||||
'listID',
|
'listID',
|
||||||
'showLabel',
|
'showLabel',
|
||||||
'context',
|
'context',
|
||||||
'viewMode',
|
'viewMode',
|
||||||
'value',
|
'value',
|
||||||
'hideBadges',
|
'hideBadges',
|
||||||
'contentChange',
|
'contentChange',
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(private cdr: ChangeDetectorRef, private themeService: ThemeService) {
|
||||||
private componentFactoryResolver: ComponentFactoryResolver,
|
|
||||||
private themeService: ThemeService
|
|
||||||
) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,27 +165,30 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
|
|||||||
|
|
||||||
const component = this.getComponent(object.getRenderTypes(), this.viewMode, this.context);
|
const component = this.getComponent(object.getRenderTypes(), this.viewMode, this.context);
|
||||||
|
|
||||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
|
|
||||||
|
|
||||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||||
viewContainerRef.clear();
|
viewContainerRef.clear();
|
||||||
|
|
||||||
this.compRef = viewContainerRef.createComponent(
|
this.compRef = viewContainerRef.createComponent(
|
||||||
componentFactory,
|
component, {
|
||||||
0,
|
index: 0,
|
||||||
undefined,
|
injector: undefined,
|
||||||
[
|
projectableNodes: [
|
||||||
[this.badges.nativeElement],
|
[this.badges.nativeElement],
|
||||||
]);
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.connectInputsAndOutputs();
|
this.connectInputsAndOutputs();
|
||||||
|
|
||||||
if ((this.compRef.instance as any).reloadedObject) {
|
if ((this.compRef.instance as any).reloadedObject) {
|
||||||
(this.compRef.instance as any).reloadedObject.pipe(take(1)).subscribe((reloadedObject: DSpaceObject) => {
|
(this.compRef.instance as any).reloadedObject.pipe(
|
||||||
|
take(1)
|
||||||
|
).subscribe((reloadedObject: DSpaceObject) => {
|
||||||
if (reloadedObject) {
|
if (reloadedObject) {
|
||||||
this.compRef.destroy();
|
this.compRef.destroy();
|
||||||
this.object = reloadedObject;
|
this.object = reloadedObject;
|
||||||
this.instantiateComponent(reloadedObject);
|
this.instantiateComponent(reloadedObject);
|
||||||
|
this.cdr.detectChanges();
|
||||||
this.contentChange.emit(reloadedObject);
|
this.contentChange.emit(reloadedObject);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<div class="mt-2 mb-2">
|
<div class="mt-2 mb-2" *ngIf="(submitter$ | async)">
|
||||||
<span class="text-muted">{{'submission.workflow.tasks.generic.submitter' | translate}} : <span class="badge badge-info">{{(submitter$ | async)?.name}}</span></span>
|
<span class="text-muted">{{'submission.workflow.tasks.generic.submitter' | translate}} : <span class="badge badge-info">{{(submitter$ | async)?.name}}</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -11,12 +11,11 @@ import { EPersonMock } from '../../../testing/eperson.mock';
|
|||||||
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
import { getMockLinkService } from '../../../mocks/link-service.mock';
|
||||||
|
|
||||||
let component: ItemSubmitterComponent;
|
let component: ItemSubmitterComponent;
|
||||||
let fixture: ComponentFixture<ItemSubmitterComponent>;
|
let fixture: ComponentFixture<ItemSubmitterComponent>;
|
||||||
|
|
||||||
const compIndex = 1;
|
|
||||||
|
|
||||||
let mockResultObject: PoolTask;
|
let mockResultObject: PoolTask;
|
||||||
|
|
||||||
const rdSumbitter = createSuccessfulRemoteDataObject(EPersonMock);
|
const rdSumbitter = createSuccessfulRemoteDataObject(EPersonMock);
|
||||||
@@ -36,6 +35,9 @@ describe('ItemSubmitterComponent', () => {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
declarations: [ItemSubmitterComponent],
|
declarations: [ItemSubmitterComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: LinkService, useValue: getMockLinkService() },
|
||||||
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(ItemSubmitterComponent, {
|
}).overrideComponent(ItemSubmitterComponent, {
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { EMPTY, Observable } from 'rxjs';
|
||||||
import { filter, find, map, mergeMap } from 'rxjs/operators';
|
import { map, mergeMap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { isNotEmpty, isNotUndefined } from '../../../empty.util';
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents a badge with submitter information.
|
* This component represents a badge with submitter information.
|
||||||
@@ -24,18 +27,38 @@ export class ItemSubmitterComponent implements OnInit {
|
|||||||
@Input() object: any;
|
@Input() object: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Eperson object
|
* The submitter object
|
||||||
*/
|
*/
|
||||||
submitter$: Observable<EPerson>;
|
submitter$: Observable<EPerson>;
|
||||||
|
|
||||||
|
public constructor(protected linkService: LinkService) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize submitter object
|
* Initialize submitter object
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.linkService.resolveLinks(this.object, followLink('workflowitem', {},
|
||||||
|
followLink('submitter',{})
|
||||||
|
));
|
||||||
this.submitter$ = (this.object.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
this.submitter$ = (this.object.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
||||||
filter((rd: RemoteData<WorkflowItem>) => (rd.hasSucceeded && isNotUndefined(rd.payload))),
|
getFirstCompletedRemoteData(),
|
||||||
mergeMap((rd: RemoteData<WorkflowItem>) => rd.payload.submitter as Observable<RemoteData<EPerson>>),
|
mergeMap((rd: RemoteData<WorkflowItem>) => {
|
||||||
find((rd: RemoteData<EPerson>) => rd.hasSucceeded && isNotEmpty(rd.payload)),
|
if (rd.hasSucceeded && isNotEmpty(rd.payload)) {
|
||||||
map((rd: RemoteData<EPerson>) => rd.payload));
|
return (rd.payload.submitter as Observable<RemoteData<EPerson>>).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rds: RemoteData<EPerson>) => {
|
||||||
|
if (rds.hasSucceeded && isNotEmpty(rds.payload)) {
|
||||||
|
return rds.payload;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
<ng-container *ngIf="(workflowitem$ | async) && (item$ | async)">
|
||||||
<ds-item-detail-preview *ngIf="workflowitem"
|
<ds-item-detail-preview [item]="item$?.value"
|
||||||
[item]="(workflowitem.item | async)?.payload"
|
|
||||||
[object]="object"
|
[object]="object"
|
||||||
[showSubmitter]="showSubmitter"
|
[showSubmitter]="showSubmitter"
|
||||||
[status]="status">
|
[status]="status">
|
||||||
</ds-item-detail-preview>
|
</ds-item-detail-preview>
|
||||||
|
|
||||||
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
|
<ds-claimed-task-actions [item]="item$.value"
|
||||||
|
[object]="dso"
|
||||||
|
[workflowitem]="workflowitem$.value"
|
||||||
|
(processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed, tick, waitForAsync, fakeAsync} from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
@@ -7,7 +7,9 @@ import { of as observableOf } from 'rxjs';
|
|||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { ClaimedTaskSearchResultDetailElementComponent } from './claimed-task-search-result-detail-element.component';
|
import { ClaimedTaskSearchResultDetailElementComponent } from './claimed-task-search-result-detail-element.component';
|
||||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import {
|
||||||
|
MyDspaceItemStatusType
|
||||||
|
} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
@@ -15,6 +17,7 @@ import { VarDirective } from '../../../utils/var.directive';
|
|||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { getMockLinkService } from '../../../mocks/link-service.mock';
|
import { getMockLinkService } from '../../../mocks/link-service.mock';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
|
|
||||||
let component: ClaimedTaskSearchResultDetailElementComponent;
|
let component: ClaimedTaskSearchResultDetailElementComponent;
|
||||||
let fixture: ComponentFixture<ClaimedTaskSearchResultDetailElementComponent>;
|
let fixture: ComponentFixture<ClaimedTaskSearchResultDetailElementComponent>;
|
||||||
@@ -58,6 +61,9 @@ const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdIt
|
|||||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||||
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||||
const linkService = getMockLinkService();
|
const linkService = getMockLinkService();
|
||||||
|
const objectCacheServiceMock = jasmine.createSpyObj('ObjectCacheService', {
|
||||||
|
remove: jasmine.createSpy('remove')
|
||||||
|
});
|
||||||
|
|
||||||
describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -65,7 +71,8 @@ describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
|||||||
imports: [NoopAnimationsModule],
|
imports: [NoopAnimationsModule],
|
||||||
declarations: [ClaimedTaskSearchResultDetailElementComponent, VarDirective],
|
declarations: [ClaimedTaskSearchResultDetailElementComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: LinkService, useValue: linkService }
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: ObjectCacheService, useValue: objectCacheServiceMock }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(ClaimedTaskSearchResultDetailElementComponent, {
|
}).overrideComponent(ClaimedTaskSearchResultDetailElementComponent, {
|
||||||
@@ -83,18 +90,16 @@ describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init workflowitem properly', (done) => {
|
it('should init workflowitem properly', fakeAsync(() => {
|
||||||
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
flush();
|
||||||
// Make sure the necessary links are being resolved
|
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
||||||
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
component.dso,
|
||||||
component.dso,
|
jasmine.objectContaining({ name: 'workflowitem' }),
|
||||||
jasmine.objectContaining({ name: 'workflowitem' }),
|
jasmine.objectContaining({ name: 'action' })
|
||||||
jasmine.objectContaining({ name: 'action' })
|
);
|
||||||
);
|
expect(component.workflowitem$.value).toEqual(workflowitem);
|
||||||
expect(workflowitemRD.payload).toEqual(workflowitem);
|
expect(component.item$.value).toEqual(item);
|
||||||
done();
|
}));
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have properly status', () => {
|
it('should have properly status', () => {
|
||||||
expect(component.status).toEqual(MyDspaceItemStatusType.VALIDATION);
|
expect(component.status).toEqual(MyDspaceItemStatusType.VALIDATION);
|
||||||
|
@@ -1,17 +1,24 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { BehaviorSubject, EMPTY, Observable } from 'rxjs';
|
||||||
|
import { mergeMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||||
import { SearchResultDetailElementComponent } from '../search-result-detail-element.component';
|
import { SearchResultDetailElementComponent } from '../search-result-detail-element.component';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import {
|
||||||
|
MyDspaceItemStatusType
|
||||||
|
} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
import { followLink } from '../../../utils/follow-link-config.model';
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders claimed task object for the search result in the detail view.
|
* This component renders claimed task object for the search result in the detail view.
|
||||||
@@ -23,7 +30,12 @@ import { LinkService } from '../../../../core/cache/builders/link.service';
|
|||||||
})
|
})
|
||||||
|
|
||||||
@listableObjectComponent(ClaimedTaskSearchResult, ViewMode.DetailedListElement)
|
@listableObjectComponent(ClaimedTaskSearchResult, ViewMode.DetailedListElement)
|
||||||
export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultDetailElementComponent<ClaimedTaskSearchResult, ClaimedTask> {
|
export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultDetailElementComponent<ClaimedTaskSearchResult, ClaimedTask> implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item object that belonging to the result object
|
||||||
|
*/
|
||||||
|
public item$: BehaviorSubject<Item> = new BehaviorSubject<Item>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if to show submitter information
|
* A boolean representing if to show submitter information
|
||||||
@@ -38,9 +50,9 @@ export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultD
|
|||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the result object
|
* The workflowitem object that belonging to the result object
|
||||||
*/
|
*/
|
||||||
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
|
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);
|
||||||
|
|
||||||
constructor(protected linkService: LinkService) {
|
constructor(protected linkService: LinkService, protected objectCache: ObjectCacheService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +65,30 @@ export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultD
|
|||||||
followLink('item', {}, followLink('bundles')),
|
followLink('item', {}, followLink('bundles')),
|
||||||
followLink('submitter')
|
followLink('submitter')
|
||||||
), followLink('action'));
|
), followLink('action'));
|
||||||
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
|
||||||
|
(this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
mergeMap((wfiRD: RemoteData<WorkflowItem>) => {
|
||||||
|
if (wfiRD.hasSucceeded) {
|
||||||
|
this.workflowitem$.next(wfiRD.payload);
|
||||||
|
return (wfiRD.payload.item as Observable<RemoteData<Item>>).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
tap((itemRD: RemoteData<Item>) => {
|
||||||
|
if (isNotEmpty(itemRD) && itemRD.hasSucceeded) {
|
||||||
|
this.item$.next(itemRD.payload);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// This ensures the object is removed from cache, when action is performed on task
|
||||||
|
this.objectCache.remove(this.dso._links.workflowitem.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
<ng-container *ngIf="(workflowitem$ | async) && (item$ | async)">
|
||||||
<ds-item-detail-preview *ngIf="workflowitem"
|
<ds-item-detail-preview [item]="item$?.value"
|
||||||
[item]="(workflowitem?.item | async)?.payload"
|
|
||||||
[object]="object"
|
[object]="object"
|
||||||
[showSubmitter]="showSubmitter"
|
[showSubmitter]="showSubmitter"
|
||||||
[status]="status"></ds-item-detail-preview>
|
[status]="status"></ds-item-detail-preview>
|
||||||
|
|
||||||
<ds-pool-task-actions *ngIf="workflowitem" [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-pool-task-actions>
|
<ds-pool-task-actions [item]="item$.value"
|
||||||
|
[object]="dso"
|
||||||
|
[workflowitem]="workflowitem$.value"
|
||||||
|
(processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-pool-task-actions>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import {
|
||||||
|
MyDspaceItemStatusType
|
||||||
|
} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||||
import { PoolSearchResultDetailElementComponent } from './pool-search-result-detail-element.component';
|
import { PoolSearchResultDetailElementComponent } from './pool-search-result-detail-element.component';
|
||||||
@@ -15,8 +17,7 @@ import { VarDirective } from '../../../utils/var.directive';
|
|||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { getMockLinkService } from '../../../mocks/link-service.mock';
|
import { getMockLinkService } from '../../../mocks/link-service.mock';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock';
|
|
||||||
|
|
||||||
let component: PoolSearchResultDetailElementComponent;
|
let component: PoolSearchResultDetailElementComponent;
|
||||||
let fixture: ComponentFixture<PoolSearchResultDetailElementComponent>;
|
let fixture: ComponentFixture<PoolSearchResultDetailElementComponent>;
|
||||||
@@ -60,6 +61,9 @@ const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdIt
|
|||||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||||
mockResultObject.indexableObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
mockResultObject.indexableObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||||
const linkService = getMockLinkService();
|
const linkService = getMockLinkService();
|
||||||
|
const objectCacheServiceMock = jasmine.createSpyObj('ObjectCacheService', {
|
||||||
|
remove: jasmine.createSpy('remove')
|
||||||
|
});
|
||||||
|
|
||||||
describe('PoolSearchResultDetailElementComponent', () => {
|
describe('PoolSearchResultDetailElementComponent', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -70,7 +74,7 @@ describe('PoolSearchResultDetailElementComponent', () => {
|
|||||||
{ provide: 'objectElementProvider', useValue: (mockResultObject) },
|
{ provide: 'objectElementProvider', useValue: (mockResultObject) },
|
||||||
{ provide: 'indexElementProvider', useValue: (compIndex) },
|
{ provide: 'indexElementProvider', useValue: (compIndex) },
|
||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
{ provide: ObjectCacheService, useValue: objectCacheServiceMock }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(PoolSearchResultDetailElementComponent, {
|
}).overrideComponent(PoolSearchResultDetailElementComponent, {
|
||||||
@@ -88,17 +92,16 @@ describe('PoolSearchResultDetailElementComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init workflowitem properly', (done) => {
|
it('should init workflowitem properly', fakeAsync(() => {
|
||||||
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
flush();
|
||||||
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
||||||
component.dso,
|
component.dso,
|
||||||
jasmine.objectContaining({ name: 'workflowitem' }),
|
jasmine.objectContaining({ name: 'workflowitem' }),
|
||||||
jasmine.objectContaining({ name: 'action' })
|
jasmine.objectContaining({ name: 'action' })
|
||||||
);
|
);
|
||||||
expect(workflowitemRD.payload).toEqual(workflowitem);
|
expect(component.workflowitem$.value).toEqual(workflowitem);
|
||||||
done();
|
expect(component.item$.value).toEqual(item);
|
||||||
});
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
it('should have properly status', () => {
|
it('should have properly status', () => {
|
||||||
expect(component.status).toEqual(MyDspaceItemStatusType.WAITING_CONTROLLER);
|
expect(component.status).toEqual(MyDspaceItemStatusType.WAITING_CONTROLLER);
|
||||||
|
@@ -1,16 +1,24 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, EMPTY, Observable } from 'rxjs';
|
||||||
|
import { mergeMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
||||||
import { SearchResultDetailElementComponent } from '../search-result-detail-element.component';
|
import { SearchResultDetailElementComponent } from '../search-result-detail-element.component';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import {
|
||||||
|
MyDspaceItemStatusType
|
||||||
|
} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
||||||
import { followLink } from '../../../utils/follow-link-config.model';
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders pool task object for the search result in the detail view.
|
* This component renders pool task object for the search result in the detail view.
|
||||||
@@ -22,7 +30,12 @@ import { LinkService } from '../../../../core/cache/builders/link.service';
|
|||||||
})
|
})
|
||||||
|
|
||||||
@listableObjectComponent(PoolTaskSearchResult, ViewMode.DetailedListElement)
|
@listableObjectComponent(PoolTaskSearchResult, ViewMode.DetailedListElement)
|
||||||
export class PoolSearchResultDetailElementComponent extends SearchResultDetailElementComponent<PoolTaskSearchResult, PoolTask> {
|
export class PoolSearchResultDetailElementComponent extends SearchResultDetailElementComponent<PoolTaskSearchResult, PoolTask> implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item object that belonging to the result object
|
||||||
|
*/
|
||||||
|
public item$: BehaviorSubject<Item> = new BehaviorSubject<Item>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if to show submitter information
|
* A boolean representing if to show submitter information
|
||||||
@@ -37,9 +50,9 @@ export class PoolSearchResultDetailElementComponent extends SearchResultDetailEl
|
|||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the result object
|
* The workflowitem object that belonging to the result object
|
||||||
*/
|
*/
|
||||||
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
|
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);
|
||||||
|
|
||||||
constructor(protected linkService: LinkService) {
|
constructor(protected linkService: LinkService, protected objectCache: ObjectCacheService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +65,31 @@ export class PoolSearchResultDetailElementComponent extends SearchResultDetailEl
|
|||||||
followLink('item', {}, followLink('bundles')),
|
followLink('item', {}, followLink('bundles')),
|
||||||
followLink('submitter')
|
followLink('submitter')
|
||||||
), followLink('action'));
|
), followLink('action'));
|
||||||
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
|
||||||
|
(this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
mergeMap((wfiRD: RemoteData<WorkflowItem>) => {
|
||||||
|
if (wfiRD.hasSucceeded) {
|
||||||
|
this.workflowitem$.next(wfiRD.payload);
|
||||||
|
return (wfiRD.payload.item as Observable<RemoteData<Item>>).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
tap((itemRD: RemoteData<Item>) => {
|
||||||
|
if (isNotEmpty(itemRD) && itemRD.hasSucceeded) {
|
||||||
|
this.item$.next(itemRD.payload);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// This ensures the object is removed from cache, when action is performed on task
|
||||||
|
this.objectCache.remove(this.dso._links.workflowitem.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -14,12 +14,14 @@
|
|||||||
(sortFieldChange)="onSortFieldChange($event)"
|
(sortFieldChange)="onSortFieldChange($event)"
|
||||||
(paginationChange)="onPaginationChange($event)"
|
(paginationChange)="onPaginationChange($event)"
|
||||||
(prev)="goPrev()"
|
(prev)="goPrev()"
|
||||||
(next)="goNext()"
|
(next)="goNext()">
|
||||||
>
|
|
||||||
<div class="row mt-2" *ngIf="objects?.hasSucceeded" @fadeIn>
|
<div class="row mt-2" *ngIf="objects?.hasSucceeded" @fadeIn>
|
||||||
<div class="col"
|
<div class="col"
|
||||||
*ngFor="let object of objects?.payload?.page">
|
*ngFor="let object of objects?.payload?.page">
|
||||||
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [context]="context"></ds-listable-object-component-loader>
|
<ds-listable-object-component-loader [object]="object"
|
||||||
|
[viewMode]="viewMode"
|
||||||
|
[context]="context"
|
||||||
|
(contentChange)="contentChange.emit($event)"></ds-listable-object-component-loader>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="objects.hasFailed" message="{{'error.objects' | translate}}"></ds-error>
|
<ds-error *ngIf="objects.hasFailed" message="{{'error.objects' | translate}}"></ds-error>
|
||||||
|
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
EventEmitter,
|
|
||||||
Input,
|
|
||||||
Output,
|
|
||||||
ViewEncapsulation
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
@@ -71,6 +64,11 @@ export class ObjectDetailComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() showPaginator = true;
|
@Input() showPaginator = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit when one of the listed object has changed.
|
||||||
|
*/
|
||||||
|
@Output() contentChange = new EventEmitter<any>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If showPaginator is set to true, emit when the previous button is clicked
|
* If showPaginator is set to true, emit when the previous button is clicked
|
||||||
*/
|
*/
|
||||||
|
@@ -1,9 +1,15 @@
|
|||||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
<ng-container *ngIf="(workflowitem$ | async) && (item$ | async)">
|
||||||
<ds-themed-item-list-preview *ngIf="workflowitem"
|
<ds-themed-item-list-preview [item]="item$.value"
|
||||||
[item]="(workflowitem?.item | async)?.payload"
|
[object]="object"
|
||||||
[object]="object"
|
[showSubmitter]="showSubmitter"
|
||||||
[showSubmitter]="showSubmitter"
|
[status]="status"></ds-themed-item-list-preview>
|
||||||
[status]="status"></ds-themed-item-list-preview>
|
|
||||||
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
|
||||||
|
<ds-claimed-task-actions [item]="item$.value"
|
||||||
|
[object]="dso"
|
||||||
|
[workflowitem]="workflowitem$.value"
|
||||||
|
(processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
@@ -7,7 +7,9 @@ import { of as observableOf } from 'rxjs';
|
|||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { ClaimedSearchResultListElementComponent } from './claimed-search-result-list-element.component';
|
import { ClaimedSearchResultListElementComponent } from './claimed-search-result-list-element.component';
|
||||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import {
|
||||||
|
MyDspaceItemStatusType
|
||||||
|
} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
@@ -20,6 +22,7 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
|||||||
import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock';
|
||||||
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
||||||
import { environment } from '../../../../../environments/environment';
|
import { environment } from '../../../../../environments/environment';
|
||||||
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
|
|
||||||
let component: ClaimedSearchResultListElementComponent;
|
let component: ClaimedSearchResultListElementComponent;
|
||||||
let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
|
let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
|
||||||
@@ -61,6 +64,9 @@ const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdIt
|
|||||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||||
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||||
const linkService = getMockLinkService();
|
const linkService = getMockLinkService();
|
||||||
|
const objectCacheServiceMock = jasmine.createSpyObj('ObjectCacheService', {
|
||||||
|
remove: jasmine.createSpy('remove')
|
||||||
|
});
|
||||||
|
|
||||||
describe('ClaimedSearchResultListElementComponent', () => {
|
describe('ClaimedSearchResultListElementComponent', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -71,7 +77,8 @@ describe('ClaimedSearchResultListElementComponent', () => {
|
|||||||
{ provide: TruncatableService, useValue: {} },
|
{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
{ provide: APP_CONFIG, useValue: environment }
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
|
{ provide: ObjectCacheService, useValue: objectCacheServiceMock }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(ClaimedSearchResultListElementComponent, {
|
}).overrideComponent(ClaimedSearchResultListElementComponent, {
|
||||||
@@ -89,17 +96,16 @@ describe('ClaimedSearchResultListElementComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init workflowitem properly', (done) => {
|
it('should init workflowitem properly', fakeAsync(() => {
|
||||||
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
flush();
|
||||||
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
||||||
component.dso,
|
component.dso,
|
||||||
jasmine.objectContaining({ name: 'workflowitem' }),
|
jasmine.objectContaining({ name: 'workflowitem' }),
|
||||||
jasmine.objectContaining({ name: 'action' })
|
jasmine.objectContaining({ name: 'action' })
|
||||||
);
|
);
|
||||||
expect(workflowitemRD.payload).toEqual(workflowitem);
|
expect(component.workflowitem$.value).toEqual(workflowitem);
|
||||||
done();
|
expect(component.item$.value).toEqual(item);
|
||||||
});
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
it('should have properly status', () => {
|
it('should have properly status', () => {
|
||||||
expect(component.status).toEqual(MyDspaceItemStatusType.VALIDATION);
|
expect(component.status).toEqual(MyDspaceItemStatusType.VALIDATION);
|
||||||
|
@@ -1,19 +1,28 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import {
|
||||||
import { Observable } from 'rxjs';
|
MyDspaceItemStatusType
|
||||||
|
} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
|
import { BehaviorSubject, EMPTY, Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { followLink } from '../../../utils/follow-link-config.model';
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
|
import {
|
||||||
|
SearchResultListElementComponent
|
||||||
|
} from '../../search-result-list-element/search-result-list-element.component';
|
||||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
|
||||||
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { mergeMap, tap } from 'rxjs/operators';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-claimed-search-result-list-element',
|
selector: 'ds-claimed-search-result-list-element',
|
||||||
@@ -21,7 +30,7 @@ import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interfac
|
|||||||
templateUrl: './claimed-search-result-list-element.component.html'
|
templateUrl: './claimed-search-result-list-element.component.html'
|
||||||
})
|
})
|
||||||
@listableObjectComponent(ClaimedTaskSearchResult, ViewMode.ListElement)
|
@listableObjectComponent(ClaimedTaskSearchResult, ViewMode.ListElement)
|
||||||
export class ClaimedSearchResultListElementComponent extends SearchResultListElementComponent<ClaimedTaskSearchResult, ClaimedTask> {
|
export class ClaimedSearchResultListElementComponent extends SearchResultListElementComponent<ClaimedTaskSearchResult, ClaimedTask> implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if to show submitter information
|
* A boolean representing if to show submitter information
|
||||||
@@ -33,15 +42,26 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
|
|||||||
*/
|
*/
|
||||||
public status = MyDspaceItemStatusType.VALIDATION;
|
public status = MyDspaceItemStatusType.VALIDATION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item object that belonging to the result object
|
||||||
|
*/
|
||||||
|
public item$: BehaviorSubject<Item> = new BehaviorSubject<Item>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the result object
|
* The workflowitem object that belonging to the result object
|
||||||
*/
|
*/
|
||||||
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
|
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display thumbnails if required by configuration
|
||||||
|
*/
|
||||||
|
showThumbnails: boolean;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected linkService: LinkService,
|
protected linkService: LinkService,
|
||||||
protected truncatableService: TruncatableService,
|
protected truncatableService: TruncatableService,
|
||||||
protected dsoNameService: DSONameService,
|
protected dsoNameService: DSONameService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
||||||
) {
|
) {
|
||||||
super(truncatableService, dsoNameService, appConfig);
|
super(truncatableService, dsoNameService, appConfig);
|
||||||
@@ -53,9 +73,35 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.linkService.resolveLinks(this.dso, followLink('workflowitem', {},
|
this.linkService.resolveLinks(this.dso, followLink('workflowitem', {},
|
||||||
followLink('item'), followLink('submitter')
|
followLink('item', {}, followLink('bundles')),
|
||||||
|
followLink('submitter')
|
||||||
), followLink('action'));
|
), followLink('action'));
|
||||||
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
|
||||||
|
(this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
mergeMap((wfiRD: RemoteData<WorkflowItem>) => {
|
||||||
|
if (wfiRD.hasSucceeded) {
|
||||||
|
this.workflowitem$.next(wfiRD.payload);
|
||||||
|
return (wfiRD.payload.item as Observable<RemoteData<Item>>).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
tap((itemRD: RemoteData<Item>) => {
|
||||||
|
if (isNotEmpty(itemRD) && itemRD.hasSucceeded) {
|
||||||
|
this.item$.next(itemRD.payload);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// This ensures the object is removed from cache, when action is performed on task
|
||||||
|
this.objectCache.remove(this.dso._links.workflowitem.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
<ng-container *ngIf="(workflowitem$ | async) && (item$ | async)">
|
||||||
<ds-themed-item-list-preview *ngIf="workflowitem"
|
<ds-themed-item-list-preview [item]="item$.value"
|
||||||
[item]="(workflowitem?.item | async)?.payload"
|
[object]="object"
|
||||||
[object]="object"
|
[showSubmitter]="showSubmitter"
|
||||||
[showSubmitter]="showSubmitter"
|
[status]="status"></ds-themed-item-list-preview>
|
||||||
[status]="status"></ds-themed-item-list-preview>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
|
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
|
||||||
<ds-pool-task-actions id="actions" *ngIf="workflowitem" [object]="dso"
|
<ds-pool-task-actions id="actions"
|
||||||
|
[item]="item$.value"
|
||||||
|
[object]="dso"
|
||||||
|
[workflowitem]="workflowitem$.value"
|
||||||
(processCompleted)="this.reloadedObject.emit($event.reloadedObject)"></ds-pool-task-actions>
|
(processCompleted)="this.reloadedObject.emit($event.reloadedObject)"></ds-pool-task-actions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { waitForAsync, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
@@ -7,7 +7,9 @@ import { of as observableOf } from 'rxjs';
|
|||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { PoolSearchResultListElementComponent } from './pool-search-result-list-element.component';
|
import { PoolSearchResultListElementComponent } from './pool-search-result-list-element.component';
|
||||||
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import {
|
||||||
|
MyDspaceItemStatusType
|
||||||
|
} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||||
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
||||||
@@ -19,6 +21,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock';
|
||||||
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
||||||
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
|
|
||||||
let component: PoolSearchResultListElementComponent;
|
let component: PoolSearchResultListElementComponent;
|
||||||
let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
|
let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
|
||||||
@@ -67,6 +70,9 @@ const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdIt
|
|||||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||||
mockResultObject.indexableObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
mockResultObject.indexableObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||||
const linkService = getMockLinkService();
|
const linkService = getMockLinkService();
|
||||||
|
const objectCacheServiceMock = jasmine.createSpyObj('ObjectCacheService', {
|
||||||
|
remove: jasmine.createSpy('remove')
|
||||||
|
});
|
||||||
|
|
||||||
describe('PoolSearchResultListElementComponent', () => {
|
describe('PoolSearchResultListElementComponent', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -77,7 +83,8 @@ describe('PoolSearchResultListElementComponent', () => {
|
|||||||
{ provide: TruncatableService, useValue: {} },
|
{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
|
{ provide: APP_CONFIG, useValue: environmentUseThumbs },
|
||||||
|
{ provide: ObjectCacheService, useValue: objectCacheServiceMock }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(PoolSearchResultListElementComponent, {
|
}).overrideComponent(PoolSearchResultListElementComponent, {
|
||||||
@@ -95,17 +102,16 @@ describe('PoolSearchResultListElementComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init workflowitem properly', (done) => {
|
it('should init workflowitem properly', fakeAsync(() => {
|
||||||
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
flush();
|
||||||
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
||||||
component.dso,
|
component.dso,
|
||||||
jasmine.objectContaining({ name: 'workflowitem' }),
|
jasmine.objectContaining({ name: 'workflowitem' }),
|
||||||
jasmine.objectContaining({ name: 'action' })
|
jasmine.objectContaining({ name: 'action' })
|
||||||
);
|
);
|
||||||
expect(workflowitemRD.payload).toEqual(workflowitem);
|
expect(component.workflowitem$.value).toEqual(workflowitem);
|
||||||
done();
|
expect(component.item$.value).toEqual(item);
|
||||||
});
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
it('should have properly status', () => {
|
it('should have properly status', () => {
|
||||||
expect(component.status).toEqual(MyDspaceItemStatusType.WAITING_CONTROLLER);
|
expect(component.status).toEqual(MyDspaceItemStatusType.WAITING_CONTROLLER);
|
||||||
|
@@ -1,20 +1,29 @@
|
|||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { BehaviorSubject, EMPTY, Observable } from 'rxjs';
|
||||||
|
import { mergeMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import {
|
||||||
|
MyDspaceItemStatusType
|
||||||
|
} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
||||||
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
|
import {
|
||||||
|
SearchResultListElementComponent
|
||||||
|
} from '../../search-result-list-element/search-result-list-element.component';
|
||||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||||
import { followLink } from '../../../utils/follow-link-config.model';
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
|
||||||
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders pool task object for the search result in the list view.
|
* This component renders pool task object for the search result in the list view.
|
||||||
@@ -26,7 +35,7 @@ import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interfac
|
|||||||
})
|
})
|
||||||
|
|
||||||
@listableObjectComponent(PoolTaskSearchResult, ViewMode.ListElement)
|
@listableObjectComponent(PoolTaskSearchResult, ViewMode.ListElement)
|
||||||
export class PoolSearchResultListElementComponent extends SearchResultListElementComponent<PoolTaskSearchResult, PoolTask> implements OnInit {
|
export class PoolSearchResultListElementComponent extends SearchResultListElementComponent<PoolTaskSearchResult, PoolTask> implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if to show submitter information
|
* A boolean representing if to show submitter information
|
||||||
@@ -38,10 +47,15 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
|
|||||||
*/
|
*/
|
||||||
public status = MyDspaceItemStatusType.WAITING_CONTROLLER;
|
public status = MyDspaceItemStatusType.WAITING_CONTROLLER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item object that belonging to the result object
|
||||||
|
*/
|
||||||
|
public item$: BehaviorSubject<Item> = new BehaviorSubject<Item>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the result object
|
* The workflowitem object that belonging to the result object
|
||||||
*/
|
*/
|
||||||
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
|
public workflowitem$: BehaviorSubject<WorkflowItem> = new BehaviorSubject<WorkflowItem>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The index of this list element
|
* The index of this list element
|
||||||
@@ -57,6 +71,7 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
|
|||||||
protected linkService: LinkService,
|
protected linkService: LinkService,
|
||||||
protected truncatableService: TruncatableService,
|
protected truncatableService: TruncatableService,
|
||||||
protected dsoNameService: DSONameService,
|
protected dsoNameService: DSONameService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
||||||
) {
|
) {
|
||||||
super(truncatableService, dsoNameService, appConfig);
|
super(truncatableService, dsoNameService, appConfig);
|
||||||
@@ -68,10 +83,34 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.linkService.resolveLinks(this.dso, followLink('workflowitem', {},
|
this.linkService.resolveLinks(this.dso, followLink('workflowitem', {},
|
||||||
followLink('item'), followLink('submitter')
|
followLink('item', {}, followLink('bundles')),
|
||||||
|
followLink('submitter')
|
||||||
), followLink('action'));
|
), followLink('action'));
|
||||||
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
|
||||||
|
(this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
mergeMap((wfiRD: RemoteData<WorkflowItem>) => {
|
||||||
|
if (wfiRD.hasSucceeded) {
|
||||||
|
this.workflowitem$.next(wfiRD.payload);
|
||||||
|
return (wfiRD.payload.item as Observable<RemoteData<Item>>).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
tap((itemRD: RemoteData<Item>) => {
|
||||||
|
if (isNotEmpty(itemRD) && itemRD.hasSucceeded) {
|
||||||
|
this.item$.next(itemRD.payload);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// This ensures the object is removed from cache, when action is performed on task
|
||||||
|
this.objectCache.remove(this.dso._links.workflowitem.href);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,8 +14,7 @@
|
|||||||
(sortFieldChange)="onSortFieldChange($event)"
|
(sortFieldChange)="onSortFieldChange($event)"
|
||||||
(paginationChange)="onPaginationChange($event)"
|
(paginationChange)="onPaginationChange($event)"
|
||||||
(prev)="goPrev()"
|
(prev)="goPrev()"
|
||||||
(next)="goNext()"
|
(next)="goNext()">
|
||||||
>
|
|
||||||
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled" [ngClass]="{'ml-4': selectable}">
|
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled" [ngClass]="{'ml-4': selectable}">
|
||||||
<li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last" [attr.data-test]="'list-object' | dsBrowserOnly">
|
<li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last" [attr.data-test]="'list-object' | dsBrowserOnly">
|
||||||
<ds-selectable-list-item-control *ngIf="selectable" [index]="i"
|
<ds-selectable-list-item-control *ngIf="selectable" [index]="i"
|
||||||
@@ -28,8 +27,7 @@
|
|||||||
(importObject)="importObject.emit($event)"></ds-importable-list-item-control>
|
(importObject)="importObject.emit($event)"></ds-importable-list-item-control>
|
||||||
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [index]="i" [context]="context" [linkType]="linkType"
|
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [index]="i" [context]="context" [linkType]="linkType"
|
||||||
[listID]="selectionConfig?.listId"
|
[listID]="selectionConfig?.listId"
|
||||||
(contentChange)="contentChange.emit()"
|
(contentChange)="contentChange.emit($event)"></ds-listable-object-component-loader>
|
||||||
></ds-listable-object-component-loader>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
|
@@ -74,7 +74,7 @@ export class ObjectListComponent {
|
|||||||
/**
|
/**
|
||||||
* Config used for the import button
|
* Config used for the import button
|
||||||
*/
|
*/
|
||||||
@Input() importConfig: { importLabel: string };
|
@Input() importConfig: { buttonLabel: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination
|
* Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination
|
||||||
@@ -221,4 +221,5 @@ export class ObjectListComponent {
|
|||||||
goNext() {
|
goNext() {
|
||||||
this.next.emit(true);
|
this.next.emit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
import { type } from '../ngrx/type';
|
import { type } from '../ngrx/type';
|
||||||
|
import { KeyValuePair } from '../key-value-pair.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For each action type in an action group, make a simple
|
* For each action type in an action group, make a simple
|
||||||
@@ -11,6 +13,8 @@ import { type } from '../ngrx/type';
|
|||||||
*/
|
*/
|
||||||
export const CSSVariableActionTypes = {
|
export const CSSVariableActionTypes = {
|
||||||
ADD: type('dspace/css-variables/ADD'),
|
ADD: type('dspace/css-variables/ADD'),
|
||||||
|
ADD_ALL: type('dspace/css-variables/ADD_ALL'),
|
||||||
|
CLEAR: type('dspace/css-variables/CLEAR'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AddCSSVariableAction implements Action {
|
export class AddCSSVariableAction implements Action {
|
||||||
@@ -24,5 +28,17 @@ export class AddCSSVariableAction implements Action {
|
|||||||
this.payload = {name, value};
|
this.payload = {name, value};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export class AddAllCSSVariablesAction implements Action {
|
||||||
|
type = CSSVariableActionTypes.ADD_ALL;
|
||||||
|
payload: KeyValuePair<string, string>[];
|
||||||
|
|
||||||
export type CSSVariableAction = AddCSSVariableAction;
|
constructor(variables: KeyValuePair<string, string>[]) {
|
||||||
|
this.payload = variables;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClearCSSVariablesAction implements Action {
|
||||||
|
type = CSSVariableActionTypes.CLEAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CSSVariableAction = AddCSSVariableAction | AddAllCSSVariablesAction | ClearCSSVariablesAction;
|
30
src/app/shared/sass-helper/css-variable.reducer.ts
Normal file
30
src/app/shared/sass-helper/css-variable.reducer.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { CSSVariableAction, CSSVariableActionTypes } from './css-variable.actions';
|
||||||
|
import { KeyValuePair } from '../key-value-pair.model';
|
||||||
|
|
||||||
|
export interface CSSVariablesState {
|
||||||
|
[name: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: CSSVariablesState = Object.create({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reducer that handles the state of CSS variables in the store
|
||||||
|
* @param state The current state of the store
|
||||||
|
* @param action The action to apply onto the current state of the store
|
||||||
|
*/
|
||||||
|
export function cssVariablesReducer(state = initialState, action: CSSVariableAction): CSSVariablesState {
|
||||||
|
switch (action.type) {
|
||||||
|
case CSSVariableActionTypes.ADD: {
|
||||||
|
const variable = action.payload;
|
||||||
|
return Object.assign({}, state, { [variable.name]: variable.value });
|
||||||
|
} case CSSVariableActionTypes.ADD_ALL: {
|
||||||
|
const variables = action.payload;
|
||||||
|
return Object.assign({}, state, ...variables.map(({ key, value }: KeyValuePair<string, string>) => {return {[key]: value};}));
|
||||||
|
} case CSSVariableActionTypes.CLEAR: {
|
||||||
|
return initialState;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
src/app/shared/sass-helper/css-variable.service.spec.ts
Normal file
78
src/app/shared/sass-helper/css-variable.service.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { CSSVariableService } from './css-variable.service';
|
||||||
|
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { buildPaginatedList } from '../../core/data/paginated-list.model';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { KeyValuePair } from '../key-value-pair.model';
|
||||||
|
|
||||||
|
describe('CSSVariableService', () => {
|
||||||
|
let store: MockStore;
|
||||||
|
|
||||||
|
let service: CSSVariableService;
|
||||||
|
let initialState;
|
||||||
|
const varKey1 = '--test-1';
|
||||||
|
const varValue1 = 'test-value-1';
|
||||||
|
const varKey2 = '--test-2';
|
||||||
|
const varValue2 = 'test-value-2';
|
||||||
|
const varKey3 = '--test-3';
|
||||||
|
const varValue3 = 'test-value-3';
|
||||||
|
const queryInAll = 'test';
|
||||||
|
const queryFor3 = '3';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
initialState = {
|
||||||
|
['cssVariables']: {
|
||||||
|
[varKey1]: varValue1,
|
||||||
|
[varKey2]: varValue2,
|
||||||
|
[varKey3]: varValue3,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
CSSVariableService,
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(CSSVariableService as any);
|
||||||
|
store = TestBed.inject(MockStore as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchVariable', () => {
|
||||||
|
it('should return the right keys and variables in a paginated list for query that returns all 3 results', () => {
|
||||||
|
const currentPage = 1;
|
||||||
|
const pageSize = 5;
|
||||||
|
const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalPages: 1, totalElements: 3 });
|
||||||
|
const page: KeyValuePair<string, string>[] = [{ key: varKey1, value: varValue1 }, { key: varKey2, value: varValue2 }, { key: varKey3, value: varValue3 }];
|
||||||
|
const result = buildPaginatedList(pageInfo, page);
|
||||||
|
getTestScheduler().expectObservable(service.searchVariable(queryInAll, { currentPage, pageSize } as any)).toBe('a', { a: result });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the right keys and variables in a paginated list for query that returns only the 3rd results', () => {
|
||||||
|
const currentPage = 1;
|
||||||
|
const pageSize = 5;
|
||||||
|
const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalPages: 1, totalElements: 1 });
|
||||||
|
const page: KeyValuePair<string, string>[] = [{ key: varKey3, value: varValue3 }];
|
||||||
|
const result = buildPaginatedList(pageInfo, page);
|
||||||
|
getTestScheduler().expectObservable(service.searchVariable(queryFor3, { currentPage, pageSize } as any)).toBe('a', { a: result });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the right keys and variables in a paginated list that\'s not longer than the page size', () => {
|
||||||
|
const currentPage = 1;
|
||||||
|
const pageSize = 2;
|
||||||
|
const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalPages: 2, totalElements: 3 });
|
||||||
|
const page: KeyValuePair<string, string>[] = [{ key: varKey1, value: varValue1 }, { key: varKey2, value: varValue2 }];
|
||||||
|
const result = buildPaginatedList(pageInfo, page);
|
||||||
|
getTestScheduler().expectObservable(service.searchVariable(queryInAll, { currentPage, pageSize } as any)).toBe('a', { a: result });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
161
src/app/shared/sass-helper/css-variable.service.ts
Normal file
161
src/app/shared/sass-helper/css-variable.service.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AppState, keySelector } from '../../app.reducer';
|
||||||
|
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
|
import { AddAllCSSVariablesAction, AddCSSVariableAction, ClearCSSVariablesAction } from './css-variable.actions';
|
||||||
|
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||||
|
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
|
import { KeyValuePair } from '../key-value-pair.model';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { CSSVariablesState } from './css-variable.reducer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This service deals with adding and retrieving CSS variables to and from the store
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CSSVariableService {
|
||||||
|
isSameDomain = (styleSheet) => {
|
||||||
|
// Internal style blocks won't have an href value
|
||||||
|
if (!styleSheet.href) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return styleSheet.href.indexOf(window.location.origin) === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Determine if the given rule is a CSSStyleRule
|
||||||
|
See: https://developer.mozilla.org/en-US/docs/Web/API/CSSRule#Type_constants
|
||||||
|
*/
|
||||||
|
isStyleRule = (rule) => rule.type === 1;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected store: Store<AppState>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a CSS variable to the store
|
||||||
|
* @param name The name/key of the CSS variable
|
||||||
|
* @param value The value of the CSS variable
|
||||||
|
*/
|
||||||
|
addCSSVariable(name: string, value: string) {
|
||||||
|
this.store.dispatch(new AddCSSVariableAction(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiples CSS variables to the store
|
||||||
|
* @param variables The key-value pairs with the CSS variables to be added
|
||||||
|
*/
|
||||||
|
addCSSVariables(variables: KeyValuePair<string, string>[]) {
|
||||||
|
this.store.dispatch(new AddAllCSSVariablesAction(variables));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all CSS variables ƒrom the store
|
||||||
|
*/
|
||||||
|
clearCSSVariables() {
|
||||||
|
this.store.dispatch(new ClearCSSVariablesAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of a specific CSS key
|
||||||
|
* @param name The name/key of the CSS value
|
||||||
|
*/
|
||||||
|
getVariable(name: string): Observable<string> {
|
||||||
|
return this.store.pipe(select(themeVariableByNameSelector(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the CSSVariablesState of the store containing all variables
|
||||||
|
*/
|
||||||
|
getAllVariables(): Observable<CSSVariablesState> {
|
||||||
|
return this.store.pipe(select(themeVariablesSelector));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to find CSS variables by their partially supplying their key. Case sensitive. Returns a paginated list of KeyValuePairs with CSS variables that match the query.
|
||||||
|
* @param query The query to look for in the keys
|
||||||
|
* @param paginationOptions The pagination options for the requested page
|
||||||
|
*/
|
||||||
|
searchVariable(query: string, paginationOptions: PaginationComponentOptions): Observable<PaginatedList<KeyValuePair<string, string>>> {
|
||||||
|
return this.store.pipe(select(themePaginatedVariablesByQuery(query, paginationOptions)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all custom properties on a page
|
||||||
|
* @return array<KeyValuePair<string, string>>
|
||||||
|
* ex; [{key: "--color-accent", value: "#b9f500"}, {key: "--color-text", value: "#252525"}, ...]
|
||||||
|
*/
|
||||||
|
getCSSVariablesFromStylesheets(document: Document): KeyValuePair<string, string>[] {
|
||||||
|
if (isNotEmpty(document.styleSheets)) {
|
||||||
|
// styleSheets is array-like, so we convert it to an array.
|
||||||
|
// Filter out any stylesheets not on this domain
|
||||||
|
return [...document.styleSheets]
|
||||||
|
.filter(this.isSameDomain)
|
||||||
|
.reduce(
|
||||||
|
(finalArr, sheet) =>
|
||||||
|
finalArr.concat(
|
||||||
|
// cssRules is array-like, so we convert it to an array
|
||||||
|
[...sheet.cssRules].filter(this.isStyleRule).reduce((propValArr, rule: any) => {
|
||||||
|
const props = [...rule.style]
|
||||||
|
.map((propName) => {
|
||||||
|
return {
|
||||||
|
key: propName.trim(),
|
||||||
|
value: rule.style.getPropertyValue(propName).trim()
|
||||||
|
} as KeyValuePair<string, string>;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Discard any props that don't start with "--". Custom props are required to.
|
||||||
|
.filter(({ key }: KeyValuePair<string, string>) => key.indexOf('--') === 0);
|
||||||
|
|
||||||
|
return [...propValArr, ...props];
|
||||||
|
}, [])
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeVariablesSelector = (state: AppState) => state.cssVariables;
|
||||||
|
|
||||||
|
const themeVariableByNameSelector = (name: string): MemoizedSelector<AppState, string> => {
|
||||||
|
return keySelector<string>(name, themeVariablesSelector);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split this up into two memoized selectors so the query search gets cached separately from the pagination,
|
||||||
|
// since the entire list has to be retrieved every time anyway
|
||||||
|
const themePaginatedVariablesByQuery = (query: string, pagination: PaginationComponentOptions): MemoizedSelector<AppState, PaginatedList<KeyValuePair<string, string>>> => {
|
||||||
|
return createSelector(themeVariablesByQuery(query), (pairs) => {
|
||||||
|
if (hasValue(pairs)) {
|
||||||
|
const { currentPage, pageSize } = pagination;
|
||||||
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
const pairsPage = pairs.slice(startIndex, endIndex);
|
||||||
|
const totalPages = Math.ceil(pairs.length / pageSize);
|
||||||
|
const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalElements: pairs.length, totalPages });
|
||||||
|
return buildPaginatedList(pageInfo, pairsPage);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const themeVariablesByQuery = (query: string): MemoizedSelector<AppState, KeyValuePair<string, string>[]> => {
|
||||||
|
return createSelector(themeVariablesSelector, (state) => {
|
||||||
|
if (hasValue(state)) {
|
||||||
|
return Object.keys(state)
|
||||||
|
.filter((key: string) => key.includes(query))
|
||||||
|
.map((key: string) => {
|
||||||
|
return { key, value: state[key] };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,20 +0,0 @@
|
|||||||
import { CSSVariableAction, CSSVariableActionTypes } from './sass-helper.actions';
|
|
||||||
|
|
||||||
export interface CSSVariablesState {
|
|
||||||
[name: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: CSSVariablesState = Object.create({});
|
|
||||||
|
|
||||||
export function cssVariablesReducer(state = initialState, action: CSSVariableAction): CSSVariablesState {
|
|
||||||
switch (action.type) {
|
|
||||||
case CSSVariableActionTypes.ADD: {
|
|
||||||
const variable = action.payload;
|
|
||||||
const t = Object.assign({}, state, { [variable.name]: variable.value });
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,30 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { AppState, keySelector } from '../../app.reducer';
|
|
||||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
|
||||||
import { AddCSSVariableAction } from './sass-helper.actions';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CSSVariableService {
|
|
||||||
constructor(
|
|
||||||
protected store: Store<AppState>) {
|
|
||||||
}
|
|
||||||
|
|
||||||
addCSSVariable(name: string, value: string) {
|
|
||||||
this.store.dispatch(new AddCSSVariableAction(name, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
getVariable(name: string) {
|
|
||||||
return this.store.pipe(select(themeVariableByNameSelector(name)));
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllVariables() {
|
|
||||||
return this.store.pipe(select(themeVariablesSelector));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeVariablesSelector = (state: AppState) => state.cssVariables;
|
|
||||||
|
|
||||||
const themeVariableByNameSelector = (name: string): MemoizedSelector<AppState, string> => {
|
|
||||||
return keySelector<string>(name, themeVariablesSelector);
|
|
||||||
};
|
|
@@ -2,9 +2,14 @@ import { Component, Injector, Input, OnInit } from '@angular/core';
|
|||||||
import { renderFilterType } from '../search-filter-type-decorator';
|
import { renderFilterType } from '../search-filter-type-decorator';
|
||||||
import { FilterType } from '../../../models/filter-type.model';
|
import { FilterType } from '../../../models/filter-type.model';
|
||||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||||
import { FILTER_CONFIG, IN_PLACE_SEARCH } from '../../../../../core/shared/search/search-filter.service';
|
import {
|
||||||
|
FILTER_CONFIG,
|
||||||
|
IN_PLACE_SEARCH,
|
||||||
|
REFRESH_FILTER
|
||||||
|
} from '../../../../../core/shared/search/search-filter.service';
|
||||||
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||||
import { SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
import { SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-filter-wrapper',
|
selector: 'ds-search-facet-filter-wrapper',
|
||||||
@@ -25,6 +30,11 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() inPlaceSearch;
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the search filters values may be stale, and so they must be refreshed.
|
||||||
|
*/
|
||||||
|
@Input() refreshFilters: BehaviorSubject<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The constructor of the search facet filter that should be rendered, based on the filter config's type
|
* The constructor of the search facet filter that should be rendered, based on the filter config's type
|
||||||
*/
|
*/
|
||||||
@@ -45,7 +55,8 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
|
|||||||
this.objectInjector = Injector.create({
|
this.objectInjector = Injector.create({
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] },
|
{ provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] },
|
||||||
{ provide: IN_PLACE_SEARCH, useFactory: () => (this.inPlaceSearch), deps: [] }
|
{ provide: IN_PLACE_SEARCH, useFactory: () => (this.inPlaceSearch), deps: [] },
|
||||||
|
{ provide: REFRESH_FILTER, useFactory: () => (this.refreshFilters), deps: [] }
|
||||||
],
|
],
|
||||||
parent: this.injector
|
parent: this.injector
|
||||||
});
|
});
|
||||||
|
@@ -5,13 +5,14 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import {
|
import {
|
||||||
FILTER_CONFIG,
|
FILTER_CONFIG,
|
||||||
IN_PLACE_SEARCH,
|
IN_PLACE_SEARCH,
|
||||||
|
REFRESH_FILTER,
|
||||||
SearchFilterService
|
SearchFilterService
|
||||||
} from '../../../../../core/shared/search/search-filter.service';
|
} from '../../../../../core/shared/search/search-filter.service';
|
||||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||||
import { FilterType } from '../../../models/filter-type.model';
|
import { FilterType } from '../../../models/filter-type.model';
|
||||||
import { FacetValue } from '../../../models/facet-value.model';
|
import { FacetValue } from '../../../models/facet-value.model';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { BehaviorSubject, of as observableOf } from 'rxjs';
|
||||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||||
import { SearchServiceStub } from '../../../../testing/search-service.stub';
|
import { SearchServiceStub } from '../../../../testing/search-service.stub';
|
||||||
import { buildPaginatedList } from '../../../../../core/data/paginated-list.model';
|
import { buildPaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||||
@@ -97,6 +98,7 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
{ provide: RemoteDataBuildService, useValue: { aggregate: () => observableOf({}) } },
|
{ provide: RemoteDataBuildService, useValue: { aggregate: () => observableOf({}) } },
|
||||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
{ provide: IN_PLACE_SEARCH, useValue: false },
|
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||||
|
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) },
|
||||||
{
|
{
|
||||||
provide: SearchFilterService, useValue: {
|
provide: SearchFilterService, useValue: {
|
||||||
getSelectedValuesForFilter: () => observableOf(selectedValues),
|
getSelectedValuesForFilter: () => observableOf(selectedValues),
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
@@ -6,10 +10,8 @@ import {
|
|||||||
Subject,
|
Subject,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators';
|
import { debounceTime, distinctUntilChanged, filter, map, mergeMap, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
|
||||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
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';
|
||||||
@@ -21,6 +23,7 @@ import { SearchService } from '../../../../../core/shared/search/search.service'
|
|||||||
import {
|
import {
|
||||||
FILTER_CONFIG,
|
FILTER_CONFIG,
|
||||||
IN_PLACE_SEARCH,
|
IN_PLACE_SEARCH,
|
||||||
|
REFRESH_FILTER,
|
||||||
SearchFilterService
|
SearchFilterService
|
||||||
} from '../../../../../core/shared/search/search-filter.service';
|
} from '../../../../../core/shared/search/search-filter.service';
|
||||||
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
|
||||||
@@ -31,6 +34,7 @@ import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-p
|
|||||||
import { currentPath } from '../../../../utils/route.utils';
|
import { currentPath } from '../../../../utils/route.utils';
|
||||||
import { getFacetValueForType, stripOperatorFromFilterValue } from '../../../search.utils';
|
import { getFacetValueForType, stripOperatorFromFilterValue } from '../../../search.utils';
|
||||||
import { createPendingRemoteDataObject } from '../../../../remote-data.utils';
|
import { createPendingRemoteDataObject } from '../../../../remote-data.utils';
|
||||||
|
import { FacetValues } from '../../../models/facet-values.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-facet-filter',
|
selector: 'ds-search-facet-filter',
|
||||||
@@ -75,6 +79,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
* Emits the active values for this filter
|
* Emits the active values for this filter
|
||||||
*/
|
*/
|
||||||
selectedValues$: Observable<FacetValue[]>;
|
selectedValues$: Observable<FacetValue[]>;
|
||||||
|
|
||||||
protected collapseNextUpdate = true;
|
protected collapseNextUpdate = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,7 +103,8 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
protected router: Router,
|
protected router: Router,
|
||||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
||||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig) {
|
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
||||||
|
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,66 +116,16 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
|
||||||
|
|
||||||
this.searchOptions$ = this.searchConfigService.searchOptions;
|
this.searchOptions$ = this.searchConfigService.searchOptions;
|
||||||
this.subs.push(this.searchOptions$.subscribe(() => this.updateFilterValueList()));
|
this.subs.push(
|
||||||
const facetValues$ = observableCombineLatest(this.searchOptions$, this.currentPage).pipe(
|
this.searchOptions$.subscribe(() => this.updateFilterValueList()),
|
||||||
map(([options, page]) => {
|
this.refreshFilters.asObservable().pipe(
|
||||||
return { options, page };
|
filter((toRefresh: boolean) => toRefresh),
|
||||||
}),
|
// NOTE This is a workaround, otherwise retrieving filter values returns tha old cached response
|
||||||
switchMap(({ options, page }) => {
|
debounceTime((100)),
|
||||||
return this.searchService.getFacetValuesFor(this.filterConfig, page, options)
|
mergeMap(() => this.retrieveFilterValues(false))
|
||||||
.pipe(
|
).subscribe()
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
map((results) => {
|
|
||||||
return {
|
|
||||||
values: observableOf(results),
|
|
||||||
page: page
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
this.retrieveFilterValues().subscribe();
|
||||||
let filterValues = [];
|
|
||||||
this.subs.push(facetValues$.subscribe((facetOutcome) => {
|
|
||||||
const newValues$ = facetOutcome.values;
|
|
||||||
|
|
||||||
if (this.collapseNextUpdate) {
|
|
||||||
this.showFirstPageOnly();
|
|
||||||
facetOutcome.page = 1;
|
|
||||||
this.collapseNextUpdate = false;
|
|
||||||
}
|
|
||||||
if (facetOutcome.page === 1) {
|
|
||||||
filterValues = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
filterValues = [...filterValues, newValues$];
|
|
||||||
|
|
||||||
this.subs.push(this.rdbs.aggregate(filterValues).pipe(
|
|
||||||
tap((rd: RemoteData<PaginatedList<FacetValue>[]>) => {
|
|
||||||
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig).pipe(
|
|
||||||
map((selectedValues) => {
|
|
||||||
return selectedValues.map((value: string) => {
|
|
||||||
const fValue = [].concat(...rd.payload.map((page) => page.page)).find((facetValue: FacetValue) => this.getFacetValue(facetValue) === value);
|
|
||||||
if (hasValue(fValue)) {
|
|
||||||
return fValue;
|
|
||||||
}
|
|
||||||
const filterValue = stripOperatorFromFilterValue(value);
|
|
||||||
return Object.assign(new FacetValue(), { label: filterValue, value: filterValue });
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
).subscribe((rd: RemoteData<PaginatedList<FacetValue>[]>) => {
|
|
||||||
this.animationState = 'ready';
|
|
||||||
this.filterValues$.next(rd);
|
|
||||||
|
|
||||||
}));
|
|
||||||
this.subs.push(newValues$.pipe(take(1)).subscribe((rd) => {
|
|
||||||
this.isLastPage$.next(hasNoValue(rd.payload.next));
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,6 +280,67 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
return getFacetValueForType(facet, this.filterConfig);
|
return getFacetValueForType(facet, this.filterConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected retrieveFilterValues(useCachedVersionIfAvailable = true): Observable<RemoteData<PaginatedList<FacetValue>[]>> {
|
||||||
|
const facetValues$ = observableCombineLatest([this.searchOptions$, this.currentPage]).pipe(
|
||||||
|
map(([options, page]) => {
|
||||||
|
return { options, page };
|
||||||
|
}),
|
||||||
|
switchMap(({ options, page }) => {
|
||||||
|
return this.searchService.getFacetValuesFor(this.filterConfig, page, options, null, useCachedVersionIfAvailable)
|
||||||
|
.pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
tap((rd: RemoteData<FacetValues>) => {
|
||||||
|
this.isLastPage$.next(hasNoValue(rd?.payload?.next));
|
||||||
|
}),
|
||||||
|
map((rd: RemoteData<FacetValues>) => ({
|
||||||
|
values: observableOf(rd),
|
||||||
|
page: page
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let filterValues = [];
|
||||||
|
return facetValues$.pipe(
|
||||||
|
mergeMap((facetOutcome) => {
|
||||||
|
const newValues$ = facetOutcome.values;
|
||||||
|
|
||||||
|
if (this.collapseNextUpdate) {
|
||||||
|
this.showFirstPageOnly();
|
||||||
|
facetOutcome.page = 1;
|
||||||
|
this.collapseNextUpdate = false;
|
||||||
|
}
|
||||||
|
if (facetOutcome.page === 1) {
|
||||||
|
filterValues = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
filterValues = [...filterValues, newValues$];
|
||||||
|
|
||||||
|
return this.rdbs.aggregate(filterValues);
|
||||||
|
}),
|
||||||
|
tap((rd: RemoteData<PaginatedList<FacetValue>[]>) => {
|
||||||
|
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig).pipe(
|
||||||
|
map((selectedValues) => {
|
||||||
|
return selectedValues.map((value: string) => {
|
||||||
|
const fValue = [].concat(...rd.payload.map((page) => page.page))
|
||||||
|
.find((facetValue: FacetValue) => this.getFacetValue(facetValue) === value);
|
||||||
|
if (hasValue(fValue)) {
|
||||||
|
return fValue;
|
||||||
|
}
|
||||||
|
const filterValue = stripOperatorFromFilterValue(value);
|
||||||
|
return Object.assign(new FacetValue(), { label: filterValue, value: filterValue });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
tap((rd: RemoteData<PaginatedList<FacetValue>[]>) => {
|
||||||
|
this.animationState = 'ready';
|
||||||
|
this.filterValues$.next(rd);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value
|
* Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value
|
||||||
* @param {FacetValue} facet The value of the facet as returned by the server
|
* @param {FacetValue} facet The value of the facet as returned by the server
|
||||||
|
@@ -19,7 +19,8 @@
|
|||||||
class="search-filter-wrapper" [ngClass]="{ 'closed' : closed, 'notab': notab }">
|
class="search-filter-wrapper" [ngClass]="{ 'closed' : closed, 'notab': notab }">
|
||||||
<ds-search-facet-filter-wrapper
|
<ds-search-facet-filter-wrapper
|
||||||
[filterConfig]="filter"
|
[filterConfig]="filter"
|
||||||
[inPlaceSearch]="inPlaceSearch">
|
[inPlaceSearch]="inPlaceSearch"
|
||||||
|
[refreshFilters]="refreshFilters" >
|
||||||
</ds-search-facet-filter-wrapper>
|
</ds-search-facet-filter-wrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Inject, Input, OnInit } from '@angular/core';
|
import { Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||||
import { filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
import { filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { SearchFilterConfig } from '../../models/search-filter-config.model';
|
import { SearchFilterConfig } from '../../models/search-filter-config.model';
|
||||||
@@ -33,6 +33,11 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() inPlaceSearch;
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the search filters values may be stale, and so they must be refreshed.
|
||||||
|
*/
|
||||||
|
@Input() refreshFilters: BehaviorSubject<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when the filter is 100% collapsed in the UI
|
* True when the filter is 100% collapsed in the UI
|
||||||
*/
|
*/
|
||||||
|
@@ -1,17 +1,18 @@
|
|||||||
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component';
|
import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component';
|
||||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||||
import {
|
import {
|
||||||
SearchFilterService,
|
|
||||||
FILTER_CONFIG,
|
FILTER_CONFIG,
|
||||||
IN_PLACE_SEARCH
|
IN_PLACE_SEARCH,
|
||||||
|
REFRESH_FILTER,
|
||||||
|
SearchFilterService
|
||||||
} from '../../../../../core/shared/search/search-filter.service';
|
} from '../../../../../core/shared/search/search-filter.service';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { SearchFiltersComponent } from '../../search-filters.component';
|
import { SearchFiltersComponent } from '../../search-filters.component';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RouterStub } from '../../../../testing/router.stub';
|
import { RouterStub } from '../../../../testing/router.stub';
|
||||||
import { SearchServiceStub } from '../../../../testing/search-service.stub';
|
import { SearchServiceStub } from '../../../../testing/search-service.stub';
|
||||||
import { of as observableOf, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
|
||||||
import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub';
|
||||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
} from '../../../../input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
} from '../../../../input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { NO_ERRORS_SCHEMA, ChangeDetectionStrategy } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
|
||||||
import { FacetValue } from '../../../models/facet-value.model';
|
import { FacetValue } from '../../../models/facet-value.model';
|
||||||
import { FilterType } from '../../../models/filter-type.model';
|
import { FilterType } from '../../../models/filter-type.model';
|
||||||
@@ -112,7 +113,8 @@ describe('SearchHierarchyFilterComponent', () => {
|
|||||||
{ provide: Router, useValue: new RouterStub() },
|
{ provide: Router, useValue: new RouterStub() },
|
||||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
{ provide: IN_PLACE_SEARCH, useValue: false },
|
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||||
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() }
|
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() },
|
||||||
|
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchHierarchyFilterComponent, {
|
}).overrideComponent(SearchHierarchyFilterComponent, {
|
||||||
@@ -140,7 +142,7 @@ describe('SearchHierarchyFilterComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to the correct filter with the query operator', () => {
|
it('should navigate to the correct filter with the query operator', () => {
|
||||||
expect((comp as any).searchService.getFacetValuesFor).toHaveBeenCalledWith(comp.filterConfig, 0, {});
|
expect((comp as any).searchService.getFacetValuesFor).toHaveBeenCalledWith(comp.filterConfig, 0, {}, null, true);
|
||||||
|
|
||||||
const searchQuery = 'MARVEL';
|
const searchQuery = 'MARVEL';
|
||||||
comp.onSubmit(searchQuery);
|
comp.onSubmit(searchQuery);
|
||||||
|
@@ -5,13 +5,14 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import {
|
import {
|
||||||
FILTER_CONFIG,
|
FILTER_CONFIG,
|
||||||
IN_PLACE_SEARCH,
|
IN_PLACE_SEARCH,
|
||||||
|
REFRESH_FILTER,
|
||||||
SearchFilterService
|
SearchFilterService
|
||||||
} from '../../../../../core/shared/search/search-filter.service';
|
} from '../../../../../core/shared/search/search-filter.service';
|
||||||
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
||||||
import { FilterType } from '../../../models/filter-type.model';
|
import { FilterType } from '../../../models/filter-type.model';
|
||||||
import { FacetValue } from '../../../models/facet-value.model';
|
import { FacetValue } from '../../../models/facet-value.model';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { BehaviorSubject, of as observableOf } from 'rxjs';
|
||||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||||
import { SearchServiceStub } from '../../../../testing/search-service.stub';
|
import { SearchServiceStub } from '../../../../testing/search-service.stub';
|
||||||
import { buildPaginatedList } from '../../../../../core/data/paginated-list.model';
|
import { buildPaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||||
@@ -104,6 +105,7 @@ describe('SearchRangeFilterComponent', () => {
|
|||||||
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf({}) } },
|
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf({}) } },
|
||||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
{ provide: IN_PLACE_SEARCH, useValue: false },
|
{ provide: IN_PLACE_SEARCH, useValue: false },
|
||||||
|
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) },
|
||||||
{
|
{
|
||||||
provide: SearchFilterService, useValue: {
|
provide: SearchFilterService, useValue: {
|
||||||
getSelectedValuesForFilter: () => selectedValues,
|
getSelectedValuesForFilter: () => selectedValues,
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { combineLatest as observableCombineLatest, Subscription } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
|
||||||
import { map, startWith } from 'rxjs/operators';
|
import { map, startWith } from 'rxjs/operators';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
@@ -10,6 +10,7 @@ import { SearchFilterConfig } from '../../../models/search-filter-config.model';
|
|||||||
import {
|
import {
|
||||||
FILTER_CONFIG,
|
FILTER_CONFIG,
|
||||||
IN_PLACE_SEARCH,
|
IN_PLACE_SEARCH,
|
||||||
|
REFRESH_FILTER,
|
||||||
SearchFilterService
|
SearchFilterService
|
||||||
} from '../../../../../core/shared/search/search-filter.service';
|
} from '../../../../../core/shared/search/search-filter.service';
|
||||||
import { SearchService } from '../../../../../core/shared/search/search.service';
|
import { SearchService } from '../../../../../core/shared/search/search.service';
|
||||||
@@ -86,8 +87,9 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
||||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
||||||
@Inject(PLATFORM_ID) private platformId: any,
|
@Inject(PLATFORM_ID) private platformId: any,
|
||||||
|
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>,
|
||||||
private route: RouteService) {
|
private route: RouteService) {
|
||||||
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig);
|
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<h3>{{"search.filters.head" | translate}}</h3>
|
<h3>{{"search.filters.head" | translate}}</h3>
|
||||||
<div *ngIf="(filters | async)?.hasSucceeded">
|
<div *ngIf="(filters | async)?.hasSucceeded">
|
||||||
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
|
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
|
||||||
<ds-search-filter [filter]="filter" [inPlaceSearch]="inPlaceSearch"></ds-search-filter>
|
<ds-search-filter [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button"><i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}</a>
|
<a class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button"><i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}</a>
|
||||||
|
@@ -7,7 +7,6 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
||||||
import { SearchFiltersComponent } from './search-filters.component';
|
import { SearchFiltersComponent } from './search-filters.component';
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
import { of as observableOf, Subject } from 'rxjs';
|
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
|
||||||
import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub';
|
||||||
|
|
||||||
@@ -64,26 +63,4 @@ describe('SearchFiltersComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when refreshSearch observable is present and emit events', () => {
|
|
||||||
|
|
||||||
let refreshFiltersEmitter: Subject<any>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(comp, 'initFilters').and.callFake(() => { /****/});
|
|
||||||
|
|
||||||
refreshFiltersEmitter = new Subject();
|
|
||||||
comp.refreshFilters = refreshFiltersEmitter.asObservable();
|
|
||||||
comp.ngOnInit();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reinitialize search filters', () => {
|
|
||||||
|
|
||||||
expect(comp.initFilters).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
refreshFiltersEmitter.next(null);
|
|
||||||
|
|
||||||
expect(comp.initFilters).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user