Skip to content

Commit f96a169

Browse files
[DURACOM-507] add tab resolver, fix missing enhancments, add labels, add missing property
1 parent da6a192 commit f96a169

18 files changed

Lines changed: 438 additions & 6 deletions

File tree

config/config.example.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,10 @@ layout:
800800
# defaultCollectionsRowStyle: ''
801801
# Whether to display collections inline (true) or in a block layout (false)
802802
isInline: true
803+
# Key/Value map to define metadata holding the style information for each entity type
804+
dynamicRefStyleMetadata:
805+
default: 'dspace.entity.style'
806+
Publication: 'example.entity.style'
803807

804808
# Configuration for customization of search results
805809
searchResults:

src/app/dynamic-layout/dynamic-layout-matrix/dynamic-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/attachment-render/types/file-download-button/file-download-button.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<a [routerLink]="bitstreamLink?.routerLink"
1212
[queryParams]="bitstreamLink?.queryParams"
1313
[target]="isBlank ? '_blank' : '_self'"
14-
[dsBtnDisabled]="(canRequestItemCopy$ | async) !== true"
14+
[dsBtnDisabled]="(canRequestACopy$ | async) !== true"
1515
class="btn btn-outline-primary"
1616
data-test="requestACopy">
1717
{{ 'dynamic-layout.advanced-attachment.requestACopy' | translate }}

src/app/dynamic-layout/dynamic-layout-matrix/dynamic-layout-box-container/boxes/metadata/rendering-types/tag/tag.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
[clickable]="false"
55
[chips]="chips"
66
[showIcons]="false"
7-
[editable]="false"></ds-chips>
7+
[editable]="false"></ds-chips>
88
}
99
</div>

