[CST-4875] Created feedback route, created feedback form, created service & guard, utilzed when user is logged in, unit testing and lint check

This commit is contained in:
Rezart Vata
2021-12-09 12:33:53 +01:00
parent 46d340a5ce
commit f74716a459
26 changed files with 521 additions and 33 deletions

View File

@@ -71,7 +71,7 @@ export function getMetaReducers(): MetaReducer<AppState>[] {
*/ */
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
(control: AbstractControl, model: any, hasFocus: boolean) => { (control: AbstractControl, model: any, hasFocus: boolean) => {
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus); return (control.touched && !hasFocus) || (control.errors ?.emailTaken && hasFocus);
}; };
const IMPORTS = [ const IMPORTS = [
@@ -114,10 +114,10 @@ const PROVIDERS = [
// Check the authentication token when the app initializes // Check the authentication token when the app initializes
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: (store: Store<AppState>,) => { useFactory: (store: Store<AppState>, ) => {
return () => store.dispatch(new CheckAuthenticationTokenAction()); return () => store.dispatch(new CheckAuthenticationTokenAction());
}, },
deps: [ Store ], deps: [Store],
multi: true multi: true
}, },
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
@@ -146,7 +146,7 @@ const PROVIDERS = [
}, },
// insert the unique id of the user that is using the application utilizing cookies // insert the unique id of the user that is using the application utilizing cookies
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: (cookieService: CookieService, uuidService: UUIDService) => { useFactory: (cookieService: CookieService, uuidService: UUIDService) => {
const correlationId = cookieService.get('CORRELATION-ID'); const correlationId = cookieService.get('CORRELATION-ID');
@@ -156,8 +156,8 @@ const PROVIDERS = [
} }
return () => true; return () => true;
}, },
multi: true, multi: true,
deps: [ CookieService, UUIDService ] deps: [CookieService, UUIDService]
}, },
{ {
provide: DYNAMIC_ERROR_MESSAGES_MATCHER, provide: DYNAMIC_ERROR_MESSAGES_MATCHER,

View File

@@ -77,6 +77,7 @@ import { MetadataSchema } from './metadata/metadata-schema.model';
import { MetadataService } from './metadata/metadata.service'; import { MetadataService } from './metadata/metadata.service';
import { RegistryService } from './registry/registry.service'; import { RegistryService } from './registry/registry.service';
import { RoleService } from './roles/role.service'; import { RoleService } from './roles/role.service';
import { FeedbackDataService } from './feedback/feedback-data.service';
import { ApiService } from './services/api.service'; import { ApiService } from './services/api.service';
import { ServerResponseService } from './services/server-response.service'; import { ServerResponseService } from './services/server-response.service';
@@ -163,6 +164,7 @@ import { RootDataService } from './data/root-data.service';
import { Root } from './data/root.model'; import { Root } from './data/root.model';
import { SearchConfig } from './shared/search/search-filters/search-config.model'; import { SearchConfig } from './shared/search/search-filters/search-config.model';
import { SequenceService } from './shared/sequence.service'; import { SequenceService } from './shared/sequence.service';
import { FeedbackGuard } from './feedback/feedback.guard';
/** /**
* When not in production, endpoint responses can be mocked for testing purposes * When not in production, endpoint responses can be mocked for testing purposes
@@ -285,6 +287,8 @@ const PROVIDERS = [
VocabularyService, VocabularyService,
VocabularyTreeviewService, VocabularyTreeviewService,
SequenceService, SequenceService,
FeedbackDataService,
// FeedbackGuard
]; ];
/** /**

View File

@@ -25,4 +25,5 @@ export enum FeatureID {
CanEditVersion = 'canEditVersion', CanEditVersion = 'canEditVersion',
CanDeleteVersion = 'canDeleteVersion', CanDeleteVersion = 'canDeleteVersion',
CanCreateVersion = 'canCreateVersion', CanCreateVersion = 'canCreateVersion',
CanSendFeedback = 'canSendFeedback',
} }

View File

@@ -0,0 +1,94 @@
import { Injectable } from '@angular/core';
import { AuthService } from '../auth/auth.service';
import { Observable } from 'rxjs';
import { map, filter, distinctUntilChanged, tap, mergeMap } from 'rxjs/operators';
import { isNotEmpty } from '../../shared/empty.util';
import { DataService } from '../data/data.service';
import { Feedback } from './models/feedback.model';
import { FEEDBACK } from './models/feedback.resource-type';
import { dataService } from 'src/app/core/cache/builders/build-decorators';
import { RequestService } from 'src/app/core/data/request.service';
import { RemoteDataBuildService } from 'src/app/core/cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { ObjectCacheService } from 'src/app/core/cache/object-cache.service';
import { HALEndpointService } from 'src/app/core/shared/hal-endpoint.service';
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DSOChangeAnalyzer } from 'src/app/core/data/dso-change-analyzer.service';
import { getFirstSucceededRemoteData, getRemoteDataPayload, getFirstCompletedRemoteData } from 'src/app/core/shared/operators';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { PostRequest } from 'src/app/core/data/request.models';
/**
* Service for checking and managing the status of the current end user agreement
*/
@Injectable()
@dataService(FEEDBACK)
export class FeedbackDataService extends DataService<Feedback> {
protected linkPath = 'feedbacks';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<any>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DSOChangeAnalyzer<Feedback>,
protected authService: AuthService, ) {
super();
}
/**
* Get feedback from its id
* @param uuid string the id of the feedback
*/
getFeedback(uuid: string): Observable<Feedback> {
return this.findById(uuid).pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
);
}
/**
* Create feedback
* @param uuid string the id of the feedback
* @return Observable<Feedback>
* server response
*/
createFeedback(payoload: Feedback): Observable<Feedback> {
return this.postToEndpoint(this.linkPath, payoload);
}
/**
* Make a new post request
*
* @param linkName
* The endpoint link name
* @param body
* The post request body
* @param options
* The [HttpOptions] object
* @return Observable<Feedback>
* server response
*/
public postToEndpoint(linkName: string, body: any, options?: HttpOptions): Observable<Feedback> {
const requestId = this.requestService.generateRequestId();
const href$ = this.halService.getEndpoint(linkName).pipe(
filter((href: string) => isNotEmpty(href)),
distinctUntilChanged(),
map((endpointURL: string) => {
const request = new PostRequest(requestId, endpointURL, body, options);
return this.requestService.send(request);
}),
).subscribe();
return this.rdbService.buildFromRequestUUID<Feedback>(requestId).pipe(
getFirstCompletedRemoteData(),
getRemoteDataPayload(),
);
}
}

