Merge branch 'DSpace:main' into DA-8586

This commit is contained in:
yingjin
2023-01-05 16:41:47 -06:00
committed by GitHub
50 changed files with 2706 additions and 6816 deletions

View File

@@ -55,6 +55,8 @@ auth:
# Form settings # Form settings
form: form:
# Sets the spellcheck textarea attribute value
spellCheck: true
# NOTE: Map server-side validators to comparative Angular form validators # NOTE: Map server-side validators to comparative Angular form validators
validatorMap: validatorMap:
required: required required: required
@@ -143,6 +145,9 @@ languages:
- code: nl - code: nl
label: Nederlands label: Nederlands
active: true active: true
- code: pl
label: Polski
active: true
- code: pt-PT - code: pt-PT
label: Português label: Português
active: true active: true
@@ -174,6 +179,7 @@ languages:
label: раї́нська label: раї́нська
active: true active: true
# Browse-By Pages # Browse-By Pages
browseBy: browseBy:
# Amount of years to display using jumps of one year (current year - oneYearLimit) # 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 { NoContent } from '../../../core/shared/NoContent.model';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator'; import { ValidateGroupExists } from './validators/group-exists.validator';
import { environment } from '../../../../environments/environment';
@Component({ @Component({
selector: 'ds-group-form', selector: 'ds-group-form',
@@ -194,6 +195,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
label: groupDescription, label: groupDescription,
name: 'groupDescription', name: 'groupDescription',
required: false, required: false,
spellCheck: environment.form.spellCheck,
}); });
this.formModel = [ this.formModel = [
this.groupName, this.groupName,

View File

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

View File

@@ -1,5 +1,6 @@
import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model'; import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model';
import { environment } from '../../../environments/environment';
export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<string> = { export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<string> = {
id: 'entityType', id: 'entityType',
@@ -26,21 +27,26 @@ export const collectionFormModels: DynamicFormControlModel[] = [
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'description', id: 'description',
name: 'dc.description', name: 'dc.description',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'abstract', id: 'abstract',
name: 'dc.description.abstract', name: 'dc.description.abstract',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'rights', id: 'rights',
name: 'dc.rights', name: 'dc.rights',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'tableofcontents', id: 'tableofcontents',
name: 'dc.description.tableofcontents', name: 'dc.description.tableofcontents',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'license', id: 'license',
name: 'dc.rights.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', id: 'statistics_collection_:id',
active: true, active: true,
visible: true, visible: true,
index: 2,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics', 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 { AuthService } from '../../core/auth/auth.service';
import { RequestService } from '../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
import { ObjectCacheService } from '../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../core/cache/object-cache.service';
import { environment } from '../../../environments/environment';
/** /**
* Form used for creating and editing communities * Form used for creating and editing communities
@@ -52,18 +53,22 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'description', id: 'description',
name: 'dc.description', name: 'dc.description',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'abstract', id: 'abstract',
name: 'dc.description.abstract', name: 'dc.description.abstract',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'rights', id: 'rights',
name: 'dc.rights', name: 'dc.rights',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'tableofcontents', id: 'tableofcontents',
name: 'dc.description.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', id: 'statistics_community_:id',
active: true, active: true,
visible: true, visible: true,
index: 2,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics', text: 'menu.section.statistics',

View File

@@ -40,7 +40,7 @@ export class LocaleService {
protected translate: TranslateService, protected translate: TranslateService,
protected authService: AuthService, protected authService: AuthService,
protected routeService: RouteService, 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 { 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 { combineLatest, Observable, of as observableOf } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators'; 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() @Injectable()
export class ServerLocaleService extends LocaleService { 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 * Get the languages list of the user in Accept-Language format
* *
@@ -50,6 +69,10 @@ export class ServerLocaleService extends LocaleService {
if (isNotEmpty(epersonLang)) { if (isNotEmpty(epersonLang)) {
languages.push(...epersonLang); languages.push(...epersonLang);
} }
if (hasValue(this.req.headers['accept-language'])) {
languages.push(...this.req.headers['accept-language'].split(',')
);
}
return languages; return languages;
}) })
); );

View File

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

View File

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

View File

@@ -128,10 +128,13 @@ describe('CollectionSelectorComponent', () => {
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler(); 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; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {

View File

@@ -1,5 +1,5 @@
nav.navbar { nav.navbar {
border-bottom: 1px var(--bs-gray-400) solid; border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid;
align-items: baseline; align-items: baseline;
} }

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

View File

@@ -4,7 +4,7 @@ import {
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
ContentChildren, ContentChildren,
EventEmitter, EventEmitter, Inject,
Input, Input,
NgZone, NgZone,
OnChanges, OnChanges,
@@ -118,6 +118,8 @@ import { RelationshipOptions } from '../models/relationship-options.model';
import { FormBuilderService } from '../form-builder.service'; import { FormBuilderService } from '../form-builder.service';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; 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 { export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
switch (model.type) { switch (model.type) {
@@ -231,6 +233,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private showErrorMessagesPreviousStage: boolean; private showErrorMessagesPreviousStage: boolean;
/**
* Determines whether to request embedded thumbnail.
*/
fetchThumbnail: boolean;
get componentType(): Type<DynamicFormControl> | null { get componentType(): Type<DynamicFormControl> | null {
return dsDynamicFormControlMapFn(this.model); return dsDynamicFormControlMapFn(this.model);
} }
@@ -253,9 +260,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private ref: ChangeDetectorRef, private ref: ChangeDetectorRef,
private formService: FormService, private formService: FormService,
private formBuilderService: FormBuilderService, private formBuilderService: FormBuilderService,
private submissionService: SubmissionService private submissionService: SubmissionService,
@Inject(APP_CONFIG) protected appConfig: AppConfig,
) { ) {
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService); 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('rightItem'),
followLink('relationshipType') followLink('relationshipType')
); );
relationshipsRD$.pipe( relationshipsRD$.pipe(
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
getPaginatedListPayload() getPaginatedListPayload()
@@ -317,8 +325,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
} }
if (hasValue(this.value) && this.value.isVirtual) { if (hasValue(this.value) && this.value.isVirtual) {
const relationship$ = this.relationshipService.findById(this.value.virtualValue, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')) const relationship$ = this.relationshipService.findById(this.value.virtualValue,
.pipe( true,
true,
... itemLinksToFollow(this.fetchThumbnail)).pipe(
getAllSucceededRemoteData(), getAllSucceededRemoteData(),
getRemoteDataPayload()); getRemoteDataPayload());
this.relationshipValue$ = observableCombineLatest([this.item$.pipe(take(1)), relationship$]).pipe( 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"> <div class="d-flex">
<span class="mr-auto text-contents"> <div class="flex-grow-1 mr-auto">
<ng-container *ngIf="!(relatedItem$ | async)"> <ng-container *ngIf="!(relatedItem$ | async)">
<ds-themed-loading [showMessage]="false"></ds-themed-loading> <ds-themed-loading [showMessage]="false"></ds-themed-loading>
</ng-container> </ng-container>
<ng-container *ngIf="(relatedItem$ | async)"> <ng-container *ngIf="(relatedItem$ | async)">
<ds-listable-object-component-loader [showLabel]="false" [viewMode]="viewType" [object]="(relatedItem$ | async)"></ds-listable-object-component-loader> <ds-listable-object-component-loader [showLabel]="false" [viewMode]="viewType" [object]="(relatedItem$ | async)"></ds-listable-object-component-loader>
</ng-container> </ng-container>
</span> </div>
<button type="button" class="btn btn-secondary" <button type="button" class="btn btn-secondary"
(click)="removeSelection()"> (click)="removeSelection()">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>

View File

@@ -69,7 +69,7 @@ describe('ExistingRelationListElementComponent', () => {
providers: [ providers: [
{ provide: SelectableListService, useValue: selectionService }, { provide: SelectableListService, useValue: selectionService },
{ provide: Store, useValue: store }, { provide: Store, useValue: store },
{ provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub }
], ],
schemas: [NO_ERRORS_SCHEMA] 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 { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { VarDirective } from '../../../../../utils/var.directive'; import { VarDirective } from '../../../../../utils/var.directive';
import { TranslateModule } from '@ngx-translate/core'; 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 { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { of as observableOf } from 'rxjs'; import { of as observableOf, EMPTY } from 'rxjs';
import { import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
createPendingRemoteDataObject$, createPendingRemoteDataObject$,
@@ -22,11 +24,13 @@ import { SelectableListService } from '../../../../../object-list/selectable-lis
import { Item } from '../../../../../../core/shared/item.model'; import { Item } from '../../../../../../core/shared/item.model';
import { Collection } from '../../../../../../core/shared/collection.model'; import { Collection } from '../../../../../../core/shared/collection.model';
import { RelationshipOptions } from '../../../models/relationship-options.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 { createPaginatedList } from '../../../../../testing/utils.test';
import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
import { ItemType } from '../../../../../../core/shared/item-relationships/item-type.model'; 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', () => { describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
let component: DsDynamicLookupRelationExternalSourceTabComponent; let component: DsDynamicLookupRelationExternalSourceTabComponent;
@@ -187,12 +191,13 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
describe('import', () => { describe('import', () => {
beforeEach(() => { 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]); component.import(externalEntries[0]);
}); });
it('should open a new ExternalSourceEntryImportModalComponent', () => { 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 { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@@ -16,7 +16,8 @@ import { PaginationComponentOptions } from '../../../../../pagination/pagination
import { RelationshipOptions } from '../../../models/relationship-options.model'; import { RelationshipOptions } from '../../../models/relationship-options.model';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component'; 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 { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
import { Item } from '../../../../../../core/shared/item.model'; import { Item } from '../../../../../../core/shared/item.model';
import { Collection } from '../../../../../../core/shared/collection.model'; import { Collection } from '../../../../../../core/shared/collection.model';
@@ -114,9 +115,9 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
modalRef: NgbModalRef; 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 * The entity types compatible with the given external source
@@ -161,30 +162,40 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
* @param entry The entry to import * @param entry The entry to import
*/ */
import(entry) { import(entry) {
this.modalRef = this.modalService.open(ExternalSourceEntryImportModalComponent, { this.modalRef = this.modalService.open(ThemedExternalSourceEntryImportModalComponent, {
size: 'lg', size: 'lg',
container: 'ds-dynamic-lookup-relation-modal' container: 'ds-dynamic-lookup-relation-modal'
}); });
const modalComp = this.modalRef.componentInstance;
modalComp.externalSourceEntry = entry; const modalComp$ = this.modalRef.componentInstance.compRef$.pipe(
modalComp.item = this.item; hasValueOperator(),
modalComp.collection = this.collection; map((compRef: ComponentRef<ExternalSourceEntryImportModalComponent>) => compRef.instance)
modalComp.relationship = this.relationship; );
modalComp.label = this.label;
modalComp.relatedEntityType = this.relatedEntityType; this.subs.push(modalComp$.subscribe((modalComp: ExternalSourceEntryImportModalComponent) => {
this.importObjectSub = modalComp.importedObject.subscribe((object) => { modalComp.externalSourceEntry = entry;
modalComp.item = this.item;
// modalComp.collection = this.collection;
modalComp.relationship = this.relationship;
modalComp.label = this.label;
modalComp.relatedEntityType = this.relatedEntityType;
}));
this.subs.push(modalComp$.pipe(
switchMap((modalComp: ExternalSourceEntryImportModalComponent) => modalComp.importedObject)
).subscribe((object) => {
this.selectableListService.selectSingle(this.listId, object); this.selectableListService.selectSingle(this.listId, object);
this.importedObject.emit(object); this.importedObject.emit(object);
}); }));
} }
/** /**
* Unsubscribe from open subscriptions * Unsubscribe from open subscriptions
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
if (hasValue(this.importObjectSub)) { this.subs
this.importObjectSub.unsubscribe(); .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 { Inject, InjectionToken } from '@angular/core';
import uniqueId from 'lodash/uniqueId'; 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 { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util';
import { FormFieldModel } from '../models/form-field.model'; 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 CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData');
export const INIT_FORM_VALUES: InjectionToken<any> = new InjectionToken<any>('initFormValues'); export const INIT_FORM_VALUES: InjectionToken<any> = new InjectionToken<any>('initFormValues');
export const PARSER_OPTIONS: InjectionToken<ParserOptions> = new InjectionToken<ParserOptions>('parserOptions'); 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 { export abstract class FieldParser {
@@ -43,7 +54,7 @@ export abstract class FieldParser {
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any; public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any;
public parse() { public parse() {
if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable)) if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable))
&& (this.configData.input.type !== ParserType.List) && (this.configData.input.type !== ParserType.List)
&& (this.configData.input.type !== ParserType.Tag) && (this.configData.input.type !== ParserType.Tag)
) { ) {
@@ -315,6 +326,7 @@ export abstract class FieldParser {
* fields in type bind, made up of a 'match' outcome (make this field visible), an 'operator' * fields in type bind, made up of a 'match' outcome (make this field visible), an 'operator'
* (OR) and a 'when' condition (the bindValues array). * (OR) and a 'when' condition (the bindValues array).
* @param configuredTypeBindValues array of types from the submission definition (CONFIG_DATA) * @param configuredTypeBindValues array of types from the submission definition (CONFIG_DATA)
* @param typeField
* @private * @private
* @return DynamicFormControlRelation[] array with one relation in it, for type bind matching to show a field * @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); 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) { 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.validators = Object.assign({}, controlModel.validators, { pattern: regex });
controlModel.errorMessages = Object.assign( 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 { DynamicOneboxModel } from '../ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { ParserOptions } from './parser-options'; import { ParserOptions } from './parser-options';
import { FieldParser } from './field-parser';
describe('OneboxFieldParser test suite', () => { describe('OneboxFieldParser test suite', () => {
let field1: FormFieldModel; let field1: FormFieldModel;
@@ -101,4 +102,51 @@ describe('OneboxFieldParser test suite', () => {
expect(fieldModel instanceof DynamicOneboxModel).toBe(true); 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, DsDynamicTextAreaModel,
DsDynamicTextAreaModelConfig DsDynamicTextAreaModelConfig
} from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; } from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
import { environment } from '../../../../../environments/environment';
export class TextareaFieldParser extends FieldParser { export class TextareaFieldParser extends FieldParser {
@@ -20,6 +21,7 @@ export class TextareaFieldParser extends FieldParser {
}; };
textAreaModelConfig.rows = 10; textAreaModelConfig.rows = 10;
textAreaModelConfig.spellCheck = environment.form.spellCheck;
this.setValues(textAreaModelConfig, fieldValue); this.setValues(textAreaModelConfig, fieldValue);
const textAreaModel = new DsDynamicTextAreaModel(textAreaModelConfig, layout); 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 { DsDynamicTypeBindRelationService } from './builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
import { FormService } from './form.service'; import { FormService } from './form.service';
import { NgxMaskModule } from 'ngx-mask'; 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 = [ const COMPONENTS = [
CustomSwitchComponent, CustomSwitchComponent,
@@ -64,6 +65,7 @@ const COMPONENTS = [
ChipsComponent, ChipsComponent,
NumberPickerComponent, NumberPickerComponent,
VocabularyTreeviewComponent, VocabularyTreeviewComponent,
ThemedExternalSourceEntryImportModalComponent
]; ];
const DIRECTIVES = [ const DIRECTIVES = [

View File

@@ -55,34 +55,34 @@ describe('MyDSpaceItemStatusComponent', () => {
component.status = MyDspaceItemStatusType.VALIDATION; component.status = MyDspaceItemStatusType.VALIDATION;
fixture.detectChanges(); fixture.detectChanges();
expect(component.badgeContent).toBe(MyDspaceItemStatusType.VALIDATION); expect(component.badgeContent).toBe(MyDspaceItemStatusType.VALIDATION);
expect(component.badgeClass).toBe('text-light badge badge-warning'); expect(component.badgeClass).toBe('text-light badge badge-validation');
}); });
it('should init badge content and class', () => { it('should init badge content and class', () => {
component.status = MyDspaceItemStatusType.WAITING_CONTROLLER; component.status = MyDspaceItemStatusType.WAITING_CONTROLLER;
fixture.detectChanges(); fixture.detectChanges();
expect(component.badgeContent).toBe(MyDspaceItemStatusType.WAITING_CONTROLLER); expect(component.badgeContent).toBe(MyDspaceItemStatusType.WAITING_CONTROLLER);
expect(component.badgeClass).toBe('text-light badge badge-info'); expect(component.badgeClass).toBe('text-light badge badge-waiting-controller');
}); });
it('should init badge content and class', () => { it('should init badge content and class', () => {
component.status = MyDspaceItemStatusType.WORKSPACE; component.status = MyDspaceItemStatusType.WORKSPACE;
fixture.detectChanges(); fixture.detectChanges();
expect(component.badgeContent).toBe(MyDspaceItemStatusType.WORKSPACE); expect(component.badgeContent).toBe(MyDspaceItemStatusType.WORKSPACE);
expect(component.badgeClass).toBe('text-light badge badge-primary'); expect(component.badgeClass).toBe('text-light badge badge-workspace');
}); });
it('should init badge content and class', () => { it('should init badge content and class', () => {
component.status = MyDspaceItemStatusType.ARCHIVED; component.status = MyDspaceItemStatusType.ARCHIVED;
fixture.detectChanges(); fixture.detectChanges();
expect(component.badgeContent).toBe(MyDspaceItemStatusType.ARCHIVED); expect(component.badgeContent).toBe(MyDspaceItemStatusType.ARCHIVED);
expect(component.badgeClass).toBe('text-light badge badge-success'); expect(component.badgeClass).toBe('text-light badge badge-archived');
}); });
it('should init badge content and class', () => { it('should init badge content and class', () => {
component.status = MyDspaceItemStatusType.WORKFLOW; component.status = MyDspaceItemStatusType.WORKFLOW;
fixture.detectChanges(); fixture.detectChanges();
expect(component.badgeContent).toBe(MyDspaceItemStatusType.WORKFLOW); expect(component.badgeContent).toBe(MyDspaceItemStatusType.WORKFLOW);
expect(component.badgeClass).toBe('text-light badge badge-info'); expect(component.badgeClass).toBe('text-light badge badge-workflow');
}); });
}); });

View File

@@ -34,19 +34,19 @@ export class MyDSpaceItemStatusComponent implements OnInit {
this.badgeClass = 'text-light badge '; this.badgeClass = 'text-light badge ';
switch (this.status) { switch (this.status) {
case MyDspaceItemStatusType.VALIDATION: case MyDspaceItemStatusType.VALIDATION:
this.badgeClass += 'badge-warning'; this.badgeClass += 'badge-validation';
break; break;
case MyDspaceItemStatusType.WAITING_CONTROLLER: case MyDspaceItemStatusType.WAITING_CONTROLLER:
this.badgeClass += 'badge-info'; this.badgeClass += 'badge-waiting-controller';
break; break;
case MyDspaceItemStatusType.WORKSPACE: case MyDspaceItemStatusType.WORKSPACE:
this.badgeClass += 'badge-primary'; this.badgeClass += 'badge-workspace';
break; break;
case MyDspaceItemStatusType.ARCHIVED: case MyDspaceItemStatusType.ARCHIVED:
this.badgeClass += 'badge-success'; this.badgeClass += 'badge-archived';
break; break;
case MyDspaceItemStatusType.WORKFLOW: case MyDspaceItemStatusType.WORKFLOW:
this.badgeClass += 'badge-info'; this.badgeClass += 'badge-workflow';
break; break;
} }
} }

View File

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

View File

@@ -71,6 +71,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed'); 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', () => { 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'); 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', () => { describe('and it extends another theme', () => {
@@ -117,6 +129,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed'); 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', () => { describe('that does match it', () => {
@@ -141,6 +159,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed'); 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', () => { 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'); 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', () => { describe('that extends another theme that does match it', () => {
@@ -193,6 +223,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed'); 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, OnDestroy,
ComponentFactoryResolver, ComponentFactoryResolver,
ChangeDetectorRef, ChangeDetectorRef,
OnChanges OnChanges,
HostBinding
} from '@angular/core'; } from '@angular/core';
import { hasValue, isNotEmpty } from '../empty.util'; 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 { 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 { GenericConstructor } from '../../core/shared/generic-constructor';
import { BASE_THEME_NAME } from './theme.constants';
@Component({ @Component({
selector: 'ds-themed', selector: 'ds-themed',
@@ -25,11 +27,22 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
@ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef; @ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef;
protected compRef: ComponentRef<T>; 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 lazyLoadSub: Subscription;
protected themeSub: Subscription; protected themeSub: Subscription;
protected inAndOutputNames: (keyof T & keyof this)[] = []; 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( constructor(
protected resolver: ComponentFactoryResolver, protected resolver: ComponentFactoryResolver,
protected cdr: ChangeDetectorRef, protected cdr: ChangeDetectorRef,
@@ -80,6 +93,7 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
} else { } else {
// otherwise import and return the default component // otherwise import and return the default component
return fromPromise(this.importUnthemedComponent()).pipe( return fromPromise(this.importUnthemedComponent()).pipe(
tap(() => this.usedTheme = BASE_THEME_NAME),
map((unthemedFile: any) => { map((unthemedFile: any) => {
return unthemedFile[this.getComponentName()]; return unthemedFile[this.getComponentName()];
}) })
@@ -90,6 +104,7 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
const factory = this.resolver.resolveComponentFactory(constructor); const factory = this.resolver.resolveComponentFactory(constructor);
this.compRef = this.vcr.createComponent(factory); this.compRef = this.vcr.createComponent(factory);
this.connectInputsAndOutputs(); this.connectInputsAndOutputs();
this.compRef$.next(this.compRef);
this.cdr.markForCheck(); this.cdr.markForCheck();
}); });
} }
@@ -123,6 +138,7 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
private resolveThemedComponent(themeName?: string, checkedThemeNames: string[] = []): Observable<any> { private resolveThemedComponent(themeName?: string, checkedThemeNames: string[] = []): Observable<any> {
if (isNotEmpty(themeName)) { if (isNotEmpty(themeName)) {
return fromPromise(this.importThemedComponent(themeName)).pipe( return fromPromise(this.importThemedComponent(themeName)).pipe(
tap(() => this.usedTheme = themeName),
catchError(() => { catchError(() => {
// Try the next ancestor theme instead // Try the next ancestor theme instead
const nextTheme = this.themeService.getThemeConfigFor(themeName)?.extends; const nextTheme = this.themeService.getThemeConfigFor(themeName)?.extends;

View File

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

View File

@@ -6,11 +6,11 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<ds-themed-loading *ngIf="isLoading()"></ds-themed-loading> <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)" (selectionChange)="selectObject($event)"
(searchComplete)="searchComplete()" (searchComplete)="searchComplete()"
(theOnlySelectable)="theOnlySelectable($event)" (theOnlySelectable)="theOnlySelectable($event)"
[entityType]="entityType"> [entityType]="entityType">
</ds-collection-dropdown> </ds-themed-collection-dropdown>
</div> </div>
</div> </div>

View File

@@ -122,7 +122,7 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => {
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { 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'); expect(dropdownMenu.classList).toContain('d-none');
}); });
})); }));

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -204,3 +204,27 @@ ds-dynamic-form-control-container.d-none {
} }
.badge-validation {
background-color: #{map-get($theme-colors, warning)};
}
.badge-waiting-controller {
background-color: #{map-get($theme-colors, info)};
}
.badge-workspace {
background-color: #{map-get($theme-colors, primary)};
}
.badge-archived {
background-color: #{map-get($theme-colors, success)};
}
.badge-workflow {
background-color: #{map-get($theme-colors, info)};
}
.badge-item-type {
background-color: #{map-get($theme-colors, info)};
}

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 { 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 { 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, CommunityListElementComponent,
CollectionListElementComponent, CollectionListElementComponent,
CollectionDropdownComponent,
]; ];
const DECLARATIONS = [ 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 { 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 { 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 { 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 = [ const DECLARATIONS = [
FileSectionComponent, FileSectionComponent,
@@ -168,6 +171,7 @@ const DECLARATIONS = [
BrowseByMetadataPageComponent, BrowseByMetadataPageComponent,
BrowseByDatePageComponent, BrowseByDatePageComponent,
BrowseByTitlePageComponent, BrowseByTitlePageComponent,
ExternalSourceEntryImportModalComponent,
]; ];

View File

@@ -1,6 +1,6 @@
nav.navbar { nav.navbar {
border-top: 1px var(--ds-header-navbar-border-top-color) solid; border-top: 1px var(--ds-header-navbar-border-top-color) solid;
border-bottom: 5px var(--bs-green) solid; border-bottom: 5px var(--ds-header-navbar-border-bottom-color) solid;
align-items: baseline; align-items: baseline;
color: var(--ds-header-icon-color); color: var(--ds-header-icon-color);
} }

View File

@@ -6,5 +6,6 @@
--ds-banner-background-gradient-width: 300px; --ds-banner-background-gradient-width: 300px;
--ds-home-news-link-color: #{$green}; --ds-home-news-link-color: #{$green};
--ds-home-news-link-hover-color: #{darken($green, 15%)}; --ds-home-news-link-hover-color: #{darken($green, 15%)};
--ds-header-navbar-border-bottom-color: #{$green};
} }

View File

@@ -10,7 +10,7 @@ $font-family-sans-serif: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI"
$navbar-dark-color: #FFFFFF; $navbar-dark-color: #FFFFFF;
/* Reassign color vars to semantic color scheme */ /* Reassign color vars to semantic color scheme */
$blue: #43515f !default; $blue: #2b4e72 !default;
$green: #92C642 !default; $green: #92C642 !default;
$cyan: #207698 !default; $cyan: #207698 !default;
$yellow: #ec9433 !default; $yellow: #ec9433 !default;
@@ -18,6 +18,7 @@ $red: #CF4444 !default;
$dark: #43515f !default; $dark: #43515f !default;
$gray-800: #343a40 !default; $gray-800: #343a40 !default;
$gray-700: #495057 !default;
$gray-400: #ced4da !default; $gray-400: #ced4da !default;
$gray-100: #f8f9fa !default; $gray-100: #f8f9fa !default;
@@ -27,3 +28,14 @@ $table-accent-bg: $gray-100 !default; // Bootstrap $gray-100
$table-hover-bg: $gray-400 !default; // Bootstrap $gray-400 $table-hover-bg: $gray-400 !default; // Bootstrap $gray-400
$yiq-contrasted-threshold: 170 !default; $yiq-contrasted-threshold: 170 !default;
$theme-colors: (
primary: $dark,
secondary: $gray-700,
success: $green,
info: $cyan,
warning: $yellow,
danger: $red,
light: $gray-100,
dark: $dark
) !default;