diff --git a/README.md b/README.md index 8f2320dbf3..cb2f41130f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [h Quick start ----------- -**Ensure you're running [Node](https://nodejs.org) >= `v8.0.x`, [npm](https://www.npmjs.com/) >= `v3.x` and [yarn](https://yarnpkg.com) >= `v0.20.x`** +**Ensure you're running [Node](https://nodejs.org) >= `v8.0.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`** ```bash # clone the repo @@ -65,7 +65,7 @@ Requirements ------------ - [Node.js](https://nodejs.org), [npm](https://www.npmjs.com/), and [yarn](https://yarnpkg.com) -- Ensure you're running node >= `v5.x`, npm >= `v3.x` and yarn >= `v0.20.x` +- Ensure you're running node >= `v8.x`, npm >= `v5.x` and yarn >= `v1.x` If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS. diff --git a/package.json b/package.json index 1f75da6c8b..cc687ea269 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "prebuild": "yarn run clean:dist", "prebuild:aot": "yarn run prebuild", "prebuild:prod": "yarn run prebuild", - "build": "webpack --progress --mode development", - "build:aot": "webpack --env.aot --env.server --mode development && webpack --env.aot --env.client --mode development", - "build:prod": "webpack --env.aot --env.server --mode production && webpack --env.aot --env.client --mode production", + "build": "node ./webpack/run-webpack.js --progress --mode development", + "build:aot": "node ./webpack/run-webpack.js --env.aot --env.server --mode development && node ./webpack/run-webpack.js --env.aot --env.client --mode development", + "build:prod": "node ./webpack/run-webpack.js --env.aot --env.server --mode production && node ./webpack/run-webpack.js --env.aot --env.client --mode production", "postbuild:prod": "yarn run rollup", "rollup": "rollup -c rollup.config.js", "prestart": "yarn run build:prod", @@ -40,7 +40,7 @@ "server": "node dist/server.js", "server:watch": "nodemon dist/server.js", "server:watch:debug": "nodemon --debug dist/server.js", - "webpack:watch": "webpack -w --mode development", + "webpack:watch": "node ./webpack/run-webpack.js -w --mode development", "watch": "yarn run build && npm-run-all -p webpack:watch server:watch", "watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug", "predebug": "yarn run build", diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index a2cec6b553..41afbf2115 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -1,7 +1,7 @@ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { Observable, Subscription } from 'rxjs'; -import { filter, flatMap, map, take } from 'rxjs/operators'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, of as observableOf, Observable, Subject } from 'rxjs'; +import { filter, flatMap, map, startWith, switchMap, take, tap } from 'rxjs/operators'; import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model'; import { SearchService } from '../+search-page/search-service/search.service'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; @@ -15,10 +15,14 @@ import { Bitstream } from '../core/shared/bitstream.model'; import { Collection } from '../core/shared/collection.model'; import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; import { Item } from '../core/shared/item.model'; -import { getSucceededRemoteData, toDSpaceObjectListRD } from '../core/shared/operators'; +import { + getSucceededRemoteData, + redirectToPageNotFoundOn404, + toDSpaceObjectListRD +} from '../core/shared/operators'; import { fadeIn, fadeInOut } from '../shared/animations/fade'; -import { hasValue, isNotEmpty } from '../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty } from '../shared/empty.util'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; @Component({ @@ -31,22 +35,23 @@ import { PaginationComponentOptions } from '../shared/pagination/pagination-comp fadeInOut ] }) -export class CollectionPageComponent implements OnInit, OnDestroy { +export class CollectionPageComponent implements OnInit { collectionRD$: Observable>; itemRD$: Observable>>; logoRD$: Observable>; paginationConfig: PaginationComponentOptions; sortConfig: SortOptions; - private subs: Subscription[] = []; - private collectionId: string; - private currentPage: number; - private pageSize: number; + private paginationChanges$: Subject<{ + paginationConfig: PaginationComponentOptions, + sortConfig: SortOptions + }>; constructor( private collectionDataService: CollectionDataService, private searchService: SearchService, private metadata: MetadataService, - private route: ActivatedRoute + private route: ActivatedRoute, + private router: Router ) { this.paginationConfig = new PaginationComponentOptions(); this.paginationConfig.id = 'collection-page-pagination'; @@ -57,7 +62,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy { ngOnInit(): void { this.collectionRD$ = this.route.data.pipe( - map((data) => data.collection), + map((data) => data.collection as RemoteData), + redirectToPageNotFoundOn404(this.router), take(1) ); this.logoRD$ = this.collectionRD$.pipe( @@ -65,42 +71,34 @@ export class CollectionPageComponent implements OnInit, OnDestroy { filter((collection: Collection) => hasValue(collection)), flatMap((collection: Collection) => collection.logo) ); - this.subs.push( - this.route.queryParams.subscribe((params) => { - this.metadata.processRemoteData(this.collectionRD$); - const page = +params.page || this.paginationConfig.currentPage; - const pageSize = +params.pageSize || this.paginationConfig.pageSize; - this.collectionRD$.subscribe((rd: RemoteData) => { - this.collectionId = rd.payload.id; - this.updatePage(page, pageSize); - }); - }) + this.paginationChanges$ = new BehaviorSubject({ + paginationConfig: this.paginationConfig, + sortConfig: this.sortConfig + }); + + this.itemRD$ = this.paginationChanges$.pipe( + switchMap((dto) => this.collectionRD$.pipe( + getSucceededRemoteData(), + map((rd) => rd.payload.id), + switchMap((id: string) => { + return this.searchService.search( + new PaginatedSearchOptions({ + scope: id, + pagination: dto.paginationConfig, + sort: dto.sortConfig, + dsoType: DSpaceObjectType.ITEM + })).pipe(toDSpaceObjectListRD()) as Observable>> + }), + startWith(undefined) // Make sure switching pages shows loading component + ) + ) ); - } - updatePage(currentPage: number, pageSize: number) { - this.itemRD$ = this.searchService.search( - new PaginatedSearchOptions({ - scope: this.collectionId, - pagination: { - currentPage, - pageSize - } as PaginationComponentOptions, - sort: this.sortConfig, - dsoType: DSpaceObjectType.ITEM - })).pipe( - toDSpaceObjectListRD(), - getSucceededRemoteData(), - take(1), - ) as Observable>>; - - this.currentPage = currentPage; - this.pageSize = pageSize; - } - - ngOnDestroy(): void { - this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + this.route.queryParams.pipe(take(1)).subscribe((params) => { + this.metadata.processRemoteData(this.collectionRD$); + this.onPaginationChange(params); + }) } isNotEmpty(object: any) { @@ -108,8 +106,14 @@ export class CollectionPageComponent implements OnInit, OnDestroy { } onPaginationChange(event) { - if (this.currentPage !== event.page || this.pageSize !== event.pageSize) { - this.updatePage(event.page, event.pageSize); - } + this.paginationConfig.currentPage = +event.page || this.paginationConfig.currentPage; + this.paginationConfig.pageSize = +event.pageSize || this.paginationConfig.pageSize; + this.sortConfig.direction = event.sortDirection || this.sortConfig.direction; + this.sortConfig.field = event.sortField || this.sortConfig.field; + + this.paginationChanges$.next({ + paginationConfig: this.paginationConfig, + sortConfig: this.sortConfig + }); } } diff --git a/src/app/+home-page/home-page.component.html b/src/app/+home-page/home-page.component.html index 6a3e20ca9d..39ba479033 100644 --- a/src/app/+home-page/home-page.component.html +++ b/src/app/+home-page/home-page.component.html @@ -1,5 +1,5 @@
- +
diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index e4d96e3bc4..89d5977583 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -1,5 +1,5 @@ -import { mergeMap, filter, map, take } from 'rxjs/operators'; +import { mergeMap, filter, map, take, tap } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; @@ -41,11 +41,6 @@ export class ItemPageComponent implements OnInit { */ itemRD$: Observable>; - /** - * The item's thumbnail - */ - thumbnail$: Observable; - /** * The view-mode we're currently on */ @@ -64,9 +59,5 @@ export class ItemPageComponent implements OnInit { redirectToPageNotFoundOn404(this.router) ); this.metadataService.processRemoteData(this.itemRD$); - this.thumbnail$ = this.itemRD$.pipe( - map((rd: RemoteData) => rd.payload), - filter((item: Item) => hasValue(item)), - mergeMap((item: Item) => item.getThumbnail())); } } diff --git a/src/app/+my-dspace-page/my-dspace-page.component.html b/src/app/+my-dspace-page/my-dspace-page.component.html index 79fad17b26..4c691028fc 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.html +++ b/src/app/+my-dspace-page/my-dspace-page.component.html @@ -6,15 +6,17 @@ id="search-sidebar" [configurationList]="(configurationList$ | async)" [resultCount]="(resultsRD$ | async)?.payload.totalElements" - [viewModeList]="viewModeList"> + [viewModeList]="viewModeList" + [inPlaceSearch]="inPlaceSearch">
+ [scopes]="(scopeListRD$ | async)" + [inPlaceSearch]="inPlaceSearch"> - +
+ [ngClass]="{'active': !(isSidebarCollapsed() | async)}" + [inPlaceSearch]="inPlaceSearch">
- +
diff --git a/src/app/+search-page/search-sidebar/search-sidebar.component.ts b/src/app/+search-page/search-sidebar/search-sidebar.component.ts index 9abcf71dcb..9ee0a74942 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.component.ts +++ b/src/app/+search-page/search-sidebar/search-sidebar.component.ts @@ -34,8 +34,14 @@ export class SearchSidebarComponent { */ @Input() viewModeList; + /** + * True when the search component should show results on the current page + */ + @Input() inPlaceSearch; + /** * Emits event when the user clicks a button to open or close the sidebar */ @Output() toggleSidebar = new EventEmitter(); + } diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index c0b359e7ea..563dce23d1 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs'; -import { distinctUntilChanged, flatMap, map, startWith, switchMap } from 'rxjs/operators'; +import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; @@ -205,17 +205,28 @@ export class RemoteDataBuildService { return observableCombineLatest(...input).pipe( map((arr) => { + // The request of an aggregate RD should be pending if at least one + // of the RDs it's based on is still in the state RequestPending const requestPending: boolean = arr .map((d: RemoteData) => d.isRequestPending) - .every((b: boolean) => b === true); + .find((b: boolean) => b === true); - const responsePending: boolean = arr + // The response of an aggregate RD should be pending if no requests + // are still pending and at least one of the RDs it's based + // on is still in the state ResponsePending + const responsePending: boolean = !requestPending && arr .map((d: RemoteData) => d.isResponsePending) - .every((b: boolean) => b === true); + .find((b: boolean) => b === true); - const isSuccessful: boolean = arr - .map((d: RemoteData) => d.hasSucceeded) - .every((b: boolean) => b === true); + let isSuccessful: boolean; + // isSuccessful should be undefined until all responses have come in. + // We can't know its state beforehand. We also can't say it's false + // because that would imply a request failed. + if (!(requestPending || responsePending)) { + isSuccessful = arr + .map((d: RemoteData) => d.hasSucceeded) + .every((b: boolean) => b === true); + } const errorMessage: string = arr .map((d: RemoteData) => d.error) diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index 008a86599d..48b316af4b 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -115,7 +115,7 @@ export class NavbarComponent extends MenuComponent implements OnInit { model: { type: MenuItemType.LINK, text: 'menu.section.statistics', - link: '#' + link: '' } as LinkMenuItemModel, index: 2 }, diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index b164abee1f..a60aeb8054 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -8,6 +8,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { Community } from '../../core/shared/community.model'; import { TranslateModule } from '@ngx-translate/core'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { SearchService } from '../../+search-page/search-service/search.service'; describe('SearchFormComponent', () => { let comp: SearchFormComponent; @@ -18,6 +19,12 @@ describe('SearchFormComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [FormsModule, RouterTestingModule, TranslateModule.forRoot()], + providers: [ + { + provide: SearchService, + useValue: {} + } + ], declarations: [SearchFormComponent] }).compileComponents(); })); diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index ea96bd8114..10c3a3ede7 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -4,6 +4,7 @@ import { Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../empty.util'; import { QueryParamsHandling } from '@angular/router/src/config'; import { MYDSPACE_ROUTE } from '../../+my-dspace-page/my-dspace-page.component'; +import { SearchService } from '../../+search-page/search-service/search.service'; /** * This component renders a simple item page. @@ -26,6 +27,11 @@ export class SearchFormComponent { */ @Input() query: string; + /** + * True when the search component should show results on the current page + */ + @Input() inPlaceSearch; + /** * The currently selected scope object's UUID */ @@ -39,7 +45,7 @@ export class SearchFormComponent { */ @Input() scopes: DSpaceObject[]; - constructor(private router: Router) { + constructor(private router: Router, private searchService: SearchService) { } /** @@ -63,14 +69,9 @@ export class SearchFormComponent { * @param data Updated parameters */ updateSearch(data: any) { - const newUrl = hasValue(this.currentUrl) ? this.currentUrl : '/search'; - let handling: QueryParamsHandling = '' ; - if (this.currentUrl === '/search' || this.currentUrl === MYDSPACE_ROUTE) { - handling = 'merge'; - } - this.router.navigate([newUrl], { + this.router.navigate(this.getSearchLinkParts(), { queryParams: Object.assign({}, { page: 1 }, data), - queryParamsHandling: handling + queryParamsHandling: 'merge' }); } @@ -81,4 +82,23 @@ export class SearchFormComponent { return isNotEmpty(object); } + /** + * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true + */ + public getSearchLink(): string { + if (this.inPlaceSearch) { + return './'; + } + return this.searchService.getSearchLink(); + } + + /** + * @returns {string[]} The base path to the search page, or the current page when inPlaceSearch is true, split in separate pieces + */ + public getSearchLinkParts(): string[] { + if (this.inPlaceSearch) { + return []; + } + return this.getSearchLink().split('/'); + } } diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.ts index b011fce6a0..dc355c6409 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.ts @@ -17,6 +17,11 @@ import { isEmpty } from '../empty.util'; export class ViewModeSwitchComponent implements OnInit, OnDestroy { @Input() viewModeList: ViewMode[]; + /** + * True when the search component should show results on the current page + */ + @Input() inPlaceSearch; + currentMode: ViewMode = ViewMode.List; viewModeEnum = ViewMode; private sub: Subscription; @@ -35,7 +40,7 @@ export class ViewModeSwitchComponent implements OnInit, OnDestroy { } switchViewTo(viewMode: ViewMode) { - this.searchService.setViewMode(viewMode); + this.searchService.setViewMode(viewMode, this.getSearchLinkParts()); } ngOnDestroy() { @@ -47,4 +52,25 @@ export class ViewModeSwitchComponent implements OnInit, OnDestroy { isToShow(viewMode: ViewMode) { return this.viewModeList && this.viewModeList.includes(viewMode); } + + /** + * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true + */ + public getSearchLink(): string { + if (this.inPlaceSearch) { + return './'; + } + return this.searchService.getSearchLink(); + } + + /** + * @returns {string[]} The base path to the search page, or the current page when inPlaceSearch is true, split in separate pieces + */ + public getSearchLinkParts(): string[] { + if (this.searchService) { + return []; + } + return this.getSearchLink().split('/'); + } + } diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index 781b3ce524..87fd0251f5 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,4 +1,4 @@
- - +
+ diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 5e2b713b31..9700e01821 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -1,5 +1,6 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from '../core/shared/bitstream.model'; +import { hasValue } from '../shared/empty.util'; /** * This component renders a given Bitstream as a thumbnail. @@ -12,19 +13,26 @@ import { Bitstream } from '../core/shared/bitstream.model'; styleUrls: ['./thumbnail.component.scss'], templateUrl: './thumbnail.component.html' }) -export class ThumbnailComponent { +export class ThumbnailComponent implements OnInit { @Input() thumbnail: Bitstream; - data: any = {}; - /** * The default 'holder.js' image */ @Input() defaultImage? = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23EEEEEE%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'; + src: string; errorHandler(event) { event.currentTarget.src = this.defaultImage; } + ngOnInit(): void { + if (hasValue(this.thumbnail) && this.thumbnail.content) { + this.src = this.thumbnail.content; + } else { + this.src = this.defaultImage + } + } + } diff --git a/webpack/run-webpack.js b/webpack/run-webpack.js new file mode 100644 index 0000000000..93f17b4619 --- /dev/null +++ b/webpack/run-webpack.js @@ -0,0 +1,13 @@ +const path = require('path'); +const child_process = require('child_process'); + +const heapSize = 4096; +const webpackPath = path.join('node_modules', 'webpack', 'bin', 'webpack.js'); + +const params = [ + '--max_old_space_size=' + heapSize, + webpackPath, + ...process.argv.slice(2) +]; + +child_process.spawn('node', params, { stdio:'inherit' });