Skip to content

Commit 5f90b29

Browse files
authored
Merge pull request #1472 from 4Science/CST-4875-Feedback-form
Feedback form
2 parents acb842e + 6bbc7f9 commit 5f90b29

28 files changed

+592
-8
lines changed

src/app/app-routing-paths.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: st
3232
};
3333
}
3434

35+
export const HOME_PAGE_PATH = 'admin';
36+
37+
export function getHomePageRoute() {
38+
return `/${HOME_PAGE_PATH}`;
39+
}
40+
3541
export const ADMIN_MODULE_PATH = 'admin';
3642

3743
export function getAdminModuleRoute() {

src/app/core/core.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import { MetadataSchema } from './metadata/metadata-schema.model';
7777
import { MetadataService } from './metadata/metadata.service';
7878
import { RegistryService } from './registry/registry.service';
7979
import { RoleService } from './roles/role.service';
80+
import { FeedbackDataService } from './feedback/feedback-data.service';
8081

8182
import { ApiService } from './services/api.service';
8283
import { ServerResponseService } from './services/server-response.service';
@@ -286,7 +287,8 @@ const PROVIDERS = [
286287
VocabularyService,
287288
VocabularyTreeviewService,
288289
SequenceService,
289-
GroupDataService
290+
GroupDataService,
291+
FeedbackDataService,
290292
];
291293

292294
/**

src/app/core/data/feature-authorization/feature-id.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ export enum FeatureID {
2727
CanDeleteVersion = 'canDeleteVersion',
2828
CanCreateVersion = 'canCreateVersion',
2929
CanViewUsageStatistics = 'canViewUsageStatistics',
30+
CanSendFeedback = 'canSendFeedback',
3031
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { FeedbackDataService } from './feedback-data.service';
2+
import { HALLink } from '../shared/hal-link.model';
3+
import { Item } from '../shared/item.model';
4+
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
5+
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
6+
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
7+
import { NotificationsService } from '../../shared/notifications/notifications.service';
8+
import { HttpClient } from '@angular/common/http';
9+
import { Store } from '@ngrx/store';
10+
import { CoreState } from '../core.reducers';
11+
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
12+
import { Feedback } from './models/feedback.model';
13+
14+
describe('FeedbackDataService', () => {
15+
let service: FeedbackDataService;
16+
let requestService;
17+
let halService;
18+
let rdbService;
19+
let notificationsService;
20+
let http;
21+
let comparator;
22+
let objectCache;
23+
let store;
24+
let item;
25+
let bundleLink;
26+
let bundleHALLink;
27+
28+
const feedbackPayload = Object.assign(new Feedback(), {
29+
30+
message: 'message',
31+
page: '/home'
32+
});
33+
34+
35+
function initTestService(): FeedbackDataService {
36+
bundleLink = '/items/0fdc0cd7-ff8c-433d-b33c-9b56108abc07/bundles';
37+
bundleHALLink = new HALLink();
38+
bundleHALLink.href = bundleLink;
39+
item = new Item();
40+
item._links = {
41+
bundles: bundleHALLink
42+
};
43+
requestService = getMockRequestService();
44+
halService = new HALEndpointServiceStub('url') as any;
45+
rdbService = {} as RemoteDataBuildService;
46+
notificationsService = {} as NotificationsService;
47+
http = {} as HttpClient;
48+
comparator = new DSOChangeAnalyzer() as any;
49+
objectCache = {
50+
51+
addPatch: () => {
52+
/* empty */
53+
},
54+
getObjectBySelfLink: () => {
55+
/* empty */
56+
}
57+
} as any;
58+
store = {} as Store<CoreState>;
59+
return new FeedbackDataService(
60+
requestService,
61+
rdbService,
62+
store,
63+
objectCache,
64+
halService,
65+
notificationsService,
66+
http,
67+
comparator,
68+
);
69+
}
70+
71+
72+
beforeEach(() => {
73+
service = initTestService();
74+
});
75+
76+
77+
describe('getFeedback', () => {
78+
beforeEach(() => {
79+
spyOn(service, 'getFeedback');
80+
service.getFeedback('3');
81+
});
82+
83+
it('should call getFeedback with the feedback link', () => {
84+
expect(service.getFeedback).toHaveBeenCalledWith('3');
85+
});
86+
});
87+
88+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Injectable } from '@angular/core';
2+
import { Observable } from 'rxjs';
3+
import { DataService } from '../data/data.service';
4+
import { Feedback } from './models/feedback.model';
5+
import { FEEDBACK } from './models/feedback.resource-type';
6+
import { dataService } from '../cache/builders/build-decorators';
7+
import { RequestService } from '../data/request.service';
8+
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
9+
import { Store } from '@ngrx/store';
10+
import { ObjectCacheService } from '../cache/object-cache.service';
11+
import { HALEndpointService } from '../shared/hal-endpoint.service';
12+
import { NotificationsService } from '../../shared/notifications/notifications.service';
13+
import { HttpClient } from '@angular/common/http';
14+
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
15+
import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
16+
17+
/**
18+
* Service for checking and managing the feedback
19+
*/
20+
@Injectable()
21+
@dataService(FEEDBACK)
22+
export class FeedbackDataService extends DataService<Feedback> {
23+
protected linkPath = 'feedbacks';
24+
25+
constructor(
26+
protected requestService: RequestService,
27+
protected rdbService: RemoteDataBuildService,
28+
protected store: Store<any>,
29+
protected objectCache: ObjectCacheService,
30+
protected halService: HALEndpointService,
31+
protected notificationsService: NotificationsService,
32+
protected http: HttpClient,
33+
protected comparator: DSOChangeAnalyzer<Feedback>,
34+
) {
35+
super();
36+
}
37+
38+
/**
39+
* Get feedback from its id
40+
* @param uuid string the id of the feedback
41+
*/
42+
getFeedback(uuid: string): Observable<Feedback> {
43+
return this.findById(uuid).pipe(
44+
getFirstSucceededRemoteData(),
45+
getRemoteDataPayload(),
46+
);
47+
}
48+
49+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree } from '@angular/router';
2+
import { Observable } from 'rxjs';
3+
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
4+
import { FeatureID } from '../data/feature-authorization/feature-id';
5+
import { Injectable } from '@angular/core';
6+
7+
/**
8+
* An guard for redirecting users to the feedback page if user is authorized
9+
*/
10+
@Injectable()
11+
export class FeedbackGuard implements CanActivate {
12+
13+
constructor(private authorizationService: AuthorizationDataService) {
14+
}
15+
16+
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
17+
return this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
18+
}
19+
20+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { autoserialize, inheritSerialization } from 'cerialize';
2+
import { typedObject } from '../../cache/builders/build-decorators';
3+
4+
import { DSpaceObject } from '../../shared/dspace-object.model';
5+
import { HALLink } from '../../shared/hal-link.model';
6+
import { FEEDBACK } from './feedback.resource-type';
7+
8+
@typedObject
9+
@inheritSerialization(DSpaceObject)
10+
export class Feedback extends DSpaceObject {
11+
static type = FEEDBACK;
12+
13+
/**
14+
* The email address
15+
*/
16+
@autoserialize
17+
public email: string;
18+
19+
/**
20+
* A string representing message the user inserted
21+
*/
22+
@autoserialize
23+
public message: string;
24+
/**
25+
* A string representing the page from which the user came from
26+
*/
27+
@autoserialize
28+
public page: string;
29+
30+
_links: {
31+
self: HALLink;
32+
};
33+
34+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ResourceType } from '../../shared/resource-type';
2+
3+
/**
4+
* The resource type for Feedback
5+
*
6+
* Needs to be in a separate file to prevent circular
7+
* dependencies in webpack.
8+
*/
9+
export const FEEDBACK = new ResourceType('feedback');

