mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
74199: Infinite scrollable dso-selector + collection/community implementation + authorized-collection-selector
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { DSOSelectorComponent } from '../dso-selector.component';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { CollectionDataService } from '../../../../core/data/collection-data.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
||||
import { SearchResult } from '../../../search/search-result.model';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-authorized-collection-selector',
|
||||
templateUrl: '../dso-selector.component.html'
|
||||
})
|
||||
/**
|
||||
* Component rendering a list of collections to select from
|
||||
*/
|
||||
export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent {
|
||||
constructor(protected searchService: SearchService,
|
||||
protected collectionDataService: CollectionDataService) {
|
||||
super(searchService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a search for authorized collections with the current query and page
|
||||
* @param query Query to search objects for
|
||||
* @param page Page to retrieve
|
||||
*/
|
||||
search(query: string, page: number): Observable<PaginatedList<SearchResult<DSpaceObject>>> {
|
||||
return this.collectionDataService.getAuthorizedCollection(query, Object.assign({
|
||||
currentPage: page,
|
||||
elementsPerPage: this.defaultPagination.pageSize
|
||||
})).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((list) => new PaginatedList(list.pageInfo, list.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col }))))
|
||||
);
|
||||
}
|
||||
}
|
@@ -7,11 +7,18 @@
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="scrollable-menu list-group">
|
||||
<div
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="0.2"
|
||||
[infiniteScrollThrottle]="300"
|
||||
[infiniteScrollContainer]="'.scrollable-menu'"
|
||||
[fromRoot]="true"
|
||||
(scrolled)="onScrollDown()">
|
||||
<button class="list-group-item list-group-item-action border-0 disabled"
|
||||
*ngIf="(listEntries$ | async)?.payload.page.length == 0">
|
||||
*ngIf="listEntries.length == 0">
|
||||
{{'dso-selector.no-results' | translate: { type: typesString } }}
|
||||
</button>
|
||||
<button *ngFor="let listEntry of (listEntries$ | async)?.payload.page"
|
||||
<button *ngFor="let listEntry of listEntries"
|
||||
class="list-group-item list-group-item-action border-0 list-entry"
|
||||
title="{{ listEntry.indexableObject.name }}"
|
||||
(click)="onSelect.emit(listEntry.indexableObject)" #listEntryElement>
|
||||
@@ -19,3 +26,4 @@
|
||||
[linkType]=linkTypes.None [context]="context"></ds-listable-object-component-loader>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,5 @@
|
||||
.scrollable-menu {
|
||||
height: auto;
|
||||
max-height: $dso-selector-list-max-height;
|
||||
overflow-x: hidden;
|
||||
}
|
@@ -3,29 +3,33 @@ import {
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { debounceTime, startWith, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SearchService } from '../../../core/shared/search/search.service';
|
||||
import { CollectionElementLinkType } from '../../object-collection/collection-element-link.type';
|
||||
import { PaginatedSearchOptions } from '../../search/paginated-search-options.model';
|
||||
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { SearchResult } from '../../search/search-result.model';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../core/shared/context.model';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { hasValue } from '../../empty.util';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { SearchResult } from '../../search/search-result.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dso-selector',
|
||||
// styleUrls: ['./dso-selector.component.scss'],
|
||||
styleUrls: ['./dso-selector.component.scss'],
|
||||
templateUrl: './dso-selector.component.html'
|
||||
})
|
||||
|
||||
@@ -33,7 +37,7 @@ import { Context } from '../../../core/shared/context.model';
|
||||
* Component to render a list of DSO's of which one can be selected
|
||||
* The user can search the list by using the input field
|
||||
*/
|
||||
export class DSOSelectorComponent implements OnInit {
|
||||
export class DSOSelectorComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The view mode of the listed objects
|
||||
*/
|
||||
@@ -64,12 +68,29 @@ export class DSOSelectorComponent implements OnInit {
|
||||
/**
|
||||
* Default pagination for this feature
|
||||
*/
|
||||
private defaultPagination = { id: 'dso-selector', currentPage: 1, pageSize: 5 } as any;
|
||||
defaultPagination = { id: 'dso-selector', currentPage: 1, pageSize: 5 } as any;
|
||||
|
||||
/**
|
||||
* List with search results of DSpace objects for the current query
|
||||
*/
|
||||
listEntries$: Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>;
|
||||
listEntries: Array<SearchResult<DSpaceObject>> = [];
|
||||
|
||||
/**
|
||||
* The current page to load
|
||||
* Dynamically goes up as the user scrolls down until it reaches the last page possible
|
||||
*/
|
||||
currentPage$ = new BehaviorSubject(1);
|
||||
|
||||
/**
|
||||
* Whether or not the list contains a next page to load
|
||||
* This allows us to avoid next pages from trying to load when there are none
|
||||
*/
|
||||
hasNextPage = false;
|
||||
|
||||
/**
|
||||
* Whether or not the list should be reset next time it receives a page to load
|
||||
*/
|
||||
resetList = false;
|
||||
|
||||
/**
|
||||
* List of element references to all elements
|
||||
@@ -91,31 +112,76 @@ export class DSOSelectorComponent implements OnInit {
|
||||
*/
|
||||
context = Context.SideBarSearchModal;
|
||||
|
||||
constructor(private searchService: SearchService) {
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
public subs: Subscription[] = [];
|
||||
|
||||
constructor(protected searchService: SearchService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the listEntries$ variable with search results based on the input field's current value
|
||||
* Fills the listEntries variable with search results based on the input field's current value and the current page
|
||||
* The search will always start with the initial currentDSOId value
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.input.setValue(this.currentDSOId);
|
||||
this.typesString = this.types.map((type: string) => type.toString().toLowerCase()).join(', ');
|
||||
this.listEntries$ = this.input.valueChanges
|
||||
.pipe(
|
||||
|
||||
this.subs.push(observableCombineLatest(
|
||||
this.input.valueChanges.pipe(
|
||||
debounceTime(this.debounceTime),
|
||||
startWith(this.currentDSOId),
|
||||
switchMap((query) => {
|
||||
tap(() => this.currentPage$.next(1))
|
||||
),
|
||||
this.currentPage$
|
||||
).pipe(
|
||||
switchMap(([query, page]: [string, number]) => {
|
||||
if (page === 1) {
|
||||
// The first page is loading, this means we should reset the list instead of adding to it
|
||||
this.resetList = true;
|
||||
}
|
||||
return this.search(query, page);
|
||||
})
|
||||
).subscribe((list) => {
|
||||
if (this.resetList) {
|
||||
this.listEntries = list.page;
|
||||
this.resetList = false;
|
||||
} else {
|
||||
this.listEntries.push(...list.page);
|
||||
}
|
||||
// Check if there are more pages available after the current one
|
||||
this.hasNextPage = list.totalElements > this.listEntries.length;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a search for the current query and page
|
||||
* @param query Query to search objects for
|
||||
* @param page Page to retrieve
|
||||
*/
|
||||
search(query: string, page: number): Observable<PaginatedList<SearchResult<DSpaceObject>>> {
|
||||
return this.searchService.search(
|
||||
new PaginatedSearchOptions({
|
||||
query: query,
|
||||
dsoTypes: this.types,
|
||||
pagination: this.defaultPagination
|
||||
pagination: Object.assign({}, this.defaultPagination, {
|
||||
currentPage: page
|
||||
})
|
||||
)
|
||||
})
|
||||
).pipe(
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page
|
||||
*/
|
||||
onScrollDown() {
|
||||
if (this.hasNextPage) {
|
||||
this.currentPage$.next(this.currentPage$.value + 1);
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,4 +192,11 @@ export class DSOSelectorComponent implements OnInit {
|
||||
this.listElements.first.nativeElement.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ds-collection-dropdown (selectionChange)="selectObject($event.collection)">
|
||||
</ds-collection-dropdown>
|
||||
<ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid ? 'search.resourceid:' + dsoRD?.payload.uuid : null" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-authorized-collection-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,24 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { SidebarSearchListElementComponent } from '../sidebar-search-list-element.component';
|
||||
|
||||
@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.SideBarSearchModal)
|
||||
@Component({
|
||||
selector: 'ds-collection-sidebar-search-list-element',
|
||||
templateUrl: '../sidebar-search-list-element.component.html'
|
||||
})
|
||||
/**
|
||||
* Component displaying a list element for a {@link CollectionSearchResult} within the context of a sidebar search modal
|
||||
*/
|
||||
export class CollectionSidebarSearchListElementComponent extends SidebarSearchListElementComponent<CollectionSearchResult, Collection> {
|
||||
/**
|
||||
* Get the description of the Collection by returning its abstract
|
||||
*/
|
||||
getDescription(): string {
|
||||
return this.firstMetadataValue('dc.description.abstract');
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { SidebarSearchListElementComponent } from '../sidebar-search-list-element.component';
|
||||
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
|
||||
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.SideBarSearchModal)
|
||||
@Component({
|
||||
selector: 'ds-collection-sidebar-search-list-element',
|
||||
templateUrl: '../sidebar-search-list-element.component.html'
|
||||
})
|
||||
/**
|
||||
* Component displaying a list element for a {@link CommunitySearchResult} within the context of a sidebar search modal
|
||||
*/
|
||||
export class CommunitySidebarSearchListElementComponent extends SidebarSearchListElementComponent<CommunitySearchResult, Community> {
|
||||
/**
|
||||
* Get the description of the Community by returning its abstract
|
||||
*/
|
||||
getDescription(): string {
|
||||
return this.firstMetadataValue('dc.description.abstract');
|
||||
}
|
||||
}
|
@@ -212,6 +212,9 @@ import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-tr
|
||||
import { CurationFormComponent } from '../curation-form/curation-form.component';
|
||||
import { PublicationSidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component';
|
||||
import { SidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/sidebar-search-list-element.component';
|
||||
import { CollectionSidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component';
|
||||
import { CommunitySidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component';
|
||||
import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -405,7 +408,8 @@ const COMPONENTS = [
|
||||
CollectionDropdownComponent,
|
||||
ExportMetadataSelectorComponent,
|
||||
ConfirmationModalComponent,
|
||||
VocabularyTreeviewComponent
|
||||
VocabularyTreeviewComponent,
|
||||
AuthorizedCollectionSelectorComponent,
|
||||
];
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
@@ -488,6 +492,9 @@ const ENTRY_COMPONENTS = [
|
||||
VocabularyTreeviewComponent,
|
||||
SidebarSearchListElementComponent,
|
||||
PublicationSidebarSearchListElementComponent,
|
||||
CollectionSidebarSearchListElementComponent,
|
||||
CommunitySidebarSearchListElementComponent,
|
||||
AuthorizedCollectionSelectorComponent,
|
||||
];
|
||||
|
||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
|
@@ -37,3 +37,5 @@ $edit-item-metadata-field-width: 190px !default;
|
||||
$edit-item-language-field-width: 43px !default;
|
||||
|
||||
$thumbnail-max-width: 175px !default;
|
||||
|
||||
$dso-selector-list-max-height: 475px !default;
|
||||
|
Reference in New Issue
Block a user