View File

@@ -0,0 +1,27 @@
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id';
import { Injectable } from '@angular/core';
/**
* An abstract guard for redirecting users to the user agreement page if a certain condition is met
* That condition is defined by abstract method hasAccepted
*/
@Injectable()
export abstract class FeedbackGuard implements CanActivate {
constructor(protected router: Router, private authorizationService: AuthorizationDataService) {
}
/**
* True when the user agreement has been accepted
* The user will be redirected to the End User Agreement page if they haven't accepted it before
* A redirect URL will be provided with the navigation so the component can redirect the user back to the blocked route
* when they're finished accepting the agreement
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
}
}

View File

@@ -0,0 +1,37 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { Observable } from 'rxjs';
import { link, typedObject } from '../../cache/builders/build-decorators';
import { PaginatedList } from '../../data/paginated-list.model';
import { RemoteData } from '../../data/remote-data';
import { DSpaceObject } from '../../shared/dspace-object.model';
import { HALLink } from '../../shared/hal-link.model';
import { FEEDBACK } from './feedback.resource-type';
@typedObject
@inheritSerialization(DSpaceObject)
export class Feedback extends DSpaceObject {
static type = FEEDBACK;
/**
* The email address
*/
@autoserialize
public email: string;
/**
* A ring representing message the user inserted
*/
@autoserialize
public message: string;
/**
* A string representing the page from which the user came from
*/
@autoserialize
public page: string;
_links: {
self: HALLink;
};
}

