Skip to content

Commit

Permalink
Fix handle theme not working with canonical prefix https://hdl.handle…
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandrevryghem committed Oct 16, 2023
1 parent ff0c1a2 commit a3dd053
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 86 deletions.
82 changes: 46 additions & 36 deletions src/app/curation-form/curation-form.component.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ScriptDataService } from '../core/data/processes/script-data.service';
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { find, map } from 'rxjs/operators';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
import { map } from 'rxjs/operators';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
import { RemoteData } from '../core/data/remote-data';
import { Router } from '@angular/router';
import { ProcessDataService } from '../core/data/processes/process-data.service';
import { Process } from '../process-page/processes/process.model';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
import { HandleService } from '../shared/handle.service';

export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';

/**
* Component responsible for rendering the Curation Task form
*/
@Component({
selector: 'ds-curation-form',
templateUrl: './curation-form.component.html'
})
export class CurationFormComponent implements OnInit {
export class CurationFormComponent implements OnDestroy, OnInit {

config: Observable<RemoteData<ConfigurationProperty>>;
tasks: string[];
Expand All @@ -33,10 +33,11 @@ export class CurationFormComponent implements OnInit {
@Input()
dsoHandle: string;

subs: Subscription[] = [];

constructor(
private scriptDataService: ScriptDataService,
private configurationDataService: ConfigurationDataService,
private processDataService: ProcessDataService,
private notificationsService: NotificationsService,
private translateService: TranslateService,
private handleService: HandleService,
Expand All @@ -45,23 +46,26 @@ export class CurationFormComponent implements OnInit {
) {
}

ngOnDestroy(): void {
this.subs.forEach((sub: Subscription) => sub.unsubscribe());
}

ngOnInit(): void {
this.form = new UntypedFormGroup({
task: new UntypedFormControl(''),
handle: new UntypedFormControl('')
});

this.config = this.configurationDataService.findByPropertyName(CURATION_CFG);
this.config.pipe(
find((rd: RemoteData<ConfigurationProperty>) => rd.hasSucceeded),
map((rd: RemoteData<ConfigurationProperty>) => rd.payload)
).subscribe((configProperties) => {
this.subs.push(this.config.pipe(
getFirstSucceededRemoteDataPayload(),
).subscribe((configProperties: ConfigurationProperty) => {
this.tasks = configProperties.values
.filter((value) => isNotEmpty(value) && value.includes('='))
.map((value) => value.split('=')[1].trim());
this.form.get('task').patchValue(this.tasks[0]);
this.cdr.detectChanges();
});
}));
}

/**
Expand All @@ -77,33 +81,39 @@ export class CurationFormComponent implements OnInit {
*/
submit() {
const taskName = this.form.get('task').value;
let handle;
let handle$: Observable<string | null>;
if (this.hasHandleValue()) {
handle = this.handleService.normalizeHandle(this.dsoHandle);
if (isEmpty(handle)) {
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
this.translateService.get('curation.form.submit.error.invalid-handle'));
return;
}
handle$ = this.handleService.normalizeHandle(this.dsoHandle).pipe(
map((handle: string | null) => {
if (isEmpty(handle)) {
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
this.translateService.get('curation.form.submit.error.invalid-handle'));
}
return handle;
}),
);
} else {
handle = this.handleService.normalizeHandle(this.form.get('handle').value);
if (isEmpty(handle)) {
handle = 'all';
}
handle$ = this.handleService.normalizeHandle(this.form.get('handle').value).pipe(
map((handle: string | null) => isEmpty(handle) ? 'all' : handle),
);
}

this.scriptDataService.invoke('curate', [
{ name: '-t', value: taskName },
{ name: '-i', value: handle },
], []).pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'),
this.translateService.get('curation.form.submit.success.content'));
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
} else {
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
this.translateService.get('curation.form.submit.error.content'));
}
});
this.subs.push(handle$.subscribe((handle: string) => {
this.subs.push(this.scriptDataService.invoke('curate', [
{ name: '-t', value: taskName },
{ name: '-i', value: handle },
], []).pipe(
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'),
this.translateService.get('curation.form.submit.success.content'));
void this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
} else {
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
this.translateService.get('curation.form.submit.error.content'));
}
}));
}));
}
}
68 changes: 51 additions & 17 deletions src/app/shared/handle.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import { Injectable } from '@angular/core';
import { isNotEmpty, isEmpty } from './empty.util';
import { isNotEmpty, isEmpty, hasValue } from './empty.util';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { map, take } from 'rxjs/operators';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data';

const PREFIX_REGEX = /handle\/([^\/]+\/[^\/]+)$/;
export const CANONICAL_PREFIX_KEY = 'handle.canonical.prefix';

const PREFIX_REGEX = (prefix: string) => new RegExp(`(${prefix}|handle\/)([^\/]+\/[^\/]+)$`);
const NO_PREFIX_REGEX = /^([^\/]+\/[^\/]+)$/;