src/app/init.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import {
1919
APP_CONFIG,
2020
AppConfig,
21+
DYNAMIC_FIELD_RENDERING_MAP,
2122
} from '@dspace/config/app-config.interface';
2223
import { CheckAuthenticationTokenAction } from '@dspace/core/auth/auth.actions';
2324
import { isAuthenticationBlocking } from '@dspace/core/auth/selectors';
@@ -42,6 +43,7 @@ import {
4243
import { environment } from '../environments/environment';
4344
import { AppState } from './app.reducer';
4445
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
46+
import { layoutBoxesMap } from './dynamic-layout/dynamic-layout-matrix/dynamic-layout-box-container/boxes/metadata/rendering-types/metadata-box-rendering-map';
4547
import { dsDynamicFormControlMapFn } from './shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn';
4648
import { MenuService } from './shared/menu/menu.service';
4749
import { MenuProviderService } from './shared/menu/menu-provider.service';
@@ -117,6 +119,10 @@ export abstract class InitService {
117119
provide: DYNAMIC_FORM_CONTROL_MAP_FN,
118120
useValue: dsDynamicFormControlMapFn,
119121
},
122+
{
123+
provide: DYNAMIC_FIELD_RENDERING_MAP,
124+
useValue: layoutBoxesMap,
125+
},
120126
];
121127
}
122128

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { PLATFORM_ID } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
import { Router } from '@angular/router';
4+
import { RouterTestingModule } from '@angular/router/testing';
5+
import {
6+
tabDetailsTest,
7+
tabFundingsTest,
8+
tabPublicationsTest,
9+
} from '@dspace/core/testing/layout-tab.mocks';
10+
import { createPaginatedList } from '@dspace/core/testing/utils.test';
11+
import {
12+
createSuccessfulRemoteDataObject,
13+
createSuccessfulRemoteDataObject$,
14+
} from '@dspace/core/utilities/remote-data.utils';
15+
import { Observable } from 'rxjs';
16+
import { take } from 'rxjs/operators';
17+
18+
import { ItemDataService } from '../core/data/item-data.service';
19+
import { PaginatedList } from '../core/data/paginated-list.model';
20+
import { RemoteData } from '../core/data/remote-data';
21+
import { DynamicLayoutTab } from '../core/layout/models/tab.model';
22+
import { TabDataService } from '../core/layout/tab-data.service';
23+
import { HardRedirectService } from '../core/services/hard-redirect.service';
24+
import { Item } from '../core/shared/item.model';
25+
import { dynamicItemPageTabResolver } from './dynamic-item-page-tab.resolver';
26+
27+
describe('CrisItemPageTabResolver', () => {
28+
beforeEach(() => {
29+
TestBed.configureTestingModule({
30+
imports: [RouterTestingModule.withRoutes([{
31+
path: 'entities/:entity-type/:id/:tab',
32+
component: {} as any,
33+
}])],
34+
});
35+
});
36+
37+
describe('dynamicItemPageTabResolver', () => {
38+
let itemService: jasmine.SpyObj<ItemDataService>;
39+
let tabService: jasmine.SpyObj<TabDataService>;
40+
let hardRedirectService: jasmine.SpyObj<HardRedirectService>;
41+
let router: Router;
42+
43+
const uuid = '1234-65487-12354-1235';
44+
const item = Object.assign(new Item(), {
45+
id: uuid,
46+
uuid: uuid,
47+
metadata: {
48+
'dspace.entity.type': [{ value: 'Publication' }],
49+
},
50+
});
51+
52+
const tabsRD = createSuccessfulRemoteDataObject(createPaginatedList([tabPublicationsTest, tabDetailsTest, tabFundingsTest]));
53+
const tabsRD$ = createSuccessfulRemoteDataObject$(createPaginatedList([tabPublicationsTest, tabDetailsTest, tabFundingsTest]));
54+
55+
const noTabsRD = createSuccessfulRemoteDataObject(createPaginatedList([]));
56+
const noTabsRD$ = createSuccessfulRemoteDataObject$(createPaginatedList([]));
57+
58+
beforeEach(() => {
59+
itemService = jasmine.createSpyObj('ItemDataService', ['findById']);
60+
tabService = jasmine.createSpyObj('TabDataService', ['findByItem']);
61+
hardRedirectService = jasmine.createSpyObj('HardRedirectService', ['redirect']);
62+
63+
TestBed.configureTestingModule({
64+
imports: [RouterTestingModule.withRoutes([{ path: 'entities/:entity-type/:id/:tab', component: {} as any }])],
65+
providers: [
66+
{ provide: ItemDataService, useValue: itemService },
67+
{ provide: TabDataService, useValue: tabService },
68+
{ provide: HardRedirectService, useValue: hardRedirectService },
69+
{ provide: PLATFORM_ID, useValue: 'browser' },
70+
],
71+
});
72+
73+
router = TestBed.inject(Router);
74+
});
75+
76+
describe('when item exists', () => {
77+
beforeEach(() => {
78+
itemService.findById.and.returnValue(createSuccessfulRemoteDataObject$(item));
79+
});
80+
81+
describe('and there are tabs', () => {
82+
beforeEach(() => {
83+
tabService.findByItem.and.returnValue(tabsRD$);
84+
spyOn(router, 'navigateByUrl');
85+
});
86+
87+
it('should redirect to root route if given tab is the first one', (done) => {
88+
const obs = TestBed.runInInjectionContext(() => {
89+
return dynamicItemPageTabResolver({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235/publications' } as any);
90+
}) as Observable<RemoteData<PaginatedList<DynamicLayoutTab>>>;
91+
92+
obs.pipe(take(1)).subscribe((resolved) => {
93+
expect(router.navigateByUrl).toHaveBeenCalledWith('/entities/publication/1234-65487-12354-1235');
94+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
95+
expect(resolved).toEqual(tabsRD);
96+
done();
97+
});
98+
});
99+
100+
it('should not redirect to root route if tab different than the main one is given', (done) => {
101+
const obs = TestBed.runInInjectionContext(() => {
102+
return dynamicItemPageTabResolver({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235/details' } as any);
103+
}) as Observable<RemoteData<PaginatedList<DynamicLayoutTab>>>;
104+
105+
obs.pipe(take(1)).subscribe((resolved) => {
106+
expect(router.navigateByUrl).not.toHaveBeenCalled();
107+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
108+
expect(resolved).toEqual(tabsRD);
109+
done();
110+
});
111+
});
112+
113+
it('should not redirect to root route if no tab is given', (done) => {
114+
const obs = TestBed.runInInjectionContext(() => {
115+
return dynamicItemPageTabResolver({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235' } as any);
116+
}) as Observable<RemoteData<PaginatedList<DynamicLayoutTab>>>;
117+
118+
obs.pipe(take(1)).subscribe((resolved) => {
119+
expect(router.navigateByUrl).not.toHaveBeenCalled();
120+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
121+
expect(resolved).toEqual(tabsRD);
122+
done();
123+
});
124+
});
125+
126+
it('should navigate to 404 if a wrong tab is given', (done) => {
127+
const obs = TestBed.runInInjectionContext(() => {
128+
return dynamicItemPageTabResolver({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235/test' } as any);
129+
}) as Observable<RemoteData<PaginatedList<DynamicLayoutTab>>>;
130+
131+
obs.pipe(take(1)).subscribe((resolved) => {
132+
expect(router.navigateByUrl).toHaveBeenCalled();
133+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
134+
expect(resolved).toEqual(tabsRD);
135+
done();
136+
});
137+
});
138+
139+
it('Should handle tab shortnames with "::" correctly', (done) => {
140+
const tabPageList = createPaginatedList([{
141+
...tabPublicationsTest,
142+
shortname: 'publication::details',
143+
}]);
144+
const tabRD = createSuccessfulRemoteDataObject(tabPageList);
145+
tabService.findByItem.and.returnValue(createSuccessfulRemoteDataObject$(tabPageList) as any);
146+
147+
const obs = TestBed.runInInjectionContext(() => {
148+
return dynamicItemPageTabResolver({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235/details' } as any);
149+
}) as Observable<RemoteData<PaginatedList<DynamicLayoutTab>>>;
150+
151+
obs.pipe(take(1)).subscribe((resolved) => {
152+
expect(router.navigateByUrl).not.toHaveBeenCalled();
153+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
154+
expect(resolved).toEqual(tabRD);
155+
done();
156+
});
157+
});
158+
159+
it('should NOT redirect to 404 when query params are present in the URL', (done) => {
160+
const obs = TestBed.runInInjectionContext(() => {
161+
return dynamicItemPageTabResolver({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235?f.subject=value&f.date=2024' } as any);
162+
}) as Observable<RemoteData<PaginatedList<DynamicLayoutTab>>>;
163+
164+
obs.pipe(take(1)).subscribe((resolved) => {
165+
expect(router.navigateByUrl).not.toHaveBeenCalled();
166+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
167+
expect(resolved).toEqual(tabsRD);
168+
done();
169+
});
170+
});
171+
172+
it('should NOT redirect to 404 when query params are present with a valid tab', (done) => {
173+
const obs = TestBed.runInInjectionContext(() => {
174+
return dynamicItemPageTabResolver({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235/details?f.subject=value' } as any);
175+
}) as Observable<RemoteData<PaginatedList<DynamicLayoutTab>>>;
176+
177+
obs.pipe(take(1)).subscribe((resolved) => {
178+
expect(router.navigateByUrl).not.toHaveBeenCalled();
179+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
180+
expect(resolved).toEqual(tabsRD);
181+
done();
182+
});
183+
});
184+
});
185+
186+
describe('and there are no tabs', () => {
187+
beforeEach(() => {
188+
tabService.findByItem.and.returnValue(noTabsRD$);
189+
spyOn(router, 'navigateByUrl');
190+
});
191+
192+
it('should not redirect nor navigate', (done) => {
193+
const obs = TestBed.runInInjectionContext(() => {
194+
return dynamicItemPageTabResolver({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235' } as any);
195+
}) as Observable<RemoteData<PaginatedList<DynamicLayoutTab>>>;
196+
197+
obs.pipe(take(1)).subscribe((resolved) => {
198+
expect(router.navigateByUrl).not.toHaveBeenCalled();
199+
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
200+
expect(resolved).toEqual(noTabsRD);
201+
done();
202+
});
203+
});
204+
});
205+
});
206+
});
207+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { isPlatformServer } from '@angular/common';
2+
import {
3+
inject,
4+
PLATFORM_ID,
5+
} from '@angular/core';
6+
import {
7+
ActivatedRouteSnapshot,
8+
ResolveFn,
9+
Router,
10+
RouterStateSnapshot,
11+
} from '@angular/router';
12+
import { getPageNotFoundRoute } from '@dspace/core/router/core-routing-paths';
13+
import { getItemPageRoute } from '@dspace/core/router/utils/dso-route.utils';
14+
import { createFailedRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils';
15+
import { Observable } from 'rxjs';
16+
import {
17+
map,
18+
switchMap,
19+
} from 'rxjs/operators';
20+
21+
import { ItemDataService } from '../core/data/item-data.service';
22+
import { PaginatedList } from '../core/data/paginated-list.model';
23+
import { RemoteData } from '../core/data/remote-data';
24+
import { DynamicLayoutTab } from '../core/layout/models/tab.model';
25+
import { TabDataService } from '../core/layout/tab-data.service';
26+
import { HardRedirectService } from '../core/services/hard-redirect.service';
27+
import { Item } from '../core/shared/item.model';
28+
import { getFirstCompletedRemoteData } from '../core/shared/operators';
29+
30+
/**
31+
* Method for resolving the tabs of item based on the parameters in the current route
32+
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
33+
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
34+
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
35+
* or an error if something went wrong
36+
*/
37+
export const dynamicItemPageTabResolver: ResolveFn<RemoteData<PaginatedList<DynamicLayoutTab>>> = (
38+
route: ActivatedRouteSnapshot,
39+
state: RouterStateSnapshot,
40+
): Observable<RemoteData<PaginatedList<DynamicLayoutTab>>> => {
41+
const platformId = inject(PLATFORM_ID);
42+
const hardRedirectService = inject(HardRedirectService);
43+
const tabService = inject(TabDataService);
44+
const itemDataService = inject(ItemDataService);
45+
const router = inject(Router);
46+
47+
return itemDataService.findById(route.params.id).pipe(
48+
getFirstCompletedRemoteData(),
49+
switchMap((itemRD: RemoteData<Item>) => {
50+
if (itemRD.hasSucceeded && itemRD.statusCode === 200) {
51+
return tabService.findByItem(
52+
itemRD.payload.uuid,
53+
true,
54+
true,
55+
).pipe(
56+
getFirstCompletedRemoteData(),
57+
map((tabsRD: RemoteData<PaginatedList<DynamicLayoutTab>>) => {
58+
if (tabsRD.hasSucceeded && tabsRD?.payload?.page?.length > 0) {
59+
// By splitting the url with uuid we can understand if the item is primary item page or a tab
60+
const urlWithoutQuery = state.url.split('?')[0];
61+
const urlSplit = urlWithoutQuery.split(route.params.id);
62+
const tabArguments = urlSplit[1]?.split('/');
63+
const givenTab = tabArguments?.[1];
64+
const itemPageRoute = getItemPageRoute(itemRD.payload);
65+
66+
const isValidTab = !givenTab || tabsRD.payload.page.some((tab) => {
67+
const shortnameSplit = tab.shortname.split('::');
68+
const shortname = shortnameSplit[shortnameSplit.length - 1];
69+
return shortname === givenTab;
70+
});
71+
72+
const mainTab = tabsRD.payload.page.length === 1
73+
? tabsRD.payload.page[0]
74+
: tabsRD.payload.page.find(tab => !tab.leading);
75+
76+
if (!isValidTab) {
77+
// If wrong tab is given redirect to 404 page
78+
router.navigateByUrl(getPageNotFoundRoute(), { skipLocationChange: true, replaceUrl: false });
79+
} else if (givenTab === mainTab.shortname) {
80+
if (isPlatformServer(platformId)) {
81+
// If first tab is given redirect to root item page
82+
hardRedirectService.redirect(itemPageRoute, 302);
83+
} else {
84+
router.navigateByUrl(itemPageRoute);
85+
}
86+
}
87+
}
88+
return tabsRD;
89+
}),
90+
);
91+
} else {
92+
return createFailedRemoteDataObject$<PaginatedList<DynamicLayoutTab>>(itemRD?.errorMessage, itemRD?.statusCode, itemRD?.timeCompleted);
93+
}
94+
}),
95+
);
96+
};

src/app/item-page/item-page-routes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MenuRoute } from '../shared/menu/menu-route.model';
1010
import { viewTrackerResolver } from '../statistics/angulartics/dspace/view-tracker.resolver';
1111
import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component';
1212
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
13+
import { dynamicItemPageTabResolver } from './dynamic-item-page-tab.resolver';
1314
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
1415
import { itemPageResolver } from './item-page.resolver';
1516
import {
@@ -60,6 +61,7 @@ export const ROUTES: Route[] = [
6061
menuRoute: MenuRoute.ITEM_PAGE,
6162
},
6263
resolve: {
64+
tabs: dynamicItemPageTabResolver,
6365
tracking: viewTrackerResolver,
6466
},
6567
},
@@ -107,6 +109,13 @@ export const ROUTES: Route[] = [
107109
menu: accessTokenResolver,
108110
},
109111
},
112+
{
113+
path: ':tab',
114+
component: ThemedItemPageComponent,
115+
resolve: {
116+
tabs: dynamicItemPageTabResolver,
117+
},
118+
},
110119
],
111120
},
112121
];

src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</div>
2121
}
2222
<!-- CC name is always displayed if the image fails to load -->
23-
@if (showName || !showImage) {
23+
@if ((showName || !showImage) && showUrl) {
2424
<div [ngClass]="{ 'row': variant === 'small', 'col': variant === 'full' }">
2525
<span>
2626
{{ showDisclaimer ? ('item.page.cc.license.disclaimer' | translate) : '' }}

0 commit comments

Comments
 (0)