View File

@@ -0,0 +1,10 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for Feedback
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const FEEDBACK = new ResourceType('feedback');

View File

@@ -40,9 +40,8 @@
<h5 class="text-uppercase">Footer Content</h5> <h5 class="text-uppercase">Footer Content</h5>
<p> <p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iste atque ea quis Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iste atque ea quis molestias. Fugiat pariatur maxime quis culpa
molestias. Fugiat pariatur maxime quis culpa corporis vitae repudiandae aliquam corporis vitae repudiandae aliquam voluptatem veniam, est atque cumque eum delectus sint!
voluptatem veniam, est atque cumque eum delectus sint!
</p> </p>
</div> </div>
<!--Grid column--> <!--Grid column-->
@@ -56,27 +55,25 @@
<div class="bottom-footer p-1 d-flex justify-content-center align-items-center text-white"> <div class="bottom-footer p-1 d-flex justify-content-center align-items-center text-white">
<div class="content-container"> <div class="content-container">
<p class="m-0"> <p class="m-0">
<a class="text-white" <a class="text-white" href="http://www.dspace.org/">{{ 'footer.link.dspace' | translate}}</a>
href="http://www.dspace.org/">{{ 'footer.link.dspace' | translate}}</a>
{{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }} {{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }}
<a class="text-white" <a class="text-white" href="https://www.lyrasis.org/">{{ 'footer.link.lyrasis' | translate}}</a>
href="https://www.lyrasis.org/">{{ 'footer.link.lyrasis' | translate}}</a>
</p> </p>
<ul class="footer-info list-unstyled small d-flex justify-content-center mb-0"> <ul class="footer-info list-unstyled small d-flex justify-content-center mb-0">
<li> <li>
<a class="text-white" href="#" <a class="text-white" href="#" (click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
(click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
</li> </li>
<li> <li>
<a class="text-white" <a class="text-white" routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
</li> </li>
<li> <li>
<a class="text-white" <a class="text-white" routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a> </li>
<li>
<a class="text-white" routerLink="info/feedback">{{ 'footer.link.feedback' | translate}}</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- Copyright --> <!-- Copyright -->
</footer> </footer>

View File

@@ -0,0 +1,40 @@
<div class="row row-offcanvas row-offcanvas-right">
<div class="col-xs-12 col-sm-12 col-md-9 main-content">
<form class="primary" [formGroup]="feedbackForm" (ngSubmit)="createFeedback()">
<h2>{{ 'info.feedback.head' | translate }}</h2>
<p>{{ 'info.feedback.info' | translate }}</p>
<fieldset class="col p-0">
<div class="row">
<div class="control-group col-sm-12">
<label class="control-label" for="email">Your Email:&nbsp;</label>
<input id="email" class="form-control" name="email" type="text" value="" formControlName="email" autofocus="autofocus" title="{{ 'info.feedback.email_help' | translate }}">
<p class="help-block">{{ 'info.feedback.email_help' | translate }}</p>
</div>
</div>
<ng-container *ngIf="feedbackForm.controls.email.invalid && (feedbackForm.controls.email.dirty || feedbackForm.controls.email.touched)"
class="alert">
<ds-error *ngIf="feedbackForm.controls.email.errors?.required" message="{{'info.feedback.error.email.required' | translate}}"></ds-error>
<ds-error *ngIf="feedbackForm.controls.email.errors?.pattern" message="{{'info.feedback.error.email.pattern' | translate}}"></ds-error>
</ng-container>
<div class="row">
<div class="control-group col-sm-12">
<label class="control-label" for="comments">{{ 'info.feedback.comments' | translate }}:&nbsp;</label>
<textarea id="comments" formControlName="message" class="form-control" name="message" cols="20" rows="5"> </textarea>
</div>
</div>
<ng-container *ngIf="feedbackForm.controls.message.invalid && (feedbackForm.controls.message.dirty || feedbackForm.controls.message.touched)"
class="alert">
<ds-error *ngIf="feedbackForm.controls.message.errors?.required" message="{{'info.feedback.error.message.required' | translate}}"></ds-error>
</ng-container>
<input class="form-control" formControlName="page" name="page" type="hidden" value="{{routeService.getPreviousUrl() | async}}"
/>
<div class="row">
<div class="control-group col-sm-12">
<button [disabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button>
</div>
</div>
</fieldset>
</form>
</div>
</div>

View File

@@ -0,0 +1,90 @@
import { EPersonMock } from './../../../shared/testing/eperson.mock';
import { FeedbackDataService } from './../../../core/feedback/feedback-data.service';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FeedbackContentComponent } from './feedback-content.component';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { RouteService } from 'src/app/core/services/route.service';
import { routeServiceStub } from 'src/app/shared/testing/route-service.stub';
import { FormBuilder } from '@angular/forms';
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
import { NotificationsServiceStub } from 'src/app/shared/testing/notifications-service.stub';
import { AuthService } from 'src/app/core/auth/auth.service';
import { AuthServiceStub } from 'src/app/shared/testing/auth-service.stub';
import { of } from 'rxjs/internal/observable/of';
import { Feedback } from '../../../core/feedback/models/feedback.model';
describe('FeedbackContentComponent', () => {
let component: FeedbackContentComponent;
let fixture: ComponentFixture<FeedbackContentComponent>;
let de: DebugElement;
const notificationService = new NotificationsServiceStub();
const feedbackDataServiceStub = jasmine.createSpyObj('feedbackDataService', {
createFeedback: of(new Feedback())
});
const authService: AuthServiceStub = Object.assign(new AuthServiceStub(), {
getAuthenticatedUserFromStore: () => {
return of(EPersonMock);
}
});
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [FeedbackContentComponent],
providers: [
{ provide: RouteService, useValue: routeServiceStub },
{ provide: FormBuilder, useValue: new FormBuilder() },
{ provide: NotificationsService, useValue: notificationService },
{ provide: FeedbackDataService, useValue: feedbackDataServiceStub },
{ provide: AuthService, useValue: authService },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FeedbackContentComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have page value', () => {
expect(de.query(By.css('input[name=page]')).nativeElement.value).toEqual('/home');
});
it('should have email if ePerson', () => {
expect(component.feedbackForm.controls.email.value).toEqual('test@test.com');
});
it('should have disabled button', () => {
expect(de.query(By.css('button')).nativeElement.disabled).toBeTrue();
});
describe('when message is inserted', () => {
beforeEach(() => {
component.feedbackForm.patchValue({ message: 'new feedback' });
fixture.detectChanges();
});
it('should not have disabled button', () => {
expect(de.query(By.css('button')).nativeElement.disabled).toBeFalse();
});
it('on submit should call createFeedback of feedbackDataServiceStub service', () => {
component.createFeedback();
fixture.detectChanges();
expect(feedbackDataServiceStub.createFeedback).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,52 @@
import { FeedbackDataService } from './../../../core/feedback/feedback-data.service';
import { Component, OnInit } from '@angular/core';
import { RouteService } from 'src/app/core/services/route.service';
import { FormBuilder, Validators } from '@angular/forms';
import { Feedback } from '../../../core/feedback/models/feedback.model';
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { EPersonDataService } from 'src/app/core/eperson/eperson-data.service';
import { AuthService } from 'src/app/core/auth/auth.service';
import { EPerson } from '../../../core/eperson/models/eperson.model';
@Component({
selector: 'ds-feedback-content',
templateUrl: './feedback-content.component.html',
styleUrls: ['./feedback-content.component.scss']
})
/**
* Component displaying the contents of the Feedback Statement
*/
export class FeedbackContentComponent implements OnInit {
feedbackForm = this.fb.group({
email: ['', [Validators.required, Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$')]],
message: ['', Validators.required],
page: [''],
});
constructor(
public routeService: RouteService,
private fb: FormBuilder,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
private feedbackDataService: FeedbackDataService,
private authService: AuthService) {
}
ngOnInit() {
this.authService.getAuthenticatedUserFromStore().subscribe((user: EPerson) => {
if (!!user) {
this.feedbackForm.patchValue({ email: user.email });
}
});
}
createFeedback() {
this.feedbackDataService.createFeedback(this.feedbackForm.value).subscribe((response: Feedback) => {
this.notificationsService.success(this.translate.instant('info.feedback.create.success'));
this.feedbackForm.reset();
});
}
}

View File

@@ -0,0 +1,3 @@
<div class="container">
<ds-feedback-content></ds-feedback-content>
</div>

View File

@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FeedbackComponent } from './feedback.component';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('FeedbackComponent', () => {
let component: FeedbackComponent;
let fixture: ComponentFixture<FeedbackComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [FeedbackComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FeedbackComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-feedback',
templateUrl: './feedback.component.html',
styleUrls: ['./feedback.component.scss']
})
/**
* Component displaying the Feedback Statement
*/
export class FeedbackComponent {
}

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { FeedbackComponent } from './feedback.component';
/**
* Themed wrapper for FeedbackComponent
*/
@Component({
selector: 'ds-themed-feedback',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedFeedbackComponent extends ThemedComponent<FeedbackComponent> {
protected getComponentName(): string {
return 'FeedbackComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/info/feedback/feedback.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./feedback.component`);
}
}

View File

@@ -2,6 +2,7 @@ import { getInfoModulePath } from '../app-routing-paths';
export const END_USER_AGREEMENT_PATH = 'end-user-agreement'; export const END_USER_AGREEMENT_PATH = 'end-user-agreement';
export const PRIVACY_PATH = 'privacy'; export const PRIVACY_PATH = 'privacy';
export const FEEDBACK_PATH = 'feedback';
export function getEndUserAgreementPath() { export function getEndUserAgreementPath() {
return getSubPath(END_USER_AGREEMENT_PATH); return getSubPath(END_USER_AGREEMENT_PATH);
@@ -11,6 +12,10 @@ export function getPrivacyPath() {
return getSubPath(PRIVACY_PATH); return getSubPath(PRIVACY_PATH);
} }
export function getFeedbackPath() {
return getSubPath(FEEDBACK_PATH);
}
function getSubPath(path: string) { function getSubPath(path: string) {
return `${getInfoModulePath()}/${path}`; return `${getInfoModulePath()}/${path}`;
} }

View File

@@ -1,9 +1,12 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { PRIVACY_PATH, END_USER_AGREEMENT_PATH } from './info-routing-paths'; import { PRIVACY_PATH, END_USER_AGREEMENT_PATH, FEEDBACK_PATH } from './info-routing-paths';
import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component'; import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component';
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component'; import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
import { FeedbackGuard } from '../core/feedback/feedback.guard';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -22,6 +25,15 @@ import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' } data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' }
} }
]),
RouterModule.forChild([
{
path: FEEDBACK_PATH,
component: ThemedFeedbackComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.feedback.title', breadcrumbKey: 'info.feedback' },
canActivate: [FeedbackGuard]
}
]) ])
] ]
}) })