@Injectable({
providedIn: 'root'
})
export class HandleService {

canonicalPrefix$: Observable<string>;

constructor(
protected configService: ConfigurationDataService,
) {
this.canonicalPrefix$ = this.configService.findByPropertyName(CANONICAL_PREFIX_KEY).pipe(
take(1),
getFirstCompletedRemoteData(),
map((configurationPropertyRD: RemoteData<ConfigurationProperty>) => {
if (configurationPropertyRD.hasSucceeded) {
return configurationPropertyRD.payload.values.length >= 1 ? configurationPropertyRD.payload.values[0] : undefined;
} else {
return undefined;
}
}),
);
}

/**
* Turns a handle string into the default 123456789/12345 format
Expand All @@ -21,21 +46,30 @@ export class HandleService {
* normalizeHandle('https://rest.api/server/handle/123456789/123456') // '123456789/123456'
* normalizeHandle('https://rest.api/server/handle/123456789') // null
*/
normalizeHandle(handle: string): string {
let matches: string[];
if (isNotEmpty(handle)) {
matches = handle.match(PREFIX_REGEX);
}

if (isEmpty(matches) || matches.length < 2) {
matches = handle.match(NO_PREFIX_REGEX);
}

if (isEmpty(matches) || matches.length < 2) {
return null;
} else {
return matches[1];
}
normalizeHandle(handle: string): Observable<string | null> {
return this.canonicalPrefix$.pipe(
map((prefix: string) => {
let matches: string[];
if (hasValue(prefix)) {
if (isNotEmpty(handle)) {
matches = handle.match(PREFIX_REGEX(prefix));
}

if (isEmpty(matches) || matches.length < 3) {
matches = handle.match(NO_PREFIX_REGEX);
}

if (isEmpty(matches) || matches.length < 2) {
return null;
} else {
return matches[matches.length - 1];
}
} else {
return null;
}
}),
take(1),
);
}

}
45 changes: 24 additions & 21 deletions src/app/shared/theme-support/theme.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { Injectable, Inject, Injector } from '@angular/core';
import { Store, createFeatureSelector, createSelector, select } from '@ngrx/store';
import { BehaviorSubject, EMPTY, Observable, of as observableOf } from 'rxjs';
import { BehaviorSubject, EMPTY, Observable, of as observableOf, from } from 'rxjs';
import { ThemeState } from './theme.reducer';
import { SetThemeAction, ThemeActionTypes } from './theme.actions';
import { expand, filter, map, switchMap, take, toArray } from 'rxjs/operators';
import { expand, filter, map, switchMap, take, toArray, first, mergeMap } from 'rxjs/operators';
import { hasNoValue, hasValue, isNotEmpty } from '../empty.util';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import {
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
getRemoteDataPayload
} from '../../core/shared/operators';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload } from '../../core/shared/operators';
import { HeadTagConfig, Theme, ThemeConfig, themeFactory } from '../../../config/theme.model';
import { NO_OP_ACTION_TYPE, NoOpAction } from '../ngrx/no-op.action';
import { followLink } from '../utils/follow-link-config.model';
Expand Down Expand Up @@ -219,7 +215,7 @@ export class ThemeService {
// create new head tags (not yet added to DOM)
const headTagFragment = this.document.createDocumentFragment();
this.createHeadTags(themeName)
.forEach(newHeadTag => headTagFragment.appendChild(newHeadTag));
.forEach(newHeadTag => headTagFragment.appendChild(newHeadTag));

// add new head tags to DOM
head.appendChild(headTagFragment);
Expand Down Expand Up @@ -268,7 +264,7 @@ export class ThemeService {

if (hasValue(headTagConfig.attributes)) {
Object.entries(headTagConfig.attributes)
.forEach(([key, value]) => tag.setAttribute(key, value));
.forEach(([key, value]) => tag.setAttribute(key, value));
}

// 'class' attribute should always be 'theme-head-tag' for removal
Expand Down Expand Up @@ -302,8 +298,10 @@ export class ThemeService {
// Start with the resolved dso and go recursively through its parents until you reach the top-level community
return observableOf(dsoRD.payload).pipe(
this.getAncestorDSOs(),
map((dsos: DSpaceObject[]) => {
const dsoMatch = this.matchThemeToDSOs(dsos, currentRouteUrl);
switchMap((dsos: DSpaceObject[]) => {
return this.matchThemeToDSOs(dsos, currentRouteUrl);
}),
map((dsoMatch: Theme) => {
return this.getActionForMatch(dsoMatch, currentTheme);
})
);
Expand All @@ -316,8 +314,10 @@ export class ThemeService {
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
this.getAncestorDSOs(),
map((dsos: DSpaceObject[]) => {
const dsoMatch = this.matchThemeToDSOs(dsos, currentRouteUrl);
switchMap((dsos: DSpaceObject[]) => {
return this.matchThemeToDSOs(dsos, currentRouteUrl);
}),
map((dsoMatch: Theme) => {
return this.getActionForMatch(dsoMatch, currentTheme);
})
);
Expand Down Expand Up @@ -433,14 +433,17 @@ export class ThemeService {
* @param currentRouteUrl The url for the current route
* @private
*/
private matchThemeToDSOs(dsos: DSpaceObject[], currentRouteUrl: string): Theme {
// iterate over the themes in order, and return the first one that matches
return this.themes.find((theme: Theme) => {
// iterate over the dsos's in order (most specific one first, so Item, Collection,
// Community), and return the first one that matches the current theme
const match = dsos.find((dso: DSpaceObject) => theme.matches(currentRouteUrl, dso));
return hasValue(match);
});
private matchThemeToDSOs(dsos: DSpaceObject[], currentRouteUrl: string): Observable<Theme> {
return from(this.themes).pipe(
mergeMap((theme: Theme) => from(dsos).pipe(
mergeMap((dso: DSpaceObject) => theme.matches(currentRouteUrl, dso)),
first(match => match === true, undefined),
map(match => match ? theme : null),
),
1, // only process one them at a time
),
first(matchingTheme => matchingTheme !== null, undefined),
);
}

/**
Expand Down
Loading

0 comments on commit a3dd053

Please sign in to comment.