Skip to content

Commit c5038f0

Browse files
Andrea Barbassovins01-4science
authored andcommitted
Merged in task/ux-plus-2023_02_x/UXP-240_squashed (pull request #50)
[UXP-240] use skeletons in ssr, get glam components Approved-by: Francesco Molinaro
2 parents 2e9d271 + 532522e commit c5038f0

File tree

64 files changed

+3016
-269
lines changed

Some content is hidden

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

64 files changed

+3016
-269
lines changed

src/app/core/layout/models/section.model.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { autoserialize, deserialize } from 'cerialize';
55
import { HALLink } from '../../shared/hal-link.model';
66
import { excludeFromEquals } from '../../utilities/equals.decorators';
77
import { ResourceType } from '../../shared/resource-type';
8+
import { SortDirection } from '../../cache/models/sort-options.model';
89

910
/**
1011
* Describes a type of Section.
@@ -71,8 +72,10 @@ export interface TopSection extends SectionComponent {
7172
}
7273

7374
export interface GridSection extends SectionComponent {
75+
order: SortDirection;
76+
sortField: string;
7477
discoveryConfigurationName: string;
75-
'main-content-link': string;
78+
mainContentLink: string;
7679
}
7780

7881
export interface SearchSection extends SectionComponent {
@@ -110,14 +113,6 @@ export interface TopSectionColumn {
110113
titleKey: string;
111114
}
112115

113-
/**
114-
* Represents the type of template to use for the section
115-
*/
116-
export enum TopSectionTemplateType {
117-
DEFAULT = 'default', // CRIS default template
118-
CARD = 'card', // Card template
119-
}
120-
121116
export enum LayoutModeEnum {
122117
LIST = 'list',
123118
CARD = 'card'
@@ -142,4 +137,53 @@ export interface CarouselSection extends SectionComponent {
142137
captionStyle: string;
143138
titleStyle: string;
144139
bundle: string;
140+
showBlurryBackdrop: boolean;
141+
}
142+
143+
144+
145+
export interface SliderSection extends SectionComponent {
146+
discoveryConfigurationName: string;
147+
order: string;
148+
sortField: string;
149+
numberOfItems: number;
150+
style: string;
151+
title: string;
152+
link: string;
153+
description: string;
154+
componentType: 'slider';
155+
targetBlank: boolean ;
156+
fitWidth: boolean;
157+
fitHeight: boolean;
158+
keepAspectRatio: boolean;
159+
aspectRatio: number;
160+
carouselHeightPx: number;
161+
captionStyle: string;
162+
titleStyle: string;
163+
showBlurryBackdrop: boolean;
164+
}
165+
166+
/**
167+
* Represents an advanced top section in the layout.
168+
*/
169+
export interface AdvancedTopSection extends Omit<TopSection, 'discoveryConfigurationName'|'componentType'> {
170+
/**
171+
* The names of the discovery configurations.
172+
*/
173+
discoveryConfigurationName: string[];
174+
175+
/**
176+
* The component type, which is always 'advanced-top-component'.
177+
*/
178+
componentType: 'advanced-top-component';
179+
}
180+
181+
/*
182+
* Represents the type of template to use for the section
183+
*/
184+
export enum TopSectionTemplateType {
185+
DEFAULT = 'default', // CRIS default template
186+
IMAGES = 'images',
187+
SLIDER = 'slider',
188+
CARD = 'card',
145189
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { inject, Injectable } from '@angular/core';
2+
3+
import { from, Observable } from 'rxjs';
4+
import { filter, map, mergeMap, reduce, switchMap, take } from 'rxjs/operators';
5+
6+
import { followLink } from '../../shared/utils/follow-link-config.model';
7+
import { getFirstCompletedRemoteData } from '../shared/operators';
8+
import { RemoteData } from '../data/remote-data';
9+
import { PaginatedList } from '../data/paginated-list.model';
10+
import { Bitstream } from '../shared/bitstream.model';
11+
import { BitstreamFormat } from '../shared/bitstream-format.model';
12+
import { hasValue } from '../../shared/empty.util';
13+
import { BitstreamDataService } from '../data/bitstream-data.service';
14+
import { Item } from '../shared/item.model';
15+
import { DSpaceObject } from '../shared/dspace-object.model';
16+
17+
interface ItemAndImage {
18+
itemUUID: string;
19+
imageHref: string;
20+
}
21+
22+
@Injectable({ providedIn: 'root' })
23+
export class BitstreamImagesService {
24+
25+
private readonly bitstreamDataService = inject(BitstreamDataService);
26+
27+
/**
28+
* Retrieve all items and their image bitstreams
29+
* @param items
30+
* @param bundleName
31+
*/
32+
getItemToImageMap(items: Item[], bundleName = 'ORIGINAL'): Observable<Map<string, string>> {
33+
return from(items).pipe(
34+
mergeMap((item) => this.findImageBitstreams(item, bundleName).pipe(
35+
take(1),
36+
map((bitstream: Bitstream) => <ItemAndImage>{
37+
itemUUID: item.uuid, imageHref: bitstream._links.content.href
38+
}),
39+
)),
40+
reduce((acc: Map<string, string>, value: ItemAndImage) => {
41+
acc.set(value.itemUUID, value.imageHref);
42+
return acc;
43+
}, new Map<string, string>()),
44+
);
45+
}
46+
47+
/**
48+
* Find all image bitstreams for an item
49+
* @param item the item for which the images should be retrieved
50+
* @param bundleName the bundle name (ORIGINAL by default)
51+
*/
52+
findImageBitstreams(item: Item | DSpaceObject, bundleName = 'ORIGINAL') {
53+
const isImageMimetypeRegex = /^image\//;
54+
55+
// retrieve all bundle's bitstreams for the item
56+
const bitstreamPayload$: Observable<Bitstream> = this.bitstreamDataService.showableByItem(
57+
item.uuid, bundleName, [], {}, true, true, followLink('format'),
58+
).pipe(
59+
getFirstCompletedRemoteData(),
60+
switchMap((rd: RemoteData<PaginatedList<Bitstream>>) => rd.hasSucceeded ? rd.payload.page : new Array<Bitstream>()),
61+
);
62+
63+
// filter bitstreams according to mime type
64+
return bitstreamPayload$.pipe(
65+
switchMap((bitstream: Bitstream) => bitstream.format.pipe(
66+
getFirstCompletedRemoteData(),
67+
filter((bitstreamFormatRD: RemoteData<BitstreamFormat>) =>
68+
bitstreamFormatRD.hasSucceeded && hasValue(bitstreamFormatRD.payload) && hasValue(bitstream) &&
69+
isImageMimetypeRegex.test(bitstreamFormatRD.payload.mimetype)
70+
),
71+
map(() => bitstream)
72+
)),
73+
);
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<div [class]="field.styleValue">
2-
<img *ngIf="(imageUrl$ | async) as url" [src]="url">
2+
<img *ngIf="isBrowser && !isLoading; else imageSkeleton" [src]="imageUrl$ | async">
3+
<ng-template #imageSkeleton>
4+
<ngx-skeleton-loader style="width: 100%; height: 100%;" [theme]="{height: '100%', width: '100%'}"></ngx-skeleton-loader>
5+
</ng-template>
36
</div>
47

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component, inject, OnInit, PLATFORM_ID } from '@angular/core';
22
import { FieldRenderingType, MetadataBoxFieldRendering } from '../metadata-box.decorator';
33

44
import { BitstreamRenderingModelComponent } from '../bitstream-rendering-model';
5-
import { map } from 'rxjs/operators';
5+
import { catchError, map } from 'rxjs/operators';
66
import { Bitstream } from '../../../../../../../core/shared/bitstream.model';
7-
import { BehaviorSubject, Observable } from 'rxjs';
7+
import { BehaviorSubject, Observable, of } from 'rxjs';
88
import { getPaginatedListPayload } from '../../../../../../../core/shared/operators';
9+
import { isPlatformBrowser } from '@angular/common';
910

1011
@Component({
1112
// eslint-disable-next-line @angular-eslint/component-selector
@@ -20,12 +21,23 @@ export class ImageComponent extends BitstreamRenderingModelComponent implements
2021

2122
imageUrl$: Observable<string>;
2223

24+
platformId = inject(PLATFORM_ID);
25+
26+
isLoading = true;
27+
isBrowser: boolean;
28+
2329
ngOnInit(): void {
30+
this.isBrowser = isPlatformBrowser(this.platformId);
2431
this.getBitstreamsByItem().pipe(
2532
getPaginatedListPayload(),
26-
map((filteredBitstreams: Bitstream[]) => filteredBitstreams.length > 0 ? filteredBitstreams[0] : null)
33+
map((filteredBitstreams: Bitstream[]) => filteredBitstreams.length > 0 ? filteredBitstreams[0] : null),
34+
catchError(() => {
35+
this.isLoading = false;
36+
return of(null);
37+
}),
2738
).subscribe((image) => {
2839
this.bitstream.next(image);
40+
this.isLoading = false;
2941
});
3042

3143
this.imageUrl$ = this.bitstream.asObservable().pipe(
@@ -34,8 +46,4 @@ export class ImageComponent extends BitstreamRenderingModelComponent implements
3446

3547
}
3648

37-
backgroundImageUrl(url: string) {
38-
return `url('${url}')`;
39-
}
40-
4149
}

src/app/cris-layout/cris-layout.module.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ import {
161161
import {
162162
ImageComponent
163163
} from './cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/image/image.component';
164+
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
164165

165166
const ENTRY_COMPONENTS = [
166167
// put only entry components that use custom decorator
@@ -236,25 +237,26 @@ const ENTRY_COMPONENTS = [
236237
ImageComponent
237238
],
238239
providers:[ LoadMoreService, NgbActiveModal ],
239-
imports: [
240-
CommonModule,
241-
SharedModule,
242-
SearchModule.withEntryComponents(),
243-
GooglemapsModule,
244-
OpenStreetMapModule,
245-
MyDSpacePageModule,
246-
ContextMenuModule.withEntryComponents(),
247-
NgbAccordionModule,
248-
ComcolModule,
249-
MiradorViewerModule,
250-
MarkdownViewerModule,
251-
ItemSharedModule,
252-
ViewersSharedModule,
253-
MetricsModule,
254-
AttachmentRenderingModule,
255-
FormModule,
256-
MediaPlayerModule,
257-
],
240+
imports: [
241+
CommonModule,
242+
SharedModule,
243+
SearchModule.withEntryComponents(),
244+
GooglemapsModule,
245+
OpenStreetMapModule,
246+
MyDSpacePageModule,
247+
ContextMenuModule.withEntryComponents(),
248+
NgbAccordionModule,
249+
ComcolModule,
250+
MiradorViewerModule,
251+
MarkdownViewerModule,
252+
ItemSharedModule,
253+
ViewersSharedModule,
254+
MetricsModule,
255+
AttachmentRenderingModule,
256+
FormModule,
257+
MediaPlayerModule,
258+
NgxSkeletonLoaderModule,
259+
],
258260
exports: [
259261
CrisLayoutComponent,
260262
CrisrefComponent,

src/app/home-page/home-page.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
[sectionId]="sectionId"
5656
[twitterSection]="$any(sectionComponent)"></ds-themed-twitter-section>
5757

58+
<ds-themed-advanced-top-section *ngSwitchCase="'advanced-top-component'"
59+
[sectionId]="sectionId"
60+
[advancedTopSection]="$any(sectionComponent)"></ds-themed-advanced-top-section>
61+
5862
</div>
5963
</div>
6064
</div>

src/app/shared/browse-most-elements/abstract-browse-elements.component.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { followLink } from '../utils/follow-link-config.model';
22
import { CollectionElementLinkType } from '../object-collection/collection-element-link.type';
33
import { Component, Input, OnChanges, OnInit, PLATFORM_ID, inject } from '@angular/core';
4-
import { isPlatformServer } from '@angular/common';
4+
import { isPlatformBrowser } from '@angular/common';
55

66
import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model';
77
import { DSpaceObject } from '../../core/shared/dspace-object.model';
@@ -21,6 +21,7 @@ import { Item } from '../../core/shared/item.model';
2121
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
2222
import { LayoutModeEnum, TopSection } from '../../core/layout/models/section.model';
2323
import { SearchManager } from '../../core/browse/search-manager';
24+
import { tap } from 'rxjs/operators';
2425

2526
@Component({
2627
template: ''
@@ -79,12 +80,17 @@ export abstract class AbstractBrowseElementsComponent implements OnInit, OnChang
7980

8081
searchResultArray$: Observable<DSpaceObject[]>;
8182

83+
isLoading = true;
84+
isBrowser: boolean;
85+
8286
ngOnChanges() {
8387
this.paginatedSearchOptions$?.next(this.paginatedSearchOptions);
8488
}
8589

8690
ngOnInit() {
87-
if (isPlatformServer(this.platformId)) {
91+
this.isBrowser = isPlatformBrowser(this.platformId);
92+
93+
if (!this.isBrowser) {
8894
return;
8995
}
9096
const followLinks = [];
@@ -112,6 +118,9 @@ export abstract class AbstractBrowseElementsComponent implements OnInit, OnChang
112118
toDSpaceObjectListRD(),
113119
getRemoteDataPayload(),
114120
getPaginatedListPayload(),
121+
tap(() => {
122+
this.isLoading = false;
123+
}),
115124
);
116125
}
117126

src/app/shared/browse-most-elements/browse-most-elements.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
</div>
2525
<div
2626
class="row justify-content-center"
27-
*ngIf="topSection && topSection.showAllResults"
28-
>
27+
*ngIf="topSection.showAllResults && (discoveryConfigurationsTotalElementsMap == null || discoveryConfigurationsTotalElementsMap.get(paginatedSearchOptions.configuration)) > 0" class="d-flex justify-content-center">
2928
<button type="button" class="btn btn-link" (click)="showAllResults()">
3029
{{ "home.top-section.view-results" | translate }}
3130
</button>

src/app/shared/browse-most-elements/browse-most-elements.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export class BrowseMostElementsComponent implements OnInit, OnChanges {
5353
*/
5454
@Input() topSection: TopSection;
5555

56+
@Input() discoveryConfigurationsTotalElementsMap: Map<string, number>;
57+
58+
5659
paginatedSearchOptions$ = new BehaviorSubject<PaginatedSearchOptions>(null);
5760

5861
sectionTemplateType: TopSectionTemplateType;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<div class="grid-wrapper" [class.max-items-4]="(totalElements$ | async) < 4">
2+
<ng-container *ngFor="let item of (searchResultArray$ | async); let i = index; let last = last">
3+
<ng-container *ngIf="isBrowser && !isLoading; else imageSkeleton">
4+
<img [src]="(itemToImageHrefMap$ | async)?.get(item.uuid)" alt="hidden-image" style="display: none" #image>
5+
<div class="thumbnail-container"
6+
[class.landscape]="(totalElements$ | async) > 6 && image.naturalWidth > maxSquareRatio * image.naturalHeight"
7+
[class.portrait]="(totalElements$ | async) > 6 && image.naturalHeight > maxSquareRatio * image.naturalWidth">
8+
<div class="thumbnail-inner-container w-100 h-100 position-relative"
9+
[dsBackgroundImage]="(itemToImageHrefMap$ | async)?.get(item.uuid)">
10+
<ds-type-badge [object]="item"></ds-type-badge>
11+
<a [routerLink]="[getItemPageRoute(item)]" class="h-100 mw-100 p-3">
12+
<p class="h4 text-center m-0">{{ item.firstMetadataValue('dc.title') }}</p>
13+
</a>
14+
</div>
15+
</div>
16+
</ng-container>
17+
<ng-template #imageSkeleton>
18+
<ngx-skeleton-loader class="thumbnail-container w-100 h-100" [theme]="{height: '100%', width: '100%'}"></ngx-skeleton-loader>
19+
</ng-template>
20+
</ng-container>
21+
</div>

0 commit comments

Comments
 (0)