src/app/footer/footer.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ <h5 class="text-uppercase">Footer Content</h5>
7575
<a class="text-white"
7676
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
7777
</li>
78+
<li>
79+
<a class="text-white"
80+
routerLink="info/feedback">{{ 'footer.link.feedback' | translate}}</a>
81+
</li>
7882
</ul>
7983
</div>
8084
</div>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<div class="row row-offcanvas row-offcanvas-right">
2+
<div class="col-xs-12 col-sm-12 col-md-9 main-content">
3+
<form class="primary" [formGroup]="feedbackForm" (ngSubmit)="createFeedback()">
4+
<h2>{{ 'info.feedback.head' | translate }}</h2>
5+
<p>{{ 'info.feedback.info' | translate }}</p>
6+
<fieldset class="col p-0">
7+
<div class="row">
8+
<div class="control-group col-sm-12">
9+
<label class="control-label" for="email">{{ 'info.feedback.email-label' | translate }}&nbsp;</label>
10+
<input id="email" class="form-control" name="email" type="text" value="" formControlName="email" autofocus="autofocus" title="{{ 'info.feedback.email_help' | translate }}">
11+
<small class="text-muted">{{ 'info.feedback.email_help' | translate }}</small>
12+
</div>
13+
</div>
14+
15+
<ng-container *ngIf="feedbackForm.controls.email.invalid && (feedbackForm.controls.email.dirty || feedbackForm.controls.email.touched)"
16+
class="alert">
17+
<ds-error *ngIf="feedbackForm.controls.email.errors?.required" message="{{'info.feedback.error.email.required' | translate}}"></ds-error>
18+
<ds-error *ngIf="feedbackForm.controls.email.errors?.pattern" message="{{'info.feedback.error.email.required' | translate}}"></ds-error>
19+
</ng-container>
20+
<div class="row">
21+
<div class="control-group col-sm-12">
22+
<label class="control-label" for="comments">{{ 'info.feedback.comments' | translate }}:&nbsp;</label>
23+
<textarea id="comments" formControlName="message" class="form-control" name="message" cols="20" rows="5"> </textarea>
24+
</div>
25+
</div>
26+
<ng-container *ngIf="feedbackForm.controls.message.invalid && (feedbackForm.controls.message.dirty || feedbackForm.controls.message.touched)"
27+
class="alert">
28+
<ds-error *ngIf="feedbackForm.controls.message.errors?.required" message="{{'info.feedback.error.message.required' | translate}}"></ds-error>
29+
</ng-container>
30+
<div class="row">
31+
<div class="control-group col-sm-12">
32+
<label class="control-label" for="page">{{ 'info.feedback.page-label' | translate }}&nbsp;</label>
33+
<input id="page" readonly class="form-control" name="page" type="text" value="" formControlName="page" autofocus="autofocus" title="{{ 'info.feedback.page_help' | translate }}">
34+
<small class="text-muted">{{ 'info.feedback.page_help' | translate }}</small>
35+
</div>
36+
</div>
37+
<div class="row py-2">
38+
<div class="control-group col-sm-12 text-right">
39+
<button [disabled]="!feedbackForm.valid" class="btn btn-primary" name="submit" type="submit">{{ 'info.feedback.send' | translate }}</button>
40+
</div>
41+
</div>
42+
</fieldset>
43+
</form>
44+
</div>
45+
</div>

0 commit comments

Comments
 (0)