Skip to content

Commit 54e3ee9

Browse files
authored
ZCU-PUB/Display full community path in sidebar search results (#1235)
* Show full community hierarchy in sidebar search * Add tooltip to parent path * Add shareReplay, type guard, and hierarchical path tests * refactor: unwrap parentTitle$ once, extract BREADCRUMB_SEPARATOR, fix tests * test: use createNoContentRemoteDataObject$ for safe fallback mock * Removed unsafe type cast
1 parent 7825421 commit 54e3ee9

8 files changed

Lines changed: 223 additions & 18 deletions

src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
1010
import { LinkService } from '../../../../../core/cache/builders/link.service';
1111
import { TranslateService } from '@ngx-translate/core';
1212
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
13+
import { DSOBreadcrumbsService } from '../../../../../core/breadcrumbs/dso-breadcrumbs.service';
1314

1415
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.SideBarSearchModal)
1516
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.SideBarSearchModalCurrent)
@@ -26,8 +27,9 @@ export class PersonSidebarSearchListElementComponent extends SidebarSearchListEl
2627
protected linkService: LinkService,
2728
protected translateService: TranslateService,
2829
public dsoNameService: DSONameService,
30+
protected dsoBreadcrumbsService: DSOBreadcrumbsService,
2931
) {
30-
super(truncatableService, linkService, dsoNameService);
32+
super(truncatableService, linkService, dsoNameService, dsoBreadcrumbsService);
3133
}
3234

3335
/**

src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CollectionSidebarSearchListElementComponent } from './collection-sideba
22
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
33
import { Collection } from '../../../../core/shared/collection.model';
44
import { Community } from '../../../../core/shared/community.model';
5-
import { createSidebarSearchListElementTests } from '../sidebar-search-list-element.component.spec';
5+
import { createHierarchicalParentTitleTests, createSidebarSearchListElementTests } from '../sidebar-search-list-element.component.spec';
66

77
const object = Object.assign(new CollectionSearchResult(), {
88
indexableObject: Object.assign(new Collection(), {
@@ -33,5 +33,9 @@ const parent = Object.assign(new Community(), {
3333
});
3434

3535
describe('CollectionSidebarSearchListElementComponent',
36-
createSidebarSearchListElementTests(CollectionSidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description')
36+
createSidebarSearchListElementTests(CollectionSidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description', [], true)
37+
);
38+
39+
describe('CollectionSidebarSearchListElementComponent - hierarchical path',
40+
createHierarchicalParentTitleTests(CollectionSidebarSearchListElementComponent, object, 'title')
3741
);

src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { listableObjectComponent } from '../../../object-collection/shared/lista
55
import { Context } from '../../../../core/shared/context.model';
66
import { ViewMode } from '../../../../core/shared/view-mode.model';
77
import { SidebarSearchListElementComponent } from '../sidebar-search-list-element.component';
8+
import { TruncatableService } from '../../../truncatable/truncatable.service';
9+
import { LinkService } from '../../../../core/cache/builders/link.service';
10+
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
11+
import { DSOBreadcrumbsService } from '../../../../core/breadcrumbs/dso-breadcrumbs.service';
812

913
@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.SideBarSearchModal)
1014
@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.SideBarSearchModalCurrent)
@@ -16,6 +20,16 @@ import { SidebarSearchListElementComponent } from '../sidebar-search-list-elemen
1620
* Component displaying a list element for a {@link CollectionSearchResult} within the context of a sidebar search modal
1721
*/
1822
export class CollectionSidebarSearchListElementComponent extends SidebarSearchListElementComponent<CollectionSearchResult, Collection> {
23+
24+
constructor(
25+
protected truncatableService: TruncatableService,
26+
protected linkService: LinkService,
27+
public dsoNameService: DSONameService,
28+
protected dsoBreadcrumbsService: DSOBreadcrumbsService,
29+
) {
30+
super(truncatableService, linkService, dsoNameService, dsoBreadcrumbsService);
31+
}
32+
1933
/**
2034
* Get the description of the Collection by returning its abstract
2135
*/

src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Community } from '../../../../core/shared/community.model';
2-
import { createSidebarSearchListElementTests } from '../sidebar-search-list-element.component.spec';
2+
import { createHierarchicalParentTitleTests, createSidebarSearchListElementTests } from '../sidebar-search-list-element.component.spec';
33
import { CommunitySidebarSearchListElementComponent } from './community-sidebar-search-list-element.component';
44
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
55

