Skip to content

Commit

Permalink
Merge pull request DSpace#2017 from 4Science/CST-7757
Browse files Browse the repository at this point in the history
Manage Subscriptions interface
  • Loading branch information
tdonohue authored Feb 13, 2023
2 parents a3e99b6 + 078bdd2 commit bd428d7
Show file tree
Hide file tree
Showing 36 changed files with 2,051 additions and 5 deletions.
6 changes: 6 additions & 0 deletions src/app/app-routing-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,9 @@ export function getRequestCopyModulePath() {
}

export const HEALTH_PAGE_PATH = 'health';

export const SUBSCRIPTIONS_MODULE_PATH = 'subscriptions';

export function getSubscriptionsModuleRoute() {
return `/${SUBSCRIPTIONS_MODULE_PATH}`;
}
6 changes: 6 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,12 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
canActivate: [GroupAdministratorGuard],
},
{
path: 'subscriptions',
loadChildren: () => import('./subscriptions-page/subscriptions-page-routing.module')
.then((m) => m.SubscriptionsPageRoutingModule),
canActivate: [AuthenticatedGuard]
},
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
]
}
Expand Down
5 changes: 4 additions & 1 deletion src/app/collection-page/collection-page.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
[title]="'collection.page.news'">
</ds-comcol-page-content>
</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>
<section class="comcol-page-browse-section">
<!-- Browse-By Links -->
Expand Down
3 changes: 3 additions & 0 deletions src/app/community-page/community-page.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
</ds-comcol-page-content>
</header>
<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>
<section class="comcol-page-browse-section">

Expand Down
2 changes: 2 additions & 0 deletions src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ import { OrcidAuthService } from './orcid/orcid-auth.service';
import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service';
import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service';
import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model';
import { Subscription } from '../shared/subscriptions/models/subscription.model';

/**
* When not in production, endpoint responses can be mocked for testing purposes
Expand Down Expand Up @@ -365,6 +366,7 @@ export const models =
OrcidHistory,
AccessStatusObject,
IdentifierData,
Subscription,
];

@NgModule({
Expand Down
1 change: 1 addition & 0 deletions src/app/core/data/feature-authorization/feature-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ export enum FeatureID {
CanSubmit = 'canSubmit',
CanEditItem = 'canEditItem',
CanRegisterDOI = 'canRegisterDOI',
CanSubscribe = 'canSubscribeDso',
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
</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]="[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>
<ds-log-out *ngIf="!inExpandableNavbar" data-test="log-out-component"></ds-log-out>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AppState } from '../../../app.reducer';
import { isAuthenticationLoading } from '../../../core/auth/selectors';
import { MYDSPACE_ROUTE } from '../../../my-dspace-page/my-dspace-page.component';
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.
Expand Down Expand Up @@ -48,6 +48,11 @@ export class UserMenuComponent implements OnInit {
*/
public profileRoute = getProfileModuleRoute();

/**
* The profile page route
*/
public subscriptionsRoute = getSubscriptionsModuleRoute();

constructor(private store: Store<AppState>,
private authService: AuthService) {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
}

}
5 changes: 5 additions & 0 deletions src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ import {
AdvancedClaimedTaskActionRatingComponent
} 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 {
DsoPageSubscriptionButtonComponent
} from './dso-page/dso-page-subscription-button/dso-page-subscription-button.component';


const MODULES = [
CommonModule,
Expand Down Expand Up @@ -351,6 +355,7 @@ const COMPONENTS = [
ItemPageTitleFieldComponent,
ThemedSearchNavbarComponent,
ListableNotificationObjectComponent,
DsoPageSubscriptionButtonComponent,
MetadataFieldWrapperComponent,
ContextHelpWrapperComponent,
];
Expand Down
73 changes: 73 additions & 0 deletions src/app/shared/subscriptions/models/subscription.model.ts
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 10 additions & 0 deletions src/app/shared/subscriptions/models/subscription.resource-type.ts
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
@@ -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">&times;</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>
Empty file.
Loading

0 comments on commit bd428d7

Please sign in to comment.