Merge remote-tracking branch 'origin/main' into w2p-92900_Admin_options_dont_appear_after_Shibboleth_authentication_PR

This commit is contained in:
Yura Bondarenko
2022-08-25 18:32:59 +02:00
39 changed files with 8395 additions and 99 deletions

View File

@@ -179,7 +179,7 @@ If needing to update default configurations values for production, update local
- Update `environment.production.ts` file in `src/environment/` for a `production` environment;
The environment object is provided for use as import in code and is extended with he runtime configuration on bootstrap of the application.
The environment object is provided for use as import in code and is extended with the runtime configuration on bootstrap of the application.
> Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap.

View File

@@ -150,6 +150,9 @@ languages:
- code: fi
label: Suomi
active: true
- code: sv
label: Svenska
active: true
- code: tr
label: Türkçe
active: true
@@ -248,3 +251,10 @@ bundle:
mediaViewer:
image: false
video: false
# Whether the end user agreement is required before users use the repository.
# If enabled, the user will be required to accept the agreement before they can use the repository.
# And whether the privacy statement should exist or not.
info:
enableEndUserAgreement: true
enablePrivacyStatement: true

View File

@@ -107,7 +107,7 @@
"mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.11.0",
"moment": "^2.29.2",
"moment": "^2.29.4",
"morgan": "^1.10.0",
"ng-mocks": "^13.1.1",
"ng2-file-upload": "1.4.0",

View File

@@ -18,6 +18,8 @@ import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-
import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators';
export const BBM_PAGINATION_ID = 'bbm';
@Component({
selector: 'ds-browse-by-metadata-page',
styleUrls: ['./browse-by-metadata-page.component.scss'],
@@ -50,7 +52,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
* The pagination config used to display the values
*/
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'bbm',
id: BBM_PAGINATION_ID,
currentPage: 1,
pageSize: 20
});

View File