View File

@@ -8,6 +8,11 @@ import { PrivacyComponent } from './privacy/privacy.component';
import { PrivacyContentComponent } from './privacy/privacy-content/privacy-content.component'; import { PrivacyContentComponent } from './privacy/privacy-content/privacy-content.component';
import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component'; import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component';
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component'; import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
import { FeedbackComponent } from './feedback/feedback.component';
import { FeedbackContentComponent } from './feedback/feedback-content/feedback-content.component';
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
import { ReactiveFormsModule } from '@angular/forms';
const DECLARATIONS = [ const DECLARATIONS = [
EndUserAgreementComponent, EndUserAgreementComponent,
@@ -15,14 +20,17 @@ const DECLARATIONS = [
EndUserAgreementContentComponent, EndUserAgreementContentComponent,
PrivacyComponent, PrivacyComponent,
PrivacyContentComponent, PrivacyContentComponent,
ThemedPrivacyComponent ThemedPrivacyComponent,
FeedbackComponent,
FeedbackContentComponent,
ThemedFeedbackComponent
]; ];
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
InfoRoutingModule InfoRoutingModule,
], ],
declarations: [ declarations: [
...DECLARATIONS ...DECLARATIONS

View File

@@ -29,7 +29,10 @@ export const routeServiceStub: any = {
return observableOf({}); return observableOf({});
}, },
getHistory: () => { getHistory: () => {
return observableOf(['/home','/collection/123','/home']); return observableOf(['/home', '/collection/123', '/home']);
},
getPreviousUrl: () => {
return observableOf('/home');
} }
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
}; };

