CSV export for Filtered Items content report (#4071)

* CSV export for Filtered Items content report

* Fixed lint errors

* Fixed lint errors

* Fixed lint errors

* Make variables for CSV export null-proof

* Attempt to fix unit tests

* Fixed styling errors

* Fixed script references in unit tests

* Fixed typo in script name

* Fixed test parameterization

* Parameterization attempt

* Parameterization test

* Parameterization rollback

* Fixed predicate encoding bug

* Parameterization test

* Fixed styling error

* Fixed query predicate parameter

* Fixed collection parameterization

* Centralized string representation of a predicate

* Fixed parameterization

* Fixed second export test

* Replaced null payload by an empty non-null one

* Requested changes

* Fixed remaining bugs

* Updated Angular control flow syntax

* Improved collection parameter handling

* Fixed styling error

* Updated config.yml to match the central dspace-angular repo

* Removed repeated content

* Cleaned up a now useless import

* Fixed collections loading and added warning message about CSV export

* Fixed styling error

* Forgot to clean up old code

---------

Co-authored-by: Jean-François Morin <jean-francois.morin@bibl.ulaval.ca>
This commit is contained in:
jeffmorin
2025-03-25 10:49:46 -04:00
committed by GitHub
parent fd59ca8053
commit 5b7d246f68
11 changed files with 416 additions and 18 deletions

View File

@@ -0,0 +1,8 @@
@if (shouldShowButton$ | async) {
<button class="export-button btn btn-dark btn-sm"
[ngbTooltip]="tooltipMsg | translate"
(click)="export()"
[title]="tooltipMsg | translate" [attr.aria-label]="tooltipMsg | translate">
<i class="fas fa-file-export fa-fw"></i>
</button>
}

View File

@@ -0,0 +1,4 @@
.export-button {
background: var(--ds-admin-sidebar-bg);
border-color: var(--ds-admin-sidebar-bg);
}

View File

@@ -0,0 +1,194 @@
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
FormControl,
FormGroup,
} from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
import { Process } from '../../../../process-page/processes/process.model';
import { Script } from '../../../../process-page/scripts/script.model';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '../../../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { FiltersComponent } from '../../filters-section/filters-section.component';
import { OptionVO } from '../option-vo.model';
import { QueryPredicate } from '../query-predicate.model';
import { FilteredItemsExportCsvComponent } from './filtered-items-export-csv.component';
describe('FilteredItemsExportCsvComponent', () => {
let component: FilteredItemsExportCsvComponent;
let fixture: ComponentFixture<FilteredItemsExportCsvComponent>;
let scriptDataService: ScriptDataService;
let authorizationDataService: AuthorizationDataService;
let notificationsService;
let router;
const script = Object.assign(new Script(), { id: 'metadata-export-filtered-items-report', name: 'metadata-export-filtered-items-report' });
const process = Object.assign(new Process(), { processId: 5, scriptName: 'metadata-export-filtered-items-report' });
const params = new FormGroup({
collections: new FormControl([OptionVO.collection('1', 'coll1')]),
queryPredicates: new FormControl([QueryPredicate.of('name', 'equals', 'coll1')]),
filters: new FormControl([FiltersComponent.getFilter('is_item')]),
});
const emptyParams = new FormGroup({
collections: new FormControl([]),
queryPredicates: new FormControl([]),
filters: new FormControl([]),
});
function initBeforeEachAsync() {
scriptDataService = jasmine.createSpyObj('scriptDataService', {
findById: createSuccessfulRemoteDataObject$(script),
invoke: createSuccessfulRemoteDataObject$(process),
});
authorizationDataService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true),
});
notificationsService = new NotificationsServiceStub();
router = jasmine.createSpyObj('authorizationService', ['navigateByUrl']);
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NgbModule, FilteredItemsExportCsvComponent],
providers: [
{ provide: ScriptDataService, useValue: scriptDataService },
{ provide: AuthorizationDataService, useValue: authorizationDataService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: Router, useValue: router },
],
}).compileComponents();
}
function initBeforeEach() {
fixture = TestBed.createComponent(FilteredItemsExportCsvComponent);
component = fixture.componentInstance;
component.reportParams = params;
fixture.detectChanges();
}
describe('init', () => {
describe('comp', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
}));
beforeEach(() => {
initBeforeEach();
});
it('should init the comp', () => {
expect(component).toBeTruthy();
});
});
describe('when the user is an admin and the metadata-export-filtered-items-report script is present ', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
}));
beforeEach(() => {
initBeforeEach();
});
it('should add the button', () => {
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
expect(debugElement).toBeDefined();
});
});
describe('when the user is not an admin', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
}));
beforeEach(() => {
initBeforeEach();
});
it('should not add the button', () => {
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
expect(debugElement).toBeNull();
});
});
describe('when the metadata-export-filtered-items-report script is not present', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
(scriptDataService.findById as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Not found', 404));
}));
beforeEach(() => {
initBeforeEach();
});
it('should should not add the button', () => {
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
expect(debugElement).toBeNull();
});
});
});
describe('export', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
}));
beforeEach(() => {
initBeforeEach();
});
it('should call the invoke script method with the correct parameters', () => {
// Parameterized export
component.export();
expect(scriptDataService.invoke).toHaveBeenCalledWith('metadata-export-filtered-items-report',
[
{ name: '-c', value: params.value.collections[0].id },
{ name: '-qp', value: QueryPredicate.toString(params.value.queryPredicates[0]) },
{ name: '-f', value: FiltersComponent.toQueryString(params.value.filters) },
], []);
fixture.detectChanges();
// Non-parameterized export
component.reportParams = emptyParams;
fixture.detectChanges();
component.export();
expect(scriptDataService.invoke).toHaveBeenCalledWith('metadata-export-filtered-items-report', [], []);
});
it('should show a success message when the script was invoked successfully and redirect to the corresponding process page', () => {
component.export();
expect(notificationsService.success).toHaveBeenCalled();
expect(router.navigateByUrl).toHaveBeenCalledWith(getProcessDetailRoute(process.processId));
});
it('should show an error message when the script was not invoked successfully and stay on the current page', () => {
(scriptDataService.invoke as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Error', 500));
component.export();
expect(notificationsService.error).toHaveBeenCalled();
expect(router.navigateByUrl).not.toHaveBeenCalled();
});
});
describe('clicking the button', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
}));
beforeEach(() => {
initBeforeEach();
});
it('should trigger the export function', () => {
spyOn(component, 'export');
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
debugElement.triggerEventHandler('click', null);
expect(component.export).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,123 @@
import { AsyncPipe } from '@angular/common';
import {
Component,
Input,
OnInit,
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
combineLatest as observableCombineLatest,
Observable,
} from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
import { Process } from '../../../../process-page/processes/process.model';
import { hasValue } from '../../../../shared/empty.util';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { FiltersComponent } from '../../filters-section/filters-section.component';
import { OptionVO } from '../option-vo.model';
import { QueryPredicate } from '../query-predicate.model';
@Component({
selector: 'ds-filtered-items-export-csv',
styleUrls: ['./filtered-items-export-csv.component.scss'],
templateUrl: './filtered-items-export-csv.component.html',
standalone: true,
imports: [NgbTooltipModule, AsyncPipe, TranslateModule],
})
/**
* Display a button to export the MetadataQuery (aka Filtered Items) Report results as csv
*/
export class FilteredItemsExportCsvComponent implements OnInit {
/**
* The current configuration of the search
*/
@Input() reportParams: FormGroup;
/**
* Observable used to determine whether the button should be shown
*/
shouldShowButton$: Observable<boolean>;
/**
* The message key used for the tooltip of the button
*/
tooltipMsg = 'metadata-export-filtered-items.tooltip';
constructor(private scriptDataService: ScriptDataService,
private authorizationDataService: AuthorizationDataService,
private notificationsService: NotificationsService,
private translateService: TranslateService,
private router: Router,
) {
}
static csvExportEnabled(scriptDataService: ScriptDataService, authorizationDataService: AuthorizationDataService): Observable<boolean> {
const scriptExists$ = scriptDataService.findById('metadata-export-filtered-items-report').pipe(
getFirstCompletedRemoteData(),
map((rd) => rd.isSuccess && hasValue(rd.payload)),
);
const isAuthorized$ = authorizationDataService.isAuthorized(FeatureID.AdministratorOf);
return observableCombineLatest([scriptExists$, isAuthorized$]).pipe(
map(([scriptExists, isAuthorized]: [boolean, boolean]) => scriptExists && isAuthorized),
);
}
ngOnInit(): void {
this.shouldShowButton$ = FilteredItemsExportCsvComponent.csvExportEnabled(this.scriptDataService, this.authorizationDataService);
}
/**
* Start the export of the items based on the selected parameters
*/
export() {
const parameters = [];
const colls = this.reportParams.value.collections || [];
for (let i = 0; i < colls.length; i++) {
if (colls[i]) {
parameters.push({ name: '-c', value: OptionVO.toString(colls[i]) });
}
}
const preds = this.reportParams.value.queryPredicates || [];
for (let i = 0; i < preds.length; i++) {
const field = preds[i].field;
const op = preds[i].operator;
if (field && op) {
parameters.push({ name: '-qp', value: QueryPredicate.toString(preds[i]) });
}
}
const filters = FiltersComponent.toQueryString(this.reportParams.value.filters) || [];
if (filters.length > 0) {
parameters.push({ name: '-f', value: filters });
}
this.scriptDataService.invoke('metadata-export-filtered-items-report', parameters, []).pipe(
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get('metadata-export-filtered-items.submit.success'));
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
} else {
this.notificationsService.error(this.translateService.get('metadata-export-filtered-items.submit.error'));
}
});
}
}

