Files
dspace-angular/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts
Giuseppe Digilio 4d4e920f0d Merge remote-tracking branch 'origin/main' into upgrade_angular10
# Conflicts:
#	src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.spec.ts
#	src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts
#	src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts
#	src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.spec.ts
#	src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.spec.ts
#	src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts
#	src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts
#	src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts
#	src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts
#	src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts
#	src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts
#	src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts
#	src/app/+bitstream-page/bitstream-page.resolver.ts
#	src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts
#	src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
#	src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts
#	src/app/+collection-page/collection-page.component.ts
#	src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts
#	src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts
#	src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts
#	src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts
#	src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts
#	src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts
#	src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts
#	src/app/+item-page/edit-item-page/item-private/item-private.component.ts
#	src/app/+item-page/edit-item-page/item-public/item-public.component.ts
#	src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts
#	src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts
#	src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts
#	src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts
#	src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts
#	src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts
#	src/app/+search-page/search-tracker.component.ts
#	src/app/app-routing.module.ts
#	src/app/community-list-page/community-list-service.ts
#	src/app/community-list-page/community-list.reducer.spec.ts
#	src/app/core/auth/auth-response-parsing.service.spec.ts
#	src/app/core/auth/auth.interceptor.spec.ts
#	src/app/core/browse/browse.service.spec.ts
#	src/app/core/browse/browse.service.ts
#	src/app/core/cache/builders/link.service.spec.ts
#	src/app/core/cache/builders/remote-data-build.service.ts
#	src/app/core/cache/object-cache.service.spec.ts
#	src/app/core/cache/response.models.ts
#	src/app/core/data/base-response-parsing.service.ts
#	src/app/core/data/bitstream-data.service.ts
#	src/app/core/data/bitstream-format-data.service.ts
#	src/app/core/data/bundle-data.service.spec.ts
#	src/app/core/data/bundle-data.service.ts
#	src/app/core/data/change-analyzer.ts
#	src/app/core/data/collection-data.service.spec.ts
#	src/app/core/data/collection-data.service.ts
#	src/app/core/data/comcol-data.service.spec.ts
#	src/app/core/data/comcol-data.service.ts
#	src/app/core/data/data.service.ts
#	src/app/core/data/entity-type-data.service.ts
#	src/app/core/data/entity-type.service.ts
#	src/app/core/data/feature-authorization/authorization-data.service.ts
#	src/app/core/data/feature-authorization/feature-data.service.ts
#	src/app/core/data/item-template-data.service.ts
#	src/app/core/data/license-data.service.ts
#	src/app/core/data/metadata-field-data.service.ts
#	src/app/core/data/processes/process-data.service.ts
#	src/app/core/data/processes/script-data.service.ts
#	src/app/core/data/relationship.service.spec.ts
#	src/app/core/data/relationship.service.ts
#	src/app/core/data/remote-data.ts
#	src/app/core/data/request.actions.ts
#	src/app/core/data/request.effects.ts
#	src/app/core/data/request.reducer.spec.ts
#	src/app/core/data/request.reducer.ts
#	src/app/core/data/request.service.spec.ts
#	src/app/core/data/request.service.ts
#	src/app/core/dspace-rest/dspace-rest.service.spec.ts
#	src/app/core/eperson/eperson-data.service.spec.ts
#	src/app/core/eperson/eperson-data.service.ts
#	src/app/core/eperson/group-data.service.spec.ts
#	src/app/core/eperson/group-data.service.ts
#	src/app/core/forward-client-ip/forward-client-ip.interceptor.spec.ts
#	src/app/core/index/index.reducer.ts
#	src/app/core/json-patch/json-patch-operations.service.spec.ts
#	src/app/core/json-patch/json-patch-operations.service.ts
#	src/app/core/locale/locale.interceptor.spec.ts
#	src/app/core/registry/registry.service.ts
#	src/app/core/resource-policy/resource-policy.service.ts
#	src/app/core/shared/operators.ts
#	src/app/core/shared/search/search.service.ts
#	src/app/core/submission/submission-object-data.service.ts
#	src/app/core/submission/submission-rest.service.ts
#	src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts
#	src/app/core/submission/vocabularies/vocabulary.service.spec.ts
#	src/app/core/submission/vocabularies/vocabulary.service.ts
#	src/app/core/submission/workflowitem-data.service.ts
#	src/app/core/tasks/tasks.service.ts
#	src/app/curation-form/curation-form.component.ts
#	src/app/process-page/process-page.resolver.ts
#	src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts
#	src/app/shared/collection-dropdown/collection-dropdown.component.ts
#	src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts
#	src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts
#	src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts
#	src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts
#	src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts
#	src/app/shared/dso-selector/dso-selector/dso-selector.component.ts
#	src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts
#	src/app/shared/form/builder/form-builder.service.ts
#	src/app/shared/item/item-versions/item-versions.component.ts
#	src/app/shared/mocks/remote-data-build.service.mock.ts
#	src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts
#	src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts
#	src/app/shared/object-select/collection-select/collection-select.component.spec.ts
#	src/app/shared/object-select/item-select/item-select.component.spec.ts
#	src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts
#	src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts
#	src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts
#	src/app/shared/search/search-query-response.model.ts
#	src/app/statistics/statistics.module.ts
#	src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.spec.ts
#	src/app/submission/sections/form/section-form.component.ts
#	src/app/submission/sections/upload/section-upload.component.spec.ts
#	src/app/submission/sections/upload/section-upload.component.ts
#	src/app/submission/submission.service.spec.ts
#	src/app/submission/submission.service.ts
2021-01-07 19:17:19 +01:00