@@ -32,5 +32,9 @@ const parent = Object.assign(new Community(), {
3232
});
3333

3434
describe('CommunitySidebarSearchListElementComponent',
35-
createSidebarSearchListElementTests(CommunitySidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description')
35+
createSidebarSearchListElementTests(CommunitySidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description', [], true)
36+
);
37+
38+
describe('CommunitySidebarSearchListElementComponent - hierarchical path',
39+
createHierarchicalParentTitleTests(CommunitySidebarSearchListElementComponent, object, 'title')
3640
);

src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,31 @@ import { ViewMode } from '../../../../core/shared/view-mode.model';
55
import { SidebarSearchListElementComponent } from '../sidebar-search-list-element.component';
66
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
77
import { Community } from '../../../../core/shared/community.model';
8+
import { TruncatableService } from '../../../truncatable/truncatable.service';
9+
import { LinkService } from '../../../../core/cache/builders/link.service';
10+
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
11+
import { DSOBreadcrumbsService } from '../../../../core/breadcrumbs/dso-breadcrumbs.service';
812

913
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.SideBarSearchModal)
1014
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.SideBarSearchModalCurrent)
1115
@Component({
12-
selector: 'ds-collection-sidebar-search-list-element',
16+
selector: 'ds-community-sidebar-search-list-element',
1317
templateUrl: '../sidebar-search-list-element.component.html'
1418
})
1519
/**
1620
* Component displaying a list element for a {@link CommunitySearchResult} within the context of a sidebar search modal
1721
*/
1822
export class CommunitySidebarSearchListElementComponent extends SidebarSearchListElementComponent<CommunitySearchResult, Community> {
23+
24+
constructor(
25+
protected truncatableService: TruncatableService,
26+
protected linkService: LinkService,
27+
public dsoNameService: DSONameService,
28+
protected dsoBreadcrumbsService: DSOBreadcrumbsService,
29+
) {
30+
super(truncatableService, linkService, dsoNameService, dsoBreadcrumbsService);
31+
}
32+
1933
/**
2034
* Get the description of the Community by returning its abstract
2135
*/

src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
<ds-truncatable-part [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'" [showToggle]="false">
2-
<div [ngClass]="isCurrent() ? 'text-light' : 'text-body'"
3-
[innerHTML]="(parentTitle$ && parentTitle$ | async) ? (parentTitle$ | async) : ('home.breadcrumbs' | translate)"></div>
2+
<ng-container *ngIf="parentTitle$ | async as parentTitle; else defaultParentTitle">
3+
<div [ngClass]="isCurrent() ? 'text-light' : 'text-body'"
4+
[title]="parentTitle || ('home.breadcrumbs' | translate)"
5+
[innerHTML]="parentTitle || ('home.breadcrumbs' | translate)"></div>
6+
</ng-container>
7+
<ng-template #defaultParentTitle>
8+
<div [ngClass]="isCurrent() ? 'text-light' : 'text-body'"
9+
[title]="'home.breadcrumbs' | translate"
10+
[innerHTML]="'home.breadcrumbs' | translate"></div>
11+
</ng-template>
412
</ds-truncatable-part>
513
<ds-truncatable-part [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'" [showToggle]="false">
614
<div class="font-weight-bold"

src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ import { SearchResult } from '../../search/models/search-result.model';
77
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
88
import { TruncatableService } from '../../truncatable/truncatable.service';
99
import { LinkService } from '../../../core/cache/builders/link.service';
10-
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
10+
import { createSuccessfulRemoteDataObject$, createNoContentRemoteDataObject$ } from '../../remote-data.utils';
1111
import { HALResource } from '../../../core/shared/hal-resource.model';
1212
import { ChildHALResource } from '../../../core/shared/child-hal-resource.model';
1313
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
14+
import { DSOBreadcrumbsService } from '../../../core/breadcrumbs/dso-breadcrumbs.service';
15+
import { Breadcrumb } from '../../../breadcrumbs/breadcrumb/breadcrumb.model';
16+
import { of as observableOf } from 'rxjs';
17+
import { BREADCRUMB_SEPARATOR } from './sidebar-search-list-element.component';
18+
import { ResourceType } from '../../../core/shared/resource-type';
1419

1520
export function createSidebarSearchListElementTests(
1621
componentClass: any,
@@ -19,26 +24,44 @@ export function createSidebarSearchListElementTests(
1924
expectedParentTitle: string,
2025
expectedTitle: string,
2126
expectedDescription: string,
22-
extraProviders: any[] = []
27+
extraProviders: any[] = [],
28+
assertBreadcrumbsUsed = false
2329
) {
2430
return () => {
2531
let component;
2632
let fixture: ComponentFixture<any>;
2733

2834
let linkService;
35+
let dsoBreadcrumbsService;
2936

3037
beforeEach(waitForAsync(() => {
38+
// Propagate the class-level static ResourceType onto the instance so that
39+
// the community/collection branch in getParentTitle() is reached correctly.
40+
const staticType: ResourceType | undefined = (object.indexableObject.constructor as any).type;
41+
if (staticType) {
42+
(object.indexableObject as any).type = staticType;
43+
}
44+
3145
linkService = jasmine.createSpyObj('linkService', {
3246
resolveLink: Object.assign(new HALResource(), {
3347
[object.indexableObject.getParentLinkKey()]: createSuccessfulRemoteDataObject$(parent)
3448
})
3549
});
50+
const breadcrumbs: Breadcrumb[] = [];
51+
if (expectedParentTitle) {
52+
breadcrumbs.push(new Breadcrumb(expectedParentTitle, ''));
53+
}
54+
breadcrumbs.push(new Breadcrumb(expectedTitle, ''));
55+
dsoBreadcrumbsService = jasmine.createSpyObj('dsoBreadcrumbsService', {
56+
getBreadcrumbs: observableOf(breadcrumbs)
57+
});
3658
TestBed.configureTestingModule({
3759
declarations: [componentClass, VarDirective],
3860
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
3961
providers: [
4062
{ provide: TruncatableService, useValue: {} },
4163
{ provide: LinkService, useValue: linkService },
64+
{ provide: DSOBreadcrumbsService, useValue: dsoBreadcrumbsService },
4265
DSONameService,
4366
...extraProviders
4467
],
@@ -61,6 +84,18 @@ export function createSidebarSearchListElementTests(
6184
});
6285
});
6386

87+
if (assertBreadcrumbsUsed) {
88+
it('should delegate to DSOBreadcrumbsService.getBreadcrumbs to resolve the parent title', (done) => {
89+
component.parentTitle$.subscribe(() => {
90+
expect(dsoBreadcrumbsService.getBreadcrumbs).toHaveBeenCalledWith(
91+
object.indexableObject,
92+
''
93+
);
94+
done();
95+
});
96+
});
97+
}
98+
6499
it('should contain the correct title', () => {
65100
expect(component.dsoTitle).toEqual(expectedTitle);
66101
});
@@ -70,3 +105,93 @@ export function createSidebarSearchListElementTests(
70105
});
71106
};
72107
}
108+
109+
/**
110+
* Shared test suite that verifies the hierarchical parent-path behaviour for community/collection
111+
* list elements: when the DSO has multiple ancestor breadcrumbs the component must join them with
112+
* {@link BREADCRUMB_SEPARATOR} and must delegate to {@link DSOBreadcrumbsService#getBreadcrumbs} rather than the simple
113+
* parent link.
114+
*
115+
* @param componentClass The component under test (community or collection sidebar element)
116+
* @param object A {@link SearchResult} whose `indexableObject` is a Community/Collection
117+
* @param expectedTitle The dc.title of the current item (last breadcrumb)
118+
* @param extraProviders Any additional providers required by the component
119+
*/
120+
export function createHierarchicalParentTitleTests(
121+
componentClass: any,
122+
object: SearchResult<DSpaceObject & ChildHALResource>,
123+
expectedTitle: string,
124+
extraProviders: any[] = []
125+
) {
126+
return () => {
127+
let component;
128+
let fixture: ComponentFixture<any>;
129+
let dsoBreadcrumbsService;
130+
131+
// Three-level hierarchy: Root → Parent → Current
132+
const rootBreadcrumb = new Breadcrumb('Root', '');
133+
const parentBreadcrumb = new Breadcrumb('Parent', '');
134+
const currentBreadcrumb = new Breadcrumb(expectedTitle, '');
135+
const breadcrumbs = [rootBreadcrumb, parentBreadcrumb, currentBreadcrumb];
136+
137+
beforeEach(waitForAsync(() => {
138+
// Propagate the class-level static ResourceType onto the instance so that
139+
// the community/collection branch in getParentTitle() is reached correctly.
140+
const staticType: ResourceType | undefined = (object.indexableObject.constructor as any).type;
141+
if (staticType) {
142+
(object.indexableObject as any).type = staticType;
143+
}
144+
145+
// Set up the linkService with a safe RemoteData observable for the parent link so that
146+
// even if the type-check guard ever regresses, the fallback getParent() path resolves
147+
// cleanly via the find() predicate (statusCode === 204) without a TypeError.
148+
const parentLinkKey = (object.indexableObject as ChildHALResource).getParentLinkKey() as string;
149+
const linkService = jasmine.createSpyObj('linkService', {
150+
resolveLink: Object.assign(new HALResource(), {
151+
[parentLinkKey]: createNoContentRemoteDataObject$()
152+
})
153+
});
154+
dsoBreadcrumbsService = jasmine.createSpyObj('dsoBreadcrumbsService', {
155+
getBreadcrumbs: observableOf(breadcrumbs)
156+
});
157+
158+
TestBed.configureTestingModule({
159+
declarations: [componentClass, VarDirective],
160+
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
161+
providers: [
162+
{ provide: TruncatableService, useValue: {} },
163+
{ provide: LinkService, useValue: linkService },
164+
{ provide: DSOBreadcrumbsService, useValue: dsoBreadcrumbsService },
165+
DSONameService,
166+
...extraProviders
167+
],
168+
schemas: [NO_ERRORS_SCHEMA]
169+
}).compileComponents();
170+
}));
171+
172+
beforeEach(() => {
173+
fixture = TestBed.createComponent(componentClass);
174+
component = fixture.componentInstance;
175+
component.object = object;
176+
component.ngOnInit();
177+
fixture.detectChanges();
178+
});
179+
180+
it('should join multiple ancestor breadcrumbs with BREADCRUMB_SEPARATOR as the parent title', (done) => {
181+
component.parentTitle$.subscribe((title) => {
182+
expect(title).toEqual(['Root', 'Parent'].join(BREADCRUMB_SEPARATOR));
183+
done();
184+
});
185+
});
186+
187+
it('should call DSOBreadcrumbsService.getBreadcrumbs to build the hierarchy path', (done) => {
188+
component.parentTitle$.subscribe(() => {
189+
expect(dsoBreadcrumbsService.getBreadcrumbs).toHaveBeenCalledWith(
190+
object.indexableObject,
191+
''
192+
);
193+
done();
194+
});
195+
});
196+
};
197+
}

0 commit comments

Comments
 (0)