mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-12 12:33:07 +00:00
Merge branch 'main' into CST-7755-refactoring
# Conflicts: # src/app/core/core.module.ts # src/app/shared/shared.module.ts
This commit is contained in:
@@ -126,3 +126,9 @@ export function getRequestCopyModulePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const HEALTH_PAGE_PATH = 'health';
|
export const HEALTH_PAGE_PATH = 'health';
|
||||||
|
|
||||||
|
export const SUBSCRIPTIONS_MODULE_PATH = 'subscriptions';
|
||||||
|
|
||||||
|
export function getSubscriptionsModuleRoute() {
|
||||||
|
return `/${SUBSCRIPTIONS_MODULE_PATH}`;
|
||||||
|
}
|
||||||
|
@@ -230,6 +230,12 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
|||||||
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
||||||
canActivate: [GroupAdministratorGuard],
|
canActivate: [GroupAdministratorGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'subscriptions',
|
||||||
|
loadChildren: () => import('./subscriptions-page/subscriptions-page-routing.module')
|
||||||
|
.then((m) => m.SubscriptionsPageRoutingModule),
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,9 @@
|
|||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
|
<div class="pl-2 space-children-mr">
|
||||||
|
<ds-dso-page-subscription-button [dso]="collection"></ds-dso-page-subscription-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
<!-- Browse-By Links -->
|
<!-- Browse-By Links -->
|
||||||
|
@@ -21,6 +21,9 @@
|
|||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
|
<div class="pl-2 space-children-mr">
|
||||||
|
<ds-dso-page-subscription-button [dso]="communityPayload"></ds-dso-page-subscription-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
|
|
||||||
|
@@ -174,6 +174,7 @@ import { OrcidAuthService } from './orcid/orcid-auth.service';
|
|||||||
import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service';
|
import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service';
|
||||||
import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service';
|
import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service';
|
||||||
import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model';
|
import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model';
|
||||||
|
import { Subscription } from '../shared/subscriptions/models/subscription.model';
|
||||||
import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service';
|
import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,6 +368,7 @@ export const models =
|
|||||||
OrcidHistory,
|
OrcidHistory,
|
||||||
AccessStatusObject,
|
AccessStatusObject,
|
||||||
IdentifierData,
|
IdentifierData,
|
||||||
|
Subscription,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -33,4 +33,5 @@ export enum FeatureID {
|
|||||||
CanSubmit = 'canSubmit',
|
CanSubmit = 'canSubmit',
|
||||||
CanEditItem = 'canEditItem',
|
CanEditItem = 'canEditItem',
|
||||||
CanRegisterDOI = 'canRegisterDOI',
|
CanRegisterDOI = 'canRegisterDOI',
|
||||||
|
CanSubscribe = 'canSubscribeDso',
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@ import { ClaimedApprovedSearchResultListElementComponent } from '../shared/objec
|
|||||||
import { ClaimedDeclinedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component';
|
import { ClaimedDeclinedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component';
|
||||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
||||||
import { ItemSubmitterComponent } from '../shared/object-collection/shared/mydspace-item-submitter/item-submitter.component';
|
import { ItemSubmitterComponent } from '../shared/object-collection/shared/mydspace-item-submitter/item-submitter.component';
|
||||||
|
import { ItemCollectionComponent } from '../shared/object-collection/shared/mydspace-item-collection/item-collection.component';
|
||||||
import { ItemDetailPreviewComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component';
|
import { ItemDetailPreviewComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component';
|
||||||
import { ItemDetailPreviewFieldComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component';
|
import { ItemDetailPreviewFieldComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component';
|
||||||
import { ItemListPreviewComponent } from '../shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component';
|
import { ItemListPreviewComponent } from '../shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component';
|
||||||
@@ -46,6 +47,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
...ENTRY_COMPONENTS,
|
...ENTRY_COMPONENTS,
|
||||||
ItemSubmitterComponent,
|
ItemSubmitterComponent,
|
||||||
|
ItemCollectionComponent,
|
||||||
ItemDetailPreviewComponent,
|
ItemDetailPreviewComponent,
|
||||||
ItemDetailPreviewFieldComponent,
|
ItemDetailPreviewFieldComponent,
|
||||||
ItemListPreviewComponent,
|
ItemListPreviewComponent,
|
||||||
|
@@ -6,6 +6,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a>
|
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a>
|
||||||
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[mydspaceRoute]" routerLinkActive="active">{{'nav.mydspace' | translate}}</a>
|
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[mydspaceRoute]" routerLinkActive="active">{{'nav.mydspace' | translate}}</a>
|
||||||
|
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[subscriptionsRoute]" routerLinkActive="active">{{'nav.subscriptions' | translate}}</a>
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<ds-log-out *ngIf="!inExpandableNavbar" data-test="log-out-component"></ds-log-out>
|
<ds-log-out *ngIf="!inExpandableNavbar" data-test="log-out-component"></ds-log-out>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -8,7 +8,7 @@ import { AppState } from '../../../app.reducer';
|
|||||||
import { isAuthenticationLoading } from '../../../core/auth/selectors';
|
import { isAuthenticationLoading } from '../../../core/auth/selectors';
|
||||||
import { MYDSPACE_ROUTE } from '../../../my-dspace-page/my-dspace-page.component';
|
import { MYDSPACE_ROUTE } from '../../../my-dspace-page/my-dspace-page.component';
|
||||||
import { AuthService } from '../../../core/auth/auth.service';
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
import { getProfileModuleRoute } from '../../../app-routing-paths';
|
import { getProfileModuleRoute, getSubscriptionsModuleRoute } from '../../../app-routing-paths';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents the user nav menu.
|
* This component represents the user nav menu.
|
||||||
@@ -48,6 +48,11 @@ export class UserMenuComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public profileRoute = getProfileModuleRoute();
|
public profileRoute = getProfileModuleRoute();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The profile page route
|
||||||
|
*/
|
||||||
|
public subscriptionsRoute = getSubscriptionsModuleRoute();
|
||||||
|
|
||||||
constructor(private store: Store<AppState>,
|
constructor(private store: Store<AppState>,
|
||||||
private authService: AuthService) {
|
private authService: AuthService) {
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
<button *ngIf="isAuthorized$ | async" data-test="subscription-button"
|
||||||
|
(click)="openSubscriptionModal()"
|
||||||
|
[ngbTooltip]="'subscriptions.tooltip' | translate"
|
||||||
|
[title]="'subscriptions.tooltip' | translate"
|
||||||
|
[attr.aria-label]="'subscriptions.tooltip' | translate"
|
||||||
|
class="subscription-button btn btn-dark btn-sm">
|
||||||
|
<i class="fas fa-bell fa-fw"></i>
|
||||||
|
</button>
|
@@ -0,0 +1,83 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DsoPageSubscriptionButtonComponent } from './dso-page-subscription-button.component';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ITEM } from '../../../core/shared/item.resource-type';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('DsoPageSubscriptionButtonComponent', () => {
|
||||||
|
let component: DsoPageSubscriptionButtonComponent;
|
||||||
|
let fixture: ComponentFixture<DsoPageSubscriptionButtonComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
|
||||||
|
const authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: jasmine.createSpy('isAuthorized') // observableOf(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
type: ITEM,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://localhost:8000/items/fake-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NgbModalModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [ DsoPageSubscriptionButtonComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DsoPageSubscriptionButtonComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
de = fixture.debugElement;
|
||||||
|
component.dso = mockItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when is authorized', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationService.isAuthorized.and.returnValue(observableOf(true));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display subscription button', () => {
|
||||||
|
expect(de.query(By.css(' [data-test="subscription-button"]'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when is not authorized', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationService.isAuthorized.and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display subscription button', () => {
|
||||||
|
expect(de.query(By.css(' [data-test="subscription-button"]'))).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,57 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { SubscriptionModalComponent } from '../../subscriptions/subscription-modal/subscription-modal.component';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-dso-page-subscription-button',
|
||||||
|
templateUrl: './dso-page-subscription-button.component.html',
|
||||||
|
styleUrls: ['./dso-page-subscription-button.component.scss']
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Display a button that opens the modal to manage subscriptions
|
||||||
|
*/
|
||||||
|
export class DsoPageSubscriptionButtonComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the current user is authorized to edit the DSpaceObject
|
||||||
|
*/
|
||||||
|
isAuthorized$: Observable<boolean> = of(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to NgbModal
|
||||||
|
*/
|
||||||
|
public modalRef: NgbModalRef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSpaceObject that is being viewed
|
||||||
|
*/
|
||||||
|
@Input() dso: DSpaceObject;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if the current DSpaceObject can be subscribed by the user
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.isAuthorized$ = this.authorizationService.isAuthorized(FeatureID.CanSubscribe, this.dso.self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the modal to subscribe to the related DSpaceObject
|
||||||
|
*/
|
||||||
|
public openSubscriptionModal() {
|
||||||
|
this.modalRef = this.modalService.open(SubscriptionModalComponent);
|
||||||
|
this.modalRef.componentInstance.dso = this.dso;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="mt-2 mb-2" *ngIf="(collection$ | async)">
|
||||||
|
<span class="text-muted">{{'collection.listelement.badge' | translate}}:
|
||||||
|
<a [routerLink]="['/collections', (collection$ | async)?.id]">
|
||||||
|
{{(collection$ | async)?.name}}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
@@ -0,0 +1,67 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { EMPTY, Observable } from 'rxjs';
|
||||||
|
import { map, mergeMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.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 collection information.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-collection',
|
||||||
|
styleUrls: ['./item-collection.component.scss'],
|
||||||
|
templateUrl: './item-collection.component.html'
|
||||||
|
})
|
||||||
|
export class ItemCollectionComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target object
|
||||||
|
*/
|
||||||
|
@Input() object: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection object
|
||||||
|
*/
|
||||||
|
collection$: Observable<Collection>;
|
||||||
|
|
||||||
|
public constructor(protected linkService: LinkService) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize collection object
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
|
||||||
|
this.linkService.resolveLinks(this.object, followLink('workflowitem', {
|
||||||
|
isOptional: true
|
||||||
|
},
|
||||||
|
followLink('collection',{})
|
||||||
|
));
|
||||||
|
this.collection$ = (this.object.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
mergeMap((rd: RemoteData<WorkflowItem>) => {
|
||||||
|
if (rd.hasSucceeded && isNotEmpty(rd.payload)) {
|
||||||
|
return (rd.payload.collection as Observable<RemoteData<Collection>>).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rds: RemoteData<Collection>) => {
|
||||||
|
if (rds.hasSucceeded && isNotEmpty(rds.payload)) {
|
||||||
|
return rds.payload;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,3 @@
|
|||||||
<div class="mt-2 mb-2" *ngIf="(submitter$ | async)">
|
<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>
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
<ds-themed-item-list-preview [item]="item$.value"
|
<ds-themed-item-list-preview [item]="item$.value"
|
||||||
[object]="object"
|
[object]="object"
|
||||||
[showSubmitter]="showSubmitter"
|
[showSubmitter]="showSubmitter"
|
||||||
|
[workflowItem]="workflowitem$.value"
|
||||||
[status]="status"></ds-themed-item-list-preview>
|
[status]="status"></ds-themed-item-list-preview>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@@ -24,7 +24,8 @@
|
|||||||
<span *ngIf="item.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);"
|
<span *ngIf="item.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);"
|
||||||
class="item-list-authors">
|
class="item-list-authors">
|
||||||
<span
|
<span
|
||||||
*ngIf="item.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length === 0">{{'mydspace.results.no-authors' | translate}}</span>
|
*ngIf="item.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length === 0">{{'mydspace.results.no-authors'
|
||||||
|
| translate}}</span>
|
||||||
<span
|
<span
|
||||||
*ngFor="let author of item.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">
|
*ngFor="let author of item.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">
|
||||||
<span [innerHTML]="author"><span [innerHTML]="author"></span></span>
|
<span [innerHTML]="author"><span [innerHTML]="author"></span></span>
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
<ds-item-submitter *ngIf="showSubmitter" [object]="object.indexableObject"></ds-item-submitter>
|
<ds-item-submitter *ngIf="showSubmitter" [object]="object.indexableObject"></ds-item-submitter>
|
||||||
|
<ds-item-collection [object]="object.indexableObject"></ds-item-collection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -8,6 +8,7 @@ import {
|
|||||||
import { SearchResult } from '../../../search/models/search-result.model';
|
import { SearchResult } from '../../../search/models/search-result.model';
|
||||||
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
|
||||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { WorkflowItem } from 'src/app/core/submission/models/workflowitem.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component show metadata for the given item object in the list view.
|
* This component show metadata for the given item object in the list view.
|
||||||
@@ -40,6 +41,11 @@ export class ItemListPreviewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() showSubmitter = false;
|
@Input() showSubmitter = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the workflow of the item
|
||||||
|
*/
|
||||||
|
@Input() workflowItem: WorkflowItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display thumbnails if required by configuration
|
* Display thumbnails if required by configuration
|
||||||
*/
|
*/
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { ChangeDetectorRef, Component, ComponentFactoryResolver, Input } from '@angular/core';
|
||||||
import { ThemedComponent } from '../../../theme-support/themed.component';
|
import { ThemedComponent } from '../../../theme-support/themed.component';
|
||||||
import { ItemListPreviewComponent } from './item-list-preview.component';
|
import { ItemListPreviewComponent } from './item-list-preview.component';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.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 { SearchResult } from '../../../search/models/search-result.model';
|
import { SearchResult } from '../../../search/models/search-result.model';
|
||||||
|
import { WorkflowItem } from 'src/app/core/submission/models/workflowitem.model';
|
||||||
|
import { ThemeService } from 'src/app/shared/theme-support/theme.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Themed wrapper for ItemListPreviewComponent
|
* Themed wrapper for ItemListPreviewComponent
|
||||||
@@ -11,10 +13,10 @@ import { SearchResult } from '../../../search/models/search-result.model';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-themed-item-list-preview',
|
selector: 'ds-themed-item-list-preview',
|
||||||
styleUrls: [],
|
styleUrls: [],
|
||||||
templateUrl: '../../../theme-support/themed.component.html',
|
templateUrl: '../../../theme-support/themed.component.html'
|
||||||
})
|
})
|
||||||
export class ThemedItemListPreviewComponent extends ThemedComponent<ItemListPreviewComponent> {
|
export class ThemedItemListPreviewComponent extends ThemedComponent<ItemListPreviewComponent> {
|
||||||
protected inAndOutputNames: (keyof ItemListPreviewComponent & keyof this)[] = ['item', 'object', 'status', 'showSubmitter'];
|
protected inAndOutputNames: (keyof ItemListPreviewComponent & keyof this)[] = ['item', 'object', 'status', 'showSubmitter', 'workflowItem'];
|
||||||
|
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
@@ -24,6 +26,19 @@ export class ThemedItemListPreviewComponent extends ThemedComponent<ItemListPrev
|
|||||||
|
|
||||||
@Input() showSubmitter = false;
|
@Input() showSubmitter = false;
|
||||||
|
|
||||||
|
@Input() workflowItem: WorkflowItem;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected resolver: ComponentFactoryResolver,
|
||||||
|
protected cdr: ChangeDetectorRef,
|
||||||
|
protected themeService: ThemeService,
|
||||||
|
) {
|
||||||
|
super(resolver, cdr, themeService);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
}
|
||||||
|
|
||||||
protected getComponentName(): string {
|
protected getComponentName(): string {
|
||||||
return 'ItemListPreviewComponent';
|
return 'ItemListPreviewComponent';
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
<ds-themed-item-list-preview [item]="item$.value"
|
<ds-themed-item-list-preview [item]="item$.value"
|
||||||
[object]="object"
|
[object]="object"
|
||||||
[showSubmitter]="showSubmitter"
|
[showSubmitter]="showSubmitter"
|
||||||
|
[workflowItem]="workflowitem$.value"
|
||||||
[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' : ''">
|
||||||
|
@@ -256,9 +256,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
AdvancedClaimedTaskActionRatingComponent
|
AdvancedClaimedTaskActionRatingComponent
|
||||||
} from './mydspace-actions/claimed-task/rating/advanced-claimed-task-action-rating.component';
|
} from './mydspace-actions/claimed-task/rating/advanced-claimed-task-action-rating.component';
|
||||||
|
import { ClaimedTaskActionsDeclineTaskComponent } from './mydspace-actions/claimed-task/decline-task/claimed-task-actions-decline-task.component';
|
||||||
import {
|
import {
|
||||||
ClaimedTaskActionsDeclineTaskComponent
|
DsoPageSubscriptionButtonComponent
|
||||||
} from './mydspace-actions/claimed-task/decline-task/claimed-task-actions-decline-task.component';
|
} from './dso-page/dso-page-subscription-button/dso-page-subscription-button.component';
|
||||||
import { EpersonGroupListComponent } from './eperson-group-list/eperson-group-list.component';
|
import { EpersonGroupListComponent } from './eperson-group-list/eperson-group-list.component';
|
||||||
import { EpersonSearchBoxComponent } from './eperson-group-list/eperson-search-box/eperson-search-box.component';
|
import { EpersonSearchBoxComponent } from './eperson-group-list/eperson-search-box/eperson-search-box.component';
|
||||||
import { GroupSearchBoxComponent } from './eperson-group-list/group-search-box/group-search-box.component';
|
import { GroupSearchBoxComponent } from './eperson-group-list/group-search-box/group-search-box.component';
|
||||||
@@ -361,6 +362,7 @@ const COMPONENTS = [
|
|||||||
ItemPageTitleFieldComponent,
|
ItemPageTitleFieldComponent,
|
||||||
ThemedSearchNavbarComponent,
|
ThemedSearchNavbarComponent,
|
||||||
ListableNotificationObjectComponent,
|
ListableNotificationObjectComponent,
|
||||||
|
DsoPageSubscriptionButtonComponent,
|
||||||
MetadataFieldWrapperComponent,
|
MetadataFieldWrapperComponent,
|
||||||
ContextHelpWrapperComponent,
|
ContextHelpWrapperComponent,
|
||||||
EpersonGroupListComponent,
|
EpersonGroupListComponent,
|
||||||
|
73
src/app/shared/subscriptions/models/subscription.model.ts
Normal file
73
src/app/shared/subscriptions/models/subscription.model.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
||||||
|
|
||||||
|
import { link, typedObject } from '../../../core/cache/builders/build-decorators';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { HALLink } from '../../../core/shared/hal-link.model';
|
||||||
|
import { SUBSCRIPTION } from './subscription.resource-type';
|
||||||
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { EPERSON } from '../../../core/eperson/models/eperson.resource-type';
|
||||||
|
import { DSPACE_OBJECT } from '../../../core/shared/dspace-object.resource-type';
|
||||||
|
|
||||||
|
@typedObject
|
||||||
|
@inheritSerialization(DSpaceObject)
|
||||||
|
export class Subscription extends DSpaceObject {
|
||||||
|
static type = SUBSCRIPTION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string representing subscription type
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string representing subscription type
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
public subscriptionType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of parameters for the subscription
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
public subscriptionParameterList: SubscriptionParameterList[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Subscription
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink;
|
||||||
|
eperson: HALLink;
|
||||||
|
resource: HALLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logo for this Community
|
||||||
|
* Will be undefined unless the logo {@link HALLink} has been resolved.
|
||||||
|
*/
|
||||||
|
@link(EPERSON)
|
||||||
|
eperson?: Observable<RemoteData<EPerson>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logo for this Community
|
||||||
|
* Will be undefined unless the logo {@link HALLink} has been resolved.
|
||||||
|
*/
|
||||||
|
@link(DSPACE_OBJECT)
|
||||||
|
resource?: Observable<RemoteData<DSpaceObject>>;
|
||||||
|
/**
|
||||||
|
* The embedded ePerson & dSpaceObject for this Subscription
|
||||||
|
*/
|
||||||
|
/* @deserialize
|
||||||
|
_embedded: {
|
||||||
|
ePerson: EPerson;
|
||||||
|
dSpaceObject: DSpaceObject;
|
||||||
|
};*/
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionParameterList {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { ResourceType } from '../../../core/shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for Subscription
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SUBSCRIPTION = new ResourceType('subscription');
|
@@ -0,0 +1,46 @@
|
|||||||
|
<form *ngIf="subscriptionForm" [formGroup]="subscriptionForm" (ngSubmit)="submit()" data-test="subscription-form">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">{{'subscriptions.modal.title' | translate}}</h4>
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="activeModal.close()">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="mb-3"><strong>{{dso.name}}</strong>
|
||||||
|
<span *ngIf="!!dso" class="float-right"><ds-type-badge *ngIf="!!dso" [object]="dso"></ds-type-badge></span>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<fieldset *ngFor="let subscriptionType of subscriptionForm?.controls | keyvalue" formGroupName="{{subscriptionType.key}}" class="form-group form-row">
|
||||||
|
<legend class="col-md-4 col-form-label float-md-left pt-0">
|
||||||
|
{{ 'subscriptions.modal.new-subscription-form.type.' + subscriptionType.key | translate }}:
|
||||||
|
</legend>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="hidden" formControlName="subscriptionId" [value]="subscriptionType?.value?.controls['subscriptionId'].value" >
|
||||||
|
<div class="form-check" formGroupName="frequencies" *ngFor="let frequency of frequencyDefaultValues">
|
||||||
|
<input type="checkbox" [id]="'checkbox-' + frequency" class="form-check-input" [formControlName]="frequency"/>
|
||||||
|
<label class="form-check-label"
|
||||||
|
[for]="'checkbox-' + frequency">{{ 'subscriptions.modal.new-subscription-form.frequency.' + frequency | translate }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-alert *ngIf="!!submitted && subscriptionType?.value?.controls['frequencies'].errors?.required" [type]="'alert-danger'">
|
||||||
|
{{ 'context-menu.actions.subscription.frequency.required' | translate }}
|
||||||
|
</ds-alert>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted" *ngIf="(showDeleteInfo$ | async)">{{'subscriptions.modal.delete-info' | translate}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
(click)="activeModal.close()">
|
||||||
|
{{'subscriptions.modal.close' | translate}}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-success" [disabled]="(processing$ | async) || !isValid">
|
||||||
|
<span *ngIf="(processing$ | async)">
|
||||||
|
<i class='fas fa-circle-notch fa-spin'></i> {{'subscriptions.modal.new-subscription-form.processing' | translate}}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="!(processing$ | async)">
|
||||||
|
{{'subscriptions.modal.new-subscription-form.submit' | translate}}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@@ -0,0 +1,260 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SubscriptionModalComponent } from './subscription-modal.component';
|
||||||
|
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||||
|
import { NotificationsService } from '../../notifications/notifications.service';
|
||||||
|
import { SubscriptionsDataService } from '../subscriptions-data.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { subscriptionMock, subscriptionMock2 } from '../../testing/subscriptions-data.mock';
|
||||||
|
|
||||||
|
describe('SubscriptionModalComponent', () => {
|
||||||
|
let component: SubscriptionModalComponent;
|
||||||
|
let fixture: ComponentFixture<SubscriptionModalComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
|
||||||
|
let subscriptionServiceStub;
|
||||||
|
|
||||||
|
const notificationServiceStub = jasmine.createSpyObj('authService', {
|
||||||
|
notificationWithAnchor: true,
|
||||||
|
success: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyPageInfo = Object.assign(new PageInfo(), {
|
||||||
|
'elementsPerPage': 0,
|
||||||
|
'totalElements': 0
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const pageInfo = Object.assign(new PageInfo(), {
|
||||||
|
'elementsPerPage': 2,
|
||||||
|
'totalElements': 2
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockEperson = Object.assign(new EPerson(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://localhost:8000/eperson/fake-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://localhost:8000/items/fake-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const authService = jasmine.createSpyObj('authService', {
|
||||||
|
getAuthenticatedUserFromStore: createSuccessfulRemoteDataObject$(mockEperson)
|
||||||
|
});
|
||||||
|
|
||||||
|
subscriptionServiceStub = jasmine.createSpyObj('SubscriptionsDataService', {
|
||||||
|
getSubscriptionsByPersonDSO: jasmine.createSpy('getSubscriptionsByPersonDSO'),
|
||||||
|
createSubscription: createSuccessfulRemoteDataObject$({}),
|
||||||
|
updateSubscription: createSuccessfulRemoteDataObject$({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NgbModalModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
declarations: [SubscriptionModalComponent],
|
||||||
|
providers: [
|
||||||
|
NgbActiveModal,
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationServiceStub },
|
||||||
|
{ provide: SubscriptionsDataService, useValue: subscriptionServiceStub },
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
NO_ERRORS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('when submitting subscriptions', () => {
|
||||||
|
|
||||||
|
const testSubscriptionId = 'test-subscription-id';
|
||||||
|
const testTypes = ['test1', 'test2'];
|
||||||
|
const testFrequencies = ['f', 'g'];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscriptionModalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.dso = mockItem;
|
||||||
|
(component as any).subscriptionDefaultTypes = testTypes;
|
||||||
|
(component as any).frequencyDefaultValues = testFrequencies;
|
||||||
|
de = fixture.debugElement;
|
||||||
|
subscriptionServiceStub.createSubscription.calls.reset();
|
||||||
|
subscriptionServiceStub.updateSubscription.calls.reset();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should edit an existing subscription', () => {
|
||||||
|
component.subscriptionForm = new FormGroup({});
|
||||||
|
for (let t of testTypes) {
|
||||||
|
const formGroup = new FormGroup({
|
||||||
|
subscriptionId: new FormControl(testSubscriptionId),
|
||||||
|
frequencies: new FormGroup({
|
||||||
|
f: new FormControl(false),
|
||||||
|
g: new FormControl(true),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
component.subscriptionForm.addControl(t, formGroup);
|
||||||
|
component.subscriptionForm.get('test1').markAsDirty();
|
||||||
|
component.subscriptionForm.get('test1').markAsTouched();
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.submit();
|
||||||
|
|
||||||
|
expect(subscriptionServiceStub.createSubscription).not.toHaveBeenCalled();
|
||||||
|
expect(subscriptionServiceStub.updateSubscription).toHaveBeenCalled();
|
||||||
|
expect(component.subscriptionForm.controls).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a new subscription', () => {
|
||||||
|
component.subscriptionForm = new FormGroup({});
|
||||||
|
for (let t of testTypes) {
|
||||||
|
const formGroup = new FormGroup({
|
||||||
|
subscriptionId: new FormControl(undefined),
|
||||||
|
frequencies: new FormGroup({
|
||||||
|
f: new FormControl(false),
|
||||||
|
g: new FormControl(true),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
component.subscriptionForm.addControl(t, formGroup);
|
||||||
|
component.subscriptionForm.get('test1').markAsDirty();
|
||||||
|
component.subscriptionForm.get('test1').markAsTouched();
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.submit();
|
||||||
|
|
||||||
|
expect(subscriptionServiceStub.createSubscription).toHaveBeenCalled();
|
||||||
|
expect(subscriptionServiceStub.updateSubscription).not.toHaveBeenCalled();
|
||||||
|
expect(component.subscriptionForm.controls).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when no subscription is given', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscriptionModalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.dso = mockItem;
|
||||||
|
(component as any).subscriptionDefaultTypes = ['test1', 'test2'];
|
||||||
|
de = fixture.debugElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and no subscriptions are present for the given dso', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
subscriptionServiceStub.getSubscriptionsByPersonDSO.and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(emptyPageInfo, [])));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init form properly', () => {
|
||||||
|
expect(de.query(By.css(' [data-test="subscription-form"]'))).toBeTruthy();
|
||||||
|
expect(component.subscriptionForm).toBeTruthy();
|
||||||
|
expect(component.subscriptionForm.get('test1')).toBeTruthy();
|
||||||
|
expect(component.subscriptionForm.get('test2')).toBeTruthy();
|
||||||
|
(component as any).frequencyDefaultValues.forEach((frequency) => {
|
||||||
|
expect(component.subscriptionForm.get('test1').get('frequencies').get(frequency)).toBeTruthy();
|
||||||
|
expect(component.subscriptionForm.get('test2').get('frequencies').get(frequency)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and subscriptions are present for the given dso', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
subscriptionServiceStub.getSubscriptionsByPersonDSO.and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(pageInfo, [subscriptionMock, subscriptionMock2])));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init form properly', () => {
|
||||||
|
expect(de.query(By.css(' [data-test="subscription-form"]'))).toBeTruthy();
|
||||||
|
expect(component.subscriptionForm).toBeTruthy();
|
||||||
|
expect(component.subscriptionForm.get('test1')).toBeTruthy();
|
||||||
|
expect(component.subscriptionForm.get('test2')).toBeTruthy();
|
||||||
|
(component as any).frequencyDefaultValues.forEach((frequency) => {
|
||||||
|
expect(component.subscriptionForm.get('test1').get('frequencies').get(frequency)).toBeTruthy();
|
||||||
|
|
||||||
|
expect(component.subscriptionForm.get('test2').get('frequencies').get(frequency)).toBeTruthy();
|
||||||
|
});
|
||||||
|
expect(component.subscriptionForm.get('test1').get('frequencies').get('D').value).toBeTrue();
|
||||||
|
expect(component.subscriptionForm.get('test1').get('frequencies').get('M').value).toBeTrue();
|
||||||
|
expect(component.subscriptionForm.get('test1').get('frequencies').get('W').value).toBeFalse();
|
||||||
|
|
||||||
|
expect(component.subscriptionForm.get('test2').get('frequencies').get('D').value).toBeTrue();
|
||||||
|
expect(component.subscriptionForm.get('test2').get('frequencies').get('M').value).toBeFalse();
|
||||||
|
expect(component.subscriptionForm.get('test2').get('frequencies').get('W').value).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when no subscription is given', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscriptionModalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.dso = mockItem;
|
||||||
|
component.subscription = subscriptionMock as any;
|
||||||
|
(component as any).subscriptionDefaultTypes = ['test1', 'test2'];
|
||||||
|
de = fixture.debugElement;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init form properly', () => {
|
||||||
|
expect(de.query(By.css(' [data-test="subscription-form"]'))).toBeTruthy();
|
||||||
|
expect(component.subscriptionForm).toBeTruthy();
|
||||||
|
expect(component.subscriptionForm.get('test1')).toBeTruthy();
|
||||||
|
(component as any).frequencyDefaultValues.forEach((frequency) => {
|
||||||
|
expect(component.subscriptionForm.get('test1').get('frequencies').get(frequency)).toBeTruthy();
|
||||||
|
});
|
||||||
|
expect(component.subscriptionForm.get('test1').get('frequencies').get('D').value).toBeTrue();
|
||||||
|
expect(component.subscriptionForm.get('test1').get('frequencies').get('M').value).toBeTrue();
|
||||||
|
expect(component.subscriptionForm.get('test1').get('frequencies').get('W').value).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,281 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
import { BehaviorSubject, combineLatest, from, shareReplay } from 'rxjs';
|
||||||
|
import { map, mergeMap, take, tap } from 'rxjs/operators';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import findIndex from 'lodash/findIndex';
|
||||||
|
|
||||||
|
import { Subscription } from '../models/subscription.model';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { SubscriptionsDataService } from '../subscriptions-data.service';
|
||||||
|
import { NotificationsService } from '../../notifications/notifications.service';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
|
import { isNotEmpty } from '../../empty.util';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-subscription-modal',
|
||||||
|
templateUrl: './subscription-modal.component.html',
|
||||||
|
styleUrls: ['./subscription-modal.component.scss']
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Modal that allows to manage the subscriptions for the selected item
|
||||||
|
*/
|
||||||
|
export class SubscriptionModalComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSpaceObject of which to get the subscriptions
|
||||||
|
*/
|
||||||
|
@Input() dso: DSpaceObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If given the subscription to edit by the form
|
||||||
|
*/
|
||||||
|
@Input() subscription: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The eperson related to the subscription
|
||||||
|
*/
|
||||||
|
ePersonId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if a request operation is pending
|
||||||
|
* @type {BehaviorSubject<boolean>}
|
||||||
|
*/
|
||||||
|
public processing$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, show a message explaining how to delete a subscription
|
||||||
|
*/
|
||||||
|
public showDeleteInfo$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reactive form group that will be used to add/edit subscriptions
|
||||||
|
*/
|
||||||
|
subscriptionForm: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to show validation errors when user submits
|
||||||
|
*/
|
||||||
|
submitted = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of subscription to be shown on select
|
||||||
|
*/
|
||||||
|
subscriptionDefaultTypes = ['content'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frequencies to be shown as checkboxes
|
||||||
|
*/
|
||||||
|
frequencyDefaultValues = ['D', 'W', 'M'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if form status has changed and at least one frequency is checked
|
||||||
|
*/
|
||||||
|
isValid = false;
|
||||||
|
/**
|
||||||
|
* Event emitted when a given subscription has been updated
|
||||||
|
*/
|
||||||
|
@Output() updateSubscription: EventEmitter<Subscription> = new EventEmitter<Subscription>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private subscriptionService: SubscriptionsDataService,
|
||||||
|
public activeModal: NgbActiveModal,
|
||||||
|
private authService: AuthService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When component starts initialize starting functionality
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.authService.getAuthenticatedUserFromStore().pipe(
|
||||||
|
take(1),
|
||||||
|
map((ePerson) => ePerson.uuid),
|
||||||
|
shareReplay(),
|
||||||
|
).subscribe((ePersonId: string) => {
|
||||||
|
this.ePersonId = ePersonId;
|
||||||
|
if (isNotEmpty(this.subscription)) {
|
||||||
|
this.initFormByGivenSubscription();
|
||||||
|
} else {
|
||||||
|
this.initFormByAllSubscriptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.subscriptionForm.valueChanges.subscribe((newValue) => {
|
||||||
|
let anyFrequencySelected = false;
|
||||||
|
for (let f of this.frequencyDefaultValues) {
|
||||||
|
anyFrequencySelected = anyFrequencySelected || newValue.content.frequencies[f];
|
||||||
|
}
|
||||||
|
this.isValid = anyFrequencySelected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initFormByAllSubscriptions(): void {
|
||||||
|
this.subscriptionForm = new FormGroup({});
|
||||||
|
for (let t of this.subscriptionDefaultTypes) {
|
||||||
|
const formGroup = new FormGroup({});
|
||||||
|
formGroup.addControl('subscriptionId', this.formBuilder.control(''));
|
||||||
|
formGroup.addControl('frequencies', this.formBuilder.group({}));
|
||||||
|
for (let f of this.frequencyDefaultValues) {
|
||||||
|
(formGroup.controls.frequencies as FormGroup).addControl(f, this.formBuilder.control(false));
|
||||||
|
}
|
||||||
|
this.subscriptionForm.addControl(t, formGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initFormDataBySubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the subscription is passed start the form with the information of subscription
|
||||||
|
*/
|
||||||
|
initFormByGivenSubscription(): void {
|
||||||
|
const formGroup = new FormGroup({});
|
||||||
|
formGroup.addControl('subscriptionId', this.formBuilder.control(this.subscription.id));
|
||||||
|
formGroup.addControl('frequencies', this.formBuilder.group({}));
|
||||||
|
(formGroup.get('frequencies') as FormGroup).addValidators(Validators.required);
|
||||||
|
for (let f of this.frequencyDefaultValues) {
|
||||||
|
const value = findIndex(this.subscription.subscriptionParameterList, ['value', f]) !== -1;
|
||||||
|
(formGroup.controls.frequencies as FormGroup).addControl(f, this.formBuilder.control(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscriptionForm = this.formBuilder.group({
|
||||||
|
[this.subscription.subscriptionType]: formGroup
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get subscriptions for the current ePerson & dso object relation.
|
||||||
|
* If there are no subscriptions then start with an empty form.
|
||||||
|
*/
|
||||||
|
initFormDataBySubscriptions(): void {
|
||||||
|
this.processing$.next(true);
|
||||||
|
this.subscriptionService.getSubscriptionsByPersonDSO(this.ePersonId, this.dso?.uuid).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
).subscribe({
|
||||||
|
next: (res: PaginatedList<Subscription>) => {
|
||||||
|
if (res.pageInfo.totalElements > 0) {
|
||||||
|
this.showDeleteInfo$.next(true);
|
||||||
|
for (let subscription of res.page) {
|
||||||
|
const type = subscription.subscriptionType;
|
||||||
|
const subscriptionGroup: FormGroup = this.subscriptionForm.get(type) as FormGroup;
|
||||||
|
if (isNotEmpty(subscriptionGroup)) {
|
||||||
|
subscriptionGroup.controls.subscriptionId.setValue(subscription.id);
|
||||||
|
for (let parameter of subscription.subscriptionParameterList.filter((p) => p.name === 'frequency')) {
|
||||||
|
(subscriptionGroup.controls.frequencies as FormGroup).controls[parameter.value]?.setValue(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.processing$.next(false);
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
this.processing$.next(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create/update subscriptions if needed
|
||||||
|
*/
|
||||||
|
submit() {
|
||||||
|
this.submitted = true;
|
||||||
|
const subscriptionTypes: string[] = Object.keys(this.subscriptionForm.controls);
|
||||||
|
const subscriptionsToBeCreated = [];
|
||||||
|
const subscriptionsToBeUpdated = [];
|
||||||
|
|
||||||
|
subscriptionTypes.forEach((subscriptionType: string) => {
|
||||||
|
const subscriptionGroup: FormGroup = this.subscriptionForm.controls[subscriptionType] as FormGroup;
|
||||||
|
if (subscriptionGroup.touched && subscriptionGroup.dirty) {
|
||||||
|
const body = this.createBody(
|
||||||
|
subscriptionGroup.controls.subscriptionId.value,
|
||||||
|
subscriptionType,
|
||||||
|
subscriptionGroup.controls.frequencies as FormGroup
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNotEmpty(body.id)) {
|
||||||
|
subscriptionsToBeUpdated.push(body);
|
||||||
|
} else if (isNotEmpty(body.subscriptionParameterList)) {
|
||||||
|
subscriptionsToBeCreated.push(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const toBeProcessed = [];
|
||||||
|
if (isNotEmpty(subscriptionsToBeCreated)) {
|
||||||
|
toBeProcessed.push(from(subscriptionsToBeCreated).pipe(
|
||||||
|
mergeMap((subscriptionBody) => {
|
||||||
|
return this.subscriptionService.createSubscription(subscriptionBody, this.ePersonId, this.dso.uuid).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
tap((res: RemoteData<Subscription>) => {
|
||||||
|
if (res.hasSucceeded) {
|
||||||
|
const msg = this.translate.instant('subscriptions.modal.create.success', { type: res.payload.subscriptionType });
|
||||||
|
this.notificationsService.success(null, msg);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(null, this.translate.instant('subscriptions.modal.create.error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNotEmpty(subscriptionsToBeUpdated)) {
|
||||||
|
toBeProcessed.push(from(subscriptionsToBeUpdated).pipe(
|
||||||
|
mergeMap((subscriptionBody) => {
|
||||||
|
return this.subscriptionService.updateSubscription(subscriptionBody, this.ePersonId, this.dso.uuid).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
tap((res: RemoteData<Subscription>) => {
|
||||||
|
if (res.hasSucceeded) {
|
||||||
|
const msg = this.translate.instant('subscriptions.modal.update.success', { type: res.payload.subscriptionType });
|
||||||
|
this.notificationsService.success(null, msg);
|
||||||
|
if (isNotEmpty(this.subscription)) {
|
||||||
|
this.updateSubscription.emit(res.payload);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(null, this.translate.instant('subscriptions.modal.update.error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
combineLatest([...toBeProcessed]).subscribe((res) => {
|
||||||
|
this.activeModal.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBody(subscriptionId: string, subscriptionType: string, frequencies: FormGroup): Partial<any> {
|
||||||
|
const body = {
|
||||||
|
id: (isNotEmpty(subscriptionId) ? subscriptionId : null),
|
||||||
|
subscriptionType: subscriptionType,
|
||||||
|
subscriptionParameterList: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let frequency of this.frequencyDefaultValues) {
|
||||||
|
if (frequencies.value[frequency]) {
|
||||||
|
body.subscriptionParameterList.push(
|
||||||
|
{
|
||||||
|
name: 'frequency',
|
||||||
|
value: frequency,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
<td class="dso-info">
|
||||||
|
<ng-container *ngIf="!!dso">
|
||||||
|
<ds-type-badge [object]="dso"></ds-type-badge>
|
||||||
|
<p><a [routerLink]="[getPageRoutePrefix(), dso.id]">{{dso.name}}</a></p>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!dso">
|
||||||
|
<p class="my-0">{{ 'subscriptions.table.not-available' | translate }}</p>
|
||||||
|
<p class="text-muted my-0">{{ 'subscriptions.table.not-available-message' | translate }}</p>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span *ngIf="!!subscription" class="subscription-type">{{subscription.subscriptionType}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="subscription-parameters">
|
||||||
|
<ng-container *ngIf="!!subscription">
|
||||||
|
<span *ngFor="let parameterList of subscription.subscriptionParameterList; let i = index">
|
||||||
|
{{ 'subscriptions.frequency.' + parameterList.value | translate}}<span
|
||||||
|
*ngIf="i < subscription.subscriptionParameterList.length-1 ">,</span>
|
||||||
|
</span>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td class="subscription-actions">
|
||||||
|
<div class="btn-group edit-field">
|
||||||
|
<button (click)="$event.preventDefault();openSubscriptionModal();" [disabled]="!dso"
|
||||||
|
[title]="'subscriptions.table.edit' | translate"
|
||||||
|
class="btn btn-outline-primary btn-sm access-control-editEPersonButton">
|
||||||
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<button (click)="deleteSubscriptionPopup(subscription)"
|
||||||
|
[title]="'subscriptions.table.delete' | translate"
|
||||||
|
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton">
|
||||||
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
@@ -0,0 +1,133 @@
|
|||||||
|
import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
// Import modules
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { SharedModule } from '../../shared.module';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { SubscriptionViewComponent } from './subscription-view.component';
|
||||||
|
|
||||||
|
// Import mocks
|
||||||
|
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||||
|
import { findByEPersonAndDsoResEmpty, subscriptionMock } from '../../testing/subscriptions-data.mock';
|
||||||
|
|
||||||
|
// Import utils
|
||||||
|
import { NotificationsService } from '../../notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
|
||||||
|
import { SubscriptionsDataService } from '../subscriptions-data.service';
|
||||||
|
import { Subscription } from '../models/subscription.model';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ITEM } from '../../../core/shared/item.resource-type';
|
||||||
|
|
||||||
|
describe('SubscriptionViewComponent', () => {
|
||||||
|
let component: SubscriptionViewComponent;
|
||||||
|
let fixture: ComponentFixture<SubscriptionViewComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
let modalService;
|
||||||
|
|
||||||
|
const subscriptionServiceStub = jasmine.createSpyObj('SubscriptionsDataService', {
|
||||||
|
getSubscriptionByPersonDSO: observableOf(findByEPersonAndDsoResEmpty),
|
||||||
|
deleteSubscription: createSuccessfulRemoteDataObject$({}),
|
||||||
|
updateSubscription: createSuccessfulRemoteDataObject$({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
type: ITEM,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://localhost:8000/items/fake-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NgbModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
BrowserModule,
|
||||||
|
RouterTestingModule,
|
||||||
|
SharedModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
declarations: [ SubscriptionViewComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: ComponentFixtureAutoDetect, useValue: true },
|
||||||
|
{ provide: NotificationsService, useValue: NotificationsServiceStub },
|
||||||
|
{ provide: SubscriptionsDataService, useValue: subscriptionServiceStub },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscriptionViewComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.eperson = 'testid123';
|
||||||
|
component.dso = mockItem;
|
||||||
|
component.subscription = Object.assign(new Subscription(), subscriptionMock);
|
||||||
|
de = fixture.debugElement;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have dso object info', () => {
|
||||||
|
expect(de.query(By.css('.dso-info > ds-type-badge'))).toBeTruthy();
|
||||||
|
expect(de.query(By.css('.dso-info > p > a'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have subscription type info', () => {
|
||||||
|
expect(de.query(By.css('.subscription-type'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have subscription paramenter info', () => {
|
||||||
|
expect(de.query(By.css('.subscription-parameters > span'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have subscription action info', () => {
|
||||||
|
expect(de.query(By.css('.btn-outline-primary'))).toBeTruthy();
|
||||||
|
expect(de.query(By.css('.btn-outline-danger'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open modal when clicked edit button', () => {
|
||||||
|
modalService = (component as any).modalService;
|
||||||
|
const modalSpy = spyOn(modalService, 'open');
|
||||||
|
|
||||||
|
const editBtn = de.query(By.css('.btn-outline-primary')).nativeElement;
|
||||||
|
editBtn.click();
|
||||||
|
|
||||||
|
expect(modalService.open).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call delete function when clicked delete button', () => {
|
||||||
|
const deleteSpy = spyOn(component, 'deleteSubscriptionPopup');
|
||||||
|
|
||||||
|
const deleteBtn = de.query(By.css('.btn-outline-danger')).nativeElement;
|
||||||
|
deleteBtn.click();
|
||||||
|
|
||||||
|
expect(deleteSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,109 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { Subscription } from '../models/subscription.model';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { hasValue } from '../../empty.util';
|
||||||
|
import { ConfirmationModalComponent } from '../../confirmation-modal/confirmation-modal.component';
|
||||||
|
import { SubscriptionsDataService } from '../subscriptions-data.service';
|
||||||
|
import { getCommunityModuleRoute } from '../../../community-page/community-page-routing-paths';
|
||||||
|
import { getCollectionModuleRoute } from '../../../collection-page/collection-page-routing-paths';
|
||||||
|
import { getItemModuleRoute } from '../../../item-page/item-page-routing-paths';
|
||||||
|
import { SubscriptionModalComponent } from '../subscription-modal/subscription-modal.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
// eslint-disable-next-line @angular-eslint/component-selector
|
||||||
|
selector: '[ds-subscription-view]',
|
||||||
|
templateUrl: './subscription-view.component.html',
|
||||||
|
styleUrls: ['./subscription-view.component.scss']
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Table row representing a subscription that displays all information and action buttons to manage it
|
||||||
|
*/
|
||||||
|
export class SubscriptionViewComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to be rendered
|
||||||
|
*/
|
||||||
|
@Input() subscription: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSpaceObject of the subscription
|
||||||
|
*/
|
||||||
|
@Input() dso: DSpaceObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EPerson of the subscription
|
||||||
|
*/
|
||||||
|
@Input() eperson: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an action is made emit a reload event
|
||||||
|
*/
|
||||||
|
@Output() reload = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to NgbModal
|
||||||
|
*/
|
||||||
|
public modalRef: NgbModalRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private subscriptionService: SubscriptionsDataService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the prefix of the route to the dso object page ( e.g. "items")
|
||||||
|
*/
|
||||||
|
getPageRoutePrefix(): string {
|
||||||
|
let routePrefix;
|
||||||
|
switch (this.dso.type.toString()) {
|
||||||
|
case 'community':
|
||||||
|
routePrefix = getCommunityModuleRoute();
|
||||||
|
break;
|
||||||
|
case 'collection':
|
||||||
|
routePrefix = getCollectionModuleRoute();
|
||||||
|
break;
|
||||||
|
case 'item':
|
||||||
|
routePrefix = getItemModuleRoute();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return routePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes Subscription, show notification on success/failure & updates list
|
||||||
|
* @param subscription Subscription to be deleted
|
||||||
|
*/
|
||||||
|
deleteSubscriptionPopup(subscription: Subscription) {
|
||||||
|
if (hasValue(subscription.id)) {
|
||||||
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
|
modalRef.componentInstance.dso = this.dso;
|
||||||
|
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-subscription.header';
|
||||||
|
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-subscription.info';
|
||||||
|
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-subscription.cancel';
|
||||||
|
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-subscription.confirm';
|
||||||
|
modalRef.componentInstance.brandColor = 'danger';
|
||||||
|
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
||||||
|
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
||||||
|
if (confirm) {
|
||||||
|
this.subscriptionService.deleteSubscription(subscription.id).subscribe( (res) => {
|
||||||
|
this.reload.emit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public openSubscriptionModal() {
|
||||||
|
this.modalRef = this.modalService.open(SubscriptionModalComponent);
|
||||||
|
this.modalRef.componentInstance.dso = this.dso;
|
||||||
|
this.modalRef.componentInstance.subscription = this.subscription;
|
||||||
|
this.modalRef.componentInstance.updateSubscription.pipe(take(1)).subscribe((subscription: Subscription) => {
|
||||||
|
this.subscription = subscription;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
133
src/app/shared/subscriptions/subscriptions-data.service.spec.ts
Normal file
133
src/app/shared/subscriptions/subscriptions-data.service.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { SubscriptionsDataService } from './subscriptions-data.service';
|
||||||
|
import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
|
import { Subscription } from './models/subscription.model';
|
||||||
|
import { DSOChangeAnalyzer } from '../../core/data/dso-change-analyzer.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NotificationsService } from '../notifications/notifications.service';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { getMockRequestService } from '../mocks/request.service.mock';
|
||||||
|
import { getMockRemoteDataBuildService } from '../mocks/remote-data-build.service.mock';
|
||||||
|
import { SearchDataImpl } from '../../core/data/base/search-data';
|
||||||
|
import { NotificationsServiceStub } from '../testing/notifications-service.stub';
|
||||||
|
import { HALEndpointServiceStub } from '../testing/hal-endpoint-service.stub';
|
||||||
|
import { createPaginatedList } from '../testing/utils.test';
|
||||||
|
|
||||||
|
|
||||||
|
describe('SubscriptionsDataService', () => {
|
||||||
|
|
||||||
|
|
||||||
|
let service: SubscriptionsDataService;
|
||||||
|
let searchData: SearchDataImpl<Subscription>;
|
||||||
|
|
||||||
|
let comparator: DSOChangeAnalyzer<Subscription>;
|
||||||
|
let http: HttpClient;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let store: Store<any>;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let nameService: DSONameService;
|
||||||
|
|
||||||
|
function initService() {
|
||||||
|
comparator = {} as any;
|
||||||
|
http = {} as HttpClient;
|
||||||
|
notificationsService = new NotificationsServiceStub() as any;
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
halService = new HALEndpointServiceStub('linkPath') as any;
|
||||||
|
service = new SubscriptionsDataService(comparator, http, notificationsService, requestService, rdbService, store, objectCache, halService, nameService);
|
||||||
|
spyOn((service as any).deleteData, 'delete').and.returnValue(createNoContentRemoteDataObject$());
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createSubscription', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create the subscription', () => {
|
||||||
|
const id = 'test-id';
|
||||||
|
const ePerson = 'test-ePerson';
|
||||||
|
const subscription = new Subscription();
|
||||||
|
service.createSubscription(subscription, ePerson, id).subscribe((res) => {
|
||||||
|
expect(requestService.generateRequestId).toHaveBeenCalled();
|
||||||
|
expect(res.hasCompleted).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteSubscription', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete the subscription', () => {
|
||||||
|
const id = 'test-id';
|
||||||
|
service.deleteSubscription(id).subscribe((res) => {
|
||||||
|
expect((service as any).deleteData.delete).toHaveBeenCalledWith(id);
|
||||||
|
expect(res.hasCompleted).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateSubscription', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the subscription', () => {
|
||||||
|
const id = 'test-id';
|
||||||
|
const ePerson = 'test-ePerson';
|
||||||
|
const subscription = new Subscription();
|
||||||
|
service.updateSubscription(subscription, ePerson, id).subscribe((res) => {
|
||||||
|
expect(requestService.generateRequestId).toHaveBeenCalled();
|
||||||
|
expect(res.hasCompleted).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findByEPerson', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the subscription', () => {
|
||||||
|
const ePersonId = 'test-ePersonId';
|
||||||
|
spyOn(service, 'findListByHref').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList()));
|
||||||
|
service.findByEPerson(ePersonId).subscribe((res) => {
|
||||||
|
expect(service.findListByHref).toHaveBeenCalled();
|
||||||
|
expect(res.hasCompleted).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSubscriptionsByPersonDSO', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the subscriptions', () => {
|
||||||
|
const id = 'test-id';
|
||||||
|
const ePersonId = 'test-ePersonId';
|
||||||
|
service.getSubscriptionsByPersonDSO(ePersonId, id).subscribe(() => {
|
||||||
|
expect(searchData.searchBy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
173
src/app/shared/subscriptions/subscriptions-data.service.ts
Normal file
173
src/app/shared/subscriptions/subscriptions-data.service.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
|
|
||||||
|
|
||||||
|
import { NotificationsService } from '../notifications/notifications.service';
|
||||||
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
|
import { RequestParam } from '../../core/cache/models/request-param.model';
|
||||||
|
import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||||
|
import { DSOChangeAnalyzer } from '../../core/data/dso-change-analyzer.service';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { CreateRequest, PutRequest } from '../../core/data/request.models';
|
||||||
|
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||||
|
import { RestRequest } from '../../core/data/rest-request.model';
|
||||||
|
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { Subscription } from './models/subscription.model';
|
||||||
|
import { dataService } from '../../core/data/base/data-service.decorator';
|
||||||
|
import { SUBSCRIPTION } from './models/subscription.resource-type';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { NoContent } from '../../core/shared/NoContent.model';
|
||||||
|
import { isNotEmpty, isNotEmptyOperator } from '../empty.util';
|
||||||
|
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { sendRequest } from 'src/app/core/shared/request.operators';
|
||||||
|
import { IdentifiableDataService } from '../../core/data/base/identifiable-data.service';
|
||||||
|
import { DeleteDataImpl } from '../../core/data/base/delete-data';
|
||||||
|
import { SearchDataImpl } from '../../core/data/base/search-data';
|
||||||
|
import { FindAllData } from '../../core/data/base/find-all-data';
|
||||||
|
import { followLink } from '../utils/follow-link-config.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods to retrieve subscription resources from the REST API related CRUD actions.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
@dataService(SUBSCRIPTION)
|
||||||
|
export class SubscriptionsDataService extends IdentifiableDataService<Subscription> {
|
||||||
|
protected findByEpersonLinkPath = 'findByEPerson';
|
||||||
|
|
||||||
|
private deleteData: DeleteDataImpl<Subscription>;
|
||||||
|
private findAllData: FindAllData<Subscription>;
|
||||||
|
private searchData: SearchDataImpl<Subscription>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected comparator: DSOChangeAnalyzer<Subscription>,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<any>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected nameService: DSONameService,
|
||||||
|
) {
|
||||||
|
super('subscriptions', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
|
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get subscriptions for a given item or community or collection & eperson.
|
||||||
|
*
|
||||||
|
* @param eperson The eperson to search for
|
||||||
|
* @param uuid The uuid of the dsobjcet to search for
|
||||||
|
*/
|
||||||
|
getSubscriptionsByPersonDSO(eperson: string, uuid: string): Observable<RemoteData<PaginatedList<Subscription>>> {
|
||||||
|
|
||||||
|
const optionsWithObject = Object.assign(new FindListOptions(), {
|
||||||
|
searchParams: [
|
||||||
|
new RequestParam('resource', uuid),
|
||||||
|
new RequestParam('eperson_id', eperson)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.searchData.searchBy('findByEPersonAndDso', optionsWithObject, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a subscription for a given item or community or collection.
|
||||||
|
*
|
||||||
|
* @param subscription The subscription to create
|
||||||
|
* @param ePerson The ePerson to create for
|
||||||
|
* @param uuid The uuid of the dsobjcet to create for
|
||||||
|
*/
|
||||||
|
createSubscription(subscription: Subscription, ePerson: string, uuid: string): Observable<RemoteData<Subscription>> {
|
||||||
|
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
take(1),
|
||||||
|
map((endpointUrl: string) => `${endpointUrl}?resource=${uuid}&eperson_id=${ePerson}`),
|
||||||
|
map((endpointURL: string) => new CreateRequest(this.requestService.generateRequestId(), endpointURL, JSON.stringify(subscription))),
|
||||||
|
sendRequest(this.requestService),
|
||||||
|
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
) as Observable<RemoteData<Subscription>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a subscription for a given item or community or collection.
|
||||||
|
*
|
||||||
|
* @param subscription The subscription to update
|
||||||
|
* @param ePerson The ePerson to update for
|
||||||
|
* @param uuid The uuid of the dsobjcet to update for
|
||||||
|
*/
|
||||||
|
updateSubscription(subscription, ePerson: string, uuid: string) {
|
||||||
|
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
take(1),
|
||||||
|
map((endpointUrl: string) => `${endpointUrl}/${subscription.id}?resource=${uuid}&eperson_id=${ePerson}`),
|
||||||
|
map((endpointURL: string) => new PutRequest(this.requestService.generateRequestId(), endpointURL, JSON.stringify(subscription))),
|
||||||
|
sendRequest(this.requestService),
|
||||||
|
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
) as Observable<RemoteData<Subscription>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the subscription with a give id
|
||||||
|
*
|
||||||
|
* @param id the id of Subscription to delete
|
||||||
|
*/
|
||||||
|
deleteSubscription(id: string): Observable<RemoteData<NoContent>> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((endpointUrl) => this.deleteData.delete(id)),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the list of subscription with {@link dSpaceObject} and {@link ePerson}
|
||||||
|
*
|
||||||
|
* @param options options for the find all request
|
||||||
|
*/
|
||||||
|
findAllSubscriptions(options?): Observable<RemoteData<PaginatedList<Subscription>>> {
|
||||||
|
return this.findAllData.findAll(options, true, true, followLink('resource'), followLink('eperson'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the list of subscription with {@link dSpaceObject} and {@link ePerson}
|
||||||
|
*
|
||||||
|
* @param ePersonId The eperson id
|
||||||
|
* @param options The options for the find all request
|
||||||
|
*/
|
||||||
|
findByEPerson(ePersonId: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Subscription>>> {
|
||||||
|
const optionsWithObject = Object.assign(new FindListOptions(), options, {
|
||||||
|
searchParams: [
|
||||||
|
new RequestParam('uuid', ePersonId)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// return this.searchData.searchBy(this.findByEpersonLinkPath, optionsWithObject, true, true, followLink('dSpaceObject'), followLink('ePerson'));
|
||||||
|
|
||||||
|
return this.getEndpoint().pipe(
|
||||||
|
map(href => `${href}/search/${this.findByEpersonLinkPath}`),
|
||||||
|
switchMap(href => this.findListByHref(href, optionsWithObject, false, true, followLink('resource'), followLink('eperson')))
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
34
src/app/shared/subscriptions/subscriptions.module.ts
Normal file
34
src/app/shared/subscriptions/subscriptions.module.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { SubscriptionViewComponent } from './subscription-view/subscription-view.component';
|
||||||
|
import { SubscriptionModalComponent } from './subscription-modal/subscription-modal.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { SharedModule } from '../shared.module';
|
||||||
|
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
const COMPONENTS = [
|
||||||
|
SubscriptionViewComponent,
|
||||||
|
SubscriptionModalComponent
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
...COMPONENTS
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NgbModalModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
TranslateModule,
|
||||||
|
RouterModule,
|
||||||
|
SharedModule
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
...COMPONENTS
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SubscriptionsModule {
|
||||||
|
}
|
@@ -9,4 +9,10 @@ export class RouterStub {
|
|||||||
navigateByUrl(url): void {
|
navigateByUrl(url): void {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
createUrlTree(commands, navigationExtras = {}) {
|
||||||
|
return '/testing-url';
|
||||||
|
}
|
||||||
|
serializeUrl(commands, navExtras = {}) {
|
||||||
|
return '/testing-url';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
160
src/app/shared/testing/subscriptions-data.mock.ts
Normal file
160
src/app/shared/testing/subscriptions-data.mock.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ITEM_TYPE } from '../../core/shared/item-relationships/item-type.resource-type';
|
||||||
|
|
||||||
|
export const mockSubscriptionEperson = Object.assign(new EPerson(), {
|
||||||
|
'id': 'fake-eperson-id',
|
||||||
|
'uuid': 'fake-eperson-id',
|
||||||
|
'handle': null,
|
||||||
|
'metadata': {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
'value': 'user',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
'value': 'testr',
|
||||||
|
'language': null,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1,
|
||||||
|
'place': 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'netid': null,
|
||||||
|
'lastActive': '2021-09-01T12:06:19.000+00:00',
|
||||||
|
'canLogIn': true,
|
||||||
|
'email': 'user@test.com',
|
||||||
|
'requireCertificate': false,
|
||||||
|
'selfRegistered': false,
|
||||||
|
'type': 'eperson',
|
||||||
|
'_links': {
|
||||||
|
'groups': {
|
||||||
|
'href': 'https://dspace.org/server/api/eperson/epersons/fake-eperson-id/groups'
|
||||||
|
},
|
||||||
|
'self': {
|
||||||
|
'href': 'https://dspace.org/server/api/eperson/epersons/fake-eperson-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockSubscriptionDSO = Object.assign(new Item(),
|
||||||
|
{
|
||||||
|
id: 'fake-item-id',
|
||||||
|
uuid: 'fake-item-id',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{ value: 'test item subscription' }]
|
||||||
|
},
|
||||||
|
type: ITEM_TYPE,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://dspace.org/server/api/core/items/fake-item-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const mockSubscriptionDSO2 = Object.assign(new Item(),
|
||||||
|
{
|
||||||
|
id: 'fake-item-id2',
|
||||||
|
uuid: 'fake-item-id2',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{ value: 'test item subscription 2' }]
|
||||||
|
},
|
||||||
|
type: ITEM_TYPE,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://dspace.org/server/api/core/items/fake-item-id2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export const findByEPersonAndDsoResEmpty = {
|
||||||
|
'type': {
|
||||||
|
'value': 'paginated-list'
|
||||||
|
},
|
||||||
|
'pageInfo': {
|
||||||
|
'elementsPerPage': 0,
|
||||||
|
'totalElements': 0,
|
||||||
|
'totalPages': 1,
|
||||||
|
'currentPage': 1
|
||||||
|
},
|
||||||
|
'_links': {
|
||||||
|
'self': {
|
||||||
|
'href': 'https://dspacecris7.4science.cloud/server/api/core/subscriptions/search/findByEPersonAndDso?resource=092b59e8-8159-4e70-98b5-93ec60bd3431&eperson_id=335647b6-8a52-4ecb-a8c1-7ebabb199bda'
|
||||||
|
},
|
||||||
|
'page': [
|
||||||
|
{
|
||||||
|
'href': 'https://dspacecris7.4science.cloud/server/api/core/subscriptions/22'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'href': 'https://dspacecris7.4science.cloud/server/api/core/subscriptions/48'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'page': []
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscriptionMock = {
|
||||||
|
'id': 21,
|
||||||
|
'type': 'subscription',
|
||||||
|
'subscriptionParameterList': [
|
||||||
|
{
|
||||||
|
'id': 77,
|
||||||
|
'name': 'frequency',
|
||||||
|
'value': 'D'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 78,
|
||||||
|
'name': 'frequency',
|
||||||
|
'value': 'M'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'subscriptionType': 'test1',
|
||||||
|
'ePerson': createSuccessfulRemoteDataObject$(mockSubscriptionEperson),
|
||||||
|
'dSpaceObject': createSuccessfulRemoteDataObject$(mockSubscriptionDSO),
|
||||||
|
'_links': {
|
||||||
|
'dSpaceObject': {
|
||||||
|
'href': 'https://dspace/server/api/core/subscriptions/21/dSpaceObject'
|
||||||
|
},
|
||||||
|
'ePerson': {
|
||||||
|
'href': 'https://dspace/server/api/core/subscriptions/21/ePerson'
|
||||||
|
},
|
||||||
|
'self': {
|
||||||
|
'href': 'https://dspace/server/api/core/subscriptions/21'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscriptionMock2 = {
|
||||||
|
'id': 21,
|
||||||
|
'type': 'subscription',
|
||||||
|
'subscriptionParameterList': [
|
||||||
|
{
|
||||||
|
'id': 77,
|
||||||
|
'name': 'frequency',
|
||||||
|
'value': 'D'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'subscriptionType': 'test2',
|
||||||
|
'ePerson': createSuccessfulRemoteDataObject$(mockSubscriptionEperson),
|
||||||
|
'dSpaceObject': createSuccessfulRemoteDataObject$(mockSubscriptionDSO2),
|
||||||
|
'_links': {
|
||||||
|
'dSpaceObject': {
|
||||||
|
'href': 'https://dspacecris7.4science.cloud/server/api/core/subscriptions/21/dSpaceObject'
|
||||||
|
},
|
||||||
|
'ePerson': {
|
||||||
|
'href': 'https://dspacecris7.4science.cloud/server/api/core/subscriptions/21/ePerson'
|
||||||
|
},
|
||||||
|
'self': {
|
||||||
|
'href': 'https://dspacecris7.4science.cloud/server/api/core/subscriptions/21'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@@ -26,7 +26,8 @@ import { BrowserOnlyMockPipe } from './browser-only-mock.pipe';
|
|||||||
BrowserOnlyMockPipe,
|
BrowserOnlyMockPipe,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
QueryParamsDirectiveStub
|
QueryParamsDirectiveStub,
|
||||||
|
RouterLinkDirectiveStub
|
||||||
],
|
],
|
||||||
schemas: [
|
schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { SubscriptionsPageModule } from './subscriptions-page.module';
|
||||||
|
import { SubscriptionsPageComponent } from './subscriptions-page.component';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
SubscriptionsPageModule,
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
data: {
|
||||||
|
title: 'subscriptions.title',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: SubscriptionsPageComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SubscriptionsPageRoutingModule {
|
||||||
|
}
|
47
src/app/subscriptions-page/subscriptions-page.component.html
Normal file
47
src/app/subscriptions-page/subscriptions-page.component.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 m-40">
|
||||||
|
<h2>{{'subscriptions.title' | translate}}</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 m-40">
|
||||||
|
<ds-themed-loading *ngIf="loading$ | async"></ds-themed-loading>
|
||||||
|
<ng-container *ngVar="(subscriptions$ | async) as subscriptions">
|
||||||
|
|
||||||
|
<ds-pagination *ngIf="subscriptions?.pageInfo?.totalElements > 0 && !(loading$ | async)"
|
||||||
|
[paginationOptions]="config"
|
||||||
|
[collectionSize]="subscriptions?.pageInfo?.totalElements"
|
||||||
|
[hideGear]="true"
|
||||||
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="formats" class="table table-striped table-hover" data-test="subscription-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 50%" scope="col">{{'subscriptions.table.dso' | translate}}</th>
|
||||||
|
<th scope="col">{{'subscriptions.table.subscription_type' | translate}}</th>
|
||||||
|
<th scope="col">{{'subscriptions.table.subscription_frequency' | translate}}</th>
|
||||||
|
<th scope="col">{{'subscriptions.table.action' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ds-subscription-view *ngFor="let subscription of subscriptions?.page"
|
||||||
|
[dso]="(subscription?.resource | async)?.payload"
|
||||||
|
[eperson]="(subscription?.eperson | async)?.payload?.id"
|
||||||
|
[subscription]="subscription"
|
||||||
|
(reload)="refresh()">
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</ds-pagination>
|
||||||
|
|
||||||
|
<ds-alert *ngIf="subscriptions?.pageInfo?.totalElements == 0 && !(loading$ | async)" [type]="'alert-info'" data-test="empty-alert">
|
||||||
|
{{ 'subscriptions.table.empty.message' | translate }}
|
||||||
|
</ds-alert>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
127
src/app/subscriptions-page/subscriptions-page.component.spec.ts
Normal file
127
src/app/subscriptions-page/subscriptions-page.component.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SubscriptionsPageComponent } from './subscriptions-page.component';
|
||||||
|
import { PaginationService } from '../core/pagination/pagination.service';
|
||||||
|
import { SubscriptionsDataService } from '../shared/subscriptions/subscriptions-data.service';
|
||||||
|
import { PaginationServiceStub } from '../shared/testing/pagination-service.stub';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
|
||||||
|
import {
|
||||||
|
mockSubscriptionEperson,
|
||||||
|
subscriptionMock,
|
||||||
|
subscriptionMock2
|
||||||
|
} from '../shared/testing/subscriptions-data.mock';
|
||||||
|
import { MockActivatedRoute } from '../shared/mocks/active-router.mock';
|
||||||
|
import { VarDirective } from '../shared/utils/var.directive';
|
||||||
|
import { SubscriptionViewComponent } from '../shared/subscriptions/subscription-view/subscription-view.component';
|
||||||
|
import { PageInfo } from '../core/shared/page-info.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { buildPaginatedList } from '../core/data/paginated-list.model';
|
||||||
|
|
||||||
|
describe('SubscriptionsPageComponent', () => {
|
||||||
|
let component: SubscriptionsPageComponent;
|
||||||
|
let fixture: ComponentFixture<SubscriptionsPageComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
|
||||||
|
const authServiceStub = jasmine.createSpyObj('authorizationService', {
|
||||||
|
getAuthenticatedUserFromStore: observableOf(mockSubscriptionEperson)
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscriptionServiceStub = jasmine.createSpyObj('SubscriptionsDataService', {
|
||||||
|
findByEPerson: jasmine.createSpy('findByEPerson')
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginationService = new PaginationServiceStub();
|
||||||
|
|
||||||
|
const mockSubscriptionList = [subscriptionMock, subscriptionMock2];
|
||||||
|
|
||||||
|
const emptyPageInfo = Object.assign(new PageInfo(), {
|
||||||
|
totalElements: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageInfo = Object.assign(new PageInfo(), {
|
||||||
|
totalElements: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
BrowserModule,
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
NoopAnimationsModule
|
||||||
|
],
|
||||||
|
declarations: [SubscriptionsPageComponent, SubscriptionViewComponent, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{ provide: SubscriptionsDataService, useValue: subscriptionServiceStub },
|
||||||
|
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||||
|
{ provide: AuthService, useValue: authServiceStub },
|
||||||
|
{ provide: PaginationService, useValue: paginationService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubscriptionsPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
de = fixture.debugElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when there are subscriptions', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
subscriptionServiceStub.findByEPerson.and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(pageInfo, mockSubscriptionList)));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show table', () => {
|
||||||
|
expect(de.query(By.css('[data-test="subscription-table"]'))).toBeTruthy();
|
||||||
|
expect(de.query(By.css('[data-test="empty-alert"]'))).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a row for each results entry',() => {
|
||||||
|
expect(de.query(By.css('[data-test="subscription-table"]'))).toBeTruthy();
|
||||||
|
expect(de.query(By.css('[data-test="empty-alert"]'))).toBeNull();
|
||||||
|
expect(de.queryAll(By.css('tbody > tr')).length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when there are no subscriptions', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
subscriptionServiceStub.findByEPerson.and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(emptyPageInfo, [])));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show table', () => {
|
||||||
|
expect(de.query(By.css('[data-test="subscription-table"]'))).toBeNull();
|
||||||
|
expect(de.query(By.css('[data-test="empty-alert"]'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
115
src/app/subscriptions-page/subscriptions-page.component.ts
Normal file
115
src/app/subscriptions-page/subscriptions-page.component.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, combineLatestWith, Observable, shareReplay, Subscription as rxjsSubscription } from 'rxjs';
|
||||||
|
import { map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { Subscription } from '../shared/subscriptions/models/subscription.model';
|
||||||
|
import { buildPaginatedList, PaginatedList } from '../core/data/paginated-list.model';
|
||||||
|
import { SubscriptionsDataService } from '../shared/subscriptions/subscriptions-data.service';
|
||||||
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
import { PaginationService } from '../core/pagination/pagination.service';
|
||||||
|
import { PageInfo } from '../core/shared/page-info.model';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||||
|
import { getAllCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-subscriptions-page',
|
||||||
|
templateUrl: './subscriptions-page.component.html',
|
||||||
|
styleUrls: ['./subscriptions-page.component.scss']
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* List and allow to manage all the active subscription for the current user
|
||||||
|
*/
|
||||||
|
export class SubscriptionsPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The subscriptions to show on this page, as an Observable list.
|
||||||
|
*/
|
||||||
|
subscriptions$: BehaviorSubject<PaginatedList<Subscription>> = new BehaviorSubject(buildPaginatedList<Subscription>(new PageInfo(), []));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current pagination configuration for the page
|
||||||
|
*/
|
||||||
|
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'elp',
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if is loading
|
||||||
|
*/
|
||||||
|
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current eperson id
|
||||||
|
*/
|
||||||
|
ePersonId$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rxjs subscription used to retrieve the result list
|
||||||
|
*/
|
||||||
|
sub: rxjsSubscription = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private paginationService: PaginationService,
|
||||||
|
private authService: AuthService,
|
||||||
|
private subscriptionService: SubscriptionsDataService
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the current eperson id and call method to retrieve the subscriptions
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.ePersonId$ = this.authService.getAuthenticatedUserFromStore().pipe(
|
||||||
|
take(1),
|
||||||
|
map((ePerson: EPerson) => ePerson.id),
|
||||||
|
shareReplay()
|
||||||
|
);
|
||||||
|
this.retrieveSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve subscription list related to the current user.
|
||||||
|
* When page is changed it will request the new subscriptions for the new page config
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private retrieveSubscriptions(): void {
|
||||||
|
this.sub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
|
combineLatestWith(this.ePersonId$),
|
||||||
|
tap(() => this.loading$.next(true)),
|
||||||
|
switchMap(([currentPagination, ePersonId]) => this.subscriptionService.findByEPerson(ePersonId,{
|
||||||
|
currentPage: currentPagination.currentPage,
|
||||||
|
elementsPerPage: currentPagination.pageSize
|
||||||
|
})),
|
||||||
|
getAllCompletedRemoteData()
|
||||||
|
).subscribe((res: RemoteData<PaginatedList<Subscription>>) => {
|
||||||
|
if (res.hasSucceeded) {
|
||||||
|
this.subscriptions$.next(res.payload);
|
||||||
|
}
|
||||||
|
this.loading$.next(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* When a subscription is deleted refresh the subscription list
|
||||||
|
*/
|
||||||
|
refresh(): void {
|
||||||
|
if (hasValue(this.sub)) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.retrieveSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.sub)) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
src/app/subscriptions-page/subscriptions-page.module.ts
Normal file
15
src/app/subscriptions-page/subscriptions-page.module.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SubscriptionsPageComponent } from './subscriptions-page.component';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { SubscriptionsModule } from '../shared/subscriptions/subscriptions.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [SubscriptionsPageComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
SubscriptionsModule,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SubscriptionsPageModule { }
|
@@ -1564,6 +1564,13 @@
|
|||||||
|
|
||||||
"confirmation-modal.delete-profile.confirm": "Delete",
|
"confirmation-modal.delete-profile.confirm": "Delete",
|
||||||
|
|
||||||
|
"confirmation-modal.delete-subscription.header": "Delete Subscription",
|
||||||
|
|
||||||
|
"confirmation-modal.delete-subscription.info": "Are you sure you want to delete subscription for \"{{ dsoName }}\"",
|
||||||
|
|
||||||
|
"confirmation-modal.delete-subscription.cancel": "Cancel",
|
||||||
|
|
||||||
|
"confirmation-modal.delete-subscription.confirm": "Delete",
|
||||||
|
|
||||||
"error.bitstream": "Error fetching bitstream",
|
"error.bitstream": "Error fetching bitstream",
|
||||||
|
|
||||||
@@ -3141,6 +3148,8 @@
|
|||||||
|
|
||||||
"nav.stop-impersonating": "Stop impersonating EPerson",
|
"nav.stop-impersonating": "Stop impersonating EPerson",
|
||||||
|
|
||||||
|
"nav.subscriptions" : "Subscriptions",
|
||||||
|
|
||||||
"nav.toggle" : "Toggle navigation",
|
"nav.toggle" : "Toggle navigation",
|
||||||
|
|
||||||
"nav.user.description" : "User profile bar",
|
"nav.user.description" : "User profile bar",
|
||||||
@@ -4760,6 +4769,77 @@
|
|||||||
"submission.workspace.generic.view-help": "Select this option to view the item's metadata.",
|
"submission.workspace.generic.view-help": "Select this option to view the item's metadata.",
|
||||||
|
|
||||||
|
|
||||||
|
"subscriptions.title": "Subscriptions",
|
||||||
|
|
||||||
|
"subscriptions.item": "Subscriptions for items",
|
||||||
|
|
||||||
|
"subscriptions.collection": "Subscriptions for collections",
|
||||||
|
|
||||||
|
"subscriptions.community": "Subscriptions for communities",
|
||||||
|
|
||||||
|
"subscriptions.subscription_type": "Subscription type",
|
||||||
|
|
||||||
|
"subscriptions.frequency": "Subscription frequency",
|
||||||
|
|
||||||
|
"subscriptions.frequency.D": "Daily",
|
||||||
|
|
||||||
|
"subscriptions.frequency.M": "Monthly",
|
||||||
|
|
||||||
|
"subscriptions.frequency.W": "Weekly",
|
||||||
|
|
||||||
|
"subscriptions.tooltip": "Subscribe",
|
||||||
|
|
||||||
|
"subscriptions.modal.title": "Subscriptions",
|
||||||
|
|
||||||
|
"subscriptions.modal.type-frequency": "Type and frequency",
|
||||||
|
|
||||||
|
"subscriptions.modal.close": "Close",
|
||||||
|
|
||||||
|
"subscriptions.modal.delete-info": "To remove this subscription, please visit the \"Subscriptions\" page under your user profile",
|
||||||
|
|
||||||
|
"subscriptions.modal.new-subscription-form.type.content": "Content",
|
||||||
|
|
||||||
|
"subscriptions.modal.new-subscription-form.frequency.D": "Daily",
|
||||||
|
|
||||||
|
"subscriptions.modal.new-subscription-form.frequency.W": "Weekly",
|
||||||
|
|
||||||
|
"subscriptions.modal.new-subscription-form.frequency.M": "Monthly",
|
||||||
|
|
||||||
|
"subscriptions.modal.new-subscription-form.submit": "Submit",
|
||||||
|
|
||||||
|
"subscriptions.modal.new-subscription-form.processing": "Processing...",
|
||||||
|
|
||||||
|
"subscriptions.modal.create.success": "Subscribed to {{ type }} successfully.",
|
||||||
|
|
||||||
|
"subscriptions.modal.delete.success": "Subscription deleted successfully",
|
||||||
|
|
||||||
|
"subscriptions.modal.update.success": "Subscription to {{ type }} updated successfully",
|
||||||
|
|
||||||
|
"subscriptions.modal.create.error": "An error occurs during the subscription creation",
|
||||||
|
|
||||||
|
"subscriptions.modal.delete.error": "An error occurs during the subscription delete",
|
||||||
|
|
||||||
|
"subscriptions.modal.update.error": "An error occurs during the subscription update",
|
||||||
|
|
||||||
|
"subscriptions.table.dso": "Subject",
|
||||||
|
|
||||||
|
"subscriptions.table.subscription_type": "Subscription Type",
|
||||||
|
|
||||||
|
"subscriptions.table.subscription_frequency": "Subscription Frequency",
|
||||||
|
|
||||||
|
"subscriptions.table.action": "Action",
|
||||||
|
|
||||||
|
"subscriptions.table.edit": "Edit",
|
||||||
|
|
||||||
|
"subscriptions.table.delete": "Delete",
|
||||||
|
|
||||||
|
"subscriptions.table.not-available": "Not available",
|
||||||
|
|
||||||
|
"subscriptions.table.not-available-message": "The subscribed item has been deleted, or you don't currently have the permission to view it",
|
||||||
|
|
||||||
|
"subscriptions.table.empty.message": "You do not have any subscriptions at this time. To subscribe to email updates for a Community or Collection, use the subscription button on the object's page.",
|
||||||
|
|
||||||
|
|
||||||
"thumbnail.default.alt": "Thumbnail Image",
|
"thumbnail.default.alt": "Thumbnail Image",
|
||||||
|
|
||||||
"thumbnail.default.placeholder": "No Thumbnail Available",
|
"thumbnail.default.placeholder": "No Thumbnail Available",
|
||||||
|
Reference in New Issue
Block a user