diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index bc84f961fb..f8c8b5df3d 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -1,5 +1,6 @@ import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store'; import * as fromRouter from '@ngrx/router-store'; +import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer'; import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer'; import { formReducer, FormState } from './shared/form/form.reducer'; import { @@ -43,6 +44,7 @@ export interface AppState { cssVariables: CSSVariablesState; menus: MenusState; objectSelection: ObjectSelectionListState; + communityList: CommunityListState; } export const appReducers: ActionReducerMap = { @@ -58,7 +60,8 @@ export const appReducers: ActionReducerMap = { truncatable: truncatableReducer, cssVariables: cssVariablesReducer, menus: menusReducer, - objectSelection: objectSelectionReducer + objectSelection: objectSelectionReducer, + communityList: CommunityListReducer, }; export const routerStateSelector = (state: AppState) => state.router; diff --git a/src/app/community-list-page/community-list-datasource.ts b/src/app/community-list-page/community-list-datasource.ts index da4bcc00cd..3a9d9f2077 100644 --- a/src/app/community-list-page/community-list-datasource.ts +++ b/src/app/community-list-page/community-list-datasource.ts @@ -3,6 +3,12 @@ import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; import { BehaviorSubject, Observable, } from 'rxjs'; import { finalize, take, } from 'rxjs/operators'; +/** + * DataSource object needed by a CDK Tree to render its nodes. + * The list of FlatNodes that this DataSource object represents gets created in the CommunityListService at + * the beginning (initial page-limited top communities) and re-calculated any time the tree state changes + * (a node gets expanded or page-limited result become larger by triggering a show more node) + */ export class CommunityListDatasource implements DataSource { private communityList$ = new BehaviorSubject([]); @@ -12,7 +18,6 @@ export class CommunityListDatasource implements DataSource { } connect(collectionViewer: CollectionViewer): Observable { - this.loadCommunities(null); return this.communityList$.asObservable(); } diff --git a/src/app/community-list-page/community-list-page.module.ts b/src/app/community-list-page/community-list-page.module.ts index 817e492bf7..2e3914fe03 100644 --- a/src/app/community-list-page/community-list-page.module.ts +++ b/src/app/community-list-page/community-list-page.module.ts @@ -6,6 +6,9 @@ import { CommunityListPageRoutingModule } from './community-list-page.routing.mo import { CommunityListComponent } from './community-list/community-list.component'; import { CdkTreeModule } from '@angular/cdk/tree'; +/** + * The page which houses a title and the community list, as described in community-list.component + */ @NgModule({ imports: [ CommonModule, diff --git a/src/app/community-list-page/community-list-page.routing.module.ts b/src/app/community-list-page/community-list-page.routing.module.ts index b226236af1..fe250cb96d 100644 --- a/src/app/community-list-page/community-list-page.routing.module.ts +++ b/src/app/community-list-page/community-list-page.routing.module.ts @@ -5,6 +5,9 @@ import { CdkTreeModule } from '@angular/cdk/tree'; import { CommunityListPageComponent } from './community-list-page.component'; import { CommunityListService } from './community-list-service'; +/** + * RouterModule to help navigate to the page with the community list tree + */ @NgModule({ imports: [ RouterModule.forChild([ diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index 3cbb76f79c..a25dbd2689 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -1,288 +1,335 @@ -import {Injectable} from '@angular/core'; -import {combineLatest as observableCombineLatest} from 'rxjs/internal/observable/combineLatest'; -import {Observable, of as observableOf} from 'rxjs'; -import {CommunityDataService} from '../core/data/community-data.service'; -import {PaginationComponentOptions} from '../shared/pagination/pagination-component-options.model'; -import {SortDirection, SortOptions} from '../core/cache/models/sort-options.model'; -import {catchError, filter, map, switchMap, take} from 'rxjs/operators'; -import {Community} from '../core/shared/community.model'; -import {Collection} from '../core/shared/collection.model'; -import {hasValue, isNotEmpty} from '../shared/empty.util'; -import {RemoteData} from '../core/data/remote-data'; -import {PaginatedList} from '../core/data/paginated-list'; -import {getCommunityPageRoute} from '../+community-page/community-page-routing.module'; -import {getCollectionPageRoute} from '../+collection-page/collection-page-routing.module'; -import {CollectionDataService} from '../core/data/collection-data.service'; +import { Injectable } from '@angular/core'; +import { createSelector, Store } from '@ngrx/store'; +import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest'; +import { Observable, of as observableOf } from 'rxjs'; +import { AppState } from '../app.reducer'; +import { CommunityDataService } from '../core/data/community-data.service'; +import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; +import { catchError, filter, map, switchMap, take } from 'rxjs/operators'; +import { Community } from '../core/shared/community.model'; +import { Collection } from '../core/shared/collection.model'; +import { hasValue, isNotEmpty } from '../shared/empty.util'; +import { RemoteData } from '../core/data/remote-data'; +import { PaginatedList } from '../core/data/paginated-list'; +import { getCommunityPageRoute } from '../+community-page/community-page-routing.module'; +import { getCollectionPageRoute } from '../+collection-page/collection-page-routing.module'; +import { CollectionDataService } from '../core/data/collection-data.service'; +import { CommunityListSaveAction } from './community-list.actions'; +import { CommunityListState } from './community-list.reducer'; +/** + * Each node in the tree is represented by a flatNode which contains info about the node itself and its position and + * state in the tree. There are nodes representing communities, collections and show more links. + */ export interface FlatNode { - isExpandable$: Observable; - name: string; - id: string; - level: number; - isExpanded?: boolean; - parent?: FlatNode; - payload: Community | Collection | ShowMoreFlatNode; - isShowMoreNode: boolean; - route?: string; - currentCommunityPage?: number; - currentCollectionPage?: number; + isExpandable$: Observable; + name: string; + id: string; + level: number; + isExpanded?: boolean; + parent?: FlatNode; + payload: Community | Collection | ShowMoreFlatNode; + isShowMoreNode: boolean; + route?: string; + currentCommunityPage?: number; + currentCollectionPage?: number; } +/** + * The show more links in the community tree are also represented by a flatNode so we know where in + * the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link) + */ export class ShowMoreFlatNode { } +// Helper method to combine an flatten an array of observables of flatNode arrays export const combineAndFlatten = (obsList: Array>): Observable => - observableCombineLatest(...obsList).pipe( - map((matrix: FlatNode[][]) => - matrix.reduce((combinedList, currentList: FlatNode[]) => [...combinedList, ...currentList])) - ); + observableCombineLatest(...obsList).pipe( + map((matrix: FlatNode[][]) => + matrix.reduce((combinedList, currentList: FlatNode[]) => [...combinedList, ...currentList])) + ); +/** + * Creates a flatNode from a community or collection + * @param c The community or collection this flatNode represents + * @param isExpandable Whether or not this node is expandable (true if it has children) + * @param level Level indicating how deep in the tree this node should be rendered + * @param isExpanded Whether or not this node already is expanded + * @param parent Parent of this node (flatNode representing its parent community) + */ export const toFlatNode = ( - c: Community | Collection, - isExpandable: Observable, - level: number, - isExpanded: boolean, - parent?: FlatNode + c: Community | Collection, + isExpandable: Observable, + level: number, + isExpanded: boolean, + parent?: FlatNode ): FlatNode => ({ - isExpandable$: isExpandable, - name: c.name, - id: c.id, - level: level, - isExpanded, - parent, - payload: c, - isShowMoreNode: false, - route: c instanceof Community ? getCommunityPageRoute(c.id) : getCollectionPageRoute(c.id), + isExpandable$: isExpandable, + name: c.name, + id: c.id, + level: level, + isExpanded, + parent, + payload: c, + isShowMoreNode: false, + route: c instanceof Community ? getCommunityPageRoute(c.id) : getCollectionPageRoute(c.id), }); +/** + * Creates a show More flatnode where only the level and parent are of importance + */ export const showMoreFlatNode = ( - id: string, - level: number, - parent: FlatNode + id: string, + level: number, + parent: FlatNode ): FlatNode => ({ - isExpandable$: observableOf(false), - name: 'Show More Flatnode', - id: id, - level: level, - isExpanded: false, - parent: parent, - payload: new ShowMoreFlatNode(), - isShowMoreNode: true, + isExpandable$: observableOf(false), + name: 'Show More Flatnode', + id: id, + level: level, + isExpanded: false, + parent: parent, + payload: new ShowMoreFlatNode(), + isShowMoreNode: true, }); +// Selectors the get the communityList data out of the store +const communityListStateSelector = (state: AppState) => state.communityList; +const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes); +const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode); + +/** + * Service class for the community list, responsible for the creating of the flat list used by communityList dataSource + * and connection to the store to retrieve and save the state of the community list + */ // tslint:disable-next-line:max-classes-per-file @Injectable() export class CommunityListService { - // page-limited list of top-level communities - payloads$: Array>>; + // page-limited list of top-level communities + payloads$: Array>>; - topCommunitiesConfig: PaginationComponentOptions; - topCommunitiesSortConfig: SortOptions; + topCommunitiesConfig: PaginationComponentOptions; + topCommunitiesSortConfig: SortOptions; - maxSubCommunitiesPerPage: number; - maxCollectionsPerPage: number; + maxSubCommunitiesPerPage: number; + maxCollectionsPerPage: number; - constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService) { - this.topCommunitiesConfig = new PaginationComponentOptions(); - this.topCommunitiesConfig.id = 'top-level-pagination'; - this.topCommunitiesConfig.pageSize = 10; - this.topCommunitiesConfig.currentPage = 1; - this.topCommunitiesSortConfig = new SortOptions('dc.title', SortDirection.ASC); - this.initTopCommunityList(); + constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService, + private store: Store) { + this.topCommunitiesConfig = new PaginationComponentOptions(); + this.topCommunitiesConfig.id = 'top-level-pagination'; + this.topCommunitiesConfig.pageSize = 10; + this.topCommunitiesConfig.currentPage = 1; + this.topCommunitiesSortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.initTopCommunityList(); - this.maxSubCommunitiesPerPage = 3; - this.maxCollectionsPerPage = 3; - } + this.maxSubCommunitiesPerPage = 3; + this.maxCollectionsPerPage = 3; + } - /** - * Increases the payload so it contains the next page of top level communities - */ - getNextPageTopCommunities(): void { - this.topCommunitiesConfig.currentPage = this.topCommunitiesConfig.currentPage + 1; - this.payloads$ = [...this.payloads$, this.communityDataService.findTop({ - currentPage: this.topCommunitiesConfig.currentPage, - elementsPerPage: this.topCommunitiesConfig.pageSize, - sort: { - field: this.topCommunitiesSortConfig.field, - direction: this.topCommunitiesSortConfig.direction - } - }).pipe( - take(1), - map((results) => results.payload), - )]; - } + saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void { + this.store.dispatch(new CommunityListSaveAction(expandedNodes, loadingNode)); + } - /** - * Gets all top communities, limited by page, and transforms this in a list of flatnodes. - * @param expandedNodes List of expanded nodes; if a node is not expanded its subcommunities and collections need not be added to the list - */ - loadCommunities(expandedNodes: FlatNode[]): Observable { - const res = this.payloads$.map((payload) => { - return payload.pipe( - take(1), - switchMap((result: PaginatedList) => { - return this.transformListOfCommunities(result, 0, null, expandedNodes); - }), - catchError(() => observableOf([])), - ); + getExpandedNodesFromStore(): Observable { + return this.store.select(expandedNodesSelector); + } + + getLoadingNodeFromStore(): Observable { + return this.store.select(loadingNodeSelector); + } + + /** + * Increases the payload so it contains the next page of top level communities + */ + getNextPageTopCommunities(): void { + this.topCommunitiesConfig.currentPage = this.topCommunitiesConfig.currentPage + 1; + this.payloads$ = [...this.payloads$, this.communityDataService.findTop({ + currentPage: this.topCommunitiesConfig.currentPage, + elementsPerPage: this.topCommunitiesConfig.pageSize, + sort: { + field: this.topCommunitiesSortConfig.field, + direction: this.topCommunitiesSortConfig.direction + } + }).pipe( + take(1), + map((results) => results.payload), + )]; + } + + /** + * Gets all top communities, limited by page, and transforms this in a list of flatNodes. + * @param expandedNodes List of expanded nodes; if a node is not expanded its subCommunities and collections need + * not be added to the list + */ + loadCommunities(expandedNodes: FlatNode[]): Observable { + const res = this.payloads$.map((payload) => { + return payload.pipe( + take(1), + switchMap((result: PaginatedList) => { + return this.transformListOfCommunities(result, 0, null, expandedNodes); + }), + catchError(() => observableOf([])), + ); + }); + return combineAndFlatten(res); + }; + + /** + * Puts the initial top level communities in a list to be called upon + */ + private initTopCommunityList(): void { + this.payloads$ = [this.communityDataService.findTop({ + currentPage: this.topCommunitiesConfig.currentPage, + elementsPerPage: this.topCommunitiesConfig.pageSize, + sort: { + field: this.topCommunitiesSortConfig.field, + direction: this.topCommunitiesSortConfig.direction + } + }).pipe( + take(1), + map((results) => results.payload), + )]; + } + + /** + * Transforms a list of communities to a list of FlatNodes according to the instructions detailed in transformCommunity + * @param listOfPaginatedCommunities Paginated list of communities to be transformed + * @param level Level the tree is currently at + * @param parent FlatNode of the parent of this list of communities + * @param expandedNodes List of expanded nodes; if a node is not expanded its subcommunities and collections need not be added to the list + */ + public transformListOfCommunities(listOfPaginatedCommunities: PaginatedList, + level: number, + parent: FlatNode, + expandedNodes: FlatNode[]): Observable { + if (isNotEmpty(listOfPaginatedCommunities.page)) { + let currentPage = this.topCommunitiesConfig.currentPage; + if (isNotEmpty(parent)) { + currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage; + } + const isNotAllCommunities = (listOfPaginatedCommunities.totalElements > (listOfPaginatedCommunities.elementsPerPage * currentPage)); + let obsList = listOfPaginatedCommunities.page + .map((community: Community) => { + return this.transformCommunity(community, level, parent, expandedNodes) }); - return combineAndFlatten(res); - }; + if (isNotAllCommunities && listOfPaginatedCommunities.currentPage > currentPage) { + obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])]; + } - /** - * Puts the initial top level communities in a list to be called upon - */ - private initTopCommunityList(): void { - this.payloads$ = [this.communityDataService.findTop({ - currentPage: this.topCommunitiesConfig.currentPage, - elementsPerPage: this.topCommunitiesConfig.pageSize, - sort: { - field: this.topCommunitiesSortConfig.field, - direction: this.topCommunitiesSortConfig.direction - } - }).pipe( - take(1), - map((results) => results.payload), - )]; + return combineAndFlatten(obsList); + } else { + return observableOf([]); + } + } + + /** + * Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself, + * followed by flatNodes of its possible subcommunities and collection + * It gets called recursively for each subcommunity to add its subcommunities and collections to the list + * Number of subcommunities and collections added, is dependant on the current page the parent is at for respectively subcommunities and collections. + * @param community Community being transformed + * @param level Depth of the community in the list, subcommunities and collections go one level deeper + * @param parent Flatnode of the parent community + * @param expandedNodes List of nodes which are expanded, if node is not expanded, it need not add its page-limited subcommunities or collections + */ + public transformCommunity(community: Community, level: number, parent: FlatNode, expandedNodes: FlatNode[]): Observable { + let isExpanded = false; + if (isNotEmpty(expandedNodes)) { + isExpanded = hasValue(expandedNodes.find((node) => (node.id === community.id))); } - /** - * Transforms a list of communities to a list of FlatNodes according to the instructions detailed in transformCommunity - * @param listOfPaginatedCommunities Paginated list of communities to be transformed - * @param level Level the tree is currently at - * @param parent FlatNode of the parent of this list of communities - * @param expandedNodes List of expanded nodes; if a node is not expanded its subcommunities and collections need not be added to the list - */ - public transformListOfCommunities(listOfPaginatedCommunities: PaginatedList, - level: number, - parent: FlatNode, - expandedNodes: FlatNode[]): Observable { - if (isNotEmpty(listOfPaginatedCommunities.page)) { - let currentPage = this.topCommunitiesConfig.currentPage; - if (isNotEmpty(parent)) { - currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage; - } - const isNotAllCommunities = (listOfPaginatedCommunities.totalElements > (listOfPaginatedCommunities.elementsPerPage * currentPage)); - let obsList = listOfPaginatedCommunities.page - .map((community: Community) => { - return this.transformCommunity(community, level, parent, expandedNodes) - }); - if (isNotAllCommunities && listOfPaginatedCommunities.currentPage > currentPage) { - obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])]; - } + const isExpandable$ = this.getIsExpandable(community); - return combineAndFlatten(obsList); + const communityFlatNode = toFlatNode(community, isExpandable$, level, isExpanded, parent); + + let obsList = [observableOf([communityFlatNode])]; + + if (isExpanded) { + const currentCommunityPage = expandedNodes.find((node: FlatNode) => node.id === community.id).currentCommunityPage; + let subcoms = []; + for (let i = 1; i <= currentCommunityPage; i++) { + const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, { + elementsPerPage: this.maxSubCommunitiesPerPage, + currentPage: i + }) + .pipe( + filter((rd: RemoteData>) => rd.hasSucceeded), + take(1), + switchMap((rd: RemoteData>) => + this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes)) + ); + + subcoms = [...subcoms, nextSetOfSubcommunitiesPage]; + } + + obsList = [...obsList, combineAndFlatten(subcoms)]; + + const currentCollectionPage = expandedNodes.find((node: FlatNode) => node.id === community.id).currentCollectionPage; + let collections = []; + for (let i = 1; i <= currentCollectionPage; i++) { + const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, { + elementsPerPage: this.maxCollectionsPerPage, + currentPage: i + }) + .pipe( + filter((rd: RemoteData>) => rd.hasSucceeded), + take(1), + map((rd: RemoteData>) => { + let nodes = rd.payload.page + .map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode)); + if ((rd.payload.elementsPerPage * currentCollectionPage) < rd.payload.totalElements && rd.payload.currentPage > currentCollectionPage) { + nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)]; + } + return nodes; + }), + ); + collections = [...collections, nextSetOfCollectionsPage]; + } + obsList = [...obsList, combineAndFlatten(collections)]; + } + + return combineAndFlatten(obsList); + } + + /** + * Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0 + * Returns an observable that combines the result.payload.totalElements fo the observables that the + * respective services return when queried + * @param community Community being checked whether it is expandable (if it has subcommunities or collections) + */ + public getIsExpandable(community: Community): Observable { + let hasSubcoms$: Observable; + let hasColls$: Observable; + hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 }) + .pipe( + filter((rd: RemoteData>) => rd.hasSucceeded), + take(1), + map((results) => results.payload.totalElements > 0), + ); + + hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 }) + .pipe( + filter((rd: RemoteData>) => rd.hasSucceeded), + take(1), + map((results) => results.payload.totalElements > 0), + ); + + let hasChildren$: Observable; + hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe( + take(1), + map((result: [boolean]) => { + if (result[0] || result[1]) { + return true; } else { - return observableOf([]); + return false; } - } + }) + ); - /** - * Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself, - * followed by flatNodes of its possible subcommunities and collection - * It gets called recursively for each subcommunity to add its subcommunities and collections to the list - * Number of subcommunities and collections added, is dependant on the current page the parent is at for respectively subcommunities and collections. - * @param community Community being transformed - * @param level Depth of the community in the list, subcommunities and collections go one level deeper - * @param parent Flatnode of the parent community - * @param expandedNodes List of nodes which are expanded, if node is not expanded, it need not add its page-limited subcommunities or collections - */ - public transformCommunity(community: Community, level: number, parent: FlatNode, expandedNodes: FlatNode[]): Observable { - let isExpanded = false; - if (isNotEmpty(expandedNodes)) { - isExpanded = hasValue(expandedNodes.find((node) => (node.id === community.id))); - } - - const isExpandable$ = this.getIsExpandable(community); - - const communityFlatNode = toFlatNode(community, isExpandable$, level, isExpanded, parent); - - let obsList = [observableOf([communityFlatNode])]; - - if (isExpanded) { - const currentCommunityPage = expandedNodes.find((node: FlatNode) => node.id === community.id).currentCommunityPage; - let subcoms = []; - for (let i = 1; i <= currentCommunityPage; i++) { - const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, { - elementsPerPage: this.maxSubCommunitiesPerPage, - currentPage: i - }) - .pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), - take(1), - switchMap((rd: RemoteData>) => - this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes)) - ); - - subcoms = [...subcoms, nextSetOfSubcommunitiesPage]; - } - - obsList = [...obsList, combineAndFlatten(subcoms)]; - - const currentCollectionPage = expandedNodes.find((node: FlatNode) => node.id === community.id).currentCollectionPage; - let collections = []; - for (let i = 1; i <= currentCollectionPage; i++) { - const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, { - elementsPerPage: this.maxCollectionsPerPage, - currentPage: i - }) - .pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), - take(1), - map((rd: RemoteData>) => { - let nodes = rd.payload.page - .map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode)); - if ((rd.payload.elementsPerPage * currentCollectionPage) < rd.payload.totalElements && rd.payload.currentPage > currentCollectionPage) { - nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)]; - } - return nodes; - }), - ); - collections = [...collections, nextSetOfCollectionsPage]; - } - obsList = [...obsList, combineAndFlatten(collections)]; - } - - return combineAndFlatten(obsList); - } - - /** - * Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0 - * Returns an observable that combines the result.payload.totalElements fo the observables that the - * respective services return when queried - * @param community Community being checked whether it is expandable (if it has subcommunities or collections) - */ - public getIsExpandable(community: Community): Observable { - let hasSubcoms$: Observable; - let hasColls$: Observable; - hasSubcoms$ = this.communityDataService.findByParent(community.uuid, {elementsPerPage: 1}) - .pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), - take(1), - map((results) => results.payload.totalElements > 0), - ); - - hasColls$ = this.collectionDataService.findByParent(community.uuid, {elementsPerPage: 1}) - .pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), - take(1), - map((results) => results.payload.totalElements > 0), - ); - - let hasChildren$: Observable; - hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe( - take(1), - map((result: [boolean]) => { - if (result[0] || result[1]) { - return true; - } else { - return false; - } - }) - ); - - return hasChildren$; - } + return hasChildren$; + } } diff --git a/src/app/community-list-page/community-list.actions.ts b/src/app/community-list-page/community-list.actions.ts new file mode 100644 index 0000000000..bfce6fba34 --- /dev/null +++ b/src/app/community-list-page/community-list.actions.ts @@ -0,0 +1,35 @@ +import { Action } from '@ngrx/store'; +import { type } from '../shared/ngrx/type'; +import { FlatNode } from './community-list-service'; + +/** + * All the action types of the community-list + */ + +export const CommunityListActionTypes = { + SAVE: type('dspace/community-list-page/SAVE') +}; + +/** + * Community list SAVE action + */ +export class CommunityListSaveAction implements Action { + + type = CommunityListActionTypes.SAVE; + + payload: { + expandedNodes: FlatNode[]; + loadingNode: FlatNode; + }; + + constructor(expandedNodes: FlatNode[], loadingNode: FlatNode) { + this.payload = { expandedNodes, loadingNode } + } +}; + +/** + * Export a type alias of all actions in this action group + * so that reducers can easily compose action types + */ + +export type CommunityListActions = CommunityListSaveAction; diff --git a/src/app/community-list-page/community-list.reducer.ts b/src/app/community-list-page/community-list.reducer.ts new file mode 100644 index 0000000000..ef53c7f94e --- /dev/null +++ b/src/app/community-list-page/community-list.reducer.ts @@ -0,0 +1,36 @@ +import { FlatNode } from './community-list-service'; +import { CommunityListActionTypes, CommunityListSaveAction } from './community-list.actions'; + +/** + * States we wish to put in store concerning the community list + */ +export interface CommunityListState { + expandedNodes: FlatNode[]; + loadingNode: FlatNode; +} + +/** + * Initial starting state of the list of expandedNodes and the current loading node of the community list + */ +const initialState: CommunityListState = { + expandedNodes: [], + loadingNode: null, +}; + +/** + * Reducer to interact with store concerning objects for the community list + * @constructor + */ +export function CommunityListReducer(state = initialState, action: CommunityListSaveAction) { + switch (action.type) { + case CommunityListActionTypes.SAVE: { + return Object.assign({}, state, { + expandedNodes: (action as CommunityListSaveAction).payload.expandedNodes, + loadingNode: (action as CommunityListSaveAction).payload.loadingNode, + }) + } + default: { + return state; + } + } +} diff --git a/src/app/community-list-page/community-list/community-list.component.ts b/src/app/community-list-page/community-list/community-list.component.ts index c51c4adde8..ddcd49cd1c 100644 --- a/src/app/community-list-page/community-list/community-list.component.ts +++ b/src/app/community-list-page/community-list/community-list.component.ts @@ -1,14 +1,22 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { take } from 'rxjs/operators'; import { CommunityListService, FlatNode } from '../community-list-service'; import { CommunityListDatasource } from '../community-list-datasource'; import { FlatTreeControl } from '@angular/cdk/tree'; import { isEmpty } from '../../shared/empty.util'; +/** + * A tree-structured list of nodes representing the communities, their subCommunities and collections. + * Initially only the page-restricted top communities are shown. + * Each node can be expanded to show its children and all children are also page-limited. + * More pages of a page-limited result can be shown by pressing a show more node/link. + * Which nodes were expanded is kept in the store, so this persists across pages. + */ @Component({ selector: 'ds-community-list', templateUrl: './community-list.component.html', }) -export class CommunityListComponent implements OnInit { +export class CommunityListComponent implements OnInit, OnDestroy { private expandedNodes: FlatNode[] = []; public loadingNode: FlatNode; @@ -24,7 +32,17 @@ export class CommunityListComponent implements OnInit { ngOnInit() { this.dataSource = new CommunityListDatasource(this.communityListService); - this.dataSource.loadCommunities(null); + this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => { + this.loadingNode = result; + }); + this.communityListService.getExpandedNodesFromStore().pipe(take(1)).subscribe((result) => { + this.expandedNodes = [...result]; + this.dataSource.loadCommunities(this.expandedNodes); + }); + } + + ngOnDestroy(): void { + this.communityListService.saveCommunityListStateToStore(this.expandedNodes, this.loadingNode); } // whether or not this node has children (subcommunities or collections)