View File

@@ -1370,6 +1370,8 @@
"footer.link.end-user-agreement":"End User Agreement", "footer.link.end-user-agreement":"End User Agreement",
"footer.link.feedback":"Send Feedback",
"forgot-email.form.header": "Forgot Password", "forgot-email.form.header": "Forgot Password",
@@ -1568,6 +1570,27 @@
"info.privacy.title": "Privacy Statement", "info.privacy.title": "Privacy Statement",
"info.feedback.breadcrumbs": "Feedback",
"info.feedback.head": "Feedback",
"info.feedback.title": "Feedback",
"info.feedback.info": "Thanks for sharing your feedback about the DSpace system. Your comments are appreciated!",
"info.feedback.email_help": "This address will be used to follow up on your feedback.",
"info.feedback.send": "Send Feedback",
"info.feedback.comments": "Comments",
"info.feedback.create.success" : "Feedback Sent Successfully!",
"info.feedback.error.email.required" : "Email is required",
"info.feedback.error.email.pattern" : "Valid Email is required",
"info.feedback.error.message.required" : "Comment is required",
"item.alerts.private": "This item is private", "item.alerts.private": "This item is private",

View File

@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { FeedbackComponent as BaseComponent } from '../../../../../app/info/feedback/feedback.component';
@Component({
selector: 'ds-feedback',
// styleUrls: ['./feedback.component.scss'],
styleUrls: ['../../../../../app/info/feedback/feedback.component.scss'],
// templateUrl: './feedback.component.html'
templateUrl: '../../../../../app/info/feedback/feedback.component.html'
})
/**
* Component displaying the feedback Statement
*/
export class FeedbackComponent extends BaseComponent { }