View File

@@ -11,11 +11,16 @@
{{'admin.reports.items.section.collectionSelector' | translate}} {{'admin.reports.items.section.collectionSelector' | translate}}
</ng-template> </ng-template>
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<select id="collSel" name="collSel" class="form-control" multiple="multiple" size="10" formControlName="collections"> @if (loadingCollections$ | async) {
@for (item of collections; track item) { <ds-loading></ds-loading>
<option [value]="item.id" [disabled]="item.disabled">{{item.name$ | async}}</option> }
} @if ((loadingCollections$ | async) !== true) {
</select> <select id="collSel" name="collSel" class="form-control" multiple="multiple" size="10" formControlName="collections">
@for (item of collections; track item) {
<option [value]="item.id" [disabled]="item.disabled">{{item.name$ | async}}</option>
}
</select>
}
<div class="row"> <div class="row">
<span class="col-3"></span> <span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button> <button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
@@ -132,6 +137,10 @@
</select> </select>
</div> </div>
<div class="row"> <div class="row">
@if (csvExportEnabled$ | async) {
<span class="col-3"></span>
<div class="warning">{{ 'metadata-export-filtered-items.columns.warning' | translate }}</div>
}
<span class="col-3"></span> <span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button> <button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div> </div>
@@ -186,9 +195,9 @@
<div> <div>
<button id="prev" class="btn btn-light" (click)="prevPage()" [dsBtnDisabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button> <button id="prev" class="btn btn-light" (click)="prevPage()" [dsBtnDisabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button>
<button id="next" class="btn btn-light" (click)="nextPage()" [dsBtnDisabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button> <button id="next" class="btn btn-light" (click)="nextPage()" [dsBtnDisabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button>
<!-- <div style="float: right; margin-right: 60px;">
<button id="export">{{'admin.reports.commons.export' | translate}}</button> <ds-filtered-items-export-csv [reportParams]="queryForm"></ds-filtered-items-export-csv>
--> </div>
</div> </div>
<table id="itemtable" class="sortable"></table> <table id="itemtable" class="sortable"></table>
</ng-template> </ng-template>

View File

@@ -1,3 +1,10 @@
.num { .num {
text-align: center; text-align: center;
} }
.warning {
color: red;
font-style: italic;
text-align: center;
width: 100%;
}

View File

@@ -20,13 +20,16 @@ import {
TranslateService, TranslateService,
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { import {
BehaviorSubject,
map, map,
Observable, Observable,
} from 'rxjs'; } from 'rxjs';
import { CollectionDataService } from 'src/app/core/data/collection-data.service'; import { CollectionDataService } from 'src/app/core/data/collection-data.service';
import { CommunityDataService } from 'src/app/core/data/community-data.service'; import { CommunityDataService } from 'src/app/core/data/community-data.service';
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
import { MetadataFieldDataService } from 'src/app/core/data/metadata-field-data.service'; import { MetadataFieldDataService } from 'src/app/core/data/metadata-field-data.service';
import { MetadataSchemaDataService } from 'src/app/core/data/metadata-schema-data.service'; import { MetadataSchemaDataService } from 'src/app/core/data/metadata-schema-data.service';
import { ScriptDataService } from 'src/app/core/data/processes/script-data.service';
import { RestRequestMethod } from 'src/app/core/data/rest-request-method'; import { RestRequestMethod } from 'src/app/core/data/rest-request-method';
import { DspaceRestService } from 'src/app/core/dspace-rest/dspace-rest.service'; import { DspaceRestService } from 'src/app/core/dspace-rest/dspace-rest.service';
import { RawRestResponse } from 'src/app/core/dspace-rest/raw-rest-response.model'; import { RawRestResponse } from 'src/app/core/dspace-rest/raw-rest-response.model';
@@ -36,10 +39,12 @@ import { Collection } from 'src/app/core/shared/collection.model';
import { Community } from 'src/app/core/shared/community.model'; import { Community } from 'src/app/core/shared/community.model';
import { getFirstSucceededRemoteListPayload } from 'src/app/core/shared/operators'; import { getFirstSucceededRemoteListPayload } from 'src/app/core/shared/operators';
import { isEmpty } from 'src/app/shared/empty.util'; import { isEmpty } from 'src/app/shared/empty.util';
import { ThemedLoadingComponent } from 'src/app/shared/loading/themed-loading.component';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive'; import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { FiltersComponent } from '../filters-section/filters-section.component'; import { FiltersComponent } from '../filters-section/filters-section.component';
import { FilteredItemsExportCsvComponent } from './filtered-items-export-csv/filtered-items-export-csv.component';
import { import {
FilteredItem, FilteredItem,
FilteredItems, FilteredItems,
@@ -62,12 +67,19 @@ import { QueryPredicate } from './query-predicate.model';
AsyncPipe, AsyncPipe,
FiltersComponent, FiltersComponent,
BtnDisabledDirective, BtnDisabledDirective,
FilteredItemsExportCsvComponent,
ThemedLoadingComponent,
], ],
standalone: true, standalone: true,
}) })
export class FilteredItemsComponent implements OnInit { export class FilteredItemsComponent implements OnInit {
collections: OptionVO[]; collections: OptionVO[];
/**
* A Boolean representing if loading the list of collections is pending
*/
loadingCollections$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
presetQueries: PresetQuery[]; presetQueries: PresetQuery[];
metadataFields: OptionVO[]; metadataFields: OptionVO[];
metadataFieldsWithAny: OptionVO[]; metadataFieldsWithAny: OptionVO[];
@@ -79,6 +91,10 @@ export class FilteredItemsComponent implements OnInit {
results: FilteredItems = new FilteredItems(); results: FilteredItems = new FilteredItems();
results$: Observable<FilteredItem[]>; results$: Observable<FilteredItem[]>;
@ViewChild('acc') accordionComponent: NgbAccordion; @ViewChild('acc') accordionComponent: NgbAccordion;
/**
* Observable used to determine whether CSV export is enabled
*/
csvExportEnabled$: Observable<boolean>;
constructor( constructor(
private communityService: CommunityDataService, private communityService: CommunityDataService,
@@ -86,6 +102,8 @@ export class FilteredItemsComponent implements OnInit {
private metadataSchemaService: MetadataSchemaDataService, private metadataSchemaService: MetadataSchemaDataService,
private metadataFieldService: MetadataFieldDataService, private metadataFieldService: MetadataFieldDataService,
private translateService: TranslateService, private translateService: TranslateService,
private scriptDataService: ScriptDataService,
private authorizationDataService: AuthorizationDataService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private restService: DspaceRestService) {} private restService: DspaceRestService) {}
@@ -100,6 +118,8 @@ export class FilteredItemsComponent implements OnInit {
new QueryPredicate().toFormGroup(this.formBuilder), new QueryPredicate().toFormGroup(this.formBuilder),
]; ];
this.csvExportEnabled$ = FilteredItemsExportCsvComponent.csvExportEnabled(this.scriptDataService, this.authorizationDataService);
this.queryForm = this.formBuilder.group({ this.queryForm = this.formBuilder.group({
collections: this.formBuilder.control([''], []), collections: this.formBuilder.control([''], []),
presetQuery: this.formBuilder.control('new', []), presetQuery: this.formBuilder.control('new', []),
@@ -111,6 +131,7 @@ export class FilteredItemsComponent implements OnInit {
} }
loadCollections(): void { loadCollections(): void {
this.loadingCollections$.next(true);
this.collections = []; this.collections = [];
const wholeRepo$ = this.translateService.stream('admin.reports.items.wholeRepo'); const wholeRepo$ = this.translateService.stream('admin.reports.items.wholeRepo');
this.collections.push(OptionVO.collectionLoc('', wholeRepo$)); this.collections.push(OptionVO.collectionLoc('', wholeRepo$));
@@ -132,6 +153,7 @@ export class FilteredItemsComponent implements OnInit {
const collVO = OptionVO.collection(collection.uuid, '' + collection.name); const collVO = OptionVO.collection(collection.uuid, '' + collection.name);
this.collections.push(collVO); this.collections.push(collVO);
}); });
this.loadingCollections$.next(false);
}, },
); );
}); });
@@ -167,10 +189,10 @@ export class FilteredItemsComponent implements OnInit {
QueryPredicate.of('dc.description.provenance', QueryPredicate.DOES_NOT_MATCH, '^.*No\. of bitstreams(.|\r|\n|\r\n)*\.(PDF|pdf|DOC|doc|PPT|ppt|DOCX|docx|PPTX|pptx).*$'), QueryPredicate.of('dc.description.provenance', QueryPredicate.DOES_NOT_MATCH, '^.*No\. of bitstreams(.|\r|\n|\r\n)*\.(PDF|pdf|DOC|doc|PPT|ppt|DOCX|docx|PPTX|pptx).*$'),
]), ]),
PresetQuery.of('q9', 'admin.reports.items.preset.hasEmptyMetadata', [ PresetQuery.of('q9', 'admin.reports.items.preset.hasEmptyMetadata', [
QueryPredicate.of('*', QueryPredicate.MATCHES, '^\s*$'), QueryPredicate.of('*', QueryPredicate.MATCHES, '^\\s*$'),
]), ]),
PresetQuery.of('q10', 'admin.reports.items.preset.hasUnbreakingDataInDescription', [ PresetQuery.of('q10', 'admin.reports.items.preset.hasUnbreakingDataInDescription', [
QueryPredicate.of('dc.description.*', QueryPredicate.MATCHES, '^.*[^\s]{50,}.*$'), QueryPredicate.of('dc.description.*', QueryPredicate.MATCHES, '^.*(\\S){50,}.*$'),
]), ]),
PresetQuery.of('q12', 'admin.reports.items.preset.hasXmlEntityInMetadata', [ PresetQuery.of('q12', 'admin.reports.items.preset.hasXmlEntityInMetadata', [
QueryPredicate.of('*', QueryPredicate.MATCHES, '^.*&#.*$'), QueryPredicate.of('*', QueryPredicate.MATCHES, '^.*&#.*$'),
@@ -344,13 +366,8 @@ export class FilteredItemsComponent implements OnInit {
const preds = this.queryForm.value.queryPredicates; const preds = this.queryForm.value.queryPredicates;
for (let i = 0; i < preds.length; i++) { for (let i = 0; i < preds.length; i++) {
const field = preds[i].field; const pred = encodeURIComponent(QueryPredicate.toString(preds[i]));
const op = preds[i].operator; params += `&queryPredicates=${pred}`;
const value = preds[i].value;
params += `&queryPredicates=${field}:${op}`;
if (value) {
params += `:${value}`;
}
} }
const filters = FiltersComponent.toQueryString(this.queryForm.value.filters); const filters = FiltersComponent.toQueryString(this.queryForm.value.filters);

View File

@@ -46,6 +46,16 @@ export class OptionVO {
subscriber.next(value); subscriber.next(value);
subscriber.complete(); subscriber.complete();
}); });
} }
static toString(obj: any): string {
if (obj) {
if (obj instanceof OptionVO && obj.id) {
return obj.id;
}
return obj as string;
}
return '';
}
} }

