mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into CST-7757
# Conflicts: # src/app/shared/shared.module.ts
This commit is contained in:
@@ -6,6 +6,10 @@ WORKDIR /app
|
||||
ADD . /app/
|
||||
EXPOSE 4000
|
||||
|
||||
# Ensure Python and other build tools are available
|
||||
# These are needed to install some node modules, especially on linux/arm64
|
||||
RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
|
||||
|
||||
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
||||
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
||||
RUN yarn install --network-timeout 300000
|
||||
|
@@ -25,12 +25,10 @@
|
||||
}
|
||||
},
|
||||
"allowedCommonJsDependencies": [
|
||||
"angular2-text-mask",
|
||||
"cerialize",
|
||||
"core-js",
|
||||
"lodash",
|
||||
"jwt-decode",
|
||||
"url-parse",
|
||||
"uuid",
|
||||
"webfontloader",
|
||||
"zone.js"
|
||||
|
@@ -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: Yкраї́нська
|
||||
active: true
|
||||
|
||||
|
||||
# Browse-By Pages
|
||||
browseBy:
|
||||
# Amount of years to display using jumps of one year (current year - oneYearLimit)
|
||||
|
70
package.json
70
package.json
@@ -54,18 +54,18 @@
|
||||
"ts-node": "10.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "~13.2.6",
|
||||
"@angular/animations": "~13.3.12",
|
||||
"@angular/cdk": "^13.2.6",
|
||||
"@angular/common": "~13.2.6",
|
||||
"@angular/compiler": "~13.2.6",
|
||||
"@angular/core": "~13.2.6",
|
||||
"@angular/forms": "~13.2.6",
|
||||
"@angular/localize": "13.2.6",
|
||||
"@angular/platform-browser": "~13.2.6",
|
||||
"@angular/platform-browser-dynamic": "~13.2.6",
|
||||
"@angular/platform-server": "~13.2.6",
|
||||
"@angular/router": "~13.2.6",
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"@angular/common": "~13.3.12",
|
||||
"@angular/compiler": "~13.3.12",
|
||||
"@angular/core": "~13.3.12",
|
||||
"@angular/forms": "~13.3.12",
|
||||
"@angular/localize": "13.3.12",
|
||||
"@angular/platform-browser": "~13.3.12",
|
||||
"@angular/platform-browser-dynamic": "~13.3.12",
|
||||
"@angular/platform-server": "~13.3.12",
|
||||
"@angular/router": "~13.3.12",
|
||||
"@babel/runtime": "7.17.2",
|
||||
"@kolkov/ngx-gallery": "^2.0.1",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -77,15 +77,15 @@
|
||||
"@ngrx/store": "^13.0.2",
|
||||
"@nguniversal/express-engine": "^13.0.2",
|
||||
"@ngx-translate/core": "^13.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^13.0.0",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"angular-idle-preload": "3.0.0",
|
||||
"angulartics2": "^12.0.0",
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "4.3.1",
|
||||
"caniuse-lite": "^1.0.30001165",
|
||||
"bootstrap": "^4.6.1",
|
||||
"cerialize": "0.1.18",
|
||||
"cli-progress": "^3.8.0",
|
||||
"colors": "^1.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "1.4.5",
|
||||
"core-js": "^3.7.0",
|
||||
@@ -95,11 +95,8 @@
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"fast-json-patch": "^3.0.0-1",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "^6.1.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"https": "1.0.0",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.1.3",
|
||||
@@ -119,43 +116,38 @@
|
||||
"ngx-infinite-scroll": "^10.0.1",
|
||||
"ngx-pagination": "5.0.0",
|
||||
"ngx-sortablejs": "^11.1.0",
|
||||
"ngx-ui-switch": "^11.0.1",
|
||||
"ngx-ui-switch": "^13.0.2",
|
||||
"nouislider": "^14.6.3",
|
||||
"pem": "1.14.4",
|
||||
"postcss-cli": "^9.1.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.5.5",
|
||||
"sanitize-html": "^2.7.2",
|
||||
"sortablejs": "1.13.0",
|
||||
"tslib": "^2.0.0",
|
||||
"url-parse": "^1.5.6",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "1.6.28",
|
||||
"zone.js": "~0.11.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "~13.1.0",
|
||||
"@angular-devkit/build-angular": "~13.2.6",
|
||||
"@angular-devkit/build-angular": "~13.3.10",
|
||||
"@angular-eslint/builder": "13.1.0",
|
||||
"@angular-eslint/eslint-plugin": "13.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "13.1.0",
|
||||
"@angular-eslint/schematics": "13.1.0",
|
||||
"@angular-eslint/template-parser": "13.1.0",
|
||||
"@angular/cli": "~13.2.6",
|
||||
"@angular/compiler-cli": "~13.2.6",
|
||||
"@angular/language-service": "~13.2.6",
|
||||
"@angular/cli": "~13.3.10",
|
||||
"@angular/compiler-cli": "~13.3.12",
|
||||
"@angular/language-service": "~13.3.12",
|
||||
"@cypress/schematic": "^1.5.0",
|
||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||
"@fortawesome/fontawesome-free": "^6.2.1",
|
||||
"@ngrx/store-devtools": "^13.0.2",
|
||||
"@ngtools/webpack": "^13.2.6",
|
||||
"@nguniversal/builders": "^13.0.2",
|
||||
"@nguniversal/builders": "^13.1.1",
|
||||
"@types/deep-freeze": "0.1.2",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jasminewd2": "~2.0.8",
|
||||
"@types/js-cookie": "2.2.6",
|
||||
"@types/lodash": "^4.14.165",
|
||||
"@types/node": "^14.14.9",
|
||||
@@ -166,26 +158,18 @@
|
||||
"compression-webpack-plugin": "^9.2.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.2.0",
|
||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||
"cssnano": "^5.0.6",
|
||||
"cypress": "9.7.0",
|
||||
"cypress-axe": "^0.14.0",
|
||||
"debug-loader": "^0.0.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^8.2.0",
|
||||
"eslint-plugin-deprecation": "^1.3.2",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsdoc": "^39.3.6",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"express-static-gzip": "^2.1.5",
|
||||
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
||||
"html-loader": "^1.3.2",
|
||||
"jasmine-core": "^3.8.0",
|
||||
"jasmine-marbles": "0.9.2",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "^6.3.14",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
@@ -193,26 +177,20 @@
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"ngx-mask": "^13.1.7",
|
||||
"nodemon": "^2.0.15",
|
||||
"nodemon": "^2.0.20",
|
||||
"postcss": "^8.1",
|
||||
"postcss-apply": "0.12.0",
|
||||
"postcss-import": "^14.0.0",
|
||||
"postcss-loader": "^4.0.3",
|
||||
"postcss-preset-env": "^7.4.2",
|
||||
"postcss-responsive-type": "1.0.0",
|
||||
"protractor": "^7.0.0",
|
||||
"protractor-istanbul-plugin": "2.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs-spy": "^8.0.2",
|
||||
"sass": "~1.32.6",
|
||||
"sass": "~1.33.0",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sass-resources-loader": "^2.1.1",
|
||||
"string-replace-loader": "^3.1.0",
|
||||
"terser-webpack-plugin": "^2.3.1",
|
||||
"ts-loader": "^5.2.0",
|
||||
"ts-node": "^8.10.2",
|
||||
"typescript": "~4.5.5",
|
||||
"webpack": "^5.69.1",
|
||||
|
@@ -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,
|
||||
|
@@ -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({
|
||||
|
@@ -10,7 +10,7 @@
|
||||
</nav>
|
||||
|
||||
<ng-template #breadcrumb let-text="text" let-url="url">
|
||||
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate">{{text | translate}}</a></div></li>
|
||||
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate" [ngbTooltip]="text | translate" placement="bottom" >{{text | translate}}</a></div></li>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #activeBreadcrumb let-text="text">
|
||||
|
@@ -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,
|
||||
})
|
||||
];
|
||||
|
@@ -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',
|
||||
|
@@ -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,
|
||||
}),
|
||||
];
|
||||
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
) {
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
})
|
||||
);
|
||||
|
@@ -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',
|
||||
|
@@ -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>
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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`);
|
||||
}
|
||||
}
|
@@ -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(() => {
|
||||
|
@@ -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(
|
||||
|
@@ -1,3 +1 @@
|
||||
span.text-contents{
|
||||
padding: var(--bs-btn-padding-y) 0;
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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]
|
||||
})
|
||||
|
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
modalComp.externalSourceEntry = entry;
|
||||
modalComp.item = this.item;
|
||||
modalComp.collection = this.collection;
|
||||
modalComp.relationship = this.relationship;
|
||||
modalComp.label = this.label;
|
||||
modalComp.relatedEntityType = this.relatedEntityType;
|
||||
this.importObjectSub = modalComp.importedObject.subscribe((object) => {
|
||||
|
||||
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.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.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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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`);
|
||||
}
|
||||
}
|
@@ -1,7 +1,12 @@
|
||||
import {Inject, InjectionToken} from '@angular/core';
|
||||
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 {
|
||||
|
||||
@@ -43,7 +54,7 @@ export abstract class FieldParser {
|
||||
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any;
|
||||
|
||||
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.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'
|
||||
* (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(
|
||||
{},
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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 = [
|
||||
|
@@ -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';
|
||||
import { DsoPageSubscriptionButtonComponent } from './dso-page/dso-page-subscription-button/dso-page-subscription-button.component';
|
||||
|
||||
const MODULES = [
|
||||
@@ -485,6 +486,7 @@ const ENTRY_COMPONENTS = [
|
||||
ClaimedTaskActionsReturnToPoolComponent,
|
||||
ClaimedTaskActionsEditMetadataComponent,
|
||||
CollectionDropdownComponent,
|
||||
ThemedCollectionDropdownComponent,
|
||||
FileDownloadLinkComponent,
|
||||
BitstreamDownloadPageComponent,
|
||||
BitstreamRequestACopyPageComponent,
|
||||
|
@@ -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');
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
|
@@ -10,7 +10,7 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<td></td>
|
||||
<th scope="col"
|
||||
*ngFor="let header of headers"
|
||||
class="{{header}}-header">
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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');
|
||||
});
|
||||
}));
|
||||
|
@@ -12,7 +12,7 @@ img {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 100%;
|
||||
padding-top: (297 / 210) * 100%; // A4 ratio
|
||||
padding-top: calc((297 / 210) * 100%); // A4 ratio
|
||||
}
|
||||
> .inner {
|
||||
position: absolute;
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -93,6 +93,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',
|
||||
@@ -196,6 +197,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 },
|
||||
|
@@ -5,5 +5,6 @@ export interface ValidatorMap {
|
||||
}
|
||||
|
||||
export interface FormConfig extends Config {
|
||||
spellCheck: boolean;
|
||||
validatorMap: ValidatorMap;
|
||||
}
|
||||
|
@@ -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',
|
||||
|
@@ -1,12 +1,14 @@
|
||||
@use 'sass:math';
|
||||
|
||||
@function calculateRem($size) {
|
||||
$remSize: $size / 16px;
|
||||
$remSize: math.div($size, 16px);
|
||||
@return $remSize;
|
||||
}
|
||||
|
||||
|
||||
@function strip-unit($number) {
|
||||
@if type-of($number) == 'number' and not unitless($number) {
|
||||
@return $number / ($number * 0 + 1);
|
||||
@return math.div($number , ($number * 0 + 1));
|
||||
}
|
||||
@return $number;
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
||||
}
|
@@ -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 {
|
||||
|
||||
}
|
@@ -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 = [
|
||||
|
@@ -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,
|
||||
|
||||
|
||||
];
|
||||
|
Reference in New Issue
Block a user