added dmp filtering (ui)
This commit is contained in:
parent
eba6aba19a
commit
570004af96
|
@ -5,6 +5,8 @@ import { DmpStatus } from '../common/enum/dmp-status';
|
|||
import { DmpVersionStatus } from '../common/enum/dmp-version-status';
|
||||
import { IsActive } from '../common/enum/is-active.enum';
|
||||
import { DmpDescriptionTemplateLookup } from './dmp-description-template.lookup';
|
||||
import { DmpUserRole } from '../common/enum/dmp-user-role';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
|
||||
export class DmpLookup extends Lookup implements DmpFilter {
|
||||
ids: Guid[];
|
||||
|
@ -18,9 +20,29 @@ export class DmpLookup extends Lookup implements DmpFilter {
|
|||
dmpDescriptionTemplateSubQuery: DmpDescriptionTemplateLookup;
|
||||
groupIds: Guid[];
|
||||
|
||||
dmpBlueprintIds: Guid[]; //TODO
|
||||
descriptionTemplateIds: Guid[]; //TODO
|
||||
roleInDmp: DmpUserRole[]; //TODO
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
from(formGroup: UntypedFormGroup): void {
|
||||
this.statuses = formGroup.get('status')?.value ? [formGroup.get('status')?.value] : null;
|
||||
this.roleInDmp = formGroup.get('role')?.value ? [formGroup.get('role')?.value] : null;
|
||||
this.dmpBlueprintIds = formGroup.get('dmpBlueprintIds')?.value ? formGroup.get('dmpBlueprintIds')?.value : null;
|
||||
this.descriptionTemplateIds = formGroup.get('descriptionTemplateIds')?.value ? formGroup.get('descriptionTemplateIds')?.value : null;
|
||||
}
|
||||
|
||||
buildForm(): UntypedFormGroup {
|
||||
return (new UntypedFormBuilder()).group({
|
||||
status: [this.statuses ? this.statuses[0] : null],
|
||||
role: [this.roleInDmp ? this.roleInDmp[0] : null],
|
||||
dmpBlueprintIds: [this.dmpBlueprintIds],
|
||||
descriptionTemplateIds: [this.descriptionTemplateIds],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface DmpFilter {
|
||||
|
@ -31,6 +53,10 @@ export interface DmpFilter {
|
|||
versionStatuses: DmpVersionStatus[];
|
||||
statuses: DmpStatus[];
|
||||
accessTypes: DmpAccessType[];
|
||||
versions: Number[]
|
||||
versions: Number[];
|
||||
groupIds: Guid[];
|
||||
|
||||
dmpBlueprintIds: Guid[]; //TODO
|
||||
descriptionTemplateIds: Guid[]; //TODO
|
||||
roleInDmp: DmpUserRole[]; //TODO
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { AnalyticsService } from '@app/core/services/matomo/analytics-service';
|
||||
import { DescriptionFiltersComponent } from '../description-filter.component';
|
||||
|
||||
@Component({
|
||||
selector: 'description-filter-dialog-component',
|
||||
templateUrl: './description-filter-dialog.component.html',
|
||||
styleUrls: ['./description-filter-dialog.component.scss']
|
||||
})
|
||||
|
||||
export class DescriptionFilterDialogComponent implements OnInit {
|
||||
|
||||
@ViewChild(DescriptionFiltersComponent, { static: true }) filter: DescriptionFiltersComponent;
|
||||
|
||||
public filtersFormGroup = new UntypedFormBuilder().group({
|
||||
status: new UntypedFormControl(),
|
||||
role: new UntypedFormControl(),
|
||||
allVersions: new UntypedFormControl(),
|
||||
descriptionTemplates: new UntypedFormControl(),
|
||||
relatedDmps: new UntypedFormControl(),
|
||||
tags: new UntypedFormControl(),
|
||||
});
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DescriptionFiltersComponent>,
|
||||
private httpClient: HttpClient,
|
||||
private analyticsService: AnalyticsService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: {
|
||||
isPublic: boolean,
|
||||
status: Number,
|
||||
filterForm: UntypedFormGroup,
|
||||
updateDataFn: Function }
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.analyticsService.trackPageView(AnalyticsService.DescriptionFiltersDialog);
|
||||
// this.filters.setfilters(this.data.filters);
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onFiltersChanged(event) {
|
||||
this.data.updateDataFn(this.filter);
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<a class="col-auto d-flex pointer" (click)="onClose()"><span class="ml-auto mt-3 material-icons clear-icon">clear</span></a>
|
||||
<app-dmp-criteria-component [showGrant]="data.showGrant" [isPublic]="data.isPublic" [criteriaFormGroup]="data.formGroup" (filtersChanged)="onFiltersChanged($event)"></app-dmp-criteria-component>
|
|
@ -1,44 +0,0 @@
|
|||
import { Inject, Component, ViewChild, OnInit, Output, EventEmitter } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { DmpCriteriaComponent } from './dmp-criteria.component';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { AnalyticsService } from '@app/core/services/matomo/analytics-service';
|
||||
|
||||
@Component({
|
||||
selector: 'dmp-criteria-dialog-component',
|
||||
templateUrl: './dmp-criteria-dialog.component.html',
|
||||
styleUrls: ['./dmp-criteria-dialog.component.scss']
|
||||
})
|
||||
|
||||
export class DmpCriteriaDialogComponent implements OnInit {
|
||||
|
||||
@ViewChild(DmpCriteriaComponent, { static: true }) criteria: DmpCriteriaComponent;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DmpCriteriaDialogComponent>,
|
||||
private httpClient: HttpClient,
|
||||
private analyticsService: AnalyticsService,
|
||||
|
||||
@Inject(MAT_DIALOG_DATA) public data: { showGrant: boolean, isPublic: boolean, criteria: DmpCriteria, formGroup: UntypedFormGroup, updateDataFn: Function }
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.analyticsService.trackPageView(AnalyticsService.DmpCriteriaDialog);
|
||||
this.criteria.setCriteria(this.data.criteria);
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onFiltersChanged(event) {
|
||||
this.data.updateDataFn(this.criteria);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
<div class="dmp-criteria">
|
||||
<div class="filters">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-10">
|
||||
<h6 class="criteria-title">{{'CRITERIA.FILTERS'| translate}}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <h6 class="filters-title">{{'CRITERIA.FILTERS'| translate}}</h6> -->
|
||||
<div class="row" style="justify-content: center;">
|
||||
<!-- Search Filter-->
|
||||
<!-- <mat-form-field class="col-11 search">
|
||||
<input matInput placeholder="{{'CRITERIA.DMP.LIKE'| translate}}" name="likeCriteria"
|
||||
[formControl]="formGroup.get('like')">
|
||||
<mat-error *ngIf="formGroup.get('like').hasError('backendError')">
|
||||
{{formGroup.get('like').getError('backendError').message}}</mat-error>
|
||||
<mat-icon matSuffix class="style-icon">search</mat-icon>
|
||||
</mat-form-field> -->
|
||||
<!-- End of Search Filter -->
|
||||
|
||||
<!-- Visibility Filter-->
|
||||
<div *ngIf="!isPublic" class="col-10">
|
||||
<h6 class="category-title">{{ 'TYPES.DMP-VISIBILITY.VISIBILITY' | translate }}</h6>
|
||||
<mat-radio-group aria-label="Select an option" [formControl]="formGroup.get('status')">
|
||||
<mat-list-item><mat-radio-button value="null" [checked]="!formGroup.get('status').value">{{ 'TYPES.DMP-VISIBILITY.ANY' | translate }}</mat-radio-button></mat-list-item>
|
||||
<mat-list-item><mat-radio-button value="2">{{ 'TYPES.DMP-VISIBILITY.PUBLIC' | translate }}</mat-radio-button></mat-list-item>
|
||||
<mat-list-item><mat-radio-button value="1">{{ 'TYPES.DMP-VISIBILITY.FINALIZED' | translate }}</mat-radio-button></mat-list-item>
|
||||
<mat-list-item><mat-radio-button value="0">{{ 'TYPES.DMP-VISIBILITY.DRAFT' | translate }}</mat-radio-button></mat-list-item>
|
||||
</mat-radio-group>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Visibility Filter-->
|
||||
|
||||
<!-- Grant Status -->
|
||||
<div class="col-10" *ngIf="isPublic">
|
||||
<h6 class="category-title">{{ 'FACET-SEARCH.GRANT-STATUS.TITLE' | translate }}</h6>
|
||||
<mat-radio-group [formControl]="formGroup.get('grantStatus')">
|
||||
<mat-list-item>
|
||||
<mat-radio-button value="null" [checked]="!formGroup.get('grantStatus').value">{{ 'FACET-SEARCH.GRANT-STATUS.OPTIONS.ANY' | translate }}</mat-radio-button>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-radio-button value="0">{{ 'FACET-SEARCH.GRANT-STATUS.OPTIONS.ACTIVE' | translate }}</mat-radio-button>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-radio-button value="1">{{ 'FACET-SEARCH.GRANT-STATUS.OPTIONS.INACTIVE' | translate }}</mat-radio-button>
|
||||
</mat-list-item>
|
||||
</mat-radio-group>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Grant Status -->
|
||||
|
||||
<!-- Related Dataset Templates Filter -->
|
||||
<div *ngIf="showGrant" class="col-10">
|
||||
<h6 class="category-title">{{ 'CRITERIA.DMP.RELATED-DATASET-TEMPLATES' | translate}}</h6>
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'CRITERIA.DMP.SELECT-DATASET-TEMPLATES' | translate }}</mat-label>
|
||||
<app-multiple-auto-complete [formControl]="formGroup.get('datasetTemplates')"
|
||||
[configuration]="datasetTemplateAutoCompleteConfiguration">
|
||||
</app-multiple-auto-complete>
|
||||
</mat-form-field>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Related Dataset Templates Filter -->
|
||||
|
||||
<!-- Related Grant Filters -->
|
||||
<div *ngIf="showGrant" class="col-10">
|
||||
<h6 class="category-title">{{ 'DMP-RELATED-GRANT.RELATED-GRANT' | translate}}</h6>
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'CRITERIA.DMP.SELECT-GRANTS' | translate }}</mat-label>
|
||||
<app-multiple-auto-complete [formControl]="formGroup.get('grants')"
|
||||
[configuration]="grantAutoCompleteConfiguration">
|
||||
</app-multiple-auto-complete>
|
||||
</mat-form-field>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Related Grants Filters -->
|
||||
|
||||
<!-- Collaborators Filter -->
|
||||
<div *ngIf="isAuthenticated()" class="col-10">
|
||||
<h6 class="category-title">{{ 'CRITERIA.DMP.RELATED-COLLABORATORS' | translate}}</h6>
|
||||
<mat-form-field>
|
||||
<mat-label>{{'CRITERIA.DMP.SELECT-COLLABORATORS' | translate}}</mat-label>
|
||||
<app-multiple-auto-complete [formControl]="formGroup.get('collaborators')"
|
||||
[configuration]="collaboratorsAutoCompleteConfiguration">
|
||||
</app-multiple-auto-complete>
|
||||
</mat-form-field>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Collaborators Filter -->
|
||||
|
||||
<!-- Role Filter -->
|
||||
<div *ngIf="isAuthenticated()" class="col-10">
|
||||
<h6 class="category-title">{{ 'DATASET-PROFILE-LISTING.COLUMNS.ROLE' | translate }}</h6>
|
||||
<mat-radio-group aria-label="Select an option" [formControl]="formGroup.get('role')">
|
||||
<mat-list-item><mat-radio-button value="null" [checked]="!formGroup.get('role').value">{{ 'TYPES.DATASET-ROLE.ANY' | translate }}</mat-radio-button></mat-list-item>
|
||||
<mat-list-item><mat-radio-button value="0">{{ 'TYPES.DATASET-ROLE.OWNER' | translate }}</mat-radio-button></mat-list-item>
|
||||
<mat-list-item><mat-radio-button value="1">{{ 'TYPES.DATASET-ROLE.MEMBER' | translate }}</mat-radio-button></mat-list-item>
|
||||
</mat-radio-group>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Role Filter -->
|
||||
|
||||
<!-- Related Organization Filter -->
|
||||
<div *ngIf="showGrant" class="col-10">
|
||||
<h6 class="category-title">{{ 'DMP-RELATED-ORGANIZATION.RELATED-ORGANIZATION' | translate }}</h6>
|
||||
<mat-form-field>
|
||||
<mat-label>{{'DMP-RELATED-ORGANIZATION.SELECT-ORGANIZATIONS' | translate}}</mat-label>
|
||||
<app-multiple-auto-complete [formControl]="formGroup.get('organisations')"
|
||||
[configuration]="organisationAutoCompleteConfiguration">
|
||||
</app-multiple-auto-complete>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- End of Related Organization Filter -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,193 +0,0 @@
|
|||
|
||||
import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration';
|
||||
import { BaseCriteriaComponent } from '@app/ui/misc/criteria/base-criteria.component';
|
||||
import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { AuthService } from '@app/core/services/auth/auth.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DmpService } from '@app/core/services/dmp/dmp.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dmp-criteria-component',
|
||||
templateUrl: './dmp-criteria.component.html',
|
||||
styleUrls: ['./dmp-criteria.component.scss'],
|
||||
})
|
||||
export class DmpCriteriaComponent extends BaseCriteriaComponent implements OnInit {
|
||||
|
||||
@Input() showGrant: boolean;
|
||||
@Input() isPublic: boolean;
|
||||
@Input() criteriaFormGroup: UntypedFormGroup;
|
||||
@Output() filtersChanged: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
filteringGrantsAsync = false;
|
||||
sizeError = false;
|
||||
maxFileSize: number = 1048576;
|
||||
filteringOrganisationsAsync = false;
|
||||
|
||||
filteredGrants: GrantListingModel[];
|
||||
public formGroup = new UntypedFormBuilder().group({
|
||||
like: new UntypedFormControl(),
|
||||
grants: new UntypedFormControl(),
|
||||
status: new UntypedFormControl(),
|
||||
role: new UntypedFormControl,
|
||||
organisations: new UntypedFormControl(),
|
||||
collaborators: new UntypedFormControl(),
|
||||
datasetTemplates: new UntypedFormControl(),
|
||||
grantStatus: new UntypedFormControl()
|
||||
});
|
||||
|
||||
collaboratorsAutoCompleteConfiguration: MultipleAutoCompleteConfiguration = {
|
||||
filterFn: this.filterCollaborators.bind(this),
|
||||
initialItems: (excludedItems: any[]) => this.filterCollaborators('').pipe(map(result => result.filter(resultItem => (excludedItems || []).map(x => x.id).indexOf(resultItem.id) === -1))),
|
||||
displayFn: (item) => item['name'],
|
||||
titleFn: (item) => item['name']
|
||||
};
|
||||
|
||||
datasetTemplateAutoCompleteConfiguration: MultipleAutoCompleteConfiguration = {
|
||||
filterFn: this.filterDatasetTemplate.bind(this),
|
||||
initialItems: (excludedItems: any[]) => this.filterDatasetTemplate('').pipe(map(result => result.filter(resultItem => (excludedItems || []).map(x => x.id).indexOf(resultItem.id) === -1))),
|
||||
displayFn: (item) => item['label'],
|
||||
titleFn: (item) => item['label'],
|
||||
subtitleFn: (item) => item['description']
|
||||
};
|
||||
|
||||
grantAutoCompleteConfiguration: MultipleAutoCompleteConfiguration = {
|
||||
filterFn: this.filterGrant.bind(this),
|
||||
initialItems: (excludedItems: any[]) => this.filterGrant('').pipe(map(result => result.filter(resultItem => (excludedItems || []).map(x => x.id).indexOf(resultItem.id) === -1))),
|
||||
displayFn: (item) => item['label'],
|
||||
titleFn: (item) => item['label']
|
||||
};
|
||||
|
||||
organisationAutoCompleteConfiguration: MultipleAutoCompleteConfiguration = {
|
||||
filterFn: this.filterOrganisations.bind(this),
|
||||
initialItems: (excludedItems: any[]) => this.filterOrganisations('').pipe(map(result => result.filter(resultItem => (excludedItems || []).map(x => x.id).indexOf(resultItem.id) === -1))),
|
||||
displayFn: (item) => item['name'],
|
||||
titleFn: (item) => item['name']
|
||||
}
|
||||
|
||||
constructor(
|
||||
public language: TranslateService,
|
||||
public grantService: GrantService,
|
||||
private dmpService: DmpService,
|
||||
public formBuilder: UntypedFormBuilder,
|
||||
private dialog: MatDialog,
|
||||
private organisationService: OrganisationService,
|
||||
private userService: UserServiceOld,
|
||||
private datasetProfileService: DatasetService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
super(new ValidationErrorModel());
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
// This if is just for passing label on chips of dialog
|
||||
if (this.formGroup && this.criteriaFormGroup) {
|
||||
this.formGroup.get('datasetTemplates').setValue(this.criteriaFormGroup.get('datasetTemplates').value);
|
||||
this.formGroup.get('grants').setValue(this.criteriaFormGroup.get('grants').value);
|
||||
this.formGroup.get('collaborators').setValue(this.criteriaFormGroup.get('collaborators').value);
|
||||
this.formGroup.get('organisations').setValue(this.criteriaFormGroup.get('organisations').value);
|
||||
this.formGroup.get('status').setValue(this.criteriaFormGroup.get('status').value);
|
||||
}
|
||||
|
||||
this.formGroup.get('role').valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('organisations').valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('status').valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('grants').valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('like').valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('collaborators').valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('datasetTemplates').valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('grantStatus').valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
//if (this.criteria == null) { this.criteria = new DataManagementPlanCriteria(); }
|
||||
}
|
||||
|
||||
setCriteria(criteria: DmpCriteria): void {
|
||||
this.formGroup.get('like').patchValue(criteria.like);
|
||||
this.formGroup.get('grants').patchValue(criteria.grants);
|
||||
this.formGroup.get('status').patchValue(criteria.status);
|
||||
this.formGroup.get('role').patchValue(criteria.role);
|
||||
this.formGroup.get('collaborators').patchValue(criteria.collaborators);
|
||||
this.formGroup.get('datasetTemplates').patchValue(criteria.datasetTemplates);
|
||||
this.formGroup.get('grantStatus').patchValue(criteria.grantStatus);
|
||||
this.formGroup.get('organisations').patchValue(criteria.organisations);
|
||||
}
|
||||
|
||||
onCallbackError(error: any) {
|
||||
this.setErrorModel(error.error);
|
||||
}
|
||||
|
||||
controlModified(): void {
|
||||
this.clearErrorModel();
|
||||
this.filtersChanged.emit();
|
||||
if (this.refreshCallback != null &&
|
||||
(this.formGroup.get('like').value == null || this.formGroup.get('like').value.length === 0 || this.formGroup.get('like').value.length > 2)
|
||||
) {
|
||||
setTimeout(() => this.refreshCallback(true));
|
||||
}
|
||||
}
|
||||
|
||||
filterGrant(query: string) {
|
||||
const fields: Array<string> = new Array<string>();
|
||||
fields.push('asc');
|
||||
const grantRequestItem: DataTableRequest<GrantCriteria> = new DataTableRequest(0, null, { fields: fields });
|
||||
grantRequestItem.criteria = new GrantCriteria();
|
||||
grantRequestItem.criteria.like = query;
|
||||
return this.isPublic ? this.grantService.getPublicPaged(grantRequestItem).pipe(map(x => x.data)) : this.grantService.getPaged(grantRequestItem, "autocomplete").pipe(map(x => x.data));
|
||||
}
|
||||
|
||||
filterOrganisations(value: string) {
|
||||
this.filteringOrganisationsAsync = true;
|
||||
const fields: Array<string> = new Array<string>();
|
||||
fields.push('asc');
|
||||
const dataTableRequest: DataTableRequest<OrganisationCriteria> = new DataTableRequest(0, null, { fields: fields });
|
||||
dataTableRequest.criteria = new OrganisationCriteria();
|
||||
dataTableRequest.criteria.labelLike = value;
|
||||
|
||||
return this.isPublic ? this.organisationService.searchPublicOrganisations(dataTableRequest).pipe(map(x => x.data)) : this.organisationService.searchInternalOrganisations(dataTableRequest).pipe(map(x => x.data));
|
||||
}
|
||||
|
||||
filterCollaborators(query: string) {
|
||||
const fields: Array<string> = new Array<string>();
|
||||
fields.push('asc');
|
||||
const collaboratorsRequestItem: DataTableRequest<UserCriteria> = new DataTableRequest(0, null, { fields: fields });
|
||||
collaboratorsRequestItem.criteria = new UserCriteria();
|
||||
collaboratorsRequestItem.criteria.collaboratorLike = query;
|
||||
return this.userService.getCollaboratorsPaged(collaboratorsRequestItem).pipe(map(x => x.data));
|
||||
}
|
||||
|
||||
filterDatasetTemplate(query: string): Observable<any> {
|
||||
const fields: Array<string> = new Array<string>();
|
||||
fields.push('+label');
|
||||
const datasetTemplateRequestItem: DataTableRequest<DatasetProfileCriteria> = new DataTableRequest(0, null, { fields: fields });
|
||||
datasetTemplateRequestItem.criteria = new DatasetProfileCriteria();
|
||||
datasetTemplateRequestItem.criteria.like = query;
|
||||
return this.isPublic ? this.datasetProfileService.getDatasetProfiles(datasetTemplateRequestItem) : this.dmpService.getDatasetProfilesUsedPaged(datasetTemplateRequestItem).pipe(map(x => x.data));
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return this.authService.currentAccountIsAuthenticated();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -15,6 +15,11 @@
|
|||
<app-navigation-breadcrumb />
|
||||
</div>
|
||||
<!-- TODO: implement filters -->
|
||||
<div *ngIf="listingItems && listingItems.length > 0 || this.lookup.like" class="filter-btn" [style.right]="dialog.getDialogById('filters') ? '446px' : '0px'" [style.width]="listingItems.length > 2 ? '57px' : '37px'" (click)="openFiltersDialog()">
|
||||
<button mat-raised-button class="p-0">
|
||||
<mat-icon class="mr-4 filter-icon">filter_alt</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<!-- <div class="filter-btn" [style.right]="this.dialog.getDialogById('filters') ? '446px' : '0px'" [style.width]="scrollbar || this.dialog.getDialogById('filters') ? '57px' : '37px'" (click)="openFiltersDialog()">
|
||||
<button mat-raised-button class="p-0">
|
||||
<mat-icon class="mr-4">filter_alt</mat-icon>
|
||||
|
|
|
@ -187,6 +187,12 @@
|
|||
width: 37px;
|
||||
transition: right 0.3s;
|
||||
transition-timing-function: ease-in-out;
|
||||
|
||||
.filter-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-btn button {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormControl } from '@angular/forms';
|
||||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
|
@ -34,6 +34,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||
import { NgDialogAnimationService } from "ng-dialog-animation";
|
||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
||||
import { nameof } from 'ts-simple-nameof';
|
||||
import { DmpFilterDialogComponent } from './filtering/dmp-filter-dialog/dmp-filter-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dmp-listing-component',
|
||||
|
@ -299,6 +300,27 @@ export class DmpListingComponent extends BaseComponent implements OnInit {
|
|||
|
||||
openFiltersDialog(): void {
|
||||
//TODO: Add filters dialog
|
||||
let dialogRef = this.dialog.open(DmpFilterDialogComponent, {
|
||||
width: '456px',
|
||||
height: '100%',
|
||||
id: 'filters',
|
||||
restoreFocus: false,
|
||||
position: { right: '0px;' },
|
||||
panelClass: 'dialog-side-panel',
|
||||
data: {
|
||||
isPublic: this.isPublic ?? true,
|
||||
filterForm: this.lookup.buildForm(),
|
||||
updateDataFn: this.updateDataFn.bind(this)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDataFn(filterForm: UntypedFormGroup): void {
|
||||
let testLookup = this.lookup;
|
||||
testLookup.from(filterForm);
|
||||
console.log('testLookup: ', testLookup);
|
||||
// this.lookup.from(filterForm);
|
||||
// this.refresh(this.lookup);
|
||||
}
|
||||
|
||||
hasScrollbar(): boolean {
|
||||
|
|
|
@ -8,12 +8,16 @@ import { CloneDmpDialogModule } from '../clone-dialog/dmp-clone-dialog.module';
|
|||
import { NewVersionDmpDialogModule } from '../new-version-dialog/dmp-new-version-dialog.module';
|
||||
import { DmpListingRoutingModule } from './dmp-listing.routing';
|
||||
import { DmpInvitationDialogModule } from '../invitation/dialog/dmp-invitation-dialog.module';
|
||||
import { DmpFilterDialogComponent } from './filtering/dmp-filter-dialog/dmp-filter-dialog.component';
|
||||
import { DmpFilterComponent } from './filtering/dmp-filter.component';
|
||||
import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonUiModule,
|
||||
CommonFormsModule,
|
||||
FormattingModule,
|
||||
AutoCompleteModule,
|
||||
CloneDmpDialogModule,
|
||||
NewVersionDmpDialogModule,
|
||||
DmpInvitationDialogModule,
|
||||
|
@ -22,6 +26,8 @@ import { DmpInvitationDialogModule } from '../invitation/dialog/dmp-invitation-d
|
|||
declarations: [
|
||||
DmpListingComponent,
|
||||
DmpListingItemComponent,
|
||||
DmpFilterDialogComponent,
|
||||
DmpFilterComponent,
|
||||
],
|
||||
exports: [
|
||||
DmpListingItemComponent
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<a class="col-auto d-flex pointer" (click)="onClose()"><span class="ml-auto mt-3 material-icons clear-icon">clear</span></a>
|
||||
<app-dmp-filter-component
|
||||
[showGrant]="data.showGrant"
|
||||
[isPublic]="data.isPublic"
|
||||
[filterFormGroup]="data.filterForm"
|
||||
(filtersChanged)="onFilterChanged($event)"
|
||||
></app-dmp-filter-component>
|
|
@ -0,0 +1,44 @@
|
|||
import { Inject, Component, ViewChild, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { AnalyticsService } from '@app/core/services/matomo/analytics-service';
|
||||
import { DmpFilterComponent } from '../dmp-filter.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dmp-filter-dialog-component',
|
||||
templateUrl: './dmp-filter-dialog.component.html',
|
||||
styleUrls: ['./dmp-filter-dialog.component.scss']
|
||||
})
|
||||
|
||||
export class DmpFilterDialogComponent implements OnInit {
|
||||
|
||||
@ViewChild(DmpFilterComponent, { static: true }) criteria: DmpFilterComponent;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DmpFilterDialogComponent>,
|
||||
private analyticsService: AnalyticsService,
|
||||
|
||||
@Inject(MAT_DIALOG_DATA) public data: {
|
||||
showGrant: boolean,
|
||||
isPublic: boolean,
|
||||
filterForm: UntypedFormGroup,
|
||||
updateDataFn: Function,
|
||||
}) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.analyticsService.trackPageView(AnalyticsService.DmpCriteriaDialog);
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onFilterChanged(formGroup: UntypedFormGroup) {
|
||||
this.data.updateDataFn(formGroup);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
<div class="dmp-criteria">
|
||||
<div class="filters container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-10">
|
||||
<h6 class="criteria-title">{{'DMP-LISTING.FILTERS.NAME'| translate}}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <h6 class="filters-title">{{'CRITERIA.FILTERS'| translate}}</h6> -->
|
||||
<div class="row" style="justify-content: center;" *ngIf="formGroup">
|
||||
<!-- Search Filter-->
|
||||
<!-- <mat-form-field class="col-11 search">
|
||||
<input matInput placeholder="{{'CRITERIA.DMP.LIKE'| translate}}" name="likeCriteria"
|
||||
[formControl]="formGroup.get('like')">
|
||||
<mat-error *ngIf="formGroup.get('like').hasError('backendError')">
|
||||
{{formGroup.get('like').getError('backendError').message}}</mat-error>
|
||||
<mat-icon matSuffix class="style-icon">search</mat-icon>
|
||||
</mat-form-field> -->
|
||||
<!-- End of Search Filter -->
|
||||
|
||||
<!-- Visibility Filter-->
|
||||
<div *ngIf="!isPublic" class="col-10">
|
||||
<h6 class="category-title">{{ 'DMP-LISTING.FILTERS.STATUS.NAME' | translate }}</h6>
|
||||
<mat-radio-group aria-label="Select an option" [formControl]="formGroup.get('status')" class="row">
|
||||
<mat-radio-button value="null" [checked]="!formGroup.get('status').value" class="col-12">{{ 'DMP-LISTING.FILTERS.STATUS.TYPE.ANY' | translate }}</mat-radio-button>
|
||||
<mat-radio-button value="2" class="col-12">{{ 'DMP-LISTING.FILTERS.STATUS.TYPE.PUBLIC' | translate }}</mat-radio-button>
|
||||
<mat-radio-button value="1" class="col-12">{{ 'DMP-LISTING.FILTERS.STATUS.TYPE.FINALIZED' | translate }}</mat-radio-button>
|
||||
<mat-radio-button value="0" class="col-12">{{ 'DMP-LISTING.FILTERS.STATUS.TYPE.DRAFT' | translate }}</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Visibility Filter-->
|
||||
|
||||
<!-- Grant Status -->
|
||||
<!-- TODO -->
|
||||
<!-- <div class="col-10" *ngIf="isPublic">
|
||||
<h6 class="category-title">{{ 'FACET-SEARCH.GRANT-STATUS.TITLE' | translate }}</h6>
|
||||
<mat-radio-group [formControl]="formGroup.get('grantStatus')">
|
||||
<mat-radio-button value="null" [checked]="!formGroup.get('grantStatus').value">{{ 'FACET-SEARCH.GRANT-STATUS.OPTIONS.ANY' | translate }}</mat-radio-button>
|
||||
<mat-radio-button value="0">{{ 'FACET-SEARCH.GRANT-STATUS.OPTIONS.ACTIVE' | translate }}</mat-radio-button>
|
||||
<mat-radio-button value="1">{{ 'FACET-SEARCH.GRANT-STATUS.OPTIONS.INACTIVE' | translate }}</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<hr>
|
||||
</div> -->
|
||||
<!-- End of Grant Status -->
|
||||
|
||||
<!-- Related Dataset Templates Filter -->
|
||||
<!-- TODO -->
|
||||
<!-- <div *ngIf="showGrant" class="col-10"> -->
|
||||
<div class="col-10">
|
||||
<h6 class="category-title">{{ 'DMP-LISTING.FILTERS.RELATED-DESCRIPTION-TEMPLATES.NAME' | translate}}</h6>
|
||||
<mat-form-field class="w-100">
|
||||
<mat-label>{{ 'DMP-LISTING.FILTERS.RELATED-DESCRIPTION-TEMPLATES.PLACEHOLDER' | translate }}</mat-label>
|
||||
<app-multiple-auto-complete [formControl]="formGroup.get('descriptionTemplateIds')" [configuration]="descriptionTemplateAutoCompleteConfiguration"></app-multiple-auto-complete>
|
||||
</mat-form-field>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Related Dataset Templates Filter -->
|
||||
|
||||
<!-- Dmp Blueprint Filter -->
|
||||
<div class="col-10">
|
||||
<h6 class="category-title">{{ 'DMP-LISTING.FILTERS.ASSOCIATED-DMP-BLUEPRINTS.NAME' | translate}}</h6>
|
||||
<mat-form-field class="w-100">
|
||||
<mat-label>{{ 'DMP-LISTING.FILTERS.ASSOCIATED-DMP-BLUEPRINTS.PLACEHOLDER' | translate }}</mat-label>
|
||||
<app-multiple-auto-complete [formControl]="formGroup.get('dmpBlueprintIds')" [configuration]="dmpBlueprintAutoCompleteConfiguration"></app-multiple-auto-complete>
|
||||
</mat-form-field>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Dmp Blueprint Filter -->
|
||||
|
||||
<!-- Related Grant Filters -->
|
||||
<!-- TODO -->
|
||||
<!-- <div *ngIf="showGrant" class="col-10">
|
||||
<h6 class="category-title">{{ 'DMP-RELATED-GRANT.RELATED-GRANT' | translate}}</h6>
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'CRITERIA.DMP.SELECT-GRANTS' | translate }}</mat-label>
|
||||
<app-multiple-auto-complete [formControl]="formGroup.get('grants')"
|
||||
[configuration]="grantAutoCompleteConfiguration">
|
||||
</app-multiple-auto-complete>
|
||||
</mat-form-field>
|
||||
<hr>
|
||||
</div> -->
|
||||
<!-- End of Related Grants Filters -->
|
||||
|
||||
<!-- Collaborators Filter -->
|
||||
<!-- TODO -->
|
||||
<!-- <div *ngIf="isAuthenticated()" class="col-10">
|
||||
<h6 class="category-title">{{ 'CRITERIA.DMP.RELATED-COLLABORATORS' | translate}}</h6>
|
||||
<mat-form-field>
|
||||
<mat-label>{{'CRITERIA.DMP.SELECT-COLLABORATORS' | translate}}</mat-label>
|
||||
<app-multiple-auto-complete [formControl]="formGroup.get('collaborators')"
|
||||
[configuration]="collaboratorsAutoCompleteConfiguration">
|
||||
</app-multiple-auto-complete>
|
||||
</mat-form-field>
|
||||
<hr>
|
||||
</div> -->
|
||||
<!-- End of Collaborators Filter -->
|
||||
|
||||
<!-- Role Filter -->
|
||||
<!-- TODO -->
|
||||
<div *ngIf="isAuthenticated()" class="col-10">
|
||||
<h6 class="category-title">{{ 'DMP-LISTING.FILTERS.ROLE.NAME' | translate }}</h6>
|
||||
<mat-radio-group aria-label="Select an option" [formControl]="formGroup.get('role')" class="row">
|
||||
<mat-radio-button value="null" [checked]="!formGroup.get('role').value" class="col-12">{{ 'DMP-LISTING.FILTERS.ROLE.TYPE.ANY' | translate }}</mat-radio-button>
|
||||
<mat-radio-button value="0" class="col-12">{{ 'DMP-LISTING.FILTERS.ROLE.TYPE.OWNER' | translate }}</mat-radio-button>
|
||||
<mat-radio-button value="1" class="col-12">{{ 'DMP-LISTING.FILTERS.ROLE.TYPE.MEMBER' | translate }}</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<hr>
|
||||
</div>
|
||||
<!-- End of Role Filter -->
|
||||
|
||||
<!-- Related Organization Filter -->
|
||||
<!-- TODO -->
|
||||
<!-- <div *ngIf="showGrant" class="col-10">
|
||||
<h6 class="category-title">{{ 'DMP-RELATED-ORGANIZATION.RELATED-ORGANIZATION' | translate }}</h6>
|
||||
<mat-form-field>
|
||||
<mat-label>{{'DMP-RELATED-ORGANIZATION.SELECT-ORGANIZATIONS' | translate}}</mat-label>
|
||||
<app-multiple-auto-complete [formControl]="formGroup.get('organisations')"
|
||||
[configuration]="organisationAutoCompleteConfiguration">
|
||||
</app-multiple-auto-complete>
|
||||
</mat-form-field>
|
||||
</div> -->
|
||||
<!-- End of Related Organization Filter -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -130,3 +130,7 @@
|
|||
::ng-deep .mat-form-field-appearance-outline .mat-form-field-infix {
|
||||
padding: 0.3rem 0rem 0.6rem 0rem !important;
|
||||
}
|
||||
|
||||
::ng-deep label {
|
||||
margin: 0;
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
|
||||
import { Component, Input, OnInit, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { BaseCriteriaComponent } from '@app/ui/misc/criteria/base-criteria.component';
|
||||
import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { AuthService } from '@app/core/services/auth/auth.service';
|
||||
import { DescriptionTemplateTypeService } from '@app/core/services/description-template-type/description-template-type.service';
|
||||
import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration';
|
||||
import { DmpBlueprintService } from '@app/core/services/dmp/dmp-blueprint.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dmp-filter-component',
|
||||
templateUrl: './dmp-filter.component.html',
|
||||
styleUrls: ['./dmp-filter.component.scss'],
|
||||
})
|
||||
export class DmpFilterComponent extends BaseCriteriaComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input() showGrant: boolean;
|
||||
@Input() isPublic: boolean;
|
||||
@Input() filterFormGroup: UntypedFormGroup;
|
||||
@Output() filtersChanged: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
filteringGrantsAsync = false;
|
||||
sizeError = false;
|
||||
maxFileSize: number = 1048576;
|
||||
filteringOrganisationsAsync = false;
|
||||
|
||||
get descriptionTemplateAutoCompleteConfiguration(): MultipleAutoCompleteConfiguration {
|
||||
return this.descriptionTemplateTypeService.getMultipleAutoCompleteSearchConfiguration();
|
||||
};
|
||||
|
||||
get dmpBlueprintAutoCompleteConfiguration(): MultipleAutoCompleteConfiguration {
|
||||
return this.dmpBlueprintService.multipleAutocompleteConfiguration;
|
||||
};
|
||||
|
||||
constructor(
|
||||
public language: TranslateService,
|
||||
public formBuilder: UntypedFormBuilder,
|
||||
private authentication: AuthService,
|
||||
private descriptionTemplateTypeService: DescriptionTemplateTypeService,
|
||||
private dmpBlueprintService: DmpBlueprintService,
|
||||
) {
|
||||
super(new ValidationErrorModel());
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['filterFormGroup']) {
|
||||
this.formGroup = this.filterFormGroup;
|
||||
|
||||
this.formGroup.get('role')?.valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('status')?.valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('descriptionTemplateIds')?.valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
this.formGroup.get('dmpBlueprintIds')?.valueChanges
|
||||
.pipe(takeUntil(this._destroyed))
|
||||
.subscribe(x => this.controlModified());
|
||||
}
|
||||
}
|
||||
|
||||
onCallbackError(error: any) {
|
||||
this.setErrorModel(error.error);
|
||||
}
|
||||
|
||||
controlModified(): void {
|
||||
this.clearErrorModel();
|
||||
this.filtersChanged.emit(this.formGroup);
|
||||
if (this.refreshCallback != null &&
|
||||
(this.formGroup.get('like').value == null || this.formGroup.get('like').value.length === 0 || this.formGroup.get('like').value.length > 2)
|
||||
) {
|
||||
setTimeout(() => this.refreshCallback(true));
|
||||
}
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return this.authentication.currentAccountIsAuthenticated();
|
||||
}
|
||||
}
|
|
@ -694,6 +694,34 @@
|
|||
"TOGGLE-DESCENDING": "Order by ascending",
|
||||
"TOGGLE-ΑSCENDING": "Order by descending"
|
||||
},
|
||||
"FILTERS": {
|
||||
"NAME": "Filters",
|
||||
"STATUS": {
|
||||
"NAME": "Status",
|
||||
"TYPE": {
|
||||
"ANY": "Any",
|
||||
"PUBLIC": "Published",
|
||||
"FINALIZED": "Finalized",
|
||||
"DRAFT": "Draft"
|
||||
}
|
||||
},
|
||||
"RELATED-DESCRIPTION-TEMPLATES": {
|
||||
"NAME":"Related Description Templates",
|
||||
"PLACEHOLDER": "Select Description Templates"
|
||||
},
|
||||
"ASSOCIATED-DMP-BLUEPRINTS": {
|
||||
"NAME": "Related DMP Blueprints",
|
||||
"PLACEHOLDER": "Select DMP Blueprints"
|
||||
},
|
||||
"ROLE": {
|
||||
"NAME": "Role",
|
||||
"TYPE": {
|
||||
"ANY": "Any",
|
||||
"OWNER": "Owner",
|
||||
"MEMBER": "Member"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EMPTY-LIST": "Nothing here yet."
|
||||
},
|
||||
"DMP-UPLOAD": {
|
||||
|
|
Loading…
Reference in New Issue