View File

@@ -29,6 +29,13 @@ export class QueryPredicate {
return pred; return pred;
} }
static toString(pred: QueryPredicate): string {
if (pred.value) {
return `${pred.field}:${pred.operator}:${pred.value}`;
}
return `${pred.field}:${pred.operator}`;
}
toFormGroup(formBuilder: FormBuilder): FormGroup { toFormGroup(formBuilder: FormBuilder): FormGroup {
return formBuilder.group({ return formBuilder.group({
field: new FormControl(this.field), field: new FormControl(this.field),

View File

@@ -6861,4 +6861,12 @@
"search.filters.access_status.metadata.only": "Metadata only", "search.filters.access_status.metadata.only": "Metadata only",
"search.filters.access_status.unknown": "Unknown", "search.filters.access_status.unknown": "Unknown",
"metadata-export-filtered-items.tooltip": "Export report output as CSV",
"metadata-export-filtered-items.submit.success": "CSV export succeeded.",
"metadata-export-filtered-items.submit.error": "CSV export failed.",
"metadata-export-filtered-items.columns.warning": "CSV export automatically includes all relevant fields, so selections in this list are not taken into account.",
} }

View File

@@ -8584,4 +8584,15 @@
//"search.filters.access_status.unknown": "Unknown", //"search.filters.access_status.unknown": "Unknown",
"search.filters.access_status.unknown": "Inconnu", "search.filters.access_status.unknown": "Inconnu",
//"metadata-export-filtered-items.tooltip": "Export report output as CSV",
"metadata-export-filtered-items.tooltip": "Exporter le rapport en CSV",
//"metadata-export-filtered-items.submit.success": "CSV export succeeded.",
"metadata-export-filtered-items.submit.success": "Exportation CSV complétée.",
//"metadata-export-filtered-items.submit.error": "CSV export failed.",
"metadata-export-filtered-items.submit.error": "L'exportation CSV n'a pas fonctionné.",
//"metadata-export-filtered-items.columns.warning": "CSV export automatically includes all relevant fields, so selections in this list are not taken into account.",
"metadata-export-filtered-items.columns.warning": "L'exportation CSV inclut automatiquement tous les champs pertinents, sans égard au contenu sélectionné de cette liste.",
} }