Merge remote-tracking branch 'origin/main' into CST-6035_external-provider-query-error

This commit is contained in:
corrad82
2022-08-30 11:41:04 +02:00
25 changed files with 8274 additions and 61 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

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

@@ -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

@@ -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

@@ -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

@@ -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 {
/**

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

File diff suppressed because it is too large Load Diff

View File

@@ -193,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 }
];

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';