478 lines
15 KiB
TypeScript

import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
import {
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicFormLayout,
DynamicFormService,
DynamicInputModel,
DynamicOptionControlModel,
DynamicRadioGroupModel,
DynamicSelectModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { FormGroup } from '@angular/forms';
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model';
import { Observable, Subscription } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { Collection } from '../../../core/shared/collection.model';
import { first, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
import { cloneDeep } from 'lodash';
import { CollectionDataService } from '../../../core/data/collection-data.service';
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { MetadataConfig } from '../../../core/shared/metadata-config.model';
import { INotification } from '../../../shared/notifications/models/notification.model';
import { RequestService } from '../../../core/data/request.service';
import { environment } from '../../../../environments/environment';
/**
* Component for managing the content source of the collection
*/
@Component({
selector: 'ds-collection-source',
templateUrl: './collection-source.component.html',
})
export class CollectionSourceComponent extends AbstractTrackableComponent implements OnInit, OnDestroy {
/**
* The current collection's remote data
*/
collectionRD$: Observable<RemoteData<Collection>>;
/**
* The collection's content source
*/
contentSource: ContentSource;
/**
* The current update to the content source
*/
update$: Observable<FieldUpdate>;
/**
* The initial harvest type we started off with
* Used to compare changes
*/
initialHarvestType: ContentSourceHarvestType;
/**
* @type {string} Key prefix used to generate form labels
*/
LABEL_KEY_PREFIX = 'collection.edit.tabs.source.form.';
/**
* @type {string} Key prefix used to generate form error messages
*/
ERROR_KEY_PREFIX = 'collection.edit.tabs.source.form.errors.';
/**
* @type {string} Key prefix used to generate form option labels
*/
OPTIONS_KEY_PREFIX = 'collection.edit.tabs.source.form.options.';
/**
* The Dynamic Input Model for the OAI Provider
*/
oaiSourceModel = new DynamicInputModel({
id: 'oaiSource',
name: 'oaiSource',
required: true,
validators: {
required: null
},
errorMessages: {
required: 'You must provide a set id of the target collection.'
}
});
/**
* The Dynamic Input Model for the OAI Set
*/
oaiSetIdModel = new DynamicInputModel({
id: 'oaiSetId',
name: 'oaiSetId'
});
/**
* The Dynamic Input Model for the Metadata Format used
*/
metadataConfigIdModel = new DynamicSelectModel({
id: 'metadataConfigId',
name: 'metadataConfigId'
});
/**
* The Dynamic Input Model for the type of harvesting
*/
harvestTypeModel = new DynamicRadioGroupModel<string>({
id: 'harvestType',
name: 'harvestType',
options: [
{
value: ContentSourceHarvestType.Metadata
},
{
value: ContentSourceHarvestType.MetadataAndRef
},
{
value: ContentSourceHarvestType.MetadataAndBitstreams
}
]
});
/**
* All input models in a simple array for easier iterations
*/
inputModels = [this.oaiSourceModel, this.oaiSetIdModel, this.metadataConfigIdModel, this.harvestTypeModel];
/**
* The dynamic form fields used for editing the content source of a collection
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
*/
formModel: DynamicFormControlModel[] = [
new DynamicFormGroupModel({
id: 'oaiSourceContainer',
group: [
this.oaiSourceModel
]
}),
new DynamicFormGroupModel({
id: 'oaiSetContainer',
group: [
this.oaiSetIdModel,
this.metadataConfigIdModel
]
}),
new DynamicFormGroupModel({
id: 'harvestTypeContainer',
group: [
this.harvestTypeModel
]
})
];
/**
* Layout used for structuring the form inputs
*/
formLayout: DynamicFormLayout = {
oaiSource: {
grid: {
host: 'col-12 d-inline-block'
}
},
oaiSetId: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
},
metadataConfigId: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
},
harvestType: {
grid: {
host: 'col-12',
option: 'btn-outline-secondary'
}
},
oaiSetContainer: {
grid: {
host: 'row'
}
},
oaiSourceContainer: {
grid: {
host: 'row'
}
},
harvestTypeContainer: {
grid: {
host: 'row'
}
}
};
/**
* The form group of this form
*/
formGroup: FormGroup;
/**
* Subscription to update the current form
*/
updateSub: Subscription;
/**
* The content harvesting type used when harvesting is disabled
*/
harvestTypeNone = ContentSourceHarvestType.None;
/**
* The previously selected harvesting type
* Used for switching between ContentSourceHarvestType.None and the previously selected value when enabling / disabling harvesting
* Defaults to ContentSourceHarvestType.Metadata
*/
previouslySelectedHarvestType = ContentSourceHarvestType.Metadata;
/**
* Notifications displayed after clicking submit
* These are cleaned up every time a user submits the form to prevent error or other notifications from staying active
* while they shouldn't be.
*/
displayedNotifications: INotification[] = [];
public constructor(public objectUpdatesService: ObjectUpdatesService,
public notificationsService: NotificationsService,
protected location: Location,
protected formService: DynamicFormService,
protected translate: TranslateService,
protected route: ActivatedRoute,
protected router: Router,
protected collectionService: CollectionDataService,
protected requestService: RequestService) {
super(objectUpdatesService, notificationsService, translate);
}
/**
* Initialize properties to setup the Field Update and Form
*/
ngOnInit(): void {
this.notificationsPrefix = 'collection.edit.tabs.source.notifications.';
this.discardTimeOut = environment.collection.edit.undoTimeout;
this.url = this.router.url;
if (this.url.indexOf('?') > 0) {
this.url = this.url.substr(0, this.url.indexOf('?'));
}
this.formGroup = this.formService.createFormGroup(this.formModel);
this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso));
this.collectionRD$.pipe(
getFirstSucceededRemoteData(),
map((col) => col.payload.uuid),
switchMap((uuid) => this.collectionService.getContentSource(uuid)),
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<ContentSource>) => {
this.initializeOriginalContentSource(rd.payload);
});
this.updateFieldTranslations();
this.translate.onLangChange
.subscribe(() => {
this.updateFieldTranslations();
});
}
/**
* Initialize the Field Update and subscribe on it to fire updates to the form whenever it changes
*/
initializeOriginalContentSource(contentSource: ContentSource) {
this.contentSource = contentSource;
this.initialHarvestType = contentSource.harvestType;
this.initializeMetadataConfigs();
const initialContentSource = cloneDeep(this.contentSource);
this.objectUpdatesService.initialize(this.url, [initialContentSource], new Date());
this.update$ = this.objectUpdatesService.getFieldUpdates(this.url, [initialContentSource]).pipe(
map((updates: FieldUpdates) => updates[initialContentSource.uuid])
);
this.updateSub = this.update$.subscribe((update: FieldUpdate) => {
if (update) {
const field = update.field as ContentSource;
let configId;
if (hasValue(this.contentSource) && isNotEmpty(this.contentSource.metadataConfigs)) {
configId = this.contentSource.metadataConfigs[0].id;
}
if (hasValue(field) && hasValue(field.metadataConfigId)) {
configId = field.metadataConfigId;
}
if (hasValue(field)) {
this.formGroup.patchValue({
oaiSourceContainer: {
oaiSource: field.oaiSource
},
oaiSetContainer: {
oaiSetId: field.oaiSetId,
metadataConfigId: configId
},
harvestTypeContainer: {
harvestType: field.harvestType
}
});
this.contentSource = cloneDeep(field);
}
this.contentSource.metadataConfigId = configId;
}
});
}
/**
* Fill the metadataConfigIdModel's options using the contentSource's metadataConfigs property
*/
initializeMetadataConfigs() {
this.metadataConfigIdModel.options = this.contentSource.metadataConfigs
.map((metadataConfig: MetadataConfig) => Object.assign({ value: metadataConfig.id, label: metadataConfig.label }));
if (this.metadataConfigIdModel.options.length > 0) {
this.formGroup.patchValue({
oaiSetContainer: {
metadataConfigId: this.metadataConfigIdModel.options[0].value
}
});
}
}
/**
* Used the update translations of errors and labels on init and on language change
*/
private updateFieldTranslations() {
this.inputModels.forEach(
(fieldModel: DynamicFormControlModel) => {
this.updateFieldTranslation(fieldModel);
}
);
}
/**
* Update the translations of a DynamicInputModel
* @param fieldModel
*/
private updateFieldTranslation(fieldModel: DynamicFormControlModel) {
fieldModel.label = this.translate.instant(this.LABEL_KEY_PREFIX + fieldModel.id);
if (isNotEmpty(fieldModel.validators)) {
fieldModel.errorMessages = {};
Object.keys(fieldModel.validators).forEach((key) => {
fieldModel.errorMessages[key] = this.translate.instant(this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key);
});
}
if (fieldModel instanceof DynamicOptionControlModel) {
if (isNotEmpty(fieldModel.options)) {
fieldModel.options.forEach((option) => {
if (hasNoValue(option.label)) {
option.label = this.translate.instant(this.OPTIONS_KEY_PREFIX + fieldModel.id + '.' + option.value);
}
});
}
}
}
/**
* Fired whenever the form receives an update and makes sure the Content Source and field update is up-to-date with the changes
* @param event
*/
onChange(event) {
this.updateContentSourceField(event.model, true);
this.saveFieldUpdate();
}
/**
* Submit the edited Content Source to the REST API, re-initialize the field update and display a notification
*/
onSubmit() {
// Remove cached harvester request to allow for latest harvester to be displayed when switching tabs
this.collectionRD$.pipe(
getFirstSucceededRemoteData(),
map((col) => col.payload.uuid),
switchMap((uuid) => this.collectionService.getHarvesterEndpoint(uuid)),
take(1)
).subscribe((endpoint) => this.requestService.removeByHrefSubstring(endpoint));
// Update harvester
this.collectionRD$.pipe(
getFirstSucceededRemoteData(),
map((col) => col.payload.uuid),
switchMap((uuid) => this.collectionService.updateContentSource(uuid, this.contentSource)),
take(1)
).subscribe((result: ContentSource | INotification) => {
if (hasValue((result as any).harvestType)) {
this.clearNotifications();
this.initializeOriginalContentSource(result as ContentSource);
this.displayedNotifications.push(this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')));
} else {
this.displayedNotifications.push(result as INotification);
}
});
}
/**
* Cancel the edit and return to the previous page
*/
onCancel() {
this.location.back();
}
/**
* Is the current form valid to be submitted ?
*/
isValid(): boolean {
return (this.contentSource.harvestType === ContentSourceHarvestType.None) || this.formGroup.valid;
}
/**
* Switch the external source on or off and fire a field update
*/
changeExternalSource() {
if (this.contentSource.harvestType === ContentSourceHarvestType.None) {
this.contentSource.harvestType = this.previouslySelectedHarvestType;
} else {
this.previouslySelectedHarvestType = this.contentSource.harvestType;
this.contentSource.harvestType = ContentSourceHarvestType.None;
}
this.updateContentSource(false);
}
/**
* Loop over all inputs and update the Content Source with their value
* @param updateHarvestType When set to false, the harvestType of the contentSource will be ignored in the update
*/
updateContentSource(updateHarvestType: boolean) {
this.inputModels.forEach(
(fieldModel: DynamicInputModel) => {
this.updateContentSourceField(fieldModel, updateHarvestType);
}
);
this.saveFieldUpdate();
}
/**
* Update the Content Source with the value from a DynamicInputModel
* @param fieldModel The fieldModel to fetch the value from and update the contentSource with
* @param updateHarvestType When set to false, the harvestType of the contentSource will be ignored in the update
*/
updateContentSourceField(fieldModel: DynamicInputModel, updateHarvestType: boolean) {
if (hasValue(fieldModel.value) && !(fieldModel.id === this.harvestTypeModel.id && !updateHarvestType)) {
this.contentSource[fieldModel.id] = fieldModel.value;
}
}
/**
* Save the current Content Source to the Object Updates cache
*/
saveFieldUpdate() {
this.objectUpdatesService.saveAddFieldUpdate(this.url, cloneDeep(this.contentSource));
}
/**
* Clear possible active notifications
*/
clearNotifications() {
this.displayedNotifications.forEach((notification: INotification) => {
this.notificationsService.remove(notification);
});
this.displayedNotifications = [];
}
/**
* Make sure open subscriptions are closed
*/
ngOnDestroy(): void {
if (this.updateSub) {
this.updateSub.unsubscribe();
}
}
}