mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
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:
@@ -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>
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
.export-button {
|
||||||
|
background: var(--ds-admin-sidebar-bg);
|
||||||
|
border-color: var(--ds-admin-sidebar-bg);
|
||||||
|
}
|
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -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'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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>
|
||||||
|
@@ -1,3 +1,10 @@
|
|||||||
.num {
|
.num {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: red;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
@@ -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);
|
||||||
|
@@ -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 '';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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),
|
||||||
|
@@ -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.",
|
||||||
}
|
}
|
||||||
|
@@ -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.",
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user