View File

@@ -79,7 +79,8 @@ import { HeaderComponent } from './app/header/header.component';
import { FooterComponent } from './app/footer/footer.component'; import { FooterComponent } from './app/footer/footer.component';
import { BreadcrumbsComponent } from './app/breadcrumbs/breadcrumbs.component'; import { BreadcrumbsComponent } from './app/breadcrumbs/breadcrumbs.component';
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component'; import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
import { FileSectionComponent} from './app/item-page/simple/field-components/file-section/file-section.component'; import { FileSectionComponent } from './app/item-page/simple/field-components/file-section/file-section.component';
import { FeedbackComponent } from './app/info/feedback/feedback.component';
const DECLARATIONS = [ const DECLARATIONS = [
FileSectionComponent, FileSectionComponent,
@@ -121,7 +122,8 @@ const DECLARATIONS = [
HeaderComponent, HeaderComponent,
NavbarComponent, NavbarComponent,
HeaderNavbarWrapperComponent, HeaderNavbarWrapperComponent,
BreadcrumbsComponent BreadcrumbsComponent,
FeedbackComponent
]; ];
@NgModule({ @NgModule({
@@ -172,12 +174,12 @@ const DECLARATIONS = [
declarations: DECLARATIONS declarations: DECLARATIONS
}) })
/** /**
* This module serves as an index for all the components in this theme. * This module serves as an index for all the components in this theme.
* It should import all other modules, so the compiler knows where to find any components referenced * It should import all other modules, so the compiler knows where to find any components referenced
* from a component in this theme * from a component in this theme
* It is purposefully not exported, it should never be imported anywhere else, its only purpose is * It is purposefully not exported, it should never be imported anywhere else, its only purpose is
* to give lazily loaded components a context in which they can be compiled successfully * to give lazily loaded components a context in which they can be compiled successfully
*/ */
class ThemeModule { class ThemeModule {
} }