@@ -19,7 +19,7 @@ export abstract class SomeFeatureAuthorizationGuard implements CanActivate {
/**
* True when user has authorization rights for the feature and object provided
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
* Redirect the user to the unauthorized page when they are not authorized for the given feature
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(

View File

@@ -38,7 +38,7 @@ export class ProcessDataService extends DataService<Process> {
}
/**
* Get the endpoint for a process his files
* Get the endpoint for the files of the process
* @param processId The ID of the process
*/
getFilesEndpoint(processId: string): Observable<string> {

View File

@@ -1,6 +1,7 @@
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { Observable, of as observableOf } from 'rxjs';
import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/authorized.operators';
import { environment } from '../../../environments/environment';
/**
* An abstract guard for redirecting users to the user agreement page if a certain condition is met
@@ -18,6 +19,9 @@ export abstract class AbstractEndUserAgreementGuard implements CanActivate {
* when they're finished accepting the agreement
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
if (!environment.info.enableEndUserAgreement) {
return observableOf(true);
}
return this.hasAccepted().pipe(
returnEndUserAgreementUrlTreeOnFalse(this.router, state.url)
);

View File

@@ -2,6 +2,7 @@ import { EndUserAgreementCurrentUserGuard } from './end-user-agreement-current-u
import { EndUserAgreementService } from './end-user-agreement.service';
import { Router, UrlTree } from '@angular/router';
import { of as observableOf } from 'rxjs';
import { environment } from '../../../environments/environment.test';
describe('EndUserAgreementGuard', () => {
let guard: EndUserAgreementCurrentUserGuard;
@@ -44,5 +45,24 @@ describe('EndUserAgreementGuard', () => {
});
});
});
describe('when the end user agreement is disabled', () => {
it('should return true', (done) => {
environment.info.enableEndUserAgreement = false;
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
console.log(result);
expect(result).toEqual(true);
done();
});
});
it('should not resolve to the end user agreement page', (done) => {
environment.info.enableEndUserAgreement = false;
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
expect(router.navigateByUrl).not.toHaveBeenCalled();
done();
});
});
});
});
});

View File

@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Observable, of as observableOf } from 'rxjs';
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
import { EndUserAgreementService } from './end-user-agreement.service';
import { Router } from '@angular/router';
import { environment } from '../../../environments/environment';
/**
* A guard redirecting logged in users to the end agreement page when they haven't accepted the latest user agreement
@@ -19,6 +20,10 @@ export class EndUserAgreementCurrentUserGuard extends AbstractEndUserAgreementGu
* True when the currently logged in user has accepted the agreements or when the user is not currently authenticated
*/
hasAccepted(): Observable<boolean> {
if (!environment.info.enableEndUserAgreement) {
return observableOf(true);
}
return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true);
}

View File

@@ -55,7 +55,7 @@ export class EndUserAgreementService {
/**
* Set the current user's accepted agreement status
* When a user is authenticated, set his/her metadata to the provided value
* When a user is authenticated, set their metadata to the provided value
* When no user is authenticated, set the cookie to the provided value
* @param accepted
*/

View File

@@ -39,7 +39,7 @@ export class MetadataValue implements MetadataValueInterface {
value: string;
/**
* The place of this MetadataValue within his list of metadata
* The place of this MetadataValue within its list of metadata
* This is used to render metadata in a specific custom order
*/
@autoserialize
@@ -105,7 +105,7 @@ export class MetadatumViewModel {
value: string;
/**
* The place of this MetadataValue within his list of metadata
* The place of this MetadataValue within its list of metadata
* This is used to render metadata in a specific custom order
*/
place: number;

View File

@@ -67,11 +67,11 @@
<a class="text-white" href="javascript:void(0);"
(click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
</li>
<li>
<li *ngIf="showPrivacyPolicy">
<a class="text-white"
routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
</li>
<li>
<li *ngIf="showEndUserAgreement">
<a class="text-white"
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
</li>

View File

@@ -1,6 +1,7 @@
import { Component, Optional } from '@angular/core';
import { hasValue } from '../shared/empty.util';
import { KlaroService } from '../shared/cookies/klaro.service';
import { environment } from '../../environments/environment';
@Component({
selector: 'ds-footer',
@@ -14,6 +15,8 @@ export class FooterComponent {
* A boolean representing if to show or not the top footer container
*/
showTopFooter = false;
showPrivacyPolicy = environment.info.enablePrivacyStatement;
showEndUserAgreement = environment.info.enableEndUserAgreement;
constructor(@Optional() private cookies: KlaroService) {
}

View File

@@ -16,7 +16,7 @@ export class HealthStatusComponent {
@Input() status: HealthStatus;
/**
* He
* Health Status
*/
HealthStatus = HealthStatus;

View File

@@ -76,7 +76,7 @@ export class EndUserAgreementComponent implements OnInit {
/**
* Cancel the agreement
* If the user is logged in, this will log him/her out
* If the user is logged in, this will log them out
* If the user is not logged in, they will be redirected to the homepage
*/
cancel() {

View File

@@ -6,35 +6,47 @@ import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
import { FeedbackGuard } from '../core/feedback/feedback.guard';
import { environment } from '../../environments/environment';
const imports = [
RouterModule.forChild([
{
path: FEEDBACK_PATH,
component: ThemedFeedbackComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.feedback.title', breadcrumbKey: 'info.feedback' },
canActivate: [FeedbackGuard]
}
])
];
if (environment.info.enableEndUserAgreement) {
imports.push(
RouterModule.forChild([
{
path: END_USER_AGREEMENT_PATH,
component: ThemedEndUserAgreementComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.end-user-agreement.title', breadcrumbKey: 'info.end-user-agreement' }
}
]));
}
if (environment.info.enablePrivacyStatement) {
imports.push(
RouterModule.forChild([
{
path: PRIVACY_PATH,
component: ThemedPrivacyComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' }
}
]));
}
@NgModule({
imports: [
RouterModule.forChild([
{
path: END_USER_AGREEMENT_PATH,
component: ThemedEndUserAgreementComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
data: { title: 'info.end-user-agreement.title', breadcrumbKey: 'info.end-user-agreement' }
}
]),
RouterModule.forChild([
{
path: PRIVACY_PATH,
component: ThemedPrivacyComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
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]
}
])
...imports
]
})
/**

View File

@@ -24,7 +24,7 @@ import { ConfirmationModalComponent } from '../../shared/confirmation-modal/conf
templateUrl: './profile-page-researcher-form.component.html',
})
/**
* Component for a user to create/delete or change his researcher profile.
* Component for a user to create/delete or change their researcher profile.
*/
export class ProfilePageResearcherFormComponent implements OnInit {

View File

@@ -240,10 +240,12 @@ describe('BrowseByComponent', () => {
});
describe('back', () => {
it('should navigate back to the main browse page', () => {
const id = 'test-pagination';
comp.back();
expect(paginationService.updateRoute).toHaveBeenCalledWith('test-pagination', {page: 1}, {
expect(paginationService.updateRoute).toHaveBeenCalledWith(id, {page: 1}, {
value: null,
startsWith: null
startsWith: null,
[id + '.return']: null
});
});
});

View File

@@ -1,10 +1,10 @@
import { Component, EventEmitter, Injector, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { fadeIn, fadeInOut } from '../animations/fade';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { ListableObject } from '../object-collection/shared/listable-object.model';
import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator';
import { PaginationService } from '../../core/pagination/pagination.service';
@@ -25,7 +25,7 @@ import { hasValue } from '../empty.util';
/**
* Component to display a browse-by page for any ListableObject
*/
export class BrowseByComponent implements OnInit {
export class BrowseByComponent implements OnInit, OnDestroy {
/**
* ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}.
@@ -112,6 +112,16 @@ export class BrowseByComponent implements OnInit {
*/
shouldDisplayResetButton$: Observable<boolean>;
/**
* Page number of the previous page
*/
previousPage$ = new BehaviorSubject<string>('1');
/**
* Subscription that has to be unsubscribed from on destroy
*/
sub: Subscription;
public constructor(private injector: Injector,
protected paginationService: PaginationService,
private routeService: RouteService,
@@ -171,9 +181,20 @@ export class BrowseByComponent implements OnInit {
this.shouldDisplayResetButton$ = observableCombineLatest([startsWith$, value$]).pipe(
map(([startsWith, value]) => hasValue(startsWith) || hasValue(value))
);
this.sub = this.routeService.getQueryParameterValue(this.paginationConfig.id + '.return').subscribe(this.previousPage$);
}
/**
* Navigate back to the previous browse by page
*/
back() {
this.paginationService.updateRoute(this.paginationConfig.id, {page: 1}, {value: null, startsWith: null});
const page = +this.previousPage$.value > 1 ? +this.previousPage$.value : 1;
this.paginationService.updateRoute(this.paginationConfig.id, {page: page}, {[this.paginationConfig.id + '.return']: null, value: null, startsWith: null});
}
ngOnDestroy(): void {
if (this.sub) {
this.sub.unsubscribe();
}
}
}

View File

@@ -63,6 +63,11 @@ export class BrowserKlaroService extends KlaroService {
* - Add and translate klaro configuration messages
*/
initialize() {
if (!environment.info.enablePrivacyStatement) {
delete this.klaroConfig.privacyPolicy;
this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
}
this.translateService.setDefaultLang(environment.defaultLanguage);
const user$: Observable<EPerson> = this.getUser$();
@@ -90,7 +95,6 @@ export class BrowserKlaroService extends KlaroService {
this.translateConfiguration();
Klaro.setup(this.klaroConfig);
});
}
/**

View File

@@ -24,7 +24,7 @@ export const klaroConfiguration: any = {
/*
Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in
the consent notice. We strongly advise against using this under most
circumstances, as it keeps the user from customizing his/her consent choices.
circumstances, as it keeps the user from customizing their consent choices.
*/
hideLearnMore: false,

View File

@@ -20,6 +20,7 @@ import {
createSuccessfulRemoteDataObject$
} from '../../../remote-data.utils';
import { ExportMetadataSelectorComponent } from './export-metadata-selector.component';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
// No way to add entryComponents yet to testbed; alternative implemented; source: https://stackoverflow.com/questions/41689468/how-to-shallow-test-a-component-with-an-entrycomponents
@NgModule({
@@ -47,6 +48,7 @@ describe('ExportMetadataSelectorComponent', () => {
let router;
let notificationService: NotificationsServiceStub;
let scriptService;
let authorizationDataService;
const mockItem = Object.assign(new Item(), {
id: 'fake-id',
@@ -95,6 +97,9 @@ describe('ExportMetadataSelectorComponent', () => {
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
}
);
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
isAuthorized: observableOf(true)
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
declarations: [ExportMetadataSelectorComponent],
@@ -102,6 +107,7 @@ describe('ExportMetadataSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub },
{ provide: NotificationsService, useValue: notificationService },
{ provide: ScriptDataService, useValue: scriptService },
{ provide: AuthorizationDataService, useValue: authorizationDataService },
{
provide: ActivatedRoute,
useValue: {
@@ -150,7 +156,7 @@ describe('ExportMetadataSelectorComponent', () => {
});
});
describe('if collection is selected', () => {
describe('if collection is selected and is admin', () => {
let scriptRequestSucceeded;
beforeEach((done) => {
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
@@ -159,7 +165,32 @@ describe('ExportMetadataSelectorComponent', () => {
done();
});
});
it('should invoke the metadata-export script with option -i uuid', () => {
it('should invoke the metadata-export script with option -i uuid and -a option', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.uuid }),
Object.assign(new ProcessParameter(), { name: '-a' }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
});
it('success notification is shown', () => {
expect(scriptRequestSucceeded).toBeTrue();
expect(notificationService.success).toHaveBeenCalled();
});
it('redirected to process page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
});
});
describe('if collection is selected and is not admin', () => {
let scriptRequestSucceeded;
beforeEach((done) => {
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
scriptRequestSucceeded = succeeded;
done();
});
});
it('should invoke the metadata-export script with option -i uuid without the -a option', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.uuid }),
];
@@ -174,7 +205,7 @@ describe('ExportMetadataSelectorComponent', () => {
});
});
describe('if community is selected', () => {
describe('if community is selected and is an admin', () => {
let scriptRequestSucceeded;
beforeEach((done) => {
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
@@ -183,7 +214,32 @@ describe('ExportMetadataSelectorComponent', () => {
done();
});
});
it('should invoke the metadata-export script with option -i uuid', () => {
it('should invoke the metadata-export script with option -i uuid and -a option if the user is an admin', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.uuid }),
Object.assign(new ProcessParameter(), { name: '-a' }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
});
it('success notification is shown', () => {
expect(scriptRequestSucceeded).toBeTrue();
expect(notificationService.success).toHaveBeenCalled();
});
it('redirected to process page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
});
});
describe('if community is selected and is not an admin', () => {
let scriptRequestSucceeded;
beforeEach((done) => {
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
component.navigate(mockCommunity).subscribe((succeeded: boolean) => {
scriptRequestSucceeded = succeeded;
done();
});
});
it('should invoke the metadata-export script with option -i uuid without the -a option', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.uuid }),
];

View File

@@ -19,6 +19,8 @@ import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { Process } from '../../../../process-page/processes/process.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
/**
* Component to wrap a list of existing dso's inside a modal
@@ -36,6 +38,7 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
protected notificationsService: NotificationsService, protected translationService: TranslateService,
protected scriptDataService: ScriptDataService,
protected authorizationDataService: AuthorizationDataService,
private modalService: NgbModal) {
super(activeModal, route);
}
@@ -82,24 +85,29 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-i', value: dso.uuid }),
];
return this.scriptDataService.invoke(METADATA_EXPORT_SCRIPT_NAME, parameterValues, [])
.pipe(
getFirstCompletedRemoteData(),
map((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
const title = this.translationService.get('process.new.notification.success.title');
const content = this.translationService.get('process.new.notification.success.content');
this.notificationsService.success(title, content);
if (isNotEmpty(rd.payload)) {
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
}
return true;
} else {
const title = this.translationService.get('process.new.notification.error.title');
const content = this.translationService.get('process.new.notification.error.content');
this.notificationsService.error(title, content);
return false;
return this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf).pipe(
switchMap((isAdmin) => {
if (isAdmin) {
parameterValues.push(Object.assign(new ProcessParameter(), {name: '-a'}));
}
return this.scriptDataService.invoke(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
}),
getFirstCompletedRemoteData(),
map((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
const title = this.translationService.get('process.new.notification.success.title');
const content = this.translationService.get('process.new.notification.success.content');
this.notificationsService.success(title, content);
if (isNotEmpty(rd.payload)) {
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
}
}));
return true;
} else {
const title = this.translationService.get('process.new.notification.error.title');
const content = this.translationService.get('process.new.notification.error.content');
this.notificationsService.error(title, content);
return false;
}
}));
}
}

View File

@@ -1,5 +1,5 @@
<div class="d-flex flex-row">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[]" [queryParams]="getQueryParams()" [queryParamsHandling]="'merge'" class="lead">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[]" [queryParams]="queryParams$ | async" [queryParamsHandling]="'merge'" class="lead">
{{object.value}}
</a>
<span *ngIf="linkType == linkTypes.None" class="lead">

View File

@@ -4,7 +4,9 @@ import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../utils/truncate.pipe';
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { RouteService } from '../../../core/services/route.service';
import { of as observableOf } from 'rxjs';
let browseEntryListElementComponent: BrowseEntryListElementComponent;
let fixture: ComponentFixture<BrowseEntryListElementComponent>;
@@ -13,12 +15,28 @@ const mockValue: BrowseEntry = Object.assign(new BrowseEntry(), {
value: 'De Langhe Kristof'
});
describe('MetadataListElementComponent', () => {
let paginationService;
let routeService;
const pageParam = 'bbm.page';
function init() {
paginationService = jasmine.createSpyObj('paginationService', {
getPageParam: pageParam
});
routeService = jasmine.createSpyObj('routeService', {
getQueryParameterValue: observableOf('1')
});
}
describe('BrowseEntryListElementComponent', () => {
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule({
declarations: [BrowseEntryListElementComponent, TruncatePipe],
providers: [
{ provide: 'objectElementProvider', useValue: { mockValue } }
{ provide: 'objectElementProvider', useValue: { mockValue } },
{provide: PaginationService, useValue: paginationService},
{provide: RouteService, useValue: routeService},
],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,9 +1,15 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
import { ViewMode } from '../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { Params } from '@angular/router';
import { BBM_PAGINATION_ID } from '../../../browse-by/browse-by-metadata-page/browse-by-metadata-page.component';
import { RouteService } from 'src/app/core/services/route.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'ds-browse-entry-list-element',
@@ -15,16 +21,35 @@ import { listableObjectComponent } from '../../object-collection/shared/listable
* This component is automatically used to create a list view for BrowseEntry objects when used in ObjectCollectionComponent
*/
@listableObjectComponent(BrowseEntry, ViewMode.ListElement)
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> {
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> implements OnInit {
/**
* Emits the query parameters for the link of this browse entry list element
*/
queryParams$: Observable<Params>;
constructor(private paginationService: PaginationService, private routeService: RouteService) {
super();
}
ngOnInit() {
this.queryParams$ = this.getQueryParams();
}
/**
* Get the query params to access the item page of this browse entry.
*/
public getQueryParams(): {[param: string]: any} {
return {
value: this.object.value,
authority: !!this.object.authority ? this.object.authority : undefined,
startsWith: undefined,
};
private getQueryParams(): Observable<Params> {
const pageParamName = this.paginationService.getPageParam(BBM_PAGINATION_ID);
return this.routeService.getQueryParameterValue(pageParamName).pipe(
map((currentPage) => {
return {
value: this.object.value,
authority: !!this.object.authority ? this.object.authority : undefined,
startsWith: undefined,
[pageParamName]: null,
[BBM_PAGINATION_ID + '.return']: currentPage
};
})
);
}
}

View File

@@ -12,7 +12,7 @@ import { metadataRepresentationComponent } from '../../../metadata-representatio
/**
* A component for displaying MetadataRepresentation objects in the form of items
* It will send the MetadataRepresentation object along with ElementViewMode.SetElement to the ItemTypeSwitcherComponent,
* which will in his turn decide how to render the item as metadata.
* which will in its turn decide how to render the item as metadata.
*/
export class ItemMetadataListElementComponent extends MetadataRepresentationListElementComponent {
/**

View File

@@ -19,6 +19,7 @@
<span *ngIf="item.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length === 0">{{'mydspace.results.no-authors' | translate}}</span>
<span *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 *ngIf="!last">; </span>
</span>
</span>

View File

@@ -1225,6 +1225,8 @@
"cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: <strong>Authentication, Preferences, Acknowledgement and Statistics</strong>. <br/> To learn more, please read our {privacyPolicy}.",
"cookies.consent.content-notice.description.no-privacy": "We collect and process your personal information for the following purposes: <strong>Authentication, Preferences, Acknowledgement and Statistics</strong>.",
"cookies.consent.content-notice.learnMore": "Customize",
"cookies.consent.content-modal.description": "Here you can see and customize the information that we collect about you.",

8078
src/assets/i18n/sv.json5 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ import { MediaViewerConfig } from './media-viewer-config.interface';
import { BrowseByConfig } from './browse-by-config.interface';
import { BundleConfig } from './bundle-config.interface';
import { ActuatorsConfig } from './actuators.config';
import { InfoConfig } from './info-config.interface';
interface AppConfig extends Config {
ui: UIServerConfig;
@@ -36,6 +37,7 @@ interface AppConfig extends Config {
mediaViewer: MediaViewerConfig;
bundle: BundleConfig;
actuators: ActuatorsConfig
info: InfoConfig;
}
/**

View File

@@ -16,6 +16,7 @@ import { ThemeConfig } from './theme.model';
import { UIServerConfig } from './ui-server-config.interface';
import { BundleConfig } from './bundle-config.interface';
import { ActuatorsConfig } from './actuators.config';
import { InfoConfig } from './info-config.interface';
export class DefaultAppConfig implements AppConfig {
production = false;
@@ -192,6 +193,7 @@ export class DefaultAppConfig implements AppConfig {
{ code: 'pt-PT', label: 'Português', active: true },
{ code: 'pt-BR', label: 'Português do Brasil', active: true },
{ code: 'fi', label: 'Suomi', active: true },
{ code: 'sv', label: 'Svenska', active: true },
{ code: 'tr', label: 'Türkçe', active: true },
{ code: 'bn', label: 'বাংলা', active: true }
];
@@ -324,4 +326,16 @@ export class DefaultAppConfig implements AppConfig {
image: false,
video: false
};
// Whether the end-user-agreement and privacy policy feature should be enabled or not.
// Disabling the end user agreement feature will result in:
// - Users no longer being forced to accept the end-user-agreement before they can access the repository
// - A 404 page if you manually try to navigate to the end-user-agreement page at info/end-user-agreement
// - All end-user-agreement related links and pages will be removed from the UI (e.g. in the footer)
// Disabling the privacy policy feature will result in:
// - A 404 page if you manually try to navigate to the privacy policy page at info/privacy
// - All mentions of the privacy policy being removed from the UI (e.g. in the footer)
info: InfoConfig = {
enableEndUserAgreement: true,
enablePrivacyStatement: true
};
}

View File

@@ -0,0 +1,6 @@
import { Config } from './config.interface';
export interface InfoConfig extends Config {
enableEndUserAgreement: boolean;
enablePrivacyStatement: boolean;
}

View File

@@ -243,5 +243,9 @@ export const environment: BuildConfig = {
mediaViewer: {
image: true,
video: true
},
info: {
enableEndUserAgreement: true,
enablePrivacyStatement: true,
}
};

5
src/styles/_vendor.scss Normal file
View File

@@ -0,0 +1,5 @@
// node_modules imports meant for all the themes
@import '~node_modules/bootstrap/scss/bootstrap.scss';
@import '~node_modules/nouislider/distribute/nouislider.min';
@import '~node_modules/ngx-ui-switch/ui-switch.component.scss';

View File

@@ -1,8 +1,6 @@
@import './helpers/font_awesome_imports.scss';
@import '../../node_modules/bootstrap/scss/bootstrap.scss';
@import '../../node_modules/nouislider/distribute/nouislider.min';
@import './_vendor.scss';
@import './_custom_variables.scss';
@import './bootstrap_variables_mapping.scss';
@import './_truncatable-part.component.scss';
@import './_global-styles.scss';
@import '../../node_modules/ngx-ui-switch/ui-switch.component.scss';

View File

@@ -4,11 +4,9 @@
@import '../../../styles/_variables.scss';
@import '../../../styles/_mixins.scss';
@import '../../../styles/helpers/font_awesome_imports.scss';
@import '../../../../node_modules/bootstrap/scss/bootstrap.scss';
@import '../../../../node_modules/nouislider/distribute/nouislider.min';
@import '../../../styles/_vendor.scss';
@import '../../../styles/_custom_variables.scss';
@import './_theme_css_variable_overrides.scss';
@import '../../../styles/bootstrap_variables_mapping.scss';
@import '../../../styles/_truncatable-part.component.scss';
@import './_global-styles.scss';
@import '../../../../node_modules/ngx-ui-switch/ui-switch.component.scss';

View File

@@ -4,11 +4,9 @@
@import '../../../styles/_variables.scss';
@import '../../../styles/_mixins.scss';
@import '../../../styles/helpers/font_awesome_imports.scss';
@import '../../../../node_modules/bootstrap/scss/bootstrap.scss';
@import '../../../../node_modules/nouislider/distribute/nouislider.min';
@import '../../../styles/_vendor.scss';
@import '../../../styles/_custom_variables.scss';
@import './_theme_css_variable_overrides.scss';
@import '../../../styles/bootstrap_variables_mapping.scss';
@import '../../../styles/_truncatable-part.component.scss';
@import './_global-styles.scss';
@import '../../../../node_modules/ngx-ui-switch/ui-switch.component.scss';

View File

@@ -8741,10 +8741,10 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
moment@^2.29.2:
version "2.29.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
morgan@^1.10.0:
version "1.10.0"
@@ -12239,9 +12239,9 @@ terser@5.11.0:
source-map-support "~0.5.20"
terser@^4.6.12, terser@^4.6.3:
version "4.8.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
version "4.8.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f"
integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==
dependencies:
commander "^2.20.0"
source-map "~0.6.1"