Skip to content

Commit 8c073ff

Browse files
[DURACOM-426] port metadata link components for authorithy
1 parent a9dc8ec commit 8c073ff

42 files changed

Lines changed: 2033 additions & 43 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/core/data/base/identifiable-data.service.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { HALEndpointService } from '../../shared/hal-endpoint.service';
2020
import { RemoteData } from '../remote-data';
2121
import { RequestService } from '../request.service';
2222
import { BaseDataService } from './base-data.service';
23+
import { FindListOptions } from "../find-list-options.model";
24+
import { RequestParam } from "@dspace/core/cache/models/request-param.model";
2325

2426
/**
2527
* Shorthand type for the method to construct an ID endpoint.
@@ -66,6 +68,32 @@ export class IdentifiableDataService<T extends CacheableObject> extends BaseData
6668
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
6769
}
6870

71+
/**
72+
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of
73+
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
74+
* @param id ID of object we want to retrieve
75+
* @param projections Array of string of projections to be added to the parameters
76+
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
77+
* no valid cached version. Defaults to true
78+
* @param reRequestOnStale Whether or not the request should automatically be re-
79+
* requested after the response becomes stale
80+
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
81+
* {@link HALLink}s should be automatically resolved
82+
*/
83+
findByIdWithProjections(id: string, projections: string[], useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
84+
const options = new FindListOptions();
85+
options.searchParams = [];
86+
87+
projections.forEach((projection) => {
88+
options.searchParams.push(new RequestParam('projection', projection));
89+
});
90+
91+
const href$ = this.getEndpoint().pipe(
92+
map((endpoint: string) => this.buildHrefFromFindOptions(endpoint + '/' + encodeURIComponent(id), options, [], ...linksToFollow)));
93+
94+
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
95+
}
96+
6997
/**
7098
* Create the HREF for a specific object based on its identifier; with possible embed query params based on linksToFollow
7199
* @param endpoint The base endpoint for the type of object

src/app/core/data/item-data.service.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,28 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
481481
}
482482
}
483483

484+
/**
485+
* Returns an observable of {@link RemoteData} of an object, based on its CustomURL or ID, with a list of
486+
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
487+
* @param id CustomUrl or UUID of object we want to retrieve
488+
* @param projections Array of string of projections to be added to the parameters
489+
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
490+
* no valid cached version. Defaults to true
491+
* @param reRequestOnStale Whether or not the request should automatically be re-
492+
* requested after the response becomes stale
493+
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
494+
* {@link HALLink}s should be automatically resolved
495+
* @param projections List of {@link projections} used to pass as parameters
496+
*/
497+
findByIdWithProjections(id: string, projections: string[], useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Item>[]): Observable<RemoteData<Item>> {
498+
499+
if (uuidValidate(id)) {
500+
return super.findByIdWithProjections(id, projections, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
501+
} else {
502+
return this.findByCustomUrl(id, useCachedVersionIfAvailable, reRequestOnStale, linksToFollow, projections);
503+
}
504+
}
505+
484506
}
485507

486508
/**

src/app/core/shared/dspace-object.model.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
126126
return Metadata.all(this.metadata, keyOrKeys, undefined, valueFilter, escapeHTML);
127127
}
128128

129+
130+
/**
131+
* Gets all matching metadata in this DSpaceObject, up to a limit.
132+
*
133+
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
134+
* @param {number} limit The maximum number of results to return.
135+
* @param {MetadataValueFilter} valueFilter The value filter to use. If unspecified, no filtering will be done.
136+
* @returns {MetadataValue[]} the matching values or an empty array.
137+
*/
138+
limitedMetadata(keyOrKeys: string | string[], limit: number, valueFilter?: MetadataValueFilter): MetadataValue[] {
139+
return Metadata.all(this.metadata, keyOrKeys, null, valueFilter, false, limit);
140+
}
141+
142+
129143
/**
130144
* Like [[allMetadata]], but only returns string values.
131145
*

src/app/core/shared/item.model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
9999
@autoserializeAs(Boolean, 'withdrawn')
100100
isWithdrawn: boolean;
101101

102+
/**
103+
* A boolean representing if this Item is currently withdrawn or not
104+
*/
105+
@autoserializeAs(String, 'entityType')
106+
entityType: string;
107+
108+
102109
/**
103110
* The {@link HALLink}s for this Item
104111
*/

src/app/core/shared/metadata.models.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export interface MetadataValueFilter {
9494

9595
/** Whether the value constraint should match as a substring. */
9696
substring?: boolean;
97+
/**
98+
* Whether to negate the filter
99+
*/
100+
negate?: boolean;
97101
}
98102

