Skip to content

Commit 0b88cd3

Browse files
Fix: Full community path in sidebar search results (DSpace#5296)
* ZCU-PUB/Display full community path in sidebar search results (DSpace#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 * fix: lint errors in sidebar search elements --------- Co-authored-by: milanmajchrak <milan.majchrak@dataquest.sk>
1 parent cd67f89 commit 0b88cd3

8 files changed

Lines changed: 226 additions & 16 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
@@ -3,6 +3,7 @@ import {
33
NgClass,
44
} from '@angular/common';
55
import { Component } from '@angular/core';
6+
import { DSOBreadcrumbsService } from '@dspace/core/breadcrumbs/dso-breadcrumbs.service';
67
import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service';
78
import { LinkService } from '@dspace/core/cache/builders/link.service';
89
import { Context } from '@dspace/core/shared/context.model';
@@ -43,8 +44,9 @@ export class PersonSidebarSearchListElementComponent extends SidebarSearchListEl
4344
protected linkService: LinkService,
4445
protected translateService: TranslateService,
4546
public dsoNameService: DSONameService,
47+
protected dsoBreadcrumbsService: DSOBreadcrumbsService,
4648
) {
47-
super(truncatableService, linkService, dsoNameService);
49+
super(truncatableService, linkService, dsoNameService, dsoBreadcrumbsService);
4850
}
4951

5052
/**

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Collection } from '@dspace/core/shared/collection.model';
22
import { Community } from '@dspace/core/shared/community.model';
33
import { CollectionSearchResult } from '@dspace/core/shared/object-collection/collection-search-result.model';
44

5-
import { createSidebarSearchListElementTests } from '../sidebar-search-list-element.component.spec';
5+
import {
6+
createHierarchicalParentTitleTests,
7+
createSidebarSearchListElementTests,
8+
} from '../sidebar-search-list-element.component.spec';
69
import { CollectionSidebarSearchListElementComponent } from './collection-sidebar-search-list-element.component';
710

811
const object = Object.assign(new CollectionSearchResult(), {
@@ -34,5 +37,9 @@ const parent = Object.assign(new Community(), {
3437
});
3538

3639
describe('CollectionSidebarSearchListElementComponent',
37-
createSidebarSearchListElementTests(CollectionSidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description'),
40+
createSidebarSearchListElementTests(CollectionSidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description', [], true),
41+
);
42+
43+
describe('CollectionSidebarSearchListElementComponent - hierarchical path',
44+
createHierarchicalParentTitleTests(CollectionSidebarSearchListElementComponent, object, 'title'),
3845
);

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import {
33
NgClass,
44
} from '@angular/common';
55
import { Component } from '@angular/core';
6+
import { DSOBreadcrumbsService } from '@dspace/core/breadcrumbs/dso-breadcrumbs.service';
7+
import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service';
8+
import { LinkService } from '@dspace/core/cache/builders/link.service';
69
import { Collection } from '@dspace/core/shared/collection.model';
710
import { Context } from '@dspace/core/shared/context.model';
811
import { CollectionSearchResult } from '@dspace/core/shared/object-collection/collection-search-result.model';
912
import { ViewMode } from '@dspace/core/shared/view-mode.model';
1013
import { TranslateModule } from '@ngx-translate/core';
14+
import { listableObjectComponent } from 'src/app/shared/object-collection/shared/listable-object/listable-object.decorator';
15+
import { TruncatableService } from 'src/app/shared/truncatable/truncatable.service';
16+
import { TruncatablePartComponent } from 'src/app/shared/truncatable/truncatable-part/truncatable-part.component';
1117

12-
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
13-
import { TruncatablePartComponent } from '../../../truncatable/truncatable-part/truncatable-part.component';
1418
import { SidebarSearchListElementComponent } from '../sidebar-search-list-element.component';
1519

1620
@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.SideBarSearchModal)
@@ -31,6 +35,16 @@ import { SidebarSearchListElementComponent } from '../sidebar-search-list-elemen
3135
* Component displaying a list element for a {@link CollectionSearchResult} within the context of a sidebar search modal
3236
*/
3337
export class CollectionSidebarSearchListElementComponent extends SidebarSearchListElementComponent<CollectionSearchResult, Collection> {
38+
39+
constructor(
40+
protected truncatableService: TruncatableService,
41+
protected linkService: LinkService,
42+
public dsoNameService: DSONameService,
43+
protected dsoBreadcrumbsService: DSOBreadcrumbsService,
44+
) {
45+
super(truncatableService, linkService, dsoNameService, dsoBreadcrumbsService);
46+
}
47+
3448
/**
3549
* Get the description of the Collection by returning its abstract
3650
*/

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Community } from '@dspace/core/shared/community.model';
22
import { CommunitySearchResult } from '@dspace/core/shared/object-collection/community-search-result.model';
33

4-
import { createSidebarSearchListElementTests } from '../sidebar-search-list-element.component.spec';
4+
import {
5+
createHierarchicalParentTitleTests,
6+
createSidebarSearchListElementTests,
7+
} from '../sidebar-search-list-element.component.spec';
58
import { CommunitySidebarSearchListElementComponent } from './community-sidebar-search-list-element.component';
69

710
const object = Object.assign(new CommunitySearchResult(), {
@@ -33,5 +36,9 @@ const parent = Object.assign(new Community(), {
3336
});
3437

3538
describe('CommunitySidebarSearchListElementComponent',
36-
createSidebarSearchListElementTests(CommunitySidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description'),
39+
createSidebarSearchListElementTests(CommunitySidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description', [], true),
40+
);
41+
42+
describe('CommunitySidebarSearchListElementComponent - hierarchical path',
43+
createHierarchicalParentTitleTests(CommunitySidebarSearchListElementComponent, object, 'title'),
3744
);

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@ import {
33
NgClass,
44
} from '@angular/common';
55
import { Component } from '@angular/core';
6+
import { DSOBreadcrumbsService } from '@dspace/core/breadcrumbs/dso-breadcrumbs.service';
7+
import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service';
8+
import { LinkService } from '@dspace/core/cache/builders/link.service';
69
import { Community } from '@dspace/core/shared/community.model';
710
import { Context } from '@dspace/core/shared/context.model';
811
import { CommunitySearchResult } from '@dspace/core/shared/object-collection/community-search-result.model';
912
import { ViewMode } from '@dspace/core/shared/view-mode.model';
1013
import { TranslateModule } from '@ngx-translate/core';
14+
import { listableObjectComponent } from 'src/app/shared/object-collection/shared/listable-object/listable-object.decorator';
15+
import { TruncatableService } from 'src/app/shared/truncatable/truncatable.service';
16+
import { TruncatablePartComponent } from 'src/app/shared/truncatable/truncatable-part/truncatable-part.component';
1117

12-
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
13-
import { TruncatablePartComponent } from '../../../truncatable/truncatable-part/truncatable-part.component';
1418
import { SidebarSearchListElementComponent } from '../sidebar-search-list-element.component';
1519

20+
1621
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.SideBarSearchModal)
1722
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.SideBarSearchModalCurrent)
1823
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.ScopeSelectorModal)
@@ -31,6 +36,16 @@ import { SidebarSearchListElementComponent } from '../sidebar-search-list-elemen
3136
* Component displaying a list element for a {@link CommunitySearchResult} within the context of a sidebar search modal
3237
*/
3338
export class CommunitySidebarSearchListElementComponent extends SidebarSearchListElementComponent<CommunitySearchResult, Community> {
39+
40+
constructor(
41+
protected truncatableService: TruncatableService,
42+
protected linkService: LinkService,
43+
public dsoNameService: DSONameService,
44+
protected dsoBreadcrumbsService: DSOBreadcrumbsService,
45+
) {
46+
super(truncatableService, linkService, dsoNameService, dsoBreadcrumbsService);
47+
}
48+
3449
/**
3550
* Get the description of the Community by returning its abstract
3651
*/

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<ds-truncatable-part [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'" [showToggle]="false">
2+
@let parentTitle = parentTitle$ | async;
23
<div [ngClass]="isCurrent() ? 'text-light' : 'text-body'"
3-
[innerHTML]="(parentTitle$ && parentTitle$ | async) ? (parentTitle$ | async) : ('home.breadcrumbs' | translate)"></div>
4+
[title]="parentTitle || ('home.breadcrumbs' | translate)"
5+
[innerHTML]="parentTitle || ('home.breadcrumbs' | translate)"></div>
46
</ds-truncatable-part>
57
<ds-truncatable-part [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'" [showToggle]="false">
68
<div class="fw-bold"

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

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,28 @@ import {
66
} from '@angular/core/testing';
77
import { RouterTestingModule } from '@angular/router/testing';
88
import { APP_CONFIG } from '@dspace/config/app-config.interface';
9+
import { DSOBreadcrumbsService } from '@dspace/core/breadcrumbs/dso-breadcrumbs.service';
910
import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service';
11+
import { Breadcrumb } from '@dspace/core/breadcrumbs/models/breadcrumb.model';
1012
import { LinkService } from '@dspace/core/cache/builders/link.service';
1113
import { ChildHALResource } from '@dspace/core/shared/child-hal-resource.model';
1214
import { DSpaceObject } from '@dspace/core/shared/dspace-object.model';
1315
import { HALResource } from '@dspace/core/shared/hal-resource.model';
16+
import { ResourceType } from '@dspace/core/shared/resource-type';
1417
import { SearchResult } from '@dspace/core/shared/search/models/search-result.model';
1518
import { mockTruncatableService } from '@dspace/core/testing/mock-trucatable.service';
16-
import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils';
19+
import {
20+
createNoContentRemoteDataObject$,
21+
createSuccessfulRemoteDataObject$,
22+
} from '@dspace/core/utilities/remote-data.utils';
1723
import { TranslateModule } from '@ngx-translate/core';
24+
import { of } from 'rxjs';
1825

1926
import { TruncatableService } from '../../truncatable/truncatable.service';
2027
import { TruncatablePartComponent } from '../../truncatable/truncatable-part/truncatable-part.component';
2128
import { VarDirective } from '../../utils/var.directive';
29+
import { BREADCRUMB_SEPARATOR } from './sidebar-search-list-element.component';
30+
2231

2332
export function createSidebarSearchListElementTests(
2433
componentClass: any,
@@ -28,12 +37,14 @@ export function createSidebarSearchListElementTests(
2837
expectedTitle: string,
2938
expectedDescription: string,
3039
extraProviders: any[] = [],
40+
assertBreadcrumbsUsed = false,
3141
) {
3242
return () => {
3343
let component;
3444
let fixture: ComponentFixture<any>;
3545

3646
let linkService;
47+
let dsoBreadcrumbsService;
3748

3849
const environment = {
3950
browseBy: {
@@ -42,17 +53,33 @@ export function createSidebarSearchListElementTests(
4253
};
4354

4455
beforeEach(waitForAsync(() => {
56+
// Propagate the class-level static ResourceType onto the instance so that
57+
// the community/collection branch in getParentTitle() is reached correctly.
58+
const staticType: ResourceType | undefined = (object.indexableObject.constructor as any).type;
59+
if (staticType) {
60+
(object.indexableObject as any).type = staticType;
61+
}
62+
4563
linkService = jasmine.createSpyObj('linkService', {
4664
resolveLink: Object.assign(new HALResource(), {
4765
[object.indexableObject.getParentLinkKey()]: createSuccessfulRemoteDataObject$(parent),
4866
}),
4967
});
68+
const breadcrumbs: Breadcrumb[] = [];
69+
if (expectedParentTitle) {
70+
breadcrumbs.push(new Breadcrumb(expectedParentTitle, ''));
71+
}
72+
breadcrumbs.push(new Breadcrumb(expectedTitle, ''));
73+
dsoBreadcrumbsService = jasmine.createSpyObj('dsoBreadcrumbsService', {
74+
getBreadcrumbs: of(breadcrumbs),
75+
});
5076
TestBed.configureTestingModule({
5177
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), VarDirective],
5278
providers: [
5379
{ provide: TruncatableService, useValue: mockTruncatableService },
5480
{ provide: LinkService, useValue: linkService },
5581
{ provide: APP_CONFIG, useValue: environment },
82+
{ provide: DSOBreadcrumbsService, useValue: dsoBreadcrumbsService },
5683
DSONameService,
5784
...extraProviders,
5885
],
@@ -75,6 +102,18 @@ export function createSidebarSearchListElementTests(
75102
});
76103
});
77104

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

0 commit comments

Comments
 (0)