Skip to content

Commit 915c51d

Browse files
milanmajchrakclaude
andcommitted
Port #1326 to dtq-dev-9-base: Add search icon to community and collection
The DSO action menu of a Community or Collection gains a scoped-search entry that opens /search restricted to that container, on the first page (/search?spc.page=1&scope=<uuid>). This is a rewrite, not a translation. On 7.x the entry was one more object in the array returned by DSOEditMenuResolver (`id: 'search-dso'`, `index: 3`). That resolver does not exist on v9 - the DSO edit menu is assembled from menu providers registered in app.menus.ts - so the port is: * new `ComColSearchMenuProvider extends DSpaceObjectPageMenuProvider`, modelled on the sibling `comcol-subscribe.menu.ts`, returning one LINK section { text: 'search.title', link: '/search', queryParams: { 'spc.page': '1', scope: dso.uuid }, icon: 'search' }; * registered FIRST inside `DsoOptionMenuProvider.withSubs([...])` with `.onRoute(MenuRoute.COMMUNITY_PAGE, MenuRoute.COLLECTION_PAGE)` - ordering on v9 comes from the position in withSubs([...]), which is why 7.x's `index: 3` is dropped rather than translated. `id: 'search-dso'` and `active: false` are dropped for the same reason: `PartialMenuSection.id` is optional and auto-assigned, and no sibling provider sets either; * `[queryParams]="itemModel.queryParams"` added to the LINK anchor in dso-edit-menu-section.component.html. `LinkMenuItemModel.queryParams` already exists on 9-base (vanilla) but the DSO-edit renderer silently dropped it, so this one line is what makes the model half work. The 7.x resolver-spec hunks have no target on v9 and are replaced by the new provider spec (2 cases). The two page specs the source commit also adds (community-page / collection-page) are deliberately NOT ported: they do not exist on 9-base or on dspace-9.3, they are written pre-standalone, and their single assertion is `expect(query(By.css('ds-dso-edit-menu'))).toBeTruthy()`, which does not exercise this feature at all. UX note, decided by the repository owner (decision O-4): on v9 every DSO_EDIT sub-provider is nested under DsoOptionMenuProvider, so the magnifier is an entry inside the "options" dropdown rather than a button next to Subscribe/Edit. The new section is visible unconditionally (a scoped search was public on 7.x too), which flips `hasSubSections$` from false to true for anonymous visitors and therefore makes the "options" button itself visible to them for the first time. That is accepted: the feature is unchanged, only the chrome around it is new. Source: fecfa07 (dtq-dev PR #1326) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent bba4d90 commit 915c51d

5 files changed

Lines changed: 119 additions & 1 deletion

File tree

