mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Prevent request with page size of 9999 (#3694)
* [DURACOM-304] Refactored item-bitstreams.component by removing page size of 9999 * [DURACOM-304] Refactored edit-bitstream-page.component by removing page size of 9999 * [DURACOM-304] Refactored scripts-select.component by using infinite scroll instead of page size 9999 * [DURACOM-304] Refactored dynamic-list.component.ts by removing page size of 9999 * [DURACOM-304] Refactored relationship-type-data.service.ts by removing page size of 9999 * [DURACOM-304] removed unneeded selectAll method (dynamic-lookup-relation-search-tab.component) * [DURACOM-304] Refactored submission-section-cc-licenses.component.ts by removing page size of 9999 * [DURACOM-304] lint fix * [DURACOM-304] test fix * [DURACOM-304] fix accessibility issue on scripts-select * [DURACOM-304] Refactor of bundle-data.service.ts by removing page size of 9999 * [DURACOM-304] other fix related to accessibility * [DURACOM-304] lint fix * [DURACOM-304] resolve conflicts * [DURACOM-304] fix lint * [DURACOM-304] add support for findAll method in dynamic-scrollable-dropdown.component.ts * [DURACOM-304] refactor to use lazy data provider * [DURACOM-304] improve loading logic for cc-licenses section and dynamic-list * [DURACOM-304] refactor, fix dynamic-list.component loading * [DURACOM-304] remove br --------- Co-authored-by: Alisa Ismailati <alisa.ismailati@4science.com>
This commit is contained in:

committed by
GitHub

parent
63c98740ba
commit
33a091d630
@@ -1,6 +1,6 @@
|
||||
<ng-container *ngVar="(bitstreamRD$ | async) as bitstreamRD">
|
||||
<div class="container" *ngVar="(bitstreamFormatsRD$ | async) as formatsRD">
|
||||
<div class="row" *ngIf="bitstreamRD?.hasSucceeded && formatsRD?.hasSucceeded">
|
||||
<div class="container">
|
||||
<div class="row" *ngIf="bitstreamRD?.hasSucceeded">
|
||||
<div class="col-md-2">
|
||||
<ds-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-thumbnail>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading"
|
||||
<ds-loading *ngIf="!bitstreamRD || bitstreamRD?.isLoading"
|
||||
message="{{'loading.bitstream' | translate}}"></ds-loading>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -261,7 +261,7 @@ describe('EditBitstreamPageComponent', () => {
|
||||
});
|
||||
|
||||
it('should select the correct format', () => {
|
||||
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id);
|
||||
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.shortDescription);
|
||||
});
|
||||
|
||||
it('should put the \"New Format\" input on invisible', () => {
|
||||
@@ -292,7 +292,13 @@ describe('EditBitstreamPageComponent', () => {
|
||||
|
||||
describe('when an unknown format is selected', () => {
|
||||
beforeEach(() => {
|
||||
comp.updateNewFormatLayout(allFormats[0].id);
|
||||
comp.onChange({
|
||||
model: {
|
||||
id: 'selectedFormat',
|
||||
value: allFormats[0],
|
||||
},
|
||||
});
|
||||
comp.updateNewFormatLayout();
|
||||
});
|
||||
|
||||
it('should remove the invisible class from the \"New Format\" input', () => {
|
||||
@@ -394,9 +400,10 @@ describe('EditBitstreamPageComponent', () => {
|
||||
|
||||
describe('when selected format has changed', () => {
|
||||
beforeEach(() => {
|
||||
comp.formGroup.patchValue({
|
||||
formatContainer: {
|
||||
selectedFormat: allFormats[2].id,
|
||||
comp.onChange({
|
||||
model: {
|
||||
id: 'selectedFormat',
|
||||
value: allFormats[2],
|
||||
},
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
@@ -21,7 +21,6 @@ import {
|
||||
DynamicFormLayout,
|
||||
DynamicFormService,
|
||||
DynamicInputModel,
|
||||
DynamicSelectModel,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import {
|
||||
TranslateModule,
|
||||
@@ -39,23 +38,24 @@ import {
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { FindAllDataImpl } from '../../core/data/base/find-all-data';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||
import { BITSTREAM_FORMAT } from '../../core/shared/bitstream-format.resource-type';
|
||||
import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level';
|
||||
import { Bundle } from '../../core/shared/bundle.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { Metadata } from '../../core/shared/metadata.utils';
|
||||
import {
|
||||
getAllSucceededRemoteDataPayload,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
@@ -72,6 +72,7 @@ import { ErrorComponent } from '../../shared/error/error.component';
|
||||
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
||||
import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||
import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
|
||||
import { DynamicScrollableDropdownModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||
import { FormComponent } from '../../shared/form/form.component';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
@@ -109,12 +110,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
||||
|
||||
/**
|
||||
* The formats their remote data observable
|
||||
* Tracks changes and updates the view
|
||||
*/
|
||||
bitstreamFormatsRD$: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||
|
||||
/**
|
||||
* The UUID of the primary bitstream for this bundle
|
||||
*/
|
||||
@@ -130,11 +125,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
originalFormat: BitstreamFormat;
|
||||
|
||||
/**
|
||||
* A list of all available bitstream formats
|
||||
*/
|
||||
formats: BitstreamFormat[];
|
||||
|
||||
/**
|
||||
* @type {string} Key prefix used to generate form messages
|
||||
*/
|
||||
@@ -178,7 +168,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Options for fetching all bitstream formats
|
||||
*/
|
||||
findAllOptions = { elementsPerPage: 9999 };
|
||||
findAllOptions = {
|
||||
elementsPerPage: 20,
|
||||
currentPage: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* The Dynamic Input Model for the file's name
|
||||
@@ -218,9 +211,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The Dynamic Input Model for the selected format
|
||||
*/
|
||||
selectedFormatModel = new DynamicSelectModel({
|
||||
selectedFormatModel = new DynamicScrollableDropdownModel({
|
||||
id: 'selectedFormat',
|
||||
name: 'selectedFormat',
|
||||
displayKey: 'shortDescription',
|
||||
repeatable: false,
|
||||
metadataFields: [],
|
||||
submissionId: '',
|
||||
hasSelectableMetadata: false,
|
||||
resourceType: BITSTREAM_FORMAT,
|
||||
formatFunction: (format: BitstreamFormat | string) => {
|
||||
if (format instanceof BitstreamFormat) {
|
||||
return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription;
|
||||
} else {
|
||||
return format;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -438,6 +444,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
* @private
|
||||
*/
|
||||
private bundle: Bundle;
|
||||
/**
|
||||
* The currently selected format
|
||||
* @private
|
||||
*/
|
||||
private selectedFormat: BitstreamFormat;
|
||||
|
||||
constructor(private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
@@ -463,18 +474,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
this.itemId = this.route.snapshot.queryParams.itemId;
|
||||
this.entityType = this.route.snapshot.queryParams.entityType;
|
||||
this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream));
|
||||
this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions);
|
||||
|
||||
const bitstream$ = this.bitstreamRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
|
||||
const allFormats$ = this.bitstreamFormatsRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
|
||||
const bundle$ = bitstream$.pipe(
|
||||
switchMap((bitstream: Bitstream) => bitstream.bundle),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
@@ -490,24 +495,31 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
switchMap((bundle: Bundle) => bundle.item),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const format$ = bitstream$.pipe(
|
||||
switchMap(bitstream => bitstream.format),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
bitstream$,
|
||||
allFormats$,
|
||||
bundle$,
|
||||
primaryBitstream$,
|
||||
item$,
|
||||
).pipe()
|
||||
.subscribe(([bitstream, allFormats, bundle, primaryBitstream, item]) => {
|
||||
this.bitstream = bitstream as Bitstream;
|
||||
this.formats = allFormats.page;
|
||||
this.bundle = bundle;
|
||||
// hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
|
||||
// be a success response, but empty
|
||||
this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
|
||||
this.itemId = item.uuid;
|
||||
this.setIiifStatus(this.bitstream);
|
||||
}),
|
||||
format$,
|
||||
).subscribe(([bitstream, bundle, primaryBitstream, item, format]) => {
|
||||
this.bitstream = bitstream as Bitstream;
|
||||
this.bundle = bundle;
|
||||
this.selectedFormat = format;
|
||||
// hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
|
||||
// be a success response, but empty
|
||||
this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
|
||||
this.itemId = item.uuid;
|
||||
this.setIiifStatus(this.bitstream);
|
||||
}),
|
||||
format$.pipe(take(1)).subscribe(
|
||||
(format) => this.originalFormat = format,
|
||||
),
|
||||
);
|
||||
|
||||
this.subs.push(
|
||||
@@ -523,7 +535,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
setForm() {
|
||||
this.formGroup = this.formService.createFormGroup(this.formModel);
|
||||
this.updateFormatModel();
|
||||
this.updateForm(this.bitstream);
|
||||
this.updateFieldTranslations();
|
||||
}
|
||||
@@ -542,6 +553,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
description: bitstream.firstMetadataValue('dc.description'),
|
||||
},
|
||||
formatContainer: {
|
||||
selectedFormat: this.selectedFormat.shortDescription,
|
||||
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined,
|
||||
},
|
||||
});
|
||||
@@ -561,36 +573,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
});
|
||||
}
|
||||
this.bitstream.format.pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
).subscribe((format: BitstreamFormat) => {
|
||||
this.originalFormat = format;
|
||||
this.formGroup.patchValue({
|
||||
formatContainer: {
|
||||
selectedFormat: format.id,
|
||||
},
|
||||
});
|
||||
this.updateNewFormatLayout(format.id);
|
||||
});
|
||||
this.updateNewFormatLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the list of unknown format IDs an add options to the selectedFormatModel
|
||||
*/
|
||||
updateFormatModel() {
|
||||
this.selectedFormatModel.options = this.formats.map((format: BitstreamFormat) =>
|
||||
Object.assign({
|
||||
value: format.id,
|
||||
label: this.isUnknownFormat(format.id) ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the layout of the "Other Format" input depending on the selected format
|
||||
* @param selectedId
|
||||
*/
|
||||
updateNewFormatLayout(selectedId: string) {
|
||||
if (this.isUnknownFormat(selectedId)) {
|
||||
updateNewFormatLayout() {
|
||||
if (this.isUnknownFormat()) {
|
||||
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout;
|
||||
} else {
|
||||
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible';
|
||||
@@ -601,9 +593,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
* Is the provided format (id) part of the list of unknown formats?
|
||||
* @param id
|
||||
*/
|
||||
isUnknownFormat(id: string): boolean {
|
||||
const format = this.formats.find((f: BitstreamFormat) => f.id === id);
|
||||
return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown;
|
||||
isUnknownFormat(): boolean {
|
||||
return hasValue(this.selectedFormat) && this.selectedFormat.supportLevel === BitstreamFormatSupportLevel.Unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -635,7 +626,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
onChange(event) {
|
||||
const model = event.model;
|
||||
if (model.id === this.selectedFormatModel.id) {
|
||||
this.updateNewFormatLayout(model.value);
|
||||
this.selectedFormat = model.value;
|
||||
this.updateNewFormatLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,8 +637,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
onSubmit() {
|
||||
const updatedValues = this.formGroup.getRawValue();
|
||||
const updatedBitstream = this.formToBitstream(updatedValues);
|
||||
const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat);
|
||||
const isNewFormat = selectedFormat.id !== this.originalFormat.id;
|
||||
const isNewFormat = this.selectedFormat.id !== this.originalFormat.id;
|
||||
const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream;
|
||||
const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid;
|
||||
|
||||
@@ -698,7 +689,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
bundle$ = observableOf(this.bundle);
|
||||
}
|
||||
if (isNewFormat) {
|
||||
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe(
|
||||
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, this.selectedFormat).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((formatResponse: RemoteData<Bitstream>) => {
|
||||
if (hasValue(formatResponse) && formatResponse.hasFailed) {
|
||||
@@ -856,4 +847,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
||||
.forEach((subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
findAllFormatsServiceFactory() {
|
||||
return () => this.bitstreamFormatService as any as FindAllDataImpl<BitstreamFormat>;
|
||||
}
|
||||
}
|
||||
|
@@ -241,11 +241,12 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
|
||||
* no valid cached version. Defaults to true
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||
* requested after the response becomes stale
|
||||
* @param options the {@link FindListOptions} for the request
|
||||
* @return {Observable<Bitstream | null>}
|
||||
* Return an observable that contains primary bitstream information or null
|
||||
*/
|
||||
public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<Bitstream | null> {
|
||||
return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe(
|
||||
public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions): Observable<Bitstream | null> {
|
||||
return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, options, followLink('primaryBitstream')).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((rd: RemoteData<Bundle>) => {
|
||||
if (!rd.hasSucceeded) {
|
||||
|
@@ -78,10 +78,14 @@ export class BundleDataService extends IdentifiableDataService<Bundle> implement
|
||||
* requested after the response becomes stale
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||
* {@link HALLink}s should be automatically resolved
|
||||
* @param options the {@link FindListOptions} for the request
|
||||
*/
|
||||
// TODO should be implemented rest side
|
||||
findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> {
|
||||
return this.findAllByItem(item, { elementsPerPage: 9999 }, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
|
||||
findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bundle>[]): Observable<RemoteData<Bundle>> {
|
||||
//Since we filter by bundleName where the pagination options are not indicated we need to load all the possible bundles.
|
||||
// This is a workaround, in substitution of the previously recursive call with expand
|
||||
const paginationOptions = options ?? { elementsPerPage: 9999 };
|
||||
return this.findAllByItem(item, paginationOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
|
||||
map((rd: RemoteData<PaginatedList<Bundle>>) => {
|
||||
if (hasValue(rd.payload) && hasValue(rd.payload.page)) {
|
||||
const matchingBundle = rd.payload.page.find((bundle: Bundle) =>
|
||||
|
@@ -50,6 +50,7 @@ import { coreSelector } from '../core.selectors';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { BundleDataService } from '../data/bundle-data.service';
|
||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||
import { FindListOptions } from '../data/find-list-options.model';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
@@ -331,6 +332,7 @@ export class HeadTagService {
|
||||
'ORIGINAL',
|
||||
true,
|
||||
true,
|
||||
new FindListOptions(),
|
||||
followLink('primaryBitstream'),
|
||||
followLink('bitstreams', {
|
||||
findListOptions: {
|
||||
|
@@ -39,6 +39,9 @@
|
||||
[isFirstTable]="isFirst"
|
||||
aria-describedby="reorder-description">
|
||||
</ds-item-edit-bitstream-bundle>
|
||||
<div class="d-flex justify-content-center" *ngIf="showLoadMoreLink$ | async">
|
||||
<button class="btn btn-link my-3" (click)="loadBundles()"> {{'item.edit.bitstreams.load-more.link' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="bundles?.length === 0"
|
||||
class="alert alert-info w-100 d-inline-block mt-4" role="alert">
|
||||
|
@@ -1,4 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
AsyncPipe,
|
||||
CommonModule,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
@@ -15,16 +20,22 @@ import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
Observable,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import { AlertComponent } from 'src/app/shared/alert/alert.component';
|
||||
import { AlertType } from 'src/app/shared/alert/alert-type';
|
||||
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||
@@ -40,10 +51,13 @@ import {
|
||||
getFirstSucceededRemoteData,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../../../shared/empty.util';
|
||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe';
|
||||
@@ -58,10 +72,13 @@ import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle/i
|
||||
templateUrl: './item-bitstreams.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
ItemEditBitstreamBundleComponent,
|
||||
RouterLink,
|
||||
NgIf,
|
||||
VarDirective,
|
||||
NgForOf,
|
||||
ThemedLoadingComponent,
|
||||
AlertComponent,
|
||||
],
|
||||
@@ -77,9 +94,18 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
protected readonly AlertType = AlertType;
|
||||
|
||||
/**
|
||||
* The currently listed bundles
|
||||
* All bundles for the current item
|
||||
*/
|
||||
bundles$: Observable<Bundle[]>;
|
||||
private bundlesSubject = new BehaviorSubject<Bundle[]>([]);
|
||||
|
||||
/**
|
||||
* The page options to use for fetching the bundles
|
||||
*/
|
||||
bundlesOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'bundles-pagination-options',
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
/**
|
||||
* The bootstrap sizes used for the columns within this table
|
||||
@@ -98,6 +124,18 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
*/
|
||||
itemUpdateSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* The flag indicating to show the load more link
|
||||
*/
|
||||
showLoadMoreLink$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||
|
||||
/**
|
||||
* The list of bundles for the current item as an observable
|
||||
*/
|
||||
get bundles$(): Observable<Bundle[]> {
|
||||
return this.bundlesSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable which emits a boolean which represents whether the service is currently handling a 'move' request
|
||||
*/
|
||||
@@ -127,14 +165,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
* Actions to perform after the item has been initialized
|
||||
*/
|
||||
postItemInit(): void {
|
||||
const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions();
|
||||
this.isProcessingMoveRequest = this.itemBitstreamsService.getPerformingMoveRequest$();
|
||||
|
||||
this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: bundlesOptions })).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page),
|
||||
);
|
||||
this.loadBundles(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,6 +230,26 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
this.notificationsPrefix = 'item.edit.bitstreams.notifications.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load bundles for the current item
|
||||
* @param currentPage The current page to load
|
||||
*/
|
||||
loadBundles(currentPage?: number) {
|
||||
this.bundlesOptions = Object.assign(new PaginationComponentOptions(), this.bundlesOptions, {
|
||||
currentPage: currentPage || this.bundlesOptions.currentPage + 1,
|
||||
});
|
||||
this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: this.bundlesOptions })).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
tap((bundlesPL: PaginatedList<Bundle>) =>
|
||||
this.showLoadMoreLink$.next(bundlesPL.pageInfo.currentPage < bundlesPL.pageInfo.totalPages),
|
||||
),
|
||||
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page),
|
||||
).subscribe((bundles: Bundle[]) => {
|
||||
this.bundlesSubject.next([...this.bundlesSubject.getValue(), ...bundles]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Submit the current changes
|
||||
@@ -208,7 +259,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
submit() {
|
||||
this.submitting = true;
|
||||
|
||||
const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$);
|
||||
const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$.pipe(take(1)));
|
||||
|
||||
// Perform the setup actions from above in order and display notifications
|
||||
removedResponses$.subscribe((responses: RemoteData<NoContent>) => {
|
||||
@@ -217,6 +268,56 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
|
||||
* refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
|
||||
* navigate the user to the correct page)
|
||||
* @param bundle The bundle to send patch requests to
|
||||
* @param event The event containing the index the bitstream came from and was dropped to
|
||||
*/
|
||||
dropBitstream(bundle: Bundle, event: any) {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) {
|
||||
const moveOperation = {
|
||||
op: 'move',
|
||||
from: `/_links/bitstreams/${event.fromIndex}/href`,
|
||||
path: `/_links/bitstreams/${event.toIndex}/href`,
|
||||
} as Operation;
|
||||
this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData<Bundle>) => {
|
||||
this.zone.run(() => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.move', [response]);
|
||||
// Remove all cached requests from this bundle and call the event's callback when the requests are cleared
|
||||
this.requestService.removeByHrefSubstring(bundle.self).pipe(
|
||||
filter((isCached) => isCached),
|
||||
take(1),
|
||||
).subscribe(() => event.finish());
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display notifications
|
||||
* - Error notification for each failed response with their message
|
||||
* - Success notification in case there's at least one successful response
|
||||
* @param key The i18n key for the notification messages
|
||||
* @param responses The returned responses to display notifications for
|
||||
*/
|
||||
displayNotifications(key: string, responses: RemoteData<any>[]) {
|
||||
if (isNotEmpty(responses)) {
|
||||
const failedResponses = responses.filter((response: RemoteData<Bundle>) => hasValue(response) && response.hasFailed);
|
||||
const successfulResponses = responses.filter((response: RemoteData<Bundle>) => hasValue(response) && response.hasSucceeded);
|
||||
|
||||
failedResponses.forEach((response: RemoteData<Bundle>) => {
|
||||
this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage);
|
||||
});
|
||||
if (successfulResponses.length > 0) {
|
||||
this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the object updates service to discard all current changes to this item
|
||||
* Shows a notification to remind the user that they can undo this
|
||||
|
@@ -1,20 +1,47 @@
|
||||
<div class="form-group" *ngIf="scripts$ | async">
|
||||
<label for="process-script">{{'process.new.select-script' | translate}}</label>
|
||||
<select required id="process-script"
|
||||
class="form-control"
|
||||
name="script"
|
||||
[(ngModel)]="selectedScript"
|
||||
#script="ngModel">
|
||||
<option [ngValue]="undefined">{{'process.new.select-script.placeholder' | translate}}</option>
|
||||
<option *ngFor="let script of scripts$ | async" [ngValue]="script.id">
|
||||
{{script.name}}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="d-flex w-100 flex-column gap-3">
|
||||
<div>
|
||||
<div ngbDropdown class="d-flex">
|
||||
<input id="process-script"
|
||||
class="form-control"
|
||||
required
|
||||
[ngModel]="selectedScript"
|
||||
placeholder="{{'process.new.select-script.placeholder' | translate}}"
|
||||
[ngModelOptions]="{standalone: true}"
|
||||
ngbDropdownToggle
|
||||
role="combobox"
|
||||
#script="ngModel">
|
||||
<div ngbDropdownMenu aria-labelledby="process-script" class="w-100 scrollable-menu"
|
||||
role="menu"
|
||||
(scroll)="onScroll($event)"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="5"
|
||||
[infiniteScrollThrottle]="300"
|
||||
[infiniteScrollUpDistance]="1.5"
|
||||
[fromRoot]="true"
|
||||
[scrollWindow]="false">
|
||||
<button class="dropdown-item"
|
||||
*ngFor="let script of scripts"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
title="{{ script.name }}"
|
||||
(click)="onSelect(script);">
|
||||
<span class="text-truncate">{{ script.name }}</span>
|
||||
</button>
|
||||
<ng-container *ngIf="(isLoading$ | async)">
|
||||
<button class="dropdown-item disabled" role="menuitem">
|
||||
<ds-loading message="{{'loading.default' | translate}}">
|
||||
</ds-loading>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div *ngIf="script.invalid && (script.dirty || script.touched)"
|
||||
class="alert alert-danger validation-error">
|
||||
<div *ngIf="script.errors.required">
|
||||
{{'process.new.select-script.required' | translate}}
|
||||
</div>
|
||||
<div *ngIf="script.errors.required">
|
||||
{{ 'process.new.select-script.required' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,23 @@
|
||||
.dropdown-item {
|
||||
padding: 0.35rem 1rem;
|
||||
|
||||
&:active {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable-menu {
|
||||
height: auto;
|
||||
max-height: var(--ds-dropdown-menu-max-height);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
li:not(:last-of-type) .dropdown-item {
|
||||
border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
|
||||
}
|
||||
|
||||
#entityControlsDropdownMenu {
|
||||
outline: 0;
|
||||
left: 0 !important;
|
||||
box-shadow: var(--bs-btn-focus-box-shadow);
|
||||
}
|
||||
|
@@ -87,7 +87,7 @@ describe('ScriptsSelectComponent', () => {
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const select = fixture.debugElement.query(By.css('select'));
|
||||
const select = fixture.debugElement.query(By.css('#process-script'));
|
||||
select.triggerEventHandler('blur', null);
|
||||
|
||||
fixture.detectChanges();
|
||||
@@ -101,7 +101,7 @@ describe('ScriptsSelectComponent', () => {
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const select = fixture.debugElement.query(By.css('select'));
|
||||
const select = fixture.debugElement.query(By.css('#process-script'));
|
||||
select.triggerEventHandler('blur', null);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
@@ -19,32 +19,29 @@ import {
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Params,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import {
|
||||
Observable,
|
||||
BehaviorSubject,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||
import {
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
} from '../../../shared/empty.util';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||
import { Script } from '../../scripts/script.model';
|
||||
import { controlContainerFactory } from '../process-form-factory';
|
||||
|
||||
@@ -61,7 +58,7 @@ const SCRIPT_QUERY_PARAMETER = 'script';
|
||||
useFactory: controlContainerFactory,
|
||||
deps: [[new Optional(), NgForm]] }],
|
||||
standalone: true,
|
||||
imports: [NgIf, FormsModule, NgFor, AsyncPipe, TranslateModule],
|
||||
imports: [NgIf, FormsModule, NgFor, AsyncPipe, TranslateModule, InfiniteScrollModule, ThemedLoadingComponent, NgbDropdownModule],
|
||||
})
|
||||
export class ScriptsSelectComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
@@ -71,9 +68,19 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* All available scripts
|
||||
*/
|
||||
scripts$: Observable<Script[]>;
|
||||
scripts: Script[] = [];
|
||||
|
||||
private _selectedScript: Script;
|
||||
private routeSub: Subscription;
|
||||
private subscription: Subscription;
|
||||
|
||||
private _isLastPage = false;
|
||||
|
||||
scriptOptions: FindListOptions = {
|
||||
elementsPerPage: 20,
|
||||
currentPage: 1,
|
||||
};
|
||||
|
||||
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
constructor(
|
||||
private scriptService: ScriptDataService,
|
||||
@@ -87,31 +94,46 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
|
||||
* Checks if the route contains a script ID and auto selects this scripts
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.scripts$ = this.scriptService.findAll({ elementsPerPage: 9999 })
|
||||
.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((paginatedList: PaginatedList<Script>) => paginatedList.page),
|
||||
);
|
||||
this.loadScripts();
|
||||
}
|
||||
|
||||
this.routeSub = this.route.queryParams
|
||||
.pipe(
|
||||
filter((params: Params) => hasNoValue(params.id)),
|
||||
map((params: Params) => params[SCRIPT_QUERY_PARAMETER]),
|
||||
distinctUntilChanged(),
|
||||
switchMap((id: string) =>
|
||||
this.scripts$
|
||||
.pipe(
|
||||
take(1),
|
||||
map((scripts) =>
|
||||
scripts.find((script) => script.id === id),
|
||||
),
|
||||
),
|
||||
),
|
||||
).subscribe((script: Script) => {
|
||||
this._selectedScript = script;
|
||||
this.select.emit(script);
|
||||
});
|
||||
/**
|
||||
* Load the scripts and check if the route contains a script
|
||||
*/
|
||||
loadScripts() {
|
||||
if (this.isLoading$.value) {return;}
|
||||
this.isLoading$.next(true);
|
||||
|
||||
this.subscription = this.scriptService.findAll(this.scriptOptions).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
tap((paginatedList: PaginatedList<Script>) => {
|
||||
this._isLastPage = paginatedList?.pageInfo?.currentPage >= paginatedList?.pageInfo?.totalPages;
|
||||
}),
|
||||
map((paginatedList: PaginatedList<Script>) => paginatedList.page),
|
||||
).subscribe((newScripts: Script[]) => {
|
||||
this.scripts = [...this.scripts, ...newScripts];
|
||||
this.isLoading$.next(false);
|
||||
|
||||
const param = this.route.snapshot.queryParams[SCRIPT_QUERY_PARAMETER];
|
||||
if (hasValue(param)) {
|
||||
this._selectedScript = this.scripts.find((script) => script.id === param);
|
||||
this.select.emit(this._selectedScript);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more scripts when the user scrolls to the bottom of the list
|
||||
* @param event The scroll event
|
||||
*/
|
||||
onScroll(event: any) {
|
||||
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) {
|
||||
if (!this.isLoading$.value && !this._isLastPage) {
|
||||
this.scriptOptions.currentPage++;
|
||||
this.loadScripts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,14 +155,25 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
selectScript(script: Script) {
|
||||
this._selectedScript = script;
|
||||
}
|
||||
|
||||
onSelect(newScript: Script) {
|
||||
this.selectScript(newScript);
|
||||
// this._selectedScript = newScript;
|
||||
this.select.emit(newScript);
|
||||
this.selectedScript = newScript.name;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set script(value: Script) {
|
||||
this._selectedScript = value;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.routeSub)) {
|
||||
this.routeSub.unsubscribe();
|
||||
if (hasValue(this.subscription)) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -28,9 +28,7 @@
|
||||
<span [ngClass]="model.layout.element?.label" [innerHTML]="item.label"></span>
|
||||
</label>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="!model.repeatable"
|
||||
@@ -60,8 +58,10 @@
|
||||
<span [ngClass]="model.layout.element?.label" [innerHTML]="item.label"></span>
|
||||
</label>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="d-flex justify-content-center" *ngIf="(isLoading$ | async)">
|
||||
<ds-loading></ds-loading>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
@@ -25,7 +27,16 @@ import {
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormValidationService,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import findKey from 'lodash/findKey';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { PaginatedList } from '../../../../../../core/data/paginated-list.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
|
||||
@@ -36,6 +47,7 @@ import {
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../../../../../empty.util';
|
||||
import { ThemedLoadingComponent } from '../../../../../loading/themed-loading.component';
|
||||
import { FormBuilderService } from '../../../form-builder.service';
|
||||
import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model';
|
||||
import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model';
|
||||
@@ -60,10 +72,13 @@ export interface ListItem {
|
||||
NgbButtonsModule,
|
||||
NgForOf,
|
||||
ReactiveFormsModule,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
ThemedLoadingComponent,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class DsDynamicListComponent extends DynamicFormControlComponent implements OnInit {
|
||||
export class DsDynamicListComponent extends DynamicFormControlComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() group: UntypedFormGroup;
|
||||
@Input() model: any;
|
||||
@@ -73,7 +88,10 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
|
||||
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
public items: ListItem[][] = [];
|
||||
protected optionsList: VocabularyEntry[];
|
||||
public isLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
protected optionsList: VocabularyEntry[] = [];
|
||||
private nextPageInfo: PageInfo;
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
constructor(private vocabularyService: VocabularyService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
@@ -89,7 +107,13 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
|
||||
*/
|
||||
ngOnInit() {
|
||||
if (this.model.vocabularyOptions && hasValue(this.model.vocabularyOptions.name)) {
|
||||
this.setOptionsFromVocabulary();
|
||||
this.initOptionsFromVocabulary();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.subs.length > 0) {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,34 +160,76 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
|
||||
/**
|
||||
* Setting up the field options from vocabulary
|
||||
*/
|
||||
protected setOptionsFromVocabulary() {
|
||||
protected initOptionsFromVocabulary() {
|
||||
if (this.model.vocabularyOptions.name && this.model.vocabularyOptions.name.length > 0) {
|
||||
const listGroup = this.group.controls[this.model.id] as UntypedFormGroup;
|
||||
if (this.model.repeatable && this.model.required) {
|
||||
listGroup.addValidators(this.hasAtLeastOneVocabularyEntry());
|
||||
}
|
||||
const pageInfo: PageInfo = new PageInfo({
|
||||
elementsPerPage: 9999, currentPage: 1,
|
||||
|
||||
this.nextPageInfo = new PageInfo({
|
||||
elementsPerPage: 20, currentPage: 1,
|
||||
} as PageInfo);
|
||||
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, pageInfo).pipe(
|
||||
|
||||
this.loadEntries(listGroup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if at least one {@link VocabularyEntry} has been selected.
|
||||
*/
|
||||
hasAtLeastOneVocabularyEntry(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return control && control.value && Object.values(control.value).find((checked: boolean) => checked === true) ? null : this.model.errorMessages;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current page state to keep track of which one to load next
|
||||
* @param response
|
||||
*/
|
||||
setPaginationInfo(response: PaginatedList<VocabularyEntry>) {
|
||||
if (response.pageInfo.currentPage < response.pageInfo.totalPages) {
|
||||
this.nextPageInfo = Object.assign(new PageInfo(), response.pageInfo, { currentPage: response.currentPage + 1 });
|
||||
this.isLoading$.next(true);
|
||||
} else {
|
||||
this.isLoading$.next(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load entries page
|
||||
*
|
||||
* @param listGroup
|
||||
*/
|
||||
loadEntries(listGroup?: UntypedFormGroup) {
|
||||
if (!hasValue(listGroup)) {
|
||||
listGroup = this.group.controls[this.model.id] as UntypedFormGroup;
|
||||
}
|
||||
|
||||
this.subs.push(
|
||||
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.nextPageInfo).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
).subscribe((entries: PaginatedList<VocabularyEntry>) => {
|
||||
let groupCounter = 0;
|
||||
tap((response) => this.setPaginationInfo(response)),
|
||||
map(entries => entries.page),
|
||||
).subscribe((allEntries: VocabularyEntry[]) => {
|
||||
this.optionsList = [...this.optionsList, ...allEntries];
|
||||
let groupCounter = this.items.length;
|
||||
let itemsPerGroup = 0;
|
||||
let tempList: ListItem[] = [];
|
||||
this.optionsList = entries.page;
|
||||
|
||||
// Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength'
|
||||
entries.page.forEach((option: VocabularyEntry, key: number) => {
|
||||
allEntries.forEach((option: VocabularyEntry) => {
|
||||
const value = option.authority || option.value;
|
||||
const checked: boolean = isNotEmpty(findKey(
|
||||
this.model.value,
|
||||
(v) => v.value === option.value));
|
||||
(v) => v?.value === option.value));
|
||||
|
||||
const item: ListItem = {
|
||||
id: `${this.model.id}_${value}`,
|
||||
label: option.display,
|
||||
value: checked,
|
||||
index: key,
|
||||
index: this.optionsList.indexOf(option),
|
||||
};
|
||||
if (this.model.repeatable) {
|
||||
this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item));
|
||||
@@ -183,18 +249,11 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
|
||||
}
|
||||
});
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
|
||||
}
|
||||
// If the paginated request did not reach the end keep loading the entries in the background
|
||||
if (this.isLoading$.value) {
|
||||
this.loadEntries();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if at least one {@link VocabularyEntry} has been selected.
|
||||
*/
|
||||
hasAtLeastOneVocabularyEntry(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
return control && control.value && Object.values(control.value).find((checked: boolean) => checked === true) ? null : this.model.errorMessages;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -52,8 +52,8 @@
|
||||
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList; let i = index"
|
||||
[class.active]="i === selectedIndex"
|
||||
(keydown.enter)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
|
||||
title="{{ listEntry.display }}" role="option" type="button"
|
||||
[attr.id]="listEntry.display === (currentValue|async) ? ('combobox_' + id + '_selected') : null">
|
||||
title="{{ inputFormatter(listEntry) }}" role="option" type="button"
|
||||
[attr.id]="inputFormatter(listEntry) === (currentValue|async) ? ('combobox_' + id + '_selected') : null">
|
||||
{{inputFormatter(listEntry)}}
|
||||
</button>
|
||||
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
|
||||
|
@@ -3,6 +3,7 @@ import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
Injector,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
@@ -29,6 +30,7 @@ import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstr
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
|
||||
import { APP_DATA_SERVICES_MAP } from '../../../../../../../config/app-config.interface';
|
||||
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
|
||||
import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
|
||||
@@ -96,11 +98,13 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => {
|
||||
TestComponent,
|
||||
],
|
||||
providers: [
|
||||
Injector,
|
||||
ChangeDetectorRef,
|
||||
DsDynamicScrollableDropdownComponent,
|
||||
{ provide: VocabularyService, useValue: vocabularyServiceStub },
|
||||
{ provide: DynamicFormLayoutService, useValue: mockDynamicFormLayoutService },
|
||||
{ provide: DynamicFormValidationService, useValue: mockDynamicFormValidationService },
|
||||
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
});
|
||||
|
@@ -8,6 +8,8 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Injector,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
@@ -27,23 +29,35 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import {
|
||||
APP_DATA_SERVICES_MAP,
|
||||
LazyDataServicesMap,
|
||||
} from 'src/config/app-config.interface';
|
||||
|
||||
import { CacheableObject } from '../../../../../../core/cache/cacheable-object.model';
|
||||
import { FindAllDataImpl } from '../../../../../../core/data/base/find-all-data';
|
||||
import {
|
||||
buildPaginatedList,
|
||||
PaginatedList,
|
||||
} from '../../../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../../../core/data/remote-data';
|
||||
import { lazyDataService } from '../../../../../../core/lazy-data-service';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
|
||||
import { PageInfo } from '../../../../../../core/shared/page-info.model';
|
||||
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
|
||||
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
|
||||
import { isEmpty } from '../../../../../empty.util';
|
||||
import {
|
||||
hasValue,
|
||||
isEmpty,
|
||||
} from '../../../../../empty.util';
|
||||
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
|
||||
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
|
||||
import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model';
|
||||
@@ -84,10 +98,28 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
||||
public selectedIndex = 0;
|
||||
public acceptableKeys = ['Space', 'NumpadMultiply', 'NumpadAdd', 'NumpadSubtract', 'NumpadDecimal', 'Semicolon', 'Equal', 'Comma', 'Minus', 'Period', 'Quote', 'Backquote'];
|
||||
|
||||
constructor(protected vocabularyService: VocabularyService,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
protected layoutService: DynamicFormLayoutService,
|
||||
protected validationService: DynamicFormValidationService,
|
||||
/**
|
||||
* If true the component can rely on the findAll method for data loading.
|
||||
* This is a behaviour activated by dependency injection through the dropdown config.
|
||||
* If a service that implements findAll is not provided in the config the component falls back on the standard vocabulary service.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private useFindAllService: boolean;
|
||||
/**
|
||||
* A service that implements FindAllData.
|
||||
* If is provided in the config will be used for data loading in stead of the VocabularyService
|
||||
* @private
|
||||
*/
|
||||
private findAllService: FindAllDataImpl<CacheableObject>;
|
||||
|
||||
constructor(
|
||||
protected vocabularyService: VocabularyService,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
protected layoutService: DynamicFormLayoutService,
|
||||
protected validationService: DynamicFormValidationService,
|
||||
protected parentInjector: Injector,
|
||||
@Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: LazyDataServicesMap,
|
||||
) {
|
||||
super(vocabularyService, layoutService, validationService);
|
||||
}
|
||||
@@ -96,21 +128,41 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
||||
* Initialize the component, setting up the init form value
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.updatePageInfo(this.model.maxOptions, 1);
|
||||
this.loadOptions(true);
|
||||
const lazyProvider$: Observable<Cache> = hasValue(this.model.resourceType) ?
|
||||
lazyDataService(this.dataServiceMap, this.model.resourceType.value, this.parentInjector) : of(null);
|
||||
|
||||
lazyProvider$.pipe(take(1)).subscribe((dataService) => {
|
||||
this.findAllService = dataService as unknown as FindAllDataImpl<CacheableObject>;
|
||||
this.useFindAllService = hasValue(this.findAllService?.findAll) && typeof this.findAllService.findAll === 'function';
|
||||
this.updatePageInfo(this.model.maxOptions, 1);
|
||||
this.loadOptions(true);
|
||||
});
|
||||
|
||||
|
||||
this.group.get(this.model.id).valueChanges.pipe(distinctUntilChanged())
|
||||
.subscribe((value) => {
|
||||
this.setCurrentValue(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service and method to use to retrieve dropdown options
|
||||
*/
|
||||
getDataFromService(): Observable<RemoteData<PaginatedList<CacheableObject>>> {
|
||||
if (this.useFindAllService) {
|
||||
return this.findAllService.findAll({ elementsPerPage: this.pageInfo.elementsPerPage, currentPage: this.pageInfo.currentPage });
|
||||
} else {
|
||||
return this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
loadOptions(fromInit: boolean) {
|
||||
this.loading = true;
|
||||
this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe(
|
||||
this.getDataFromService().pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
catchError(() => observableOf(buildPaginatedList(new PageInfo(), []))),
|
||||
tap(() => this.loading = false),
|
||||
).subscribe((list: PaginatedList<VocabularyEntry>) => {
|
||||
).subscribe((list: PaginatedList<CacheableObject>) => {
|
||||
this.optionsList = list.page;
|
||||
if (fromInit && this.model.value) {
|
||||
this.setCurrentValue(this.model.value, true);
|
||||
@@ -130,7 +182,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
||||
/**
|
||||
* Converts an item from the result list to a `string` to display in the `<input>` field.
|
||||
*/
|
||||
inputFormatter = (x: VocabularyEntry): string => x.display || x.value;
|
||||
inputFormatter = (x: any): string => (this.model.formatFunction ? this.model.formatFunction(x) : (x.display || x.value));
|
||||
|
||||
/**
|
||||
* Opens dropdown menu
|
||||
@@ -233,7 +285,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
||||
this.pageInfo.totalElements,
|
||||
this.pageInfo.totalPages,
|
||||
);
|
||||
this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe(
|
||||
this.getDataFromService().pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
catchError(() => observableOf(buildPaginatedList(
|
||||
new PageInfo(),
|
||||
@@ -241,7 +293,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
||||
)),
|
||||
),
|
||||
tap(() => this.loading = false))
|
||||
.subscribe((list: PaginatedList<VocabularyEntry>) => {
|
||||
.subscribe((list: PaginatedList<any>) => {
|
||||
this.optionsList = this.optionsList.concat(list.page);
|
||||
this.updatePageInfo(
|
||||
list.pageInfo.elementsPerPage,
|
||||
@@ -272,7 +324,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
||||
setCurrentValue(value: any, init = false): void {
|
||||
let result: Observable<string>;
|
||||
|
||||
if (init) {
|
||||
if (init && !this.useFindAllService) {
|
||||
result = this.getInitValueFromModel().pipe(
|
||||
map((formValue: FormFieldMetadataValueObject) => formValue.display),
|
||||
);
|
||||
@@ -281,6 +333,8 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
||||
result = observableOf('');
|
||||
} else if (typeof value === 'string') {
|
||||
result = observableOf(value);
|
||||
} else if (this.useFindAllService) {
|
||||
result = observableOf(value[this.model.displayKey]);
|
||||
} else {
|
||||
result = observableOf(value.display);
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
serializable,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
|
||||
import { ResourceType } from '../../../../../../core/shared/resource-type';
|
||||
import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||
import {
|
||||
DsDynamicInputModel,
|
||||
@@ -13,15 +14,27 @@ import {
|
||||
export const DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN = 'SCROLLABLE_DROPDOWN';
|
||||
|
||||
export interface DynamicScrollableDropdownModelConfig extends DsDynamicInputModelConfig {
|
||||
vocabularyOptions: VocabularyOptions;
|
||||
vocabularyOptions?: VocabularyOptions;
|
||||
maxOptions?: number;
|
||||
value?: any;
|
||||
displayKey?: string;
|
||||
formatFunction?: (value: any) => string;
|
||||
resourceType?: ResourceType;
|
||||
}
|
||||
|
||||
export class DynamicScrollableDropdownModel extends DsDynamicInputModel {
|
||||
|
||||
@serializable() maxOptions: number;
|
||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN;
|
||||
@serializable() displayKey: string;
|
||||
/**
|
||||
* Configurable function for display value formatting in input
|
||||
*/
|
||||
formatFunction: (value: any) => string;
|
||||
/**
|
||||
* Resource type to match data service
|
||||
*/
|
||||
resourceType: ResourceType;
|
||||
|
||||
constructor(config: DynamicScrollableDropdownModelConfig, layout?: DynamicFormControlLayout) {
|
||||
|
||||
@@ -30,6 +43,9 @@ export class DynamicScrollableDropdownModel extends DsDynamicInputModel {
|
||||
this.autoComplete = AUTOCOMPLETE_OFF;
|
||||
this.vocabularyOptions = config.vocabularyOptions;
|
||||
this.maxOptions = config.maxOptions || 10;
|
||||
this.displayKey = config.displayKey || 'display';
|
||||
this.formatFunction = config.formatFunction;
|
||||
this.resourceType = config.resourceType;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -164,18 +164,6 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectAll', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component.selectObject, 'emit');
|
||||
component.selectAll();
|
||||
});
|
||||
|
||||
it('should emit the page filtered from already selected objects and call select on the service for all objects', () => {
|
||||
expect(component.selectObject.emit).toHaveBeenCalledWith(searchResult3);
|
||||
expect(selectableListService.select).toHaveBeenCalledWith(listID, [searchResult1, searchResult2, searchResult3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deselectAll', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component.deselectObject, 'emit');
|
||||
|
@@ -16,13 +16,7 @@ import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
mapTo,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service';
|
||||
import { PaginatedList } from '../../../../../../core/data/paginated-list.model';
|
||||
@@ -44,7 +38,6 @@ import { hasValue } from '../../../../../empty.util';
|
||||
import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type';
|
||||
import { ListableObject } from '../../../../../object-collection/shared/listable-object.model';
|
||||
import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
|
||||
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
|
||||
import { SearchObjects } from '../../../../../search/models/search-objects.model';
|
||||
import { SearchResult } from '../../../../../search/models/search-result.model';
|
||||
import { ThemedSearchComponent } from '../../../../../search/themed-search.component';
|
||||
@@ -240,35 +233,6 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
||||
this.selectableListService.deselect(this.listId, page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all items that were found using the current search query
|
||||
*/
|
||||
selectAll() {
|
||||
this.allSelected = true;
|
||||
this.selectAllLoading = true;
|
||||
const fullPagination = Object.assign(new PaginationComponentOptions(), {
|
||||
currentPage: 1,
|
||||
pageSize: 9999,
|
||||
});
|
||||
const fullSearchConfig = Object.assign(this.lookupRelationService.searchConfig, { pagination: fullPagination });
|
||||
const results$ = this.searchService.search<Item>(fullSearchConfig);
|
||||
results$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
map((resultsRD) => resultsRD.payload.page),
|
||||
tap(() => this.selectAllLoading = false),
|
||||
switchMap((results) => this.selection$.pipe(
|
||||
take(1),
|
||||
tap((selection: SearchResult<Item>[]) => {
|
||||
const filteredResults = results.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0);
|
||||
this.selectObject.emit(...filteredResults);
|
||||
}),
|
||||
mapTo(results),
|
||||
)),
|
||||
).subscribe((results) => {
|
||||
this.selectableListService.select(this.listId, results);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* setSelectedIds select all the items from the results that have relationship
|
||||
* @param idOfItems the uuid of items that are being checked
|
||||
|
@@ -1,38 +1,39 @@
|
||||
<div class="mb-4 ccLicense-select">
|
||||
<ds-select
|
||||
[disabled]="!submissionCcLicenses">
|
||||
@if (submissionCcLicenses) {
|
||||
<div class="mb-4 ccLicense-select">
|
||||
<div ngbDropdown>
|
||||
<input id="cc-license-dropdown"
|
||||
class="form-control"
|
||||
[(ngModel)]="selectedCcLicense.name"
|
||||
placeholder="{{ !storedCcLicenseLink ? ('submission.sections.ccLicense.select' | translate) : ('submission.sections.ccLicense.change' | translate)}}"
|
||||
[ngModelOptions]="{standalone: true}"
|
||||
ngbDropdownToggle
|
||||
role="combobox"
|
||||
#script="ngModel">
|
||||
<div ngbDropdownMenu aria-labelledby="cc-license-dropdown" class="w-100 scrollable-menu"
|
||||
role="menu"
|
||||
infiniteScroll
|
||||
(scroll)="onScroll($event)"
|
||||
[infiniteScrollDistance]="5"
|
||||
[infiniteScrollThrottle]="300"
|
||||
[infiniteScrollUpDistance]="1.5"
|
||||
[fromRoot]="true"
|
||||
[scrollWindow]="false">
|
||||
|
||||
<ng-container class="selection">
|
||||
<span *ngIf="!submissionCcLicenses">
|
||||
<ds-loading></ds-loading>
|
||||
</span>
|
||||
<span *ngIf="getSelectedCcLicense()">
|
||||
{{ getSelectedCcLicense().name }}
|
||||
</span>
|
||||
<span *ngIf="submissionCcLicenses && !getSelectedCcLicense()">
|
||||
<ng-container *ngIf="storedCcLicenseLink">
|
||||
{{ 'submission.sections.ccLicense.change' | translate }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!storedCcLicenseLink">
|
||||
{{ 'submission.sections.ccLicense.select' | translate }}
|
||||
</ng-container>
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container class="menu">
|
||||
<button *ngIf="submissionCcLicenses?.length === 0"
|
||||
class="dropdown-item disabled">
|
||||
{{ 'submission.sections.ccLicense.none' | translate }}
|
||||
</button>
|
||||
<button *ngFor="let license of submissionCcLicenses"
|
||||
class="dropdown-item"
|
||||
(click)="selectCcLicense(license)">
|
||||
{{ license.name }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
</ds-select>
|
||||
</div>
|
||||
@if(submissionCcLicenses?.length === 0) {
|
||||
<button class="dropdown-item disabled">
|
||||
{{ 'submission.sections.ccLicense.none' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
@for(license of submissionCcLicenses; track license.id) {
|
||||
<button class="dropdown-item" (click)="selectCcLicense(license)">
|
||||
{{ license.name }}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="getSelectedCcLicense()">
|
||||
|
||||
|
@@ -1,3 +1,13 @@
|
||||
.options-select-menu {
|
||||
max-height: 25vh;
|
||||
}
|
||||
|
||||
.ccLicense-select {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.scrollable-menu {
|
||||
height: auto;
|
||||
max-height: var(--ds-dropdown-menu-max-height);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
@@ -209,10 +209,10 @@ describe('SubmissionSectionCcLicensesComponent', () => {
|
||||
|
||||
it('should display a dropdown with the different cc licenses', () => {
|
||||
expect(
|
||||
de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(1)')).nativeElement.innerText,
|
||||
de.query(By.css('.ccLicense-select .scrollable-menu button:nth-child(1)')).nativeElement.innerText,
|
||||
).toContain('test license name 1');
|
||||
expect(
|
||||
de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(2)')).nativeElement.innerText,
|
||||
de.query(By.css('.ccLicense-select .scrollable-menu button:nth-child(2)')).nativeElement.innerText,
|
||||
).toContain('test license name 2');
|
||||
});
|
||||
|
||||
@@ -226,9 +226,7 @@ describe('SubmissionSectionCcLicensesComponent', () => {
|
||||
});
|
||||
|
||||
it('should display the selected cc license', () => {
|
||||
expect(
|
||||
de.query(By.css('.ccLicense-select ds-select button.selection')).nativeElement.innerText,
|
||||
).toContain('test license name 2');
|
||||
expect(component.selectedCcLicense.name).toContain('test license name 2');
|
||||
});
|
||||
|
||||
it('should display all field labels of the selected cc license only', () => {
|
||||
|
@@ -4,14 +4,18 @@ import {
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Inject,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
NgbDropdownModule,
|
||||
NgbModal,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
@@ -22,14 +26,16 @@ import {
|
||||
filter,
|
||||
map,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
||||
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import {
|
||||
@@ -64,6 +70,9 @@ import { SectionsType } from '../sections-type';
|
||||
VarDirective,
|
||||
NgForOf,
|
||||
DsSelectComponent,
|
||||
NgbDropdownModule,
|
||||
FormsModule,
|
||||
InfiniteScrollModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
@@ -95,7 +104,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
|
||||
/**
|
||||
* Cache of the available Creative Commons licenses.
|
||||
*/
|
||||
submissionCcLicenses: SubmissionCcLicence[];
|
||||
submissionCcLicenses: SubmissionCcLicence[] = [];
|
||||
|
||||
/**
|
||||
* Reference to NgbModal
|
||||
@@ -107,6 +116,25 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
|
||||
*/
|
||||
defaultJurisdiction: string;
|
||||
|
||||
/**
|
||||
* The currently selected cc licence
|
||||
*/
|
||||
selectedCcLicense: SubmissionCcLicence = new SubmissionCcLicence();
|
||||
|
||||
/**
|
||||
* Options for paginated data loading
|
||||
*/
|
||||
ccLicenceOptions: FindListOptions = {
|
||||
elementsPerPage: 20,
|
||||
currentPage: 1,
|
||||
};
|
||||
/**
|
||||
* Check to stop paginated search
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _isLastPage: boolean;
|
||||
|
||||
/**
|
||||
* The Creative Commons link saved in the workspace item.
|
||||
*/
|
||||
@@ -131,6 +159,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
|
||||
protected submissionCcLicenseUrlDataService: SubmissionCcLicenseUrlDataService,
|
||||
protected operationsBuilder: JsonPatchOperationsBuilder,
|
||||
protected configService: ConfigurationDataService,
|
||||
protected ref: ChangeDetectorRef,
|
||||
@Inject('collectionIdProvider') public injectedCollectionId: string,
|
||||
@Inject('sectionDataProvider') public injectedSectionData: SectionDataObject,
|
||||
@Inject('submissionIdProvider') public injectedSubmissionId: string,
|
||||
@@ -154,9 +183,10 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
|
||||
* @param ccLicense the Creative Commons license to select.
|
||||
*/
|
||||
selectCcLicense(ccLicense: SubmissionCcLicence) {
|
||||
if (!!this.getSelectedCcLicense() && this.getSelectedCcLicense().id === ccLicense.id) {
|
||||
if (this.selectedCcLicense.id === ccLicense.id) {
|
||||
return;
|
||||
}
|
||||
this.selectedCcLicense = ccLicense;
|
||||
this.setAccepted(false);
|
||||
this.updateSectionData({
|
||||
ccLicense: {
|
||||
@@ -300,13 +330,6 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
|
||||
}
|
||||
this.sectionData.data = data;
|
||||
}),
|
||||
this.submissionCcLicensesDataService.findAll({ elementsPerPage: 9999 }).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((list) => list.page),
|
||||
).subscribe(
|
||||
(licenses) => this.submissionCcLicenses = licenses,
|
||||
),
|
||||
this.configService.findByPropertyName('cc.license.jurisdiction').pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
@@ -319,6 +342,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.loadCcLicences();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -338,4 +362,31 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent
|
||||
updateSectionData(data: WorkspaceitemSectionCcLicenseObject) {
|
||||
this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, Object.assign({}, this.data, data));
|
||||
}
|
||||
|
||||
onScroll(event) {
|
||||
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) {
|
||||
if (!this.isLoading && !this._isLastPage) {
|
||||
this.ccLicenceOptions.currentPage++;
|
||||
this.loadCcLicences();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadCcLicences() {
|
||||
this.isLoading = true;
|
||||
|
||||
this.subscriptions.push(
|
||||
this.submissionCcLicensesDataService.findAll(this.ccLicenceOptions).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
tap((response) => this._isLastPage = response.pageInfo.currentPage === response.pageInfo.totalPages),
|
||||
map((list) => list.page),
|
||||
).subscribe(
|
||||
(licenses) => {
|
||||
this.submissionCcLicenses = [...this.submissionCcLicenses, ...licenses];
|
||||
this.isLoading = false;
|
||||
this.ref.detectChanges();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1684,6 +1684,8 @@
|
||||
|
||||
"deny-request-copy.success": "Successfully denied item request",
|
||||
|
||||
"dynamic-list.load-more": "Load more",
|
||||
|
||||
"dropdown.clear": "Clear selection",
|
||||
|
||||
"dropdown.clear.tooltip": "Clear the selected option",
|
||||
@@ -2292,6 +2294,8 @@
|
||||
|
||||
"item.edit.bitstreams.upload-button": "Upload",
|
||||
|
||||
"item.edit.bitstreams.load-more.link": "Load more",
|
||||
|
||||
"item.edit.delete.cancel": "Cancel",
|
||||
|
||||
"item.edit.delete.confirm": "Delete",
|
||||
|
Reference in New Issue
Block a user