99103
export class MetadatumViewModel {

src/app/core/shared/metadata.utils.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,16 @@ export class Metadata {
4848
* @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute
4949
* @returns {MetadataValue[]} the matching values or an empty array.
5050
*/
51-
public static all(metadata: MetadataMapInterface, keyOrKeys: string | string[], hitHighlights?: MetadataMapInterface, filter?: MetadataValueFilter, escapeHTML?: boolean): MetadataValue[] {
51+
public static all(metadata: MetadataMapInterface, keyOrKeys: string | string[], hitHighlights?: MetadataMapInterface, filter?: MetadataValueFilter, escapeHTML?: boolean, limit?: number): MetadataValue[] {
5252
const matches: MetadataValue[] = [];
5353
if (isNotEmpty(hitHighlights)) {
5454
for (const mdKey of Metadata.resolveKeys(hitHighlights, keyOrKeys)) {
5555
if (hitHighlights[mdKey]) {
5656
for (const candidate of hitHighlights[mdKey]) {
5757
if (Metadata.valueMatches(candidate as MetadataValue, filter)) {
58-
matches.push(candidate as MetadataValue);
58+
if (isEmpty(limit) || (hasValue(limit) && matches.length < limit)) {
59+
matches.push(candidate as MetadataValue);
60+
}
5961
}
6062
}
6163
}
@@ -212,11 +214,14 @@ export class Metadata {
212214
fValue = filter.value.toLowerCase();
213215
mValue = mdValue.value.toLowerCase();
214216
}
217+
let result: boolean;
218+
215219
if (filter.substring) {
216-
return mValue.includes(fValue);
220+
result = mValue.includes(fValue);
217221
} else {
218-
return mValue === fValue;
222+
result = mValue === fValue;
219223
}
224+
return filter.negate ? !result : result;
220225
}
221226
return true;
222227
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { Component } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
TestBed,
5+
} from '@angular/core/testing';
6+
import { By } from '@angular/platform-browser';
7+
8+
import { EntityIconDirective } from './entity-icon.directive';
9+
10+
describe('EntityIconDirective', () => {
11+
let component: TestComponent;
12+
let fixture: ComponentFixture<TestComponent>;
13+
14+
beforeEach(async () => {
15+
await TestBed.configureTestingModule({
16+
imports: [EntityIconDirective,
17+
TestComponent],
18+
})
19+
.compileComponents();
20+
});
21+
22+
describe('with default value provided', () => {
23+
beforeEach(() => {
24+
fixture = TestBed.createComponent(TestComponent);
25+
component = fixture.componentInstance;
26+
fixture.detectChanges();
27+
});
28+
29+
it('should create', () => {
30+
expect(component).toBeTruthy();
31+
});
32+
33+
it('should display a text-success icon', () => {
34+
const successIcon = fixture.debugElement.query(By.css('[data-test="entityTestComponent"]')).query(By.css('i.text-success'));
35+
expect(successIcon).toBeTruthy();
36+
});
37+
38+
it('should display a text-success icon after span', () => {
39+
const successIcon = fixture.debugElement.query(By.css('[data-test="entityTestComponent"]')).query(By.css('i.text-success'));
40+
const entityElement = fixture.debugElement.query(By.css('[data-test="entityTestComponent"]'));
41+
// position 1 because the icon is after the span
42+
expect(entityElement.nativeElement.children[1]).toBe(successIcon.nativeNode);
43+
});
44+
});
45+
46+
describe('with primary value provided', () => {
47+
beforeEach(() => {
48+
fixture = TestBed.createComponent(TestComponent);
49+
component = fixture.componentInstance;
50+
component.metadata.entityType = 'person';
51+
component.metadata.entityStyle = 'personStaff';
52+
component.iconPosition = 'before';
53+
fixture.detectChanges();
54+
});
55+
56+
it('should display a text-primary icon', () => {
57+
const primaryIcon = fixture.debugElement.query(By.css('[data-test="entityTestComponent"]')).query(By.css('i.text-primary'));
58+
expect(primaryIcon).toBeTruthy();
59+
});
60+
61+
it('should display a text-primary icon before span', () => {
62+
const primaryIcon = fixture.debugElement.query(By.css('[data-test="entityTestComponent"]')).query(By.css('i.text-primary'));
63+
const entityElement = fixture.debugElement.query(By.css('[data-test="entityTestComponent"]'));
64+
// position 0 because the icon is before the span
65+
expect(entityElement.nativeElement.children[0]).toBe(primaryIcon.nativeNode);
66+
});
67+
});
68+
69+
describe('when given type doesn\'t exist and fallback on default disabled', () => {
70+
beforeEach(() => {
71+
fixture = TestBed.createComponent(TestComponent);
72+
component = fixture.componentInstance;
73+
component.fallbackOnDefault = false;
74+
component.metadata.entityType = 'TESTFAKE';
75+
component.metadata.entityStyle = 'personFallback';
76+
component.iconPosition = 'before';
77+
fixture.detectChanges();
78+
});
79+
80+
it('should not display a text-primary icon', () => {
81+
const primaryIcon = fixture.debugElement.query(By.css('[data-test="entityTestComponent"]')).query(By.css('i'));
82+
expect(primaryIcon).toBeFalsy();
83+
});
84+
});
85+
86+
describe('when given style doesn\'t exist and fallback on default disabled', () => {
87+
beforeEach(() => {
88+
fixture = TestBed.createComponent(TestComponent);
89+
component = fixture.componentInstance;
90+
component.fallbackOnDefault = false;
91+
component.metadata.entityType = 'person';
92+
component.metadata.entityStyle = 'personFallback';
93+
component.iconPosition = 'before';
94+
fixture.detectChanges();
95+
});
96+
97+
it('should not display a text-primary icon', () => {
98+
const primaryIcon = fixture.debugElement.query(By.css('[data-test="entityTestComponent"]')).query(By.css('i'));
99+
expect(primaryIcon).toBeFalsy();
100+
});
101+
});
102+
103+
});
104+
105+
// declare a test component
106+
@Component({
107+
selector: 'ds-test-cmp',
108+
template: `
109+
<div [attr.data-test]="'entityTestComponent'">
110+
<span dsEntityIcon
111+
[iconPosition]="iconPosition"
112+
[entityType]="metadata.entityType"
113+
[entityStyle]="metadata.entityStyle"
114+
[fallbackOnDefault]="fallbackOnDefault">{{ metadata.value }}</span></div>`,
115+
standalone: true,
116+
imports: [
117+
EntityIconDirective,
118+
],
119+
})
120+
class TestComponent {
121+
122+
metadata = {
123+
authority: null,
124+
value: 'Test',
125+
orcidAuthenticated: null,
126+
entityType: 'default',
127+
entityStyle: 'default',
128+
};
129+
iconPosition = 'after';
130+
fallbackOnDefault = true;
131+
132+
}

0 commit comments

Comments
 (0)