src/app/app.menus.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.me
1313
import { BrowseMenuProvider } from './shared/menu/providers/browse.menu';
1414
import { ClarinAdminMenuProvider } from './shared/menu/providers/clarin-admin.menu';
1515
import { CoarNotifyMenuProvider } from './shared/menu/providers/coar-notify.menu';
16+
import { ComColSearchMenuProvider } from './shared/menu/providers/comcol-search.menu';
1617
import { SubscribeMenuProvider } from './shared/menu/providers/comcol-subscribe.menu';
1718
import { CommunityListMenuProvider } from './shared/menu/providers/community-list.menu';
1819
import { CreateReportMenuProvider } from './shared/menu/providers/create-report.menu';
@@ -81,6 +82,11 @@ export const MENUS = buildMenuStructure({
8182
],
8283
[MenuID.DSO_EDIT]: [
8384
DsoOptionMenuProvider.withSubs([
85+
// CLARIN/LINDAT: scoped search on the container's own page
86+
ComColSearchMenuProvider.onRoute(
87+
MenuRoute.COMMUNITY_PAGE,
88+
MenuRoute.COLLECTION_PAGE,
89+
),
8490
SubscribeMenuProvider.onRoute(
8591
MenuRoute.COMMUNITY_PAGE,
8692
MenuRoute.COLLECTION_PAGE,

src/app/shared/dso-page/dso-edit-menu/dso-edit-menu-section/dso-edit-menu-section.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
[ngbTooltip]="itemModel.text | translate">
44
@if (!section.model.disabled) {
55
<a class="btn btn-dark btn-sm"
6-
[routerLink]="itemModel.link">
6+
[routerLink]="itemModel.link"
7+
[queryParams]="itemModel.queryParams">
78
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
89
<span class="sr-only">{{itemModel.text | translate}}</span>
910
</a>

src/app/shared/dso-page/dso-edit-menu/dso-edit-menu-section/dso-edit-menu-section.component.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { By } from '@angular/platform-browser';
88
import {
99
ActivatedRoute,
1010
Router,
11+
RouterLink,
1112
} from '@angular/router';
1213
import { TranslateModule } from '@ngx-translate/core';
1314
import { of } from 'rxjs';
@@ -73,6 +74,9 @@ describe('DsoEditMenuSectionComponent', () => {
7374
disabled: false,
7475
text: 'text',
7576
link: 'link',
77+
queryParams: {
78+
scope: 'test-scope-id',
79+
},
7680
},
7781
icon: iconString,
7882
};
@@ -173,6 +177,15 @@ describe('DsoEditMenuSectionComponent', () => {
173177
expect(fixture.debugElement.query(By.css('a'))).not.toBeNull();
174178
});
175179

180+
it('should bind queryParams on the link element', () => {
181+
const link = fixture.debugElement.query(By.css('a'));
182+
183+
// v9: RouterLink is a standalone directive, so its inputs are no longer mirrored into
184+
// DebugElement.properties (that map is the raw DOM element on Angular 20) - read the
185+
// directive instance instead, which is what the 7.x `link.properties.queryParams` meant.
186+
expect(link.injector.get(RouterLink).queryParams).toEqual({ scope: 'test-scope-id' });
187+
});
188+
176189
});
177190
});
178191

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { Collection } from '../../../core/shared/collection.model';
4+
import { Community } from '../../../core/shared/community.model';
5+
import { LinkMenuItemModel } from '../menu-item/models/link.model';
6+
import { MenuItemType } from '../menu-item-type.model';
7+
import { ComColSearchMenuProvider } from './comcol-search.menu';
8+
9+
describe('ComColSearchMenuProvider', () => {
10+
11+
let provider: ComColSearchMenuProvider;
12+
13+
const community: Community = Object.assign(new Community(), { uuid: 'test-community-uuid' });
14+
const collection: Collection = Object.assign(new Collection(), { uuid: 'test-collection-uuid' });
15+
16+
beforeEach(() => {
17+
TestBed.configureTestingModule({
18+
providers: [
19+
ComColSearchMenuProvider,
20+
],
21+
});
22+
provider = TestBed.inject(ComColSearchMenuProvider);
23+
});
24+
25+
describe('getSectionsForContext', () => {
26+
it('should return a single visible scoped-search link section for a community', (done) => {
27+
provider.getSectionsForContext(community).subscribe((sections) => {
28+
expect(sections.length).toEqual(1);
29+
expect(sections[0].visible).toBeTrue();
30+
expect(sections[0].icon).toEqual('search');
31+
expect(sections[0].model.type).toEqual(MenuItemType.LINK);
32+
expect((sections[0].model as LinkMenuItemModel).text).toEqual('search.title');
33+
expect((sections[0].model as LinkMenuItemModel).link).toEqual('/search');
34+
expect((sections[0].model as LinkMenuItemModel).queryParams['spc.page']).toEqual('1');
35+
expect((sections[0].model as LinkMenuItemModel).queryParams.scope).toEqual('test-community-uuid');
36+
done();
37+
});
38+
});
39+
40+
it('should scope the search to the collection uuid when the context is a collection', (done) => {
41+
provider.getSectionsForContext(collection).subscribe((sections) => {
42+
expect(sections.length).toEqual(1);
43+
expect(sections[0].visible).toBeTrue();
44+
expect((sections[0].model as LinkMenuItemModel).queryParams.scope).toEqual('test-collection-uuid');
45+
done();
46+
});
47+
});
48+
});
49+
50+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* The contents of this file are subject to the license and copyright
3+
* detailed in the LICENSE and NOTICE files at the root of the source
4+
* tree and available online at
5+
*
6+
* http://www.dspace.org/license/
7+
*/
8+
import { Injectable } from '@angular/core';
9+
import {
10+
Observable,
11+
of,
12+
} from 'rxjs';
13+
14+
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
15+
import { LinkMenuItemModel } from '../menu-item/models/link.model';
16+
import { MenuItemType } from '../menu-item-type.model';
17+
import { PartialMenuSection } from '../menu-provider.model';
18+
import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu';
19+
20+
/**
21+
* CLARIN/LINDAT: menu provider to create the scoped-search option in the DSO edit menu of a
22+
* Community or Collection. It opens /search restricted to that container, on the first page.
23+
*
24+
* The section is visible unconditionally - a scoped search is a public action, exactly as it was on
25+
* 7.x, where the same entry was added by DSOEditMenuResolver without an authorization check.
26+
*/
27+
@Injectable()
28+
export class ComColSearchMenuProvider extends DSpaceObjectPageMenuProvider {
29+
30+
public getSectionsForContext(dso: DSpaceObject): Observable<PartialMenuSection[]> {
31+
return of([
32+
{
33+
visible: true,
34+
model: {
35+
type: MenuItemType.LINK,
36+
disabled: false,
37+
text: 'search.title',
38+
link: '/search',
39+
queryParams: {
40+
'spc.page': '1',
41+
scope: dso.uuid,
42+
},
43+
} as LinkMenuItemModel,
44+
icon: 'search',
45+
},
46+
] as PartialMenuSection[]);
47+
}
48+
}

0 commit comments

Comments
 (0)