Merge remote-tracking branch 'dspaceMain/main' into issue-815-controlled-vocabulary-hierarchical-facets-7.4

This commit is contained in:
Jens Vannerum
2023-01-02 10:15:18 +01:00
43 changed files with 2656 additions and 6803 deletions

View File

@@ -55,6 +55,8 @@ auth:
# Form settings
form:
# Sets the spellcheck textarea attribute value
spellCheck: true
# NOTE: Map server-side validators to comparative Angular form validators
validatorMap:
required: required
@@ -143,6 +145,9 @@ languages:
- code: nl
label: Nederlands
active: true
- code: pl
label: Polski
active: true
- code: pt-PT
label: Português
active: true
@@ -174,6 +179,7 @@ languages:
label: раї́нська
active: true
# Browse-By Pages
browseBy:
# Amount of years to display using jumps of one year (current year - oneYearLimit)

View File

@@ -46,6 +46,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
import { NoContent } from '../../../core/shared/NoContent.model';
import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator';
import { environment } from '../../../../environments/environment';
@Component({
selector: 'ds-group-form',
@@ -194,6 +195,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
label: groupDescription,
name: 'groupDescription',
required: false,
spellCheck: environment.form.spellCheck,
});
this.formModel = [
this.groupName,

View File

@@ -15,6 +15,7 @@ import { Router } from '@angular/router';
import { hasValue, isEmpty } from '../../../../shared/empty.util';
import { TranslateService } from '@ngx-translate/core';
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths';
import { environment } from '../../../../../environments/environment';
/**
* The component responsible for rendering the form to create/edit a bitstream format
@@ -90,6 +91,7 @@ export class FormatFormComponent implements OnInit {
name: 'description',
label: 'admin.registries.bitstream-formats.edit.description.label',
hint: 'admin.registries.bitstream-formats.edit.description.hint',
spellCheck: environment.form.spellCheck,
}),
new DynamicSelectModel({

View File

@@ -1,5 +1,6 @@
import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model';
import { environment } from '../../../environments/environment';
export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<string> = {
id: 'entityType',
@@ -26,21 +27,26 @@ export const collectionFormModels: DynamicFormControlModel[] = [
new DynamicTextAreaModel({
id: 'description',
name: 'dc.description',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'abstract',
name: 'dc.description.abstract',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'rights',
name: 'dc.rights',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'tableofcontents',
name: 'dc.description.tableofcontents',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'license',
name: 'dc.rights.license',
spellCheck: environment.form.spellCheck,
})
];

View File

@@ -72,6 +72,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
id: 'statistics_collection_:id',
active: true,
visible: true,
index: 2,
model: {
type: MenuItemType.LINK,
text: 'menu.section.statistics',

View File

@@ -13,6 +13,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { AuthService } from '../../core/auth/auth.service';
import { RequestService } from '../../core/data/request.service';
import { ObjectCacheService } from '../../core/cache/object-cache.service';
import { environment } from '../../../environments/environment';
/**
* Form used for creating and editing communities
@@ -52,18 +53,22 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
new DynamicTextAreaModel({
id: 'description',
name: 'dc.description',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'abstract',
name: 'dc.description.abstract',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'rights',
name: 'dc.rights',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'tableofcontents',
name: 'dc.description.tableofcontents',
spellCheck: environment.form.spellCheck,
}),
];

View File

@@ -55,6 +55,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
id: 'statistics_community_:id',
active: true,
visible: true,
index: 2,
model: {
type: MenuItemType.LINK,
text: 'menu.section.statistics',

View File

@@ -40,7 +40,7 @@ export class LocaleService {
protected translate: TranslateService,
protected authService: AuthService,
protected routeService: RouteService,
@Inject(DOCUMENT) private document: any
@Inject(DOCUMENT) protected document: any
) {
}

View File

@@ -1,12 +1,31 @@
import { LANG_ORIGIN, LocaleService } from './locale.service';
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { CookieService } from '../services/cookie.service';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '../auth/auth.service';
import { RouteService } from '../services/route.service';
import { DOCUMENT } from '@angular/common';
@Injectable()
export class ServerLocaleService extends LocaleService {
constructor(
@Inject(NativeWindowService) protected _window: NativeWindowRef,
@Inject(REQUEST) protected req: Request,
protected cookie: CookieService,
protected translate: TranslateService,
protected authService: AuthService,
protected routeService: RouteService,
@Inject(DOCUMENT) protected document: any
) {
super(_window, cookie, translate, authService, routeService, document);
}
/**
* Get the languages list of the user in Accept-Language format
*
@@ -50,6 +69,10 @@ export class ServerLocaleService extends LocaleService {
if (isNotEmpty(epersonLang)) {
languages.push(...epersonLang);
}
if (hasValue(this.req.headers['accept-language'])) {
languages.push(...this.req.headers['accept-language'].split(',')
);
}
return languages;
})
);

View File

@@ -67,6 +67,7 @@ import { OrcidPageGuard } from './orcid-page/orcid-page.guard';
id: 'statistics_item_:id',
active: true,
visible: true,
index: 2,
model: {
type: MenuItemType.LINK,
text: 'menu.section.statistics',

View File

@@ -5,7 +5,7 @@
</button>
</div>
<div class="modal-body">
<ds-collection-dropdown (selectionChange)="selectObject($event)">
</ds-collection-dropdown>
<ds-themed-collection-dropdown (selectionChange)="selectObject($event)">
</ds-themed-collection-dropdown>
</div>
</div>

View File

@@ -128,10 +128,13 @@ describe('CollectionSelectorComponent', () => {
beforeEach(() => {
scheduler = getTestScheduler();
fixture = TestBed.createComponent(CollectionSelectorComponent);
fixture = TestBed.overrideComponent(CollectionSelectorComponent, {
set: {
template: '<ds-collection-dropdown (selectionChange)="selectObject($event)"></ds-collection-dropdown>'
}
}).createComponent(CollectionSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {

View File

@@ -0,0 +1,33 @@
import { CollectionDropdownComponent, CollectionListEntry } from './collection-dropdown.component';
import { ThemedComponent } from '../theme-support/themed.component';
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'ds-themed-collection-dropdown',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedCollectionDropdownComponent extends ThemedComponent<CollectionDropdownComponent> {
@Input() entityType: string;
@Output() searchComplete = new EventEmitter<any>();
@Output() theOnlySelectable = new EventEmitter<CollectionListEntry>();
@Output() selectionChange = new EventEmitter<CollectionListEntry>();
protected inAndOutputNames: (keyof CollectionDropdownComponent & keyof this)[] = ['entityType', 'searchComplete', 'theOnlySelectable', 'selectionChange'];
protected getComponentName(): string {
return 'CollectionDropdownComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/shared/collection-dropdown/collection-dropdown.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./collection-dropdown.component`);
}
}

View File

@@ -79,6 +79,8 @@ import { FormService } from '../../form.service';
import { SubmissionService } from '../../../../submission/submission.service';
import { FormBuilderService } from '../form-builder.service';
import { NgxMaskModule } from 'ngx-mask';
import { APP_CONFIG } from '../../../../../config/app-config.interface';
import { environment } from '../../../../../environments/environment';
function getMockDsDynamicTypeBindRelationService(): DsDynamicTypeBindRelationService {
return jasmine.createSpyObj('DsDynamicTypeBindRelationService', {
@@ -230,7 +232,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
findById: () => observableOf(createSuccessfulRemoteDataObject(testWSI))
}
},
{ provide: NgZone, useValue: new NgZone({}) }
{ provide: NgZone, useValue: new NgZone({}) },
{ provide: APP_CONFIG, useValue: environment }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents().then(() => {

View File

@@ -4,7 +4,7 @@ import {
Component,
ComponentFactoryResolver,
ContentChildren,
EventEmitter,
EventEmitter, Inject,
Input,
NgZone,
OnChanges,
@@ -118,6 +118,8 @@ import { RelationshipOptions } from '../models/relationship-options.model';
import { FormBuilderService } from '../form-builder.service';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
import { itemLinksToFollow } from '../../../utils/relation-query.utils';
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
switch (model.type) {
@@ -231,6 +233,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private showErrorMessagesPreviousStage: boolean;
/**
* Determines whether to request embedded thumbnail.
*/
fetchThumbnail: boolean;
get componentType(): Type<DynamicFormControl> | null {
return dsDynamicFormControlMapFn(this.model);
}
@@ -253,9 +260,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private ref: ChangeDetectorRef,
private formService: FormService,
private formBuilderService: FormBuilderService,
private submissionService: SubmissionService
private submissionService: SubmissionService,
@Inject(APP_CONFIG) protected appConfig: AppConfig,
) {
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
}
/**
@@ -285,7 +294,6 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
followLink('rightItem'),
followLink('relationshipType')
);
relationshipsRD$.pipe(
getFirstSucceededRemoteDataPayload(),
getPaginatedListPayload()
@@ -317,8 +325,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
}
if (hasValue(this.value) && this.value.isVirtual) {
const relationship$ = this.relationshipService.findById(this.value.virtualValue, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType'))
.pipe(
const relationship$ = this.relationshipService.findById(this.value.virtualValue,
true,
true,
... itemLinksToFollow(this.fetchThumbnail)).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload());
this.relationshipValue$ = observableCombineLatest([this.item$.pipe(take(1)), relationship$]).pipe(

View File

@@ -1,3 +1 @@
span.text-contents{
padding: var(--bs-btn-padding-y) 0;
}

View File

@@ -1,12 +1,12 @@
<div class="d-flex">
<span class="mr-auto text-contents">
<div class="flex-grow-1 mr-auto">
<ng-container *ngIf="!(relatedItem$ | async)">
<ds-themed-loading [showMessage]="false"></ds-themed-loading>
</ng-container>
<ng-container *ngIf="(relatedItem$ | async)">
<ds-listable-object-component-loader [showLabel]="false" [viewMode]="viewType" [object]="(relatedItem$ | async)"></ds-listable-object-component-loader>
</ng-container>
</span>
</div>
<button type="button" class="btn btn-secondary"
(click)="removeSelection()">
<i class="fas fa-trash" aria-hidden="true"></i>

View File

@@ -69,7 +69,7 @@ describe('ExistingRelationListElementComponent', () => {
providers: [
{ provide: SelectableListService, useValue: selectionService },
{ provide: Store, useValue: store },
{ provide: SubmissionService, useClass: SubmissionServiceStub },
{ provide: SubmissionService, useClass: SubmissionServiceStub }
],
schemas: [NO_ERRORS_SCHEMA]
})

View File

@@ -1,4 +1,6 @@
import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-lookup-relation-external-source-tab.component';
import {
DsDynamicLookupRelationExternalSourceTabComponent
} from './dynamic-lookup-relation-external-source-tab.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { VarDirective } from '../../../../../utils/var.directive';
import { TranslateModule } from '@ngx-translate/core';
@@ -6,7 +8,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { of as observableOf } from 'rxjs';
import { of as observableOf, EMPTY } from 'rxjs';
import {
createFailedRemoteDataObject$,
createPendingRemoteDataObject$,
@@ -22,11 +24,13 @@ import { SelectableListService } from '../../../../../object-list/selectable-lis
import { Item } from '../../../../../../core/shared/item.model';
import { Collection } from '../../../../../../core/shared/collection.model';
import { RelationshipOptions } from '../../../models/relationship-options.model';
import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component';
import { createPaginatedList } from '../../../../../testing/utils.test';
import { PaginationService } from '../../../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
import { ItemType } from '../../../../../../core/shared/item-relationships/item-type.model';
import {
ThemedExternalSourceEntryImportModalComponent
} from './external-source-entry-import-modal/themed-external-source-entry-import-modal.component';
describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
let component: DsDynamicLookupRelationExternalSourceTabComponent;
@@ -187,12 +191,13 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
describe('import', () => {
beforeEach(() => {
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ importedObject: new EventEmitter<any>() }) }));
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ importedObject: new EventEmitter<any>(), compRef$: EMPTY }) }));
component.modalRef = modalService.open(ThemedExternalSourceEntryImportModalComponent, { size: 'lg', container: 'ds-dynamic-lookup-relation-modal' });
component.import(externalEntries[0]);
});
it('should open a new ExternalSourceEntryImportModalComponent', () => {
expect(modalService.open).toHaveBeenCalledWith(ExternalSourceEntryImportModalComponent, jasmine.any(Object));
expect(modalService.open).toHaveBeenCalledWith(ThemedExternalSourceEntryImportModalComponent, jasmine.any(Object));
});
});
});

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ComponentRef } from '@angular/core';
import { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { Router } from '@angular/router';
@@ -16,7 +16,8 @@ import { PaginationComponentOptions } from '../../../../../pagination/pagination
import { RelationshipOptions } from '../../../models/relationship-options.model';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component';
import { hasValue } from '../../../../../empty.util';
import { ThemedExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/themed-external-source-entry-import-modal.component';
import { hasValue, hasValueOperator } from '../../../../../empty.util';
import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
import { Item } from '../../../../../../core/shared/item.model';
import { Collection } from '../../../../../../core/shared/collection.model';
@@ -114,9 +115,9 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
modalRef: NgbModalRef;
/**
* Subscription to the modal's importedObject event-emitter
* Array to track all subscriptions and unsubscribe them onDestroy
*/
importObjectSub: Subscription;
protected subs: Subscription[] = [];
/**
* The entity types compatible with the given external source
@@ -161,30 +162,40 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
* @param entry The entry to import
*/
import(entry) {
this.modalRef = this.modalService.open(ExternalSourceEntryImportModalComponent, {
this.modalRef = this.modalService.open(ThemedExternalSourceEntryImportModalComponent, {
size: 'lg',
container: 'ds-dynamic-lookup-relation-modal'
});
const modalComp = this.modalRef.componentInstance;
const modalComp$ = this.modalRef.componentInstance.compRef$.pipe(
hasValueOperator(),
map((compRef: ComponentRef<ExternalSourceEntryImportModalComponent>) => compRef.instance)
);
this.subs.push(modalComp$.subscribe((modalComp: ExternalSourceEntryImportModalComponent) => {
modalComp.externalSourceEntry = entry;
modalComp.item = this.item;
modalComp.collection = this.collection;
// modalComp.collection = this.collection;
modalComp.relationship = this.relationship;
modalComp.label = this.label;
modalComp.relatedEntityType = this.relatedEntityType;
this.importObjectSub = modalComp.importedObject.subscribe((object) => {
}));
this.subs.push(modalComp$.pipe(
switchMap((modalComp: ExternalSourceEntryImportModalComponent) => modalComp.importedObject)
).subscribe((object) => {
this.selectableListService.selectSingle(this.listId, object);
this.importedObject.emit(object);
});
}));
}
/**
* Unsubscribe from open subscriptions
*/
ngOnDestroy(): void {
if (hasValue(this.importObjectSub)) {
this.importObjectSub.unsubscribe();
}
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
/**

View File

@@ -0,0 +1,22 @@
import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal.component';
import { ThemedComponent } from '../../../../../../theme-support/themed.component';
import { Component } from '@angular/core';
@Component({
selector: 'ds-themed-external-source-entry-import-modal',
styleUrls: [],
templateUrl: '../../../../../../../shared/theme-support/themed.component.html',
})
export class ThemedExternalSourceEntryImportModalComponent extends ThemedComponent<ExternalSourceEntryImportModalComponent> {
protected getComponentName(): string {
return 'ExternalSourceEntryImportModalComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../../../../../../themes/${themeName}/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./external-source-entry-import-modal.component`);
}
}

View File

@@ -1,7 +1,12 @@
import { Inject, InjectionToken } from '@angular/core';
import uniqueId from 'lodash/uniqueId';
import {DynamicFormControlLayout, DynamicFormControlRelation, MATCH_VISIBLE, OR_OPERATOR} from '@ng-dynamic-forms/core';
import {
DynamicFormControlLayout,
DynamicFormControlRelation,
MATCH_VISIBLE,
OR_OPERATOR
} from '@ng-dynamic-forms/core';
import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util';
import { FormFieldModel } from '../models/form-field.model';
@@ -22,6 +27,12 @@ export const SUBMISSION_ID: InjectionToken<string> = new InjectionToken<string>(
export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData');
export const INIT_FORM_VALUES: InjectionToken<any> = new InjectionToken<any>('initFormValues');
export const PARSER_OPTIONS: InjectionToken<ParserOptions> = new InjectionToken<ParserOptions>('parserOptions');
/**
* This pattern checks that a regex field uses the common ECMAScript format: `/{pattern}/{flags}`, in which the flags
* are part of the regex, or a simpler one with only pattern `/{pattern}/` or `{pattern}`.
* The regex itself is encapsulated inside a `RegExp` object, that will validate the pattern syntax.
*/
export const REGEX_FIELD_VALIDATOR = new RegExp('(\\/?)(.+)\\1([gimsuy]*)', 'i');
export abstract class FieldParser {
@@ -315,6 +326,7 @@ export abstract class FieldParser {
* fields in type bind, made up of a 'match' outcome (make this field visible), an 'operator'
* (OR) and a 'when' condition (the bindValues array).
* @param configuredTypeBindValues array of types from the submission definition (CONFIG_DATA)
* @param typeField
* @private
* @return DynamicFormControlRelation[] array with one relation in it, for type bind matching to show a field
*/
@@ -343,8 +355,21 @@ export abstract class FieldParser {
return hasValue(this.configData.input.regex);
}
/**
* Adds pattern validation to `controlModel`, it uses the encapsulated `configData` to test the regex,
* contained in the input config, against the common `ECMAScript` standard validator {@link REGEX_FIELD_VALIDATOR},
* and creates an equivalent `RegExp` object that will be used during form-validation against the user-input.
* @param controlModel
* @protected
*/
protected addPatternValidator(controlModel) {
const regex = new RegExp(this.configData.input.regex);
const validatorMatcher = this.configData.input.regex.match(REGEX_FIELD_VALIDATOR);
let regex;
if (validatorMatcher != null && validatorMatcher.length > 3) {
regex = new RegExp(validatorMatcher[2], validatorMatcher[3]);
} else {
regex = new RegExp(this.configData.input.regex);
}
controlModel.validators = Object.assign({}, controlModel.validators, { pattern: regex });
controlModel.errorMessages = Object.assign(
{},

View File

@@ -4,6 +4,7 @@ import { DynamicQualdropModel } from '../ds-dynamic-form-ui/models/ds-dynamic-qu
import { DynamicOneboxModel } from '../ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { ParserOptions } from './parser-options';
import { FieldParser } from './field-parser';
describe('OneboxFieldParser test suite', () => {
let field1: FormFieldModel;
@@ -101,4 +102,51 @@ describe('OneboxFieldParser test suite', () => {
expect(fieldModel instanceof DynamicOneboxModel).toBe(true);
});
describe('should handle a DynamicOneboxModel with regex', () => {
let regexField: FormFieldModel;
let parser: FieldParser;
let fieldModel: any;
beforeEach(() => {
regexField = {
input: { type: 'onebox', regex: '/[a-z]+/mi' },
label: 'Title',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the events, if any.',
selectableMetadata: [
{
metadata: 'title',
controlledVocabulary: 'EVENTAuthority',
closed: false
}
],
languageCodes: []
} as FormFieldModel;
parser = new OneboxFieldParser(submissionId, regexField, initFormValues, parserOptions);
fieldModel = parser.parse();
});
it('should have initialized pattern validator', () => {
expect(fieldModel instanceof DynamicOneboxModel).toBe(true);
expect(fieldModel.validators).not.toBeNull();
expect(fieldModel.validators.pattern).not.toBeNull();
});
it('should mark valid not case sensitive basic characters regex in multiline', () => {
let pattern = fieldModel.validators.pattern as RegExp;
expect(pattern.test('HELLO')).toBe(true);
expect(pattern.test('hello')).toBe(true);
expect(pattern.test('hello\nhello\nhello')).toBe(true);
expect(pattern.test('HeLlO')).toBe(true);
});
it('should be invalid for non-basic alphabet characters', () => {
let pattern = fieldModel.validators.pattern as RegExp;
expect(pattern.test('12345')).toBe(false);
expect(pattern.test('àèìòùáéíóú')).toBe(false);
});
});
});

View File

@@ -5,6 +5,7 @@ import {
DsDynamicTextAreaModel,
DsDynamicTextAreaModelConfig
} from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
import { environment } from '../../../../../environments/environment';
export class TextareaFieldParser extends FieldParser {
@@ -20,6 +21,7 @@ export class TextareaFieldParser extends FieldParser {
};
textAreaModelConfig.rows = 10;
textAreaModelConfig.spellCheck = environment.form.spellCheck;
this.setValues(textAreaModelConfig, fieldValue);
const textAreaModel = new DsDynamicTextAreaModel(textAreaModelConfig, layout);

View File

@@ -37,6 +37,7 @@ import { FormBuilderService } from './builder/form-builder.service';
import { DsDynamicTypeBindRelationService } from './builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
import { FormService } from './form.service';
import { NgxMaskModule } from 'ngx-mask';
import { ThemedExternalSourceEntryImportModalComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component';
const COMPONENTS = [
CustomSwitchComponent,
@@ -64,6 +65,7 @@ const COMPONENTS = [
ChipsComponent,
NumberPickerComponent,
VocabularyTreeviewComponent,
ThemedExternalSourceEntryImportModalComponent
];
const DIRECTIVES = [

View File

@@ -315,6 +315,7 @@ import { MenuModule } from './menu/menu.module';
import {
ListableNotificationObjectComponent
} from './object-list/listable-notification-object/listable-notification-object.component';
import { ThemedCollectionDropdownComponent } from './collection-dropdown/themed-collection-dropdown.component';
const MODULES = [
CommonModule,
@@ -484,6 +485,7 @@ const ENTRY_COMPONENTS = [
ClaimedTaskActionsReturnToPoolComponent,
ClaimedTaskActionsEditMetadataComponent,
CollectionDropdownComponent,
ThemedCollectionDropdownComponent,
FileDownloadLinkComponent,
BitstreamDownloadPageComponent,
BitstreamRequestACopyPageComponent,

View File

@@ -71,6 +71,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.usedTheme).toEqual('custom');
});
}));
});
describe('when the current theme doesn\'t match a themed component', () => {
@@ -92,6 +98,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
it(`should set usedTheme to the name of the base theme`, waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.usedTheme).toEqual('base');
});
}));
});
describe('and it extends another theme', () => {
@@ -117,6 +129,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
it(`should set usedTheme to the name of the base theme`, waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.usedTheme).toEqual('base');
});
}));
});
describe('that does match it', () => {
@@ -141,6 +159,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.usedTheme).toEqual('custom');
});
}));
});
describe('that extends another theme that doesn\'t match it either', () => {
@@ -167,6 +191,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
it(`should set usedTheme to the name of the base theme`, waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.usedTheme).toEqual('base');
});
}));
});
describe('that extends another theme that does match it', () => {
@@ -193,6 +223,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.usedTheme).toEqual('custom');
});
}));
});
});
});

View File

@@ -8,13 +8,15 @@ import {
OnDestroy,
ComponentFactoryResolver,
ChangeDetectorRef,
OnChanges
OnChanges,
HostBinding
} from '@angular/core';
import { hasValue, isNotEmpty } from '../empty.util';
import { from as fromPromise, Observable, of as observableOf, Subscription } from 'rxjs';
import { from as fromPromise, Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs';
import { ThemeService } from './theme.service';
import { catchError, switchMap, map } from 'rxjs/operators';
import { catchError, switchMap, map, tap } from 'rxjs/operators';
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { BASE_THEME_NAME } from './theme.constants';
@Component({
selector: 'ds-themed',
@@ -25,11 +27,22 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
@ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef;
protected compRef: ComponentRef<T>;
/**
* A reference to the themed component. Will start as undefined and emit every time the themed
* component is rendered
*/
public compRef$: BehaviorSubject<ComponentRef<T>> = new BehaviorSubject(undefined);
protected lazyLoadSub: Subscription;
protected themeSub: Subscription;
protected inAndOutputNames: (keyof T & keyof this)[] = [];
/**
* A data attribute on the ThemedComponent to indicate which theme the rendered component came from.
*/
@HostBinding('attr.data-used-theme') usedTheme: string;
constructor(
protected resolver: ComponentFactoryResolver,
protected cdr: ChangeDetectorRef,
@@ -80,6 +93,7 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
} else {
// otherwise import and return the default component
return fromPromise(this.importUnthemedComponent()).pipe(
tap(() => this.usedTheme = BASE_THEME_NAME),
map((unthemedFile: any) => {
return unthemedFile[this.getComponentName()];
})
@@ -90,6 +104,7 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
const factory = this.resolver.resolveComponentFactory(constructor);
this.compRef = this.vcr.createComponent(factory);
this.connectInputsAndOutputs();
this.compRef$.next(this.compRef);
this.cdr.markForCheck();
});
}
@@ -123,6 +138,7 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
private resolveThemedComponent(themeName?: string, checkedThemeNames: string[] = []): Observable<any> {
if (isNotEmpty(themeName)) {
return fromPromise(this.importThemedComponent(themeName)).pipe(
tap(() => this.usedTheme = themeName),
catchError(() => {
// Try the next ancestor theme instead
const nextTheme = this.themeService.getThemeConfigFor(themeName)?.extends;

View File

@@ -35,9 +35,9 @@
class="dropdown-menu"
id="collectionControlsDropdownMenu"
aria-labelledby="collectionControlsMenuButton">
<ds-collection-dropdown
<ds-themed-collection-dropdown
(selectionChange)="onSelect($event)">
</ds-collection-dropdown>
</ds-themed-collection-dropdown>
</div>
</div>
</div>

View File

@@ -6,11 +6,11 @@
</div>
<div class="modal-body">
<ds-themed-loading *ngIf="isLoading()"></ds-themed-loading>
<ds-collection-dropdown [ngClass]="{'d-none': isLoading()}"
<ds-themed-collection-dropdown [ngClass]="{'d-none': isLoading()}"
(selectionChange)="selectObject($event)"
(searchComplete)="searchComplete()"
(theOnlySelectable)="theOnlySelectable($event)"
[entityType]="entityType">
</ds-collection-dropdown>
</ds-themed-collection-dropdown>
</div>
</div>

View File

@@ -122,7 +122,7 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const dropdownMenu = fixture.debugElement.query(By.css('ds-collection-dropdown')).nativeElement;
const dropdownMenu = fixture.debugElement.query(By.css('ds-themed-collection-dropdown')).nativeElement;
expect(dropdownMenu.classList).toContain('d-none');
});
}));

File diff suppressed because it is too large Load Diff

View File

@@ -94,6 +94,7 @@ export class DefaultAppConfig implements AppConfig {
// Form settings
form: FormConfig = {
spellCheck: true,
// NOTE: Map server-side validators to comparative Angular form validators
validatorMap: {
required: 'required',
@@ -197,6 +198,7 @@ export class DefaultAppConfig implements AppConfig {
{ code: 'lv', label: 'Latviešu', active: true },
{ code: 'hu', label: 'Magyar', active: true },
{ code: 'nl', label: 'Nederlands', active: true },
{ code: 'pl', label: 'Polski', active: true },
{ code: 'pt-PT', label: 'Português', active: true },
{ code: 'pt-BR', label: 'Português do Brasil', active: true },
{ code: 'fi', label: 'Suomi', active: true },

View File

@@ -5,5 +5,6 @@ export interface ValidatorMap {
}
export interface FormConfig extends Config {
spellCheck: boolean;
validatorMap: ValidatorMap;
}

View File

@@ -78,6 +78,7 @@ export const environment: BuildConfig = {
// Form settings
form: {
spellCheck: true,
// NOTE: Map server-side validators to comparative Angular form validators
validatorMap: {
required: 'required',

View File

@@ -0,0 +1,15 @@
import {
CollectionDropdownComponent as BaseComponent
} from '../../../../../app/shared/collection-dropdown/collection-dropdown.component';
import { Component } from '@angular/core';
@Component({
selector: 'ds-collection-dropdown',
templateUrl: '../../../../../app/shared/collection-dropdown/collection-dropdown.component.html',
// templateUrl: './collection-dropdown.component.html',
styleUrls: ['../../../../../app/shared/collection-dropdown/collection-dropdown.component.scss']
// styleUrls: ['./collection-dropdown.component.scss']
})
export class CollectionDropdownComponent extends BaseComponent {
}

View File

@@ -0,0 +1,15 @@
import {
ExternalSourceEntryImportModalComponent as BaseComponent
} from '../../../../../../../../../../app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component';
import { Component } from '@angular/core';
@Component({
selector: 'ds-external-source-entry-import-modal',
styleUrls: ['../../../../../../../../../../app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.scss'],
// styleUrls: ['./external-source-entry-import-modal.component.scss'],
templateUrl: '../../../../../../../../../../app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html',
// templateUrl: './external-source-entry-import-modal.component.html'
})
export class ExternalSourceEntryImportModalComponent extends BaseComponent {
}

View File

@@ -43,6 +43,7 @@ import {
import { CommunityListElementComponent } from './app/shared/object-list/community-list-element/community-list-element.component';
import { CollectionListElementComponent} from './app/shared/object-list/collection-list-element/collection-list-element.component';
import { CollectionDropdownComponent } from './app/shared/collection-dropdown/collection-dropdown.component';
/**
@@ -58,6 +59,7 @@ const ENTRY_COMPONENTS = [
CommunityListElementComponent,
CollectionListElementComponent,
CollectionDropdownComponent,
];
const DECLARATIONS = [

View File

@@ -114,6 +114,9 @@ import { ObjectListComponent } from './app/shared/object-list/object-list.compon
import { BrowseByMetadataPageComponent } from './app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseByDatePageComponent } from './app/browse-by/browse-by-date-page/browse-by-date-page.component';
import { BrowseByTitlePageComponent } from './app/browse-by/browse-by-title-page/browse-by-title-page.component';
import {
ExternalSourceEntryImportModalComponent
} from './app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component';
const DECLARATIONS = [
FileSectionComponent,
@@ -168,6 +171,7 @@ const DECLARATIONS = [
BrowseByMetadataPageComponent,
BrowseByDatePageComponent,
BrowseByTitlePageComponent,
ExternalSourceEntryImportModalComponent,
];