diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts new file mode 100644 index 0000000000..ff9fa81368 --- /dev/null +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts @@ -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>> { + 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 })))) + ); + } +} diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index 8a2f9272c4..a0b6aff2a3 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -7,15 +7,23 @@
+
- +
diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss b/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss new file mode 100644 index 0000000000..37d2ebeca7 --- /dev/null +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss @@ -0,0 +1,5 @@ +.scrollable-menu { + height: auto; + max-height: $dso-selector-list-max-height; + overflow-x: hidden; +} diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts index d0404d61c9..8d2d6f758b 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts @@ -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>>>; + listEntries: Array> = []; + + /** + * 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) => { - return this.searchService.search( - new PaginatedSearchOptions({ - query: query, - dsoTypes: this.types, - pagination: this.defaultPagination - }) - ) - } - ) - ) + 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>> { + return this.searchService.search( + new PaginatedSearchOptions({ + query: query, + dsoTypes: this.types, + 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()); + } } diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html index ef8865ad87..a188b08b60 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html @@ -5,7 +5,6 @@ diff --git a/src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..8439ca53f7 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.ts @@ -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 { + /** + * Get the description of the Collection by returning its abstract + */ + getDescription(): string { + return this.firstMetadataValue('dc.description.abstract'); + } +} diff --git a/src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..02e09c3fd4 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.ts @@ -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 { + /** + * Get the description of the Community by returning its abstract + */ + getDescription(): string { + return this.firstMetadataValue('dc.description.abstract'); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 5b849eb403..d8d8b51331 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -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 = [ diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index c1f155fa39..bc1dfda7e7 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -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;