Skip to content

Commit

Permalink
Merge branch 'w2p-116047_fixed-metadataRepresentationComponent-not-al…
Browse files Browse the repository at this point in the history
…ways-using-correct-fallback_contribute-7.2' into dspace-7.6

# Conflicts:
#	src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.ts
#	src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts
#	src/app/shared/log-in/methods/password/log-in-password.component.ts
#	src/themes/custom/entry-components.ts
#	src/themes/custom/lazy-theme.module.ts
  • Loading branch information
alexandrevryghem committed Jun 26, 2024
2 parents 404ccd9 + 281a82a commit 9f4e082
Show file tree
Hide file tree
Showing 17 changed files with 206 additions and 178 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { hasNoValue, hasValue, isNotEmpty } from '../empty.util';
import { DEFAULT_THEME } from '../object-collection/shared/listable-object/listable-object.decorator';
import { ThemeConfig } from '../../../config/theme.model';
import { environment } from '../../../environments/environment';

/**
* A class used to compare two matches and their relevancy to determine which of the two gains priority over the other
*
* "level" represents the index of the first default value that was used to find the match with:
* ViewMode being index 0, Context index 1 and theme index 2. Examples:
* - If a default value was used for context, but not view-mode and theme, the "level" will be 1
* - If a default value was used for view-mode and context, but not for theme, the "level" will be 0
* - If no default value was used for any of the fields, the "level" will be 3
*
* "relevancy" represents the amount of values that didn't require a default value to fall back on. Examples:
* - If a default value was used for theme, but not view-mode and context, the "relevancy" will be 2
* - If a default value was used for view-mode and context, but not for theme, the "relevancy" will be 1
* - If a default value was used for all fields, the "relevancy" will be 0
* - If no default value was used for any of the fields, the "relevancy" will be 3
*
* To determine which of two MatchRelevancies is the most relevant, we compare "level" and "relevancy" in that order.
* If any of the two is higher than the other, that match is most relevant. Examples:
* - { level: 1, relevancy: 1 } is more relevant than { level: 0, relevancy: 2 }
* - { level: 1, relevancy: 1 } is less relevant than { level: 1, relevancy: 2 }
* - { level: 1, relevancy: 1 } is more relevant than { level: 1, relevancy: 0 }
* - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
* - { level: 1, relevancy: 1 } is more relevant than null
*/
export class MatchRelevancy {
constructor(public match: any,
public level: number,
public relevancy: number) {
}

isMoreRelevantThan(otherMatch: MatchRelevancy): boolean {
if (hasNoValue(otherMatch)) {
return true;
}
if (otherMatch.level > this.level) {
return false;
}
return !(otherMatch.level === this.level && otherMatch.relevancy > this.relevancy);
}

isLessRelevantThan(otherMatch: MatchRelevancy): boolean {
return !this.isMoreRelevantThan(otherMatch);
}
}

/**
* Find an object within a nested map, matching the provided keys as good as possible, falling back on defaults wherever
* needed.
*
* Starting off with a Map, it loops over the provided keys, going deeper into the map until it finds a value
* If at some point, no value is found, it'll attempt to use the default value for that index instead
* If the default value exists, the index is stored in the "level"
* If no default value exists, 1 is added to "relevancy"
* See {@link MatchRelevancy} what these represent
*
* @param typeMap a multidimensional map
* @param keys the keys of the multidimensional map to loop over. Each key represents a level within the map
* @param defaults the default values to use for each level, in case no value is found for the key at that index
* @returns matchAndLevel a {@link MatchRelevancy} object containing the match and its level of relevancy
*/
export function getMatch(typeMap: Map<any, any>, keys: any[], defaults: any[]): MatchRelevancy {
let currentMap = typeMap;
let level = -1;
let relevancy = 0;
for (let i = 0; i < keys.length; i++) {
// If we're currently checking the theme, resolve it first to take extended themes into account
let currentMatch = defaults[i] === DEFAULT_THEME ? resolveTheme(currentMap, keys[i]) : currentMap.get(keys[i]);
if (hasNoValue(currentMatch)) {
currentMatch = currentMap.get(defaults[i]);
if (level === -1) {
level = i;
}
} else {
relevancy++;
}
if (hasValue(currentMatch)) {
if (currentMatch instanceof Map) {
currentMap = currentMatch as Map<any, any>;
} else {
return new MatchRelevancy(currentMatch, level > -1 ? level : i + 1, relevancy);
}
} else {
return null;
}
}
return null;
}

/**
* Searches for a ThemeConfig by its name;
*/
export const getThemeConfigFor = (themeName: string): ThemeConfig => {
return environment.themes.find((theme: ThemeConfig) => theme.name === themeName);
};

/**
* Find a match in the given map for the given theme name, taking theme extension into account
*
* @param contextMap A map of theme names to components
* @param themeName The name of the theme to check
* @param checkedThemeNames The list of theme names that are already checked
*/
export const resolveTheme = (contextMap: Map<any, any>, themeName: string, checkedThemeNames: string[] = []): any => {
const match = contextMap.get(themeName);
if (hasValue(match)) {
return match;
} else {
const cfg: ThemeConfig = getThemeConfigFor(themeName);
if (hasValue(cfg) && isNotEmpty(cfg.extends)) {
const nextTheme: string = cfg.extends;
const nextCheckedThemeNames: string[] = [...checkedThemeNames, themeName];
if (checkedThemeNames.includes(nextTheme)) {
throw new Error('Theme extension cycle detected: ' + [...nextCheckedThemeNames, nextTheme].join(' -> '));
} else {
return resolveTheme(contextMap, nextTheme, nextCheckedThemeNames);
}
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { AuthMethod } from '../../../core/auth/models/auth.method';
import { AuthServiceStub } from '../../testing/auth-service.stub';
import { createTestComponent } from '../../testing/utils.test';
import { HardRedirectService } from '../../../core/services/hard-redirect.service';
import { ThemeService } from '../../theme-support/theme.service';
import { getMockThemeService } from '../../mocks/theme-service.mock';

describe('LogInContainerComponent', () => {

Expand Down Expand Up @@ -43,6 +45,7 @@ describe('LogInContainerComponent', () => {
providers: [
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: HardRedirectService, useValue: hardRedirectService },
{ provide: ThemeService, useValue: getMockThemeService() },
LogInContainerComponent
],
schemas: [
Expand Down
13 changes: 6 additions & 7 deletions src/app/shared/log-in/container/log-in-container.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, Injector, Input, OnInit } from '@angular/core';

import { rendersAuthMethodType } from '../methods/log-in.methods-decorator';
import { AuthMethod } from '../../../core/auth/models/auth.method';
import { ThemeService } from '../../theme-support/theme.service';

/**
* This component represents a component container for log-in methods available.
Expand All @@ -27,12 +28,10 @@ export class LogInContainerComponent implements OnInit {
*/
public objectInjector: Injector;

/**
* Initialize instance variables
*
* @param {Injector} injector
*/
constructor(private injector: Injector) {
constructor(
private injector: Injector,
private themeService: ThemeService,
) {
}

/**
Expand All @@ -52,7 +51,7 @@ export class LogInContainerComponent implements OnInit {
* Find the correct component based on the AuthMethod's type
*/
getAuthMethodContent(): string {
return rendersAuthMethodType(this.authMethod.authMethodType);
return rendersAuthMethodType(this.authMethod.authMethodType, this.themeService.getThemeName());
}

}
4 changes: 3 additions & 1 deletion src/app/shared/log-in/log-in.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { HardRedirectService } from '../../core/services/hard-redirect.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { of } from 'rxjs';
import { ThemeService } from '../theme-support/theme.service';
import { getMockThemeService } from '../mocks/theme-service.mock';

describe('LogInComponent', () => {

Expand Down Expand Up @@ -70,10 +72,10 @@ describe('LogInComponent', () => {
providers: [
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
// { provide: Router, useValue: new RouterStub() },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: HardRedirectService, useValue: hardRedirectService },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: ThemeService, useValue: getMockThemeService() },
provideMockStore({ initialState }),
LogInComponent
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Router } from '@angular/router';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';

import { provideMockStore } from '@ngrx/store/testing';
Expand All @@ -17,7 +17,6 @@ import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
import { LogInExternalProviderComponent } from './log-in-external-provider.component';
import { NativeWindowService } from '../../../../core/services/window.service';
import { RouterStub } from '../../../testing/router.stub';
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';

Expand Down Expand Up @@ -73,7 +72,6 @@ describe('LogInExternalProviderComponent', () => {
{ provide: 'isStandalonePage', useValue: true },
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
{ provide: Router, useValue: new RouterStub() },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
{ provide: HardRedirectService, useValue: hardRedirectService },
provideMockStore({ initialState }),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ export class LogInExternalProviderComponent implements OnInit {
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
@Inject('isStandalonePage') public isStandalonePage: boolean,
@Inject(NativeWindowService) protected _window: NativeWindowRef,
private authService: AuthService,
private hardRedirectService: HardRedirectService,
private store: Store<CoreState>
protected authService: AuthService,
protected hardRedirectService: HardRedirectService,
protected store: Store<CoreState>
) {
this.authMethod = injectedAuthMethodModel;
}
Expand Down
19 changes: 12 additions & 7 deletions src/app/shared/log-in/methods/log-in.methods-decorator.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { AuthMethodType } from '../../../core/auth/models/auth.method-type';
import { DEFAULT_THEME } from '../../object-collection/shared/listable-object/listable-object.decorator';
import { hasNoValue } from '../../empty.util';
import { getMatch } from '../../abstract-component-loader/dynamic-component-loader.utils';

export const DEFAULT_AUTH_METHOD_TYPE = AuthMethodType.Password;

const authMethodsMap = new Map();

export function renderAuthMethodFor(authMethodType: AuthMethodType) {
return function decorator(objectElement: any) {
if (!objectElement) {
return;
export function renderAuthMethodFor(authMethodType: AuthMethodType, theme = DEFAULT_THEME) {
return function decorator(component: any) {
if (hasNoValue(authMethodsMap.get(authMethodType))) {
authMethodsMap.set(authMethodType, new Map());
}
authMethodsMap.set(authMethodType, objectElement);
authMethodsMap.get(authMethodType).set(theme, component);
};
}

export function rendersAuthMethodType(authMethodType: AuthMethodType) {
return authMethodsMap.get(authMethodType);
export function rendersAuthMethodType(authMethodType: AuthMethodType, theme: string) {
return getMatch(authMethodsMap, [authMethodType, theme], [DEFAULT_AUTH_METHOD_TYPE, DEFAULT_THEME]).match;
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ export class LogInPasswordComponent implements OnInit {
constructor(
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
@Inject('isStandalonePage') public isStandalonePage: boolean,
private authService: AuthService,
private hardRedirectService: HardRedirectService,
private formBuilder: UntypedFormBuilder,
private store: Store<CoreState>
protected authService: AuthService,
protected hardRedirectService: HardRedirectService,
protected formBuilder: UntypedFormBuilder,
protected store: Store<CoreState>
) {
this.authMethod = injectedAuthMethodModel;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { hasNoValue, hasValue } from '../empty.util';
import { Context } from '../../core/shared/context.model';
import { InjectionToken } from '@angular/core';
import { GenericConstructor } from '../../core/shared/generic-constructor';
import {
resolveTheme,
DEFAULT_THEME, DEFAULT_CONTEXT
} from '../object-collection/shared/listable-object/listable-object.decorator';
import { DEFAULT_THEME, DEFAULT_CONTEXT } from '../object-collection/shared/listable-object/listable-object.decorator';
import { getMatch } from '../abstract-component-loader/dynamic-component-loader.utils';

export const METADATA_REPRESENTATION_COMPONENT_FACTORY = new InjectionToken<(entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context, theme: string) => GenericConstructor<any>>('getMetadataRepresentationComponent', {
providedIn: 'root',
Expand Down Expand Up @@ -53,30 +51,5 @@ export function metadataRepresentationComponent(entityType: string, mdRepresenta
* @param theme the theme to match
*/
export function getMetadataRepresentationComponent(entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) {
const mapForEntity = map.get(entityType);
if (hasValue(mapForEntity)) {
const entityAndMDRepMap = mapForEntity.get(mdRepresentationType);
if (hasValue(entityAndMDRepMap)) {
const contextMap = entityAndMDRepMap.get(context);
if (hasValue(contextMap)) {
const match = resolveTheme(contextMap, theme);
if (hasValue(match)) {
return match;
}
if (hasValue(contextMap.get(DEFAULT_THEME))) {
return contextMap.get(DEFAULT_THEME);
}
}
if (hasValue(entityAndMDRepMap.get(DEFAULT_CONTEXT)) &&
hasValue(entityAndMDRepMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) {
return entityAndMDRepMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME);
}
}
if (hasValue(mapForEntity.get(DEFAULT_REPRESENTATION_TYPE)) &&
hasValue(mapForEntity.get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT)) &&
hasValue(mapForEntity.get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) {
return mapForEntity.get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME);
}
}
return map.get(DEFAULT_ENTITY_TYPE).get(DEFAULT_REPRESENTATION_TYPE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME);
return getMatch(map, [entityType, mdRepresentationType, context, theme], [DEFAULT_ENTITY_TYPE, DEFAULT_REPRESENTATION_TYPE, DEFAULT_CONTEXT, DEFAULT_THEME]).match;
}
Loading

0 comments on commit 9f4e082

Please sign in to comment.