mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-09 19:13:08 +00:00
Merge branch 'main' into iiif-mirador
This commit is contained in:
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -53,6 +53,9 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install the latest chromedriver compatible with the installed chrome version
|
||||
run: yarn global add chromedriver --detect_chromedriver_version
|
||||
|
||||
- name: Install Yarn dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
@@ -94,7 +97,9 @@ jobs:
|
||||
run: curl http://localhost:8080/server/api
|
||||
|
||||
- name: Run e2e tests (integration tests)
|
||||
run: yarn run e2e:ci
|
||||
run: |
|
||||
chromedriver --url-base='/wd/hub' --port=4444 &
|
||||
yarn run e2e:ci
|
||||
|
||||
- name: Shutdown Docker containers
|
||||
run: docker-compose -f ./docker/docker-compose-ci.yml down
|
||||
|
@@ -56,6 +56,11 @@
|
||||
"input": "src/themes/custom/styles/theme.scss",
|
||||
"inject": false,
|
||||
"bundleName": "custom-theme"
|
||||
},
|
||||
{
|
||||
"input": "src/themes/dspace/styles/theme.scss",
|
||||
"inject": false,
|
||||
"bundleName": "dspace-theme"
|
||||
}
|
||||
],
|
||||
"scripts": []
|
||||
|
@@ -22,7 +22,8 @@ services:
|
||||
networks:
|
||||
dspacenet: {}
|
||||
environment:
|
||||
- LOADASSETS=https://www.dropbox.com/s/v3ahfcuatklbmi0/assetstore-2019-11-28.tar.gz?dl=1
|
||||
# This assetstore zip is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||
- LOADASSETS=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
|
@@ -19,4 +19,33 @@ services:
|
||||
image: dspace/dspace-postgres-pgcrypto:loadsql
|
||||
environment:
|
||||
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
||||
- LOADSQL=https://www.dropbox.com/s/4ap1y6deseoc8ws/dspace7-entities-2019-11-28.sql?dl=1
|
||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||
- LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql
|
||||
dspace:
|
||||
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||
# 2. Then, run database migration to init database tables
|
||||
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
||||
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
||||
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
||||
# 4. Finally, start Tomcat
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||
/dspace/bin/dspace database migrate
|
||||
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/5" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/8" submission-name="OrgUnit"/> \
|
||||
<name-map collection-handle="123456789/6" submission-name="Person"/> \
|
||||
<name-map collection-handle="123456789/279" submission-name="Person"/> \
|
||||
<name-map collection-handle="123456789/7" submission-name="Project"/> \
|
||||
<name-map collection-handle="123456789/280" submission-name="Project"/> \
|
||||
<name-map collection-handle="123456789/28" submission-name="Journal"/> \
|
||||
<name-map collection-handle="123456789/29" submission-name="JournalVolume"/> \
|
||||
<name-map collection-handle="123456789/30" submission-name="JournalIssue"/>' /dspace/config/item-submission.xml
|
||||
catalina.sh run
|
@@ -8,9 +8,13 @@
|
||||
|
||||
# Docker Compose for running the DSpace backend for e2e testing in a CI environment
|
||||
# This is used by our GitHub CI at .github/workflows/build.yml
|
||||
# It is based heavily on the Backend's Docker Compose:
|
||||
# https://github.com/DSpace/DSpace/blob/main/docker-compose.yml
|
||||
version: '3.7'
|
||||
networks:
|
||||
dspacenet:
|
||||
services:
|
||||
# DSpace (backend) webapp container
|
||||
dspace:
|
||||
container_name: dspace
|
||||
depends_on:
|
||||
@@ -26,12 +30,28 @@ services:
|
||||
volumes:
|
||||
- assetstore:/dspace/assetstore
|
||||
- "./local.cfg:/dspace/config/local.cfg"
|
||||
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
||||
- solr_configs:/dspace/solr
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||
# 2. Then, run database migration to init database tables
|
||||
# 3. Finally, start Tomcat
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||
/dspace/bin/dspace database migrate
|
||||
catalina.sh run
|
||||
# DSpace database container
|
||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||
dspacedb:
|
||||
container_name: dspacedb
|
||||
environment:
|
||||
# This LOADSQL should be kept in sync with the LOADSQL in
|
||||
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
||||
LOADSQL: https://www.dropbox.com/s/4ap1y6deseoc8ws/dspace7-entities-2019-11-28.sql?dl=1
|
||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql
|
||||
PGDATA: /pgdata
|
||||
image: dspace/dspace-postgres-pgcrypto:loadsql
|
||||
networks:
|
||||
@@ -40,9 +60,14 @@ services:
|
||||
tty: true
|
||||
volumes:
|
||||
- pgdata:/pgdata
|
||||
# DSpace Solr container
|
||||
dspacesolr:
|
||||
container_name: dspacesolr
|
||||
image: dspace/dspace-solr
|
||||
# Uses official Solr image at https://hub.docker.com/_/solr/
|
||||
image: solr:8.8
|
||||
# Needs main 'dspace' container to start first to guarantee access to solr_configs
|
||||
depends_on:
|
||||
- dspace
|
||||
networks:
|
||||
dspacenet:
|
||||
ports:
|
||||
@@ -50,16 +75,27 @@ services:
|
||||
target: 8983
|
||||
stdin_open: true
|
||||
tty: true
|
||||
working_dir: /var/solr/data
|
||||
volumes:
|
||||
- solr_authority:/opt/solr/server/solr/authority/data
|
||||
- solr_oai:/opt/solr/server/solr/oai/data
|
||||
- solr_search:/opt/solr/server/solr/search/data
|
||||
- solr_statistics:/opt/solr/server/solr/statistics/data
|
||||
version: '3.7'
|
||||
# Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder)
|
||||
# This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume
|
||||
- solr_configs:/opt/solr/server/solr/configsets/dspace
|
||||
# Keep Solr data directory between reboots
|
||||
- solr_data:/var/solr/data
|
||||
# Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
init-var-solr
|
||||
precreate-core authority /opt/solr/server/solr/configsets/dspace/authority
|
||||
precreate-core oai /opt/solr/server/solr/configsets/dspace/oai
|
||||
precreate-core search /opt/solr/server/solr/configsets/dspace/search
|
||||
precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics
|
||||
exec solr -f
|
||||
volumes:
|
||||
assetstore:
|
||||
pgdata:
|
||||
solr_authority:
|
||||
solr_oai:
|
||||
solr_search:
|
||||
solr_statistics:
|
||||
solr_data:
|
||||
# Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above)
|
||||
solr_configs:
|
@@ -14,6 +14,7 @@ version: '3.7'
|
||||
networks:
|
||||
dspacenet:
|
||||
services:
|
||||
# DSpace (backend) webapp container
|
||||
dspace:
|
||||
container_name: dspace
|
||||
image: dspace/dspace:dspace-7_x-test
|
||||
@@ -29,6 +30,8 @@ services:
|
||||
volumes:
|
||||
- assetstore:/dspace/assetstore
|
||||
- "./local.cfg:/dspace/config/local.cfg"
|
||||
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
||||
- solr_configs:/dspace/solr
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||
# 2. Then, run database migration to init database tables
|
||||
@@ -40,6 +43,7 @@ services:
|
||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||
/dspace/bin/dspace database migrate
|
||||
catalina.sh run
|
||||
# DSpace database container
|
||||
dspacedb:
|
||||
container_name: dspacedb
|
||||
environment:
|
||||
@@ -54,9 +58,14 @@ services:
|
||||
tty: true
|
||||
volumes:
|
||||
- pgdata:/pgdata
|
||||
# DSpace Solr container
|
||||
dspacesolr:
|
||||
container_name: dspacesolr
|
||||
image: dspace/dspace-solr
|
||||
# Uses official Solr image at https://hub.docker.com/_/solr/
|
||||
image: solr:8.8
|
||||
# Needs main 'dspace' container to start first to guarantee access to solr_configs
|
||||
depends_on:
|
||||
- dspace
|
||||
networks:
|
||||
dspacenet:
|
||||
ports:
|
||||
@@ -64,15 +73,27 @@ services:
|
||||
target: 8983
|
||||
stdin_open: true
|
||||
tty: true
|
||||
working_dir: /var/solr/data
|
||||
volumes:
|
||||
- solr_authority:/opt/solr/server/solr/authority/data
|
||||
- solr_oai:/opt/solr/server/solr/oai/data
|
||||
- solr_search:/opt/solr/server/solr/search/data
|
||||
- solr_statistics:/opt/solr/server/solr/statistics/data
|
||||
# Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder)
|
||||
# This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume
|
||||
- solr_configs:/opt/solr/server/solr/configsets/dspace
|
||||
# Keep Solr data directory between reboots
|
||||
- solr_data:/var/solr/data
|
||||
# Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
init-var-solr
|
||||
precreate-core authority /opt/solr/server/solr/configsets/dspace/authority
|
||||
precreate-core oai /opt/solr/server/solr/configsets/dspace/oai
|
||||
precreate-core search /opt/solr/server/solr/configsets/dspace/search
|
||||
precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics
|
||||
exec solr -f
|
||||
volumes:
|
||||
assetstore:
|
||||
pgdata:
|
||||
solr_authority:
|
||||
solr_oai:
|
||||
solr_search:
|
||||
solr_statistics:
|
||||
solr_data:
|
||||
# Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above)
|
||||
solr_configs:
|
||||
|
@@ -64,69 +64,3 @@ In order to start using one of these services, select it from the [Angulartics P
|
||||
|
||||
The Google Analytics script was added in [`main.browser.ts`](https://github.com/DSpace/dspace-angular/blob/ff04760f4af91ac3e7add5e7424a46cb2439e874/src/main.browser.ts#L33) instead of the `<head>` tag in `index.html` to ensure events get sent when the page is shown in a client's browser, and not when it's rendered on the universal server. Likely you'll want to do the same when adding a new service.
|
||||
|
||||
## SEO when hosting REST Api and UI on different servers
|
||||
|
||||
Indexers such as Google Scholar require that files are hosted on the same domain as the page that links them. In DSpace 7, Bitstreams are served from the REST server. So if you use different servers for the REST api and the UI you'll want to ensure that Bitstream downloads are proxied through the UI server.
|
||||
|
||||
In order to achieve this we'll need to do two things:
|
||||
- **Proxy the Bitstream downloads through the UI server.** You'll need to put a webserver such as httpd or nginx in front of the UI server in order to achieve this. [Below](#apache-http-server-config) you'll find a section explaining how to do it in httpd.
|
||||
- **Update the URLs for Bitstream downloads to match the UI server.** This can be done using a setting in the UI environment file.
|
||||
|
||||
### UI config
|
||||
If you set the property `rewriteDownloadUrls` to `true` in your `environment.prod.ts` file, the [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin) of any download URL will be replaced by the origin of the UI. This will also happen for the `citation_pdf_url` `<meta>` tag on Item pages.
|
||||
|
||||
The app will determine the UI origin currently in use, so the external UI URL doesn't need to be configured anywhere and rewrites will still work if you host the UI from multiple domains.
|
||||
|
||||
### Apache HTTP Server config
|
||||
|
||||
#### Basics
|
||||
In order to be able to host bitstreams from the UI Server you'll need to enable mod_proxy and add the following to the httpd config of your UI server:
|
||||
|
||||
```
|
||||
ProxyPassMatch "/server/api/core/bitstreams/([^/]+)/content" "http://rest.api/server/api/core/bitstreams/$1/content"
|
||||
ProxyPassReverse "/server/api/core/bitstreams/([^/]+)/content" "http://rest.api/server/api/core/bitstreams/$1/content"
|
||||
```
|
||||
|
||||
Replace http://rest.api in with the correct origin for your REST server.
|
||||
|
||||
The `ProxyPassMatch` line forwards all requests matching the regular expression for a bitstream download URL to the corresponding path on the REST server
|
||||
|
||||
The `ProxyPassReverse` ensures that if the REST server were to return redirect response, httpd would also swap out its hostname for the hostname of the UI before forwarding the response to the client.
|
||||
|
||||
#### Using HTTPS
|
||||
If your REST server uses https, you'll need to enable mod_ssl and ensure `SSLProxyEngine on` is part of your UI server's httpd config as well
|
||||
|
||||
If the UI hostname doesn't match the CN in the SSL certificate of the REST server (which is likely if they're on different domains), you'll also need to add the following lines
|
||||
|
||||
```
|
||||
SSLProxyCheckPeerCN off
|
||||
SSLProxyCheckPeerName off
|
||||
```
|
||||
These are two names for [the same directive](https://httpd.apache.org/docs/trunk/mod/mod_ssl.html#sslproxycheckpeername) that have been used for various versions of httpd, old versions need the former, then some in-between versions need both, and newer versions only need the latter. Keeping them both doesn't harm anything.
|
||||
|
||||
So the entire config becomes:
|
||||
|
||||
```
|
||||
SSLProxyEngine on
|
||||
SSLProxyCheckPeerCN off
|
||||
SSLProxyCheckPeerName off
|
||||
ProxyPassMatch "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
|
||||
ProxyPassReverse "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
|
||||
```
|
||||
|
||||
If you don't want httpd to verify the certificate of the REST server, you can also turn all checks off with the following config:
|
||||
|
||||
```
|
||||
SSLProxyEngine on
|
||||
SSLProxyVerify none
|
||||
SSLProxyCheckPeerCN off
|
||||
SSLProxyCheckPeerName off
|
||||
SSLProxyCheckPeerExpire off
|
||||
ProxyPassMatch "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
|
||||
ProxyPassReverse "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -7,4 +7,8 @@ config.capabilities = {
|
||||
}
|
||||
};
|
||||
|
||||
// don't use protractor's webdriver, as it may be incompatible with the installed chrome version
|
||||
config.directConnect = false;
|
||||
config.seleniumAddress = 'http://localhost:4444/wd/hub';
|
||||
|
||||
exports.config = config;
|
||||
|
@@ -32,7 +32,7 @@
|
||||
"lint": "ng lint",
|
||||
"lint-fix": "ng lint --fix=true",
|
||||
"e2e": "ng e2e",
|
||||
"e2e:ci": "ng e2e --protractor-config=./e2e/protractor-ci.conf.js",
|
||||
"e2e:ci": "ng e2e --webdriver-update=false --protractor-config=./e2e/protractor-ci.conf.js",
|
||||
"compile:server": "webpack --config webpack.server.config.js --progress --color",
|
||||
"serve:ssr": "node dist/server",
|
||||
"clean:coverage": "rimraf coverage",
|
||||
|
@@ -166,6 +166,11 @@ function ngApp(req, res) {
|
||||
}, (err, data) => {
|
||||
if (hasNoValue(err) && hasValue(data)) {
|
||||
res.send(data);
|
||||
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
||||
// When this error occurs we can't fall back to CSR because the response has already been
|
||||
// sent. These errors occur for various reasons in universal, not all of which are in our
|
||||
// control to solve.
|
||||
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
||||
} else {
|
||||
console.warn('Error in SSR, serving for direct CSR.');
|
||||
if (hasValue(err)) {
|
||||
|
@@ -14,8 +14,7 @@
|
||||
[pageInfoState]="(bitstreamFormats | async)?.payload"
|
||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="formats" class="table table-striped table-hover">
|
||||
<thead>
|
||||
|
@@ -23,6 +23,11 @@ import {
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('BitstreamFormatsComponent', () => {
|
||||
let comp: BitstreamFormatsComponent;
|
||||
@@ -30,6 +35,7 @@ describe('BitstreamFormatsComponent', () => {
|
||||
let bitstreamFormatService;
|
||||
let scheduler: TestScheduler;
|
||||
let notificationsServiceStub;
|
||||
let paginationService;
|
||||
|
||||
const bitstreamFormat1 = new BitstreamFormat();
|
||||
bitstreamFormat1.uuid = 'test-uuid-1';
|
||||
@@ -79,6 +85,10 @@ describe('BitstreamFormatsComponent', () => {
|
||||
];
|
||||
const mockFormatsRD = createSuccessfulRemoteDataObject(createPaginatedList(mockFormatsList));
|
||||
|
||||
const pagination = Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 });
|
||||
const sort = new SortOptions('score', SortDirection.DESC);
|
||||
const findlistOptions = Object.assign(new FindListOptions(), { currentPage: 1, elementsPerPage: 20 });
|
||||
|
||||
const initAsync = () => {
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
@@ -95,13 +105,16 @@ describe('BitstreamFormatsComponent', () => {
|
||||
clearBitStreamFormatRequests: observableOf('cleared')
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub }
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
{ provide: PaginationService, useValue: paginationService }
|
||||
]
|
||||
}).compileComponents();
|
||||
};
|
||||
@@ -217,13 +230,16 @@ describe('BitstreamFormatsComponent', () => {
|
||||
clearBitStreamFormatRequests: observableOf('cleared')
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub }
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
{ provide: PaginationService, useValue: paginationService }
|
||||
]
|
||||
}).compileComponents();
|
||||
}
|
||||
@@ -263,13 +279,16 @@ describe('BitstreamFormatsComponent', () => {
|
||||
clearBitStreamFormatRequests: observableOf('cleared')
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub }
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
{ provide: PaginationService, useValue: paginationService }
|
||||
]
|
||||
}).compileComponents();
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
@@ -12,6 +12,7 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
|
||||
/**
|
||||
* This component renders a list of bitstream formats
|
||||
@@ -20,18 +21,13 @@ import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
selector: 'ds-bitstream-formats',
|
||||
templateUrl: './bitstream-formats.component.html'
|
||||
})
|
||||
export class BitstreamFormatsComponent implements OnInit {
|
||||
export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* A paginated list of bitstream formats to be shown on the page
|
||||
*/
|
||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||
|
||||
/**
|
||||
* A BehaviourSubject that keeps track of the pageState used to update the currently displayed bitstreamFormats
|
||||
*/
|
||||
pageState: BehaviorSubject<string>;
|
||||
|
||||
/**
|
||||
* The current pagination configuration for the page used by the FindAll method
|
||||
* Currently simply renders all bitstream formats
|
||||
@@ -45,16 +41,19 @@ export class BitstreamFormatsComponent implements OnInit {
|
||||
* Currently simply renders all bitstream formats
|
||||
*/
|
||||
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-bitstreamformats-pagination',
|
||||
id: 'rbp',
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
constructor(private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private translateService: TranslateService,
|
||||
private bitstreamFormatService: BitstreamFormatDataService) {
|
||||
private bitstreamFormatService: BitstreamFormatDataService,
|
||||
private paginationService: PaginationService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the currently selected formats from the registry and updates the presented list
|
||||
*/
|
||||
@@ -80,10 +79,8 @@ export class BitstreamFormatsComponent implements OnInit {
|
||||
|
||||
this.deselectAll();
|
||||
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({}, { page: 1 }),
|
||||
queryParamsHandling: 'merge'
|
||||
}); });
|
||||
this.paginationService.resetPage(this.pageConfig.id);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -141,31 +138,17 @@ export class BitstreamFormatsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When the page is changed, make sure to update the list of bitstreams to match the new page
|
||||
* @param event The page change event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config = Object.assign(new FindListOptions(), this.config, {
|
||||
currentPage: event,
|
||||
});
|
||||
this.pageConfig.currentPage = event;
|
||||
this.pageState.next('pageChange');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pageState = new BehaviorSubject('init');
|
||||
this.bitstreamFormats = this.pageState.pipe(
|
||||
switchMap(() => {
|
||||
return this.updateFormats()
|
||||
;
|
||||
}));
|
||||
|
||||
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
||||
switchMap((findListOptions: FindListOptions) => {
|
||||
return this.bitstreamFormatService.findAll(findListOptions);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all formats based on the current config
|
||||
*/
|
||||
private updateFormats() {
|
||||
return this.bitstreamFormatService.findAll(this.config);
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.pageConfig.id);
|
||||
}
|
||||
}
|
||||
|
@@ -13,8 +13,7 @@
|
||||
[paginationOptions]="config"
|
||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-schemas" class="table table-striped table-hover">
|
||||
|
@@ -18,11 +18,17 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications-
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('MetadataRegistryComponent', () => {
|
||||
let comp: MetadataRegistryComponent;
|
||||
let fixture: ComponentFixture<MetadataRegistryComponent>;
|
||||
let registryService: RegistryService;
|
||||
let paginationService;
|
||||
const mockSchemasList = [
|
||||
{
|
||||
id: 1,
|
||||
@@ -62,6 +68,8 @@ describe('MetadataRegistryComponent', () => {
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
@@ -69,6 +77,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -13,6 +13,7 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-registry',
|
||||
@@ -34,7 +35,7 @@ export class MetadataRegistryComponent {
|
||||
* Pagination config used to display the list of metadata schemas
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-metadataschemas-pagination',
|
||||
id: 'rm',
|
||||
pageSize: 25
|
||||
});
|
||||
|
||||
@@ -46,26 +47,20 @@ export class MetadataRegistryComponent {
|
||||
constructor(private registryService: RegistryService,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private paginationService: PaginationService,
|
||||
private translateService: TranslateService) {
|
||||
this.updateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.forceUpdateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of schemas by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateSchemas() {
|
||||
|
||||
this.metadataSchemas = this.needsUpdate$.pipe(
|
||||
filter((update) => update === true),
|
||||
switchMap(() => this.registryService.getMetadataSchemas(toFindListOptions(this.config)))
|
||||
switchMap(() => this.paginationService.getCurrentPagination(this.config.id, this.config)),
|
||||
switchMap((currentPagination) => this.registryService.getMetadataSchemas(toFindListOptions(currentPagination)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -169,7 +164,7 @@ export class MetadataRegistryComponent {
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount })
|
||||
this.translateService.get(`${prefix}.deleted.${suffix}`, {amount: amount})
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
if (success) {
|
||||
@@ -179,4 +174,8 @@ export class MetadataRegistryComponent {
|
||||
}
|
||||
});
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -19,8 +19,7 @@
|
||||
[pageInfoState]="fields"
|
||||
[collectionSize]="fields?.totalElements"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-fields" class="table table-striped table-hover">
|
||||
<thead>
|
||||
|
@@ -23,6 +23,11 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('MetadataSchemaComponent', () => {
|
||||
let comp: MetadataSchemaComponent;
|
||||
@@ -125,6 +130,8 @@ describe('MetadataSchemaComponent', () => {
|
||||
})
|
||||
});
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
@@ -134,6 +141,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
combineLatest as observableCombineLatest,
|
||||
combineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
zip
|
||||
} from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
@@ -17,12 +18,10 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../../../core/shared/operators';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-schema',
|
||||
@@ -48,7 +47,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
* Pagination config used to display the list of metadata fields
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-metadatafields-pagination',
|
||||
id: 'rm',
|
||||
pageSize: 25,
|
||||
pageSizeOptions: [25, 50, 100, 200]
|
||||
});
|
||||
@@ -62,6 +61,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
private route: ActivatedRoute,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private paginationService: PaginationService,
|
||||
private translateService: TranslateService) {
|
||||
|
||||
}
|
||||
@@ -81,25 +81,17 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.forceUpdateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of fields by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateFields() {
|
||||
this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
|
||||
switchMap(([schema, update]: [MetadataSchema, boolean]) => {
|
||||
this.metadataFields$ = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
switchMap((currentPagination) => combineLatest(this.metadataSchema$, this.needsUpdate$, observableOf(currentPagination))),
|
||||
switchMap(([schema, update, currentPagination]: [MetadataSchema, boolean, PaginationComponentOptions]) => {
|
||||
if (update) {
|
||||
this.needsUpdate$.next(false);
|
||||
}
|
||||
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), !update, true);
|
||||
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(currentPagination), !update, true);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -216,4 +208,8 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
||||
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
|
||||
|
||||
const EDIT_BITSTREAM_PATH = ':id/edit';
|
||||
|
||||
@@ -12,6 +13,13 @@ const EDIT_BITSTREAM_PATH = ':id/edit';
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path:':id/download',
|
||||
component: BitstreamDownloadPageComponent,
|
||||
resolve: {
|
||||
bitstream: BitstreamPageResolver
|
||||
},
|
||||
},
|
||||
{
|
||||
path: EDIT_BITSTREAM_PATH,
|
||||
component: EditBitstreamPageComponent,
|
||||
|
@@ -18,11 +18,17 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-
|
||||
import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('BrowseByDatePageComponent', () => {
|
||||
let comp: BrowseByDatePageComponent;
|
||||
let fixture: ComponentFixture<BrowseByDatePageComponent>;
|
||||
let route: ActivatedRoute;
|
||||
let paginationService;
|
||||
|
||||
const mockCommunity = Object.assign(new Community(), {
|
||||
id: 'test-uuid',
|
||||
@@ -65,6 +71,8 @@ describe('BrowseByDatePageComponent', () => {
|
||||
detectChanges: () => fixture.detectChanges()
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
@@ -74,6 +82,7 @@ describe('BrowseByDatePageComponent', () => {
|
||||
{ provide: BrowseService, useValue: mockBrowseService },
|
||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: ChangeDetectorRef, useValue: mockCdRef }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -8,12 +8,16 @@ import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||
import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-date-page',
|
||||
@@ -37,30 +41,32 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
protected browseService: BrowseService,
|
||||
protected dsoService: DSpaceObjectDataService,
|
||||
protected router: Router,
|
||||
protected paginationService: PaginationService,
|
||||
protected cdRef: ChangeDetectorRef) {
|
||||
super(route, browseService, dsoService, router);
|
||||
super(route, browseService, dsoService, paginationService, router);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||
this.startsWithType = StartsWithType.date;
|
||||
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, this.sortConfig));
|
||||
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
this.route.params,
|
||||
this.route.queryParams,
|
||||
this.route.data,
|
||||
(params, queryParams, data ) => {
|
||||
return Object.assign({}, params, queryParams, data);
|
||||
observableCombineLatest([this.route.params, this.route.queryParams, this.route.data,
|
||||
this.currentPagination$, this.currentSort$]).pipe(
|
||||
map(([routeParams, queryParams, data, currentPage, currentSort]) => {
|
||||
return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort];
|
||||
})
|
||||
.subscribe((params) => {
|
||||
const metadataField = params.metadataField || this.defaultMetadataField;
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
const searchOptions = browseParamsToOptions(params, Object.assign({}), this.sortConfig, this.browseId);
|
||||
this.updatePageWithItems(searchOptions, this.value);
|
||||
this.updateParent(params.scope);
|
||||
this.updateStartsWithOptions(this.browseId, metadataField, params.scope);
|
||||
}));
|
||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||
const metadataField = params.metadataField || this.defaultMetadataField;
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
|
||||
this.updatePageWithItems(searchOptions, this.value);
|
||||
this.updateParent(params.scope);
|
||||
this.updateStartsWithOptions(this.browseId, metadataField, params.scope);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -27,15 +27,13 @@
|
||||
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + browseId | translate, value: (value)? '"' + value + '"': ''} }}"
|
||||
parentname="{{(parent$ | async)?.payload?.name || ''}}"
|
||||
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
||||
[paginationConfig]="paginationConfig"
|
||||
[sortConfig]="sortConfig"
|
||||
[paginationConfig]="(currentPagination$ |async)"
|
||||
[sortConfig]="(currentSort$ |async)"
|
||||
[type]="startsWithType"
|
||||
[startsWithOptions]="startsWithOptions"
|
||||
[enableArrows]="true"
|
||||
(prev)="goPrev()"
|
||||
(next)="goNext()"
|
||||
(pageSizeChange)="pageSizeChange($event)"
|
||||
(sortDirectionChange)="sortDirectionChange($event)">
|
||||
(next)="goNext()">
|
||||
</ds-browse-by>
|
||||
<ds-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-loading>
|
||||
</div>
|
||||
|
@@ -14,7 +14,7 @@ import { RemoteData } from '../../core/data/remote-data';
|
||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
@@ -22,12 +22,16 @@ import { RouterMock } from '../../shared/mocks/router.mock';
|
||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('BrowseByMetadataPageComponent', () => {
|
||||
let comp: BrowseByMetadataPageComponent;
|
||||
let fixture: ComponentFixture<BrowseByMetadataPageComponent>;
|
||||
let browseService: BrowseService;
|
||||
let route: ActivatedRoute;
|
||||
let paginationService;
|
||||
|
||||
const mockCommunity = Object.assign(new Community(), {
|
||||
id: 'test-uuid',
|
||||
@@ -82,6 +86,8 @@ describe('BrowseByMetadataPageComponent', () => {
|
||||
params: observableOf({})
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
@@ -90,6 +96,7 @@ describe('BrowseByMetadataPageComponent', () => {
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: BrowseService, useValue: mockBrowseService },
|
||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: Router, useValue: new RouterMock() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -133,18 +140,23 @@ describe('BrowseByMetadataPageComponent', () => {
|
||||
let result: BrowseEntrySearchOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
const paramsWithPaginationAndScope = {
|
||||
page: 5,
|
||||
pageSize: 10,
|
||||
sortDirection: SortDirection.ASC,
|
||||
sortField: 'fake-field',
|
||||
const paramsScope = {
|
||||
scope: 'fake-scope'
|
||||
};
|
||||
const paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
currentPage: 5,
|
||||
pageSize: 10,
|
||||
});
|
||||
const sortOptions = {
|
||||
direction: SortDirection.ASC,
|
||||
field: 'fake-field',
|
||||
};
|
||||
|
||||
result = browseParamsToOptions(paramsWithPaginationAndScope, Object.assign({}), Object.assign({}), 'author');
|
||||
result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author');
|
||||
});
|
||||
|
||||
it('should return BrowseEntrySearchOptions with the correct properties', () => {
|
||||
|
||||
expect(result.metadataDefinition).toEqual('author');
|
||||
expect(result.pagination.currentPage).toEqual(5);
|
||||
expect(result.pagination.pageSize).toEqual(10);
|
||||
|
@@ -4,7 +4,7 @@ import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||
@@ -15,6 +15,8 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
|
||||
import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-metadata-page',
|
||||
@@ -48,15 +50,20 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
* The pagination config used to display the values
|
||||
*/
|
||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'browse-by-metadata-pagination',
|
||||
id: 'bbm',
|
||||
currentPage: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
/**
|
||||
* The sorting config used to sort the values (defaults to Ascending)
|
||||
* The pagination observable
|
||||
*/
|
||||
sortConfig: SortOptions = new SortOptions('default', SortDirection.ASC);
|
||||
currentPagination$: Observable<PaginationComponentOptions>;
|
||||
|
||||
/**
|
||||
* The sorting config observable
|
||||
*/
|
||||
currentSort$: Observable<SortOptions>;
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
@@ -100,23 +107,25 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
protected browseService: BrowseService,
|
||||
protected dsoService: DSpaceObjectDataService,
|
||||
protected paginationService: PaginationService,
|
||||
protected router: Router) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, this.sortConfig));
|
||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
this.route.params,
|
||||
this.route.queryParams,
|
||||
(params, queryParams, ) => {
|
||||
return Object.assign({}, params, queryParams);
|
||||
observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe(
|
||||
map(([routeParams, queryParams, currentPage, currentSort]) => {
|
||||
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
|
||||
})
|
||||
.subscribe((params) => {
|
||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.value = +params.value || params.value || '';
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
const searchOptions = browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.browseId);
|
||||
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
|
||||
if (isNotEmpty(this.value)) {
|
||||
this.updatePageWithItems(searchOptions, this.value);
|
||||
} else {
|
||||
@@ -158,6 +167,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
* @param value The value of the browse-entry to display items for
|
||||
*/
|
||||
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
||||
console.log('updatePAge', searchOptions);
|
||||
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
||||
}
|
||||
|
||||
@@ -203,32 +213,12 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the page size
|
||||
* @param size
|
||||
*/
|
||||
pageSizeChange(size) {
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({ pageSize: size }),
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the sorting direction
|
||||
* @param direction
|
||||
*/
|
||||
sortDirectionChange(direction) {
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({ sortDirection: direction }),
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
this.paginationService.clearPagination(this.paginationConfig.id);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,20 +234,8 @@ export function browseParamsToOptions(params: any,
|
||||
metadata?: string): BrowseEntrySearchOptions {
|
||||
return new BrowseEntrySearchOptions(
|
||||
metadata,
|
||||
Object.assign({},
|
||||
paginationConfig,
|
||||
{
|
||||
currentPage: +params.page || paginationConfig.currentPage,
|
||||
pageSize: +params.pageSize || paginationConfig.pageSize
|
||||
}
|
||||
),
|
||||
Object.assign({},
|
||||
sortConfig,
|
||||
{
|
||||
direction: params.sortDirection || sortConfig.direction,
|
||||
field: params.sortField || sortConfig.field
|
||||
}
|
||||
),
|
||||
paginationConfig,
|
||||
sortConfig,
|
||||
+params.startsWith || params.startsWith,
|
||||
params.scope
|
||||
);
|
||||
|
@@ -18,6 +18,11 @@ import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('BrowseByTitlePageComponent', () => {
|
||||
let comp: BrowseByTitlePageComponent;
|
||||
@@ -61,6 +66,8 @@ describe('BrowseByTitlePageComponent', () => {
|
||||
data: observableOf({ metadata: 'title' })
|
||||
});
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
@@ -69,6 +76,7 @@ describe('BrowseByTitlePageComponent', () => {
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: BrowseService, useValue: mockBrowseService },
|
||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: Router, useValue: new RouterMock() }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import {
|
||||
BrowseByMetadataPageComponent,
|
||||
@@ -11,6 +11,9 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
|
||||
import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-title-page',
|
||||
@@ -26,26 +29,26 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
protected browseService: BrowseService,
|
||||
protected dsoService: DSpaceObjectDataService,
|
||||
protected paginationService: PaginationService,
|
||||
protected router: Router) {
|
||||
super(route, browseService, dsoService, router);
|
||||
super(route, browseService, dsoService, paginationService, router);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, this.sortConfig));
|
||||
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
this.route.params,
|
||||
this.route.queryParams,
|
||||
this.route.data,
|
||||
(params, queryParams, data ) => {
|
||||
return Object.assign({}, params, queryParams, data);
|
||||
observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe(
|
||||
map(([routeParams, queryParams, currentPage, currentSort]) => {
|
||||
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
|
||||
})
|
||||
.subscribe((params) => {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.updatePageWithItems(browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.browseId), undefined);
|
||||
this.updateParent(params.scope);
|
||||
}));
|
||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined);
|
||||
this.updateParent(params.scope);
|
||||
}));
|
||||
this.updateStartsWithTextOptions();
|
||||
}
|
||||
|
||||
|
@@ -52,8 +52,7 @@
|
||||
[config]="paginationConfig"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="itemRD"
|
||||
[hideGear]="true"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
[hideGear]="true">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
|
||||
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
@@ -26,7 +26,7 @@ import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import {PaginationChangeEvent} from '../shared/pagination/paginationChangeEvent.interface';
|
||||
import { PaginationService } from '../core/pagination/pagination.service';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
||||
@@ -69,10 +69,11 @@ export class CollectionPageComponent implements OnInit {
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private paginationService: PaginationService,
|
||||
private authorizationDataService: AuthorizationDataService,
|
||||
) {
|
||||
this.paginationConfig = new PaginationComponentOptions();
|
||||
this.paginationConfig.id = 'collection-page-pagination';
|
||||
this.paginationConfig.id = 'cp';
|
||||
this.paginationConfig.pageSize = 5;
|
||||
this.paginationConfig.currentPage = 1;
|
||||
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
|
||||
@@ -96,16 +97,19 @@ export class CollectionPageComponent implements OnInit {
|
||||
sortConfig: this.sortConfig
|
||||
});
|
||||
|
||||
this.itemRD$ = this.paginationChanges$.pipe(
|
||||
switchMap((dto) => this.collectionRD$.pipe(
|
||||
const currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||
const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig);
|
||||
|
||||
this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe(
|
||||
switchMap(([currentPagination, currentSort ]) => this.collectionRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
map((rd) => rd.payload.id),
|
||||
switchMap((id: string) => {
|
||||
return this.searchService.search(
|
||||
new PaginatedSearchOptions({
|
||||
scope: id,
|
||||
pagination: dto.paginationConfig,
|
||||
sort: dto.sortConfig,
|
||||
pagination: currentPagination,
|
||||
sort: currentSort,
|
||||
dsoTypes: [DSpaceObjectType.ITEM]
|
||||
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
|
||||
}),
|
||||
@@ -128,19 +132,9 @@ export class CollectionPageComponent implements OnInit {
|
||||
return isNotEmpty(object);
|
||||
}
|
||||
|
||||
onPaginationChange(event: PaginationChangeEvent) {
|
||||
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||
currentPage: event.pagination.currentPage || this.paginationConfig.currentPage,
|
||||
pageSize: event.pagination.pageSize || this.paginationConfig.pageSize,
|
||||
id: 'collection-page-pagination'
|
||||
});
|
||||
this.sortConfig = Object.assign(new SortOptions('dc.date.accessioned', SortDirection.DESC), {
|
||||
direction: event.sort.direction || this.sortConfig.direction,
|
||||
field: event.sort.field || this.sortConfig.field
|
||||
});
|
||||
this.paginationChanges$.next({
|
||||
paginationConfig: this.paginationConfig,
|
||||
sortConfig: this.sortConfig
|
||||
});
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.paginationConfig.id);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -5,8 +5,7 @@
|
||||
[config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="subCollectionsRD"
|
||||
[hideGear]="false"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
[hideGear]="false">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error>
|
||||
|
@@ -18,8 +18,13 @@ import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('CommunityPageSubCollectionList Component', () => {
|
||||
let comp: CommunityPageSubCollectionListComponent;
|
||||
@@ -113,6 +118,8 @@ describe('CommunityPageSubCollectionList Component', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
themeService = getMockThemeService();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -128,6 +135,7 @@ describe('CommunityPageSubCollectionList Component', () => {
|
||||
providers: [
|
||||
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: SelectableListService, useValue: {} },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
],
|
||||
@@ -161,28 +169,4 @@ describe('CommunityPageSubCollectionList Component', () => {
|
||||
const subComHead = fixture.debugElement.queryAll(By.css('h2'));
|
||||
expect(subComHead.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update list of collections on pagination change', () => {
|
||||
subCollList = collections;
|
||||
fixture.detectChanges();
|
||||
|
||||
const pagination = Object.create({
|
||||
pagination:{
|
||||
id: comp.pageId,
|
||||
currentPage: 2,
|
||||
pageSize: 5
|
||||
},
|
||||
sort: {
|
||||
field: 'dc.title',
|
||||
direction: 'ASC'
|
||||
}
|
||||
});
|
||||
comp.onPaginationChange(pagination);
|
||||
fixture.detectChanges();
|
||||
|
||||
const collList = fixture.debugElement.queryAll(By.css('li'));
|
||||
expect(collList.length).toEqual(2);
|
||||
expect(collList[0].nativeElement.textContent).toContain('Collection 6');
|
||||
expect(collList[1].nativeElement.textContent).toContain('Collection 7');
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
@@ -10,7 +10,8 @@ import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { takeUntilCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-page-sub-collection-list',
|
||||
@@ -29,7 +30,7 @@ export class CommunityPageSubCollectionListComponent implements OnInit {
|
||||
/**
|
||||
* The pagination id
|
||||
*/
|
||||
pageId = 'community-collections-pagination';
|
||||
pageId = 'cmcl';
|
||||
|
||||
/**
|
||||
* The sorting configuration
|
||||
@@ -41,7 +42,10 @@ export class CommunityPageSubCollectionListComponent implements OnInit {
|
||||
*/
|
||||
subCollectionsRDObs: BehaviorSubject<RemoteData<PaginatedList<Collection>>> = new BehaviorSubject<RemoteData<PaginatedList<Collection>>>({} as any);
|
||||
|
||||
constructor(private cds: CollectionDataService) {}
|
||||
constructor(private cds: CollectionDataService,
|
||||
private paginationService: PaginationService,
|
||||
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.config = new PaginationComponentOptions();
|
||||
@@ -49,31 +53,31 @@ export class CommunityPageSubCollectionListComponent implements OnInit {
|
||||
this.config.pageSize = 5;
|
||||
this.config.currentPage = 1;
|
||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||
this.updatePage();
|
||||
this.initPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the pagination settings is changed
|
||||
* @param event The new pagination data
|
||||
* Initialise the list of collections
|
||||
*/
|
||||
onPaginationChange(event) {
|
||||
this.config.currentPage = event.pagination.currentPage;
|
||||
this.config.pageSize = event.pagination.pageSize;
|
||||
this.sortConfig.field = event.sort.field;
|
||||
this.sortConfig.direction = event.sort.direction;
|
||||
this.updatePage();
|
||||
}
|
||||
initPage() {
|
||||
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
|
||||
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
|
||||
|
||||
/**
|
||||
* Update the list of collections
|
||||
*/
|
||||
updatePage() {
|
||||
this.cds.findByParent(this.community.id,{
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize,
|
||||
sort: { field: this.sortConfig.field, direction: this.sortConfig.direction }
|
||||
}).pipe(takeUntilCompletedRemoteData()).subscribe((results) => {
|
||||
observableCombineLatest([pagination$, sort$]).pipe(
|
||||
switchMap(([currentPagination, currentSort]) => {
|
||||
return this.cds.findByParent(this.community.id, {
|
||||
currentPage: currentPagination.currentPage,
|
||||
elementsPerPage: currentPagination.pageSize,
|
||||
sort: {field: currentSort.field, direction: currentSort.direction}
|
||||
});
|
||||
})
|
||||
).subscribe((results) => {
|
||||
this.subCollectionsRDObs.next(results);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,8 +5,7 @@
|
||||
[config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="subCommunitiesRD"
|
||||
[hideGear]="false"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
[hideGear]="false">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
|
||||
|
@@ -18,8 +18,13 @@ import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('CommunityPageSubCommunityListComponent Component', () => {
|
||||
let comp: CommunityPageSubCommunityListComponent;
|
||||
@@ -114,6 +119,8 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
themeService = getMockThemeService();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -129,6 +136,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
|
||||
providers: [
|
||||
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: SelectableListService, useValue: {} },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
],
|
||||
@@ -163,28 +171,4 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
|
||||
const subComHead = fixture.debugElement.queryAll(By.css('h2'));
|
||||
expect(subComHead.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update list of sub-communities on pagination change', () => {
|
||||
subCommList = subcommunities;
|
||||
fixture.detectChanges();
|
||||
|
||||
const pagination = Object.create({
|
||||
pagination:{
|
||||
id: comp.pageId,
|
||||
currentPage: 2,
|
||||
pageSize: 5
|
||||
},
|
||||
sort: {
|
||||
field: 'dc.title',
|
||||
direction: 'ASC'
|
||||
}
|
||||
});
|
||||
comp.onPaginationChange(pagination);
|
||||
fixture.detectChanges();
|
||||
|
||||
const collList = fixture.debugElement.queryAll(By.css('li'));
|
||||
expect(collList.length).toEqual(2);
|
||||
expect(collList[0].nativeElement.textContent).toContain('SubCommunity 6');
|
||||
expect(collList[1].nativeElement.textContent).toContain('SubCommunity 7');
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
@@ -10,6 +10,8 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { takeUntilCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-page-sub-community-list',
|
||||
@@ -31,7 +33,7 @@ export class CommunityPageSubCommunityListComponent implements OnInit {
|
||||
/**
|
||||
* The pagination id
|
||||
*/
|
||||
pageId = 'community-subCommunities-pagination';
|
||||
pageId = 'cmscm';
|
||||
|
||||
/**
|
||||
* The sorting configuration
|
||||
@@ -43,7 +45,9 @@ export class CommunityPageSubCommunityListComponent implements OnInit {
|
||||
*/
|
||||
subCommunitiesRDObs: BehaviorSubject<RemoteData<PaginatedList<Community>>> = new BehaviorSubject<RemoteData<PaginatedList<Community>>>({} as any);
|
||||
|
||||
constructor(private cds: CommunityDataService) {
|
||||
constructor(private cds: CommunityDataService,
|
||||
private paginationService: PaginationService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -52,25 +56,29 @@ export class CommunityPageSubCommunityListComponent implements OnInit {
|
||||
this.config.pageSize = 5;
|
||||
this.config.currentPage = 1;
|
||||
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||
this.updatePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the pagination settings is changed
|
||||
* @param event The new pagination data
|
||||
*/
|
||||
onPaginationChange(event) {
|
||||
this.config.currentPage = event.pagination.currentPage;
|
||||
this.config.pageSize = event.pagination.pageSize;
|
||||
this.sortConfig.field = event.sort.field;
|
||||
this.sortConfig.direction = event.sort.direction;
|
||||
this.updatePage();
|
||||
this.initPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of sub-communities
|
||||
*/
|
||||
updatePage() {
|
||||
initPage() {
|
||||
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
|
||||
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
|
||||
|
||||
observableCombineLatest([pagination$, sort$]).pipe(
|
||||
switchMap(([currentPagination, currentSort]) => {
|
||||
return this.cds.findByParent(this.community.id, {
|
||||
currentPage: currentPagination.currentPage,
|
||||
elementsPerPage: currentPagination.pageSize,
|
||||
sort: { field: currentSort.field, direction: currentSort.direction }
|
||||
});
|
||||
})
|
||||
).subscribe((results) => {
|
||||
this.subCommunitiesRDObs.next(results);
|
||||
});
|
||||
|
||||
|
||||
this.cds.findByParent(this.community.id, {
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize,
|
||||
@@ -79,4 +87,9 @@ export class CommunityPageSubCommunityListComponent implements OnInit {
|
||||
this.subCommunitiesRDObs.next(results);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="container">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div>
|
||||
<h1 class="display-3">Welcome to the DSpace 7 Preview</h1>
|
||||
<h1 class="display-3">DSpace 7</h1>
|
||||
<p class="lead">DSpace is the world leading open source repository platform that enables organisations to:</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,6 +13,8 @@
|
||||
</li>
|
||||
<li>issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI</li>
|
||||
</ul>
|
||||
<p>Join an international community of <A HREF="https://wiki.duraspace.org/display/DSPACE/DSpace+Positioning" TARGET="_NEW">leading institutions using DSpace</A>.</p>
|
||||
<p>Join an international community of <a href="https://wiki.lyrasis.org/display/DSPACE/DSpace+Positioning"
|
||||
target="_blank">leading institutions using DSpace</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -6,8 +6,7 @@
|
||||
[config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="communitiesRD$ | async"
|
||||
[hideGear]="true"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
[hideGear]="true">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="communitiesRD?.hasFailed " message="{{'error.top-level-communites' | translate}}"></ds-error>
|
||||
|
@@ -18,13 +18,19 @@ import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('TopLevelCommunityList Component', () => {
|
||||
let comp: TopLevelCommunityListComponent;
|
||||
let fixture: ComponentFixture<TopLevelCommunityListComponent>;
|
||||
let communityDataServiceStub: any;
|
||||
let paginationService;
|
||||
let themeService;
|
||||
|
||||
const topCommList = [Object.assign(new Community(), {
|
||||
@@ -104,6 +110,8 @@ describe('TopLevelCommunityList Component', () => {
|
||||
}
|
||||
};
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
themeService = getMockThemeService();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -119,6 +127,7 @@ describe('TopLevelCommunityList Component', () => {
|
||||
providers: [
|
||||
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: SelectableListService, useValue: {} },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
],
|
||||
@@ -143,25 +152,4 @@ describe('TopLevelCommunityList Component', () => {
|
||||
expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4');
|
||||
expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5');
|
||||
});
|
||||
|
||||
it('should update list of top-communities on pagination change', () => {
|
||||
const pagination = Object.create({
|
||||
pagination: {
|
||||
id: comp.pageId,
|
||||
currentPage: 2,
|
||||
pageSize: 5
|
||||
},
|
||||
sort: {
|
||||
field: 'dc.title',
|
||||
direction: 'ASC'
|
||||
}
|
||||
});
|
||||
comp.onPaginationChange(pagination);
|
||||
fixture.detectChanges();
|
||||
|
||||
const collList = fixture.debugElement.queryAll(By.css('li'));
|
||||
expect(collList.length).toEqual(2);
|
||||
expect(collList[0].nativeElement.textContent).toContain('TopCommunity 6');
|
||||
expect(collList[1].nativeElement.textContent).toContain('TopCommunity 7');
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
|
||||
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
@@ -10,6 +10,8 @@ import { Community } from '../../core/shared/community.model';
|
||||
import { fadeInOut } from '../../shared/animations/fade';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
|
||||
/**
|
||||
* this component renders the Top-Level Community list
|
||||
@@ -36,7 +38,7 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The pagination id
|
||||
*/
|
||||
pageId = 'top-level-pagination';
|
||||
pageId = 'tl';
|
||||
|
||||
/**
|
||||
* The sorting configuration
|
||||
@@ -48,7 +50,8 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
currentPageSubscription: Subscription;
|
||||
|
||||
constructor(private cds: CommunityDataService) {
|
||||
constructor(private cds: CommunityDataService,
|
||||
private paginationService: PaginationService) {
|
||||
this.config = new PaginationComponentOptions();
|
||||
this.config.id = this.pageId;
|
||||
this.config.pageSize = 5;
|
||||
@@ -57,31 +60,26 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.updatePage();
|
||||
this.initPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the pagination settings is changed
|
||||
* @param event The new pagination data
|
||||
*/
|
||||
onPaginationChange(event) {
|
||||
this.config.currentPage = event.pagination.currentPage;
|
||||
this.config.pageSize = event.pagination.pageSize;
|
||||
this.sortConfig.field = event.sort.field;
|
||||
this.sortConfig.direction = event.sort.direction;
|
||||
this.updatePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of top communities
|
||||
*/
|
||||
updatePage() {
|
||||
this.unsubscribe();
|
||||
this.currentPageSubscription = this.cds.findTop({
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize,
|
||||
sort: { field: this.sortConfig.field, direction: this.sortConfig.direction }
|
||||
}).subscribe((results) => {
|
||||
initPage() {
|
||||
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
|
||||
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
|
||||
|
||||
this.currentPageSubscription = observableCombineLatest([pagination$, sort$]).pipe(
|
||||
switchMap(([currentPagination, currentSort]) => {
|
||||
return this.cds.findTop({
|
||||
currentPage: currentPagination.currentPage,
|
||||
elementsPerPage: currentPagination.pageSize,
|
||||
sort: {field: currentSort.field, direction: currentSort.direction}
|
||||
});
|
||||
})
|
||||
).subscribe((results) => {
|
||||
this.communitiesRD$.next(results);
|
||||
});
|
||||
}
|
||||
@@ -100,5 +98,7 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.unsubscribe();
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,9 +4,7 @@
|
||||
[hidePaginationDetail]="true"
|
||||
[paginationOptions]="options"
|
||||
[pageInfoState]="(objectsRD$ | async)?.payload"
|
||||
[collectionSize]="(objectsRD$ | async)?.payload?.totalElements"
|
||||
[disableRouteParameterUpdate]="true"
|
||||
(pageChange)="switchPage($event)">
|
||||
[collectionSize]="(objectsRD$ | async)?.payload?.totalElements">
|
||||
<ng-container *ngIf="!(loading$ | async)">
|
||||
<div [id]="bundle.id" class="bundle-bitstreams-list"
|
||||
[ngClass]="{'mb-3': (objectsRD$ | async)?.payload?.totalElements > pageSize}"
|
||||
|
@@ -16,6 +16,11 @@ import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-si
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../../../../shared/testing/utils.test';
|
||||
import { RequestService } from '../../../../../core/data/request.service';
|
||||
import { PaginationService } from '../../../../../core/pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../../../../core/data/request.models';
|
||||
import { PaginationServiceStub } from '../../../../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
||||
let comp: PaginatedDragAndDropBitstreamListComponent;
|
||||
@@ -24,6 +29,7 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
||||
let bundleService: BundleDataService;
|
||||
let objectValuesPipe: ObjectValuesPipe;
|
||||
let requestService: RequestService;
|
||||
let paginationService;
|
||||
|
||||
const columnSizes = new ResponsiveTableSizes([
|
||||
new ResponsiveColumnSizes(2, 2, 3, 4, 4),
|
||||
@@ -109,6 +115,8 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
||||
hasByHref$: observableOf(true)
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective],
|
||||
@@ -116,7 +124,8 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||
{ provide: BundleDataService, useValue: bundleService },
|
||||
{ provide: ObjectValuesPipe, useValue: objectValuesPipe },
|
||||
{ provide: RequestService, useValue: requestService }
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: PaginationService, useValue: paginationService }
|
||||
], schemas: [
|
||||
NO_ERRORS_SCHEMA
|
||||
]
|
||||
|
@@ -10,6 +10,8 @@ import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-siz
|
||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||
import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe';
|
||||
import { RequestService } from '../../../../../core/data/request.service';
|
||||
import { PaginationService } from '../../../../../core/pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-paginated-drag-and-drop-bitstream-list',
|
||||
@@ -37,8 +39,9 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
|
||||
protected elRef: ElementRef,
|
||||
protected objectValuesPipe: ObjectValuesPipe,
|
||||
protected bundleService: BundleDataService,
|
||||
protected paginationService: PaginationService,
|
||||
protected requestService: RequestService) {
|
||||
super(objectUpdatesService, elRef, objectValuesPipe);
|
||||
super(objectUpdatesService, elRef, objectValuesPipe, paginationService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -50,8 +53,8 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
|
||||
*/
|
||||
initializeObjectsRD(): void {
|
||||
this.objectsRD$ = this.currentPage$.pipe(
|
||||
switchMap((page: number) => {
|
||||
const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })});
|
||||
switchMap((page: PaginationComponentOptions) => {
|
||||
const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, page)});
|
||||
return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe(
|
||||
switchMap((href) => this.requestService.hasByHref$(href)),
|
||||
switchMap(() => this.bundleService.getBitstreams(
|
||||
|
@@ -8,8 +8,7 @@
|
||||
[paginationOptions]="originalOptions"
|
||||
[pageInfoState]="originals"
|
||||
[collectionSize]="originals?.totalElements"
|
||||
[disableRouteParameterUpdate]="true"
|
||||
(pageChange)="switchOriginalPage($event)">
|
||||
[retainScrollPosition]="true">
|
||||
|
||||
|
||||
<div class="file-section row" *ngFor="let file of originals?.page;">
|
||||
@@ -34,7 +33,7 @@
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<ds-file-download-link [href]="file._links.content.href" [download]="file.name">
|
||||
<ds-file-download-link [bitstream]="file">
|
||||
{{"item.page.filesection.download" | translate}}
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
@@ -51,8 +50,7 @@
|
||||
[paginationOptions]="licenseOptions"
|
||||
[pageInfoState]="licenses"
|
||||
[collectionSize]="licenses?.totalElements"
|
||||
[disableRouteParameterUpdate]="true"
|
||||
(pageChange)="switchLicensePage($event)">
|
||||
[retainScrollPosition]="true">
|
||||
|
||||
|
||||
<div class="file-section row" *ngFor="let file of licenses?.page;">
|
||||
@@ -76,7 +74,7 @@
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<ds-file-download-link [href]="file._links.content.href" [download]="file.name">
|
||||
<ds-file-download-link [bitstream]="file">
|
||||
{{"item.page.filesection.download" | translate}}
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
|
@@ -16,6 +16,11 @@ import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../../../core/data/request.models';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('FullFileSectionComponent', () => {
|
||||
let comp: FullFileSectionComponent;
|
||||
@@ -52,6 +57,8 @@ describe('FullFileSectionComponent', () => {
|
||||
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream, mockBitstream, mockBitstream]))
|
||||
});
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
@@ -64,7 +71,8 @@ describe('FullFileSectionComponent', () => {
|
||||
declarations: [FullFileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent],
|
||||
providers: [
|
||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: PaginationService, useValue: paginationService }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -82,39 +90,5 @@ describe('FullFileSectionComponent', () => {
|
||||
const fileSection = fixture.debugElement.queryAll(By.css('.file-section'));
|
||||
expect(fileSection.length).toEqual(6);
|
||||
});
|
||||
|
||||
describe('when we press the pageChange button for original bundle', () => {
|
||||
beforeEach(() => {
|
||||
comp.switchOriginalPage(2);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should give the value to the currentpage', () => {
|
||||
expect(comp.originalOptions.currentPage).toBe(2);
|
||||
});
|
||||
it('should call the next function on the originalCurrentPage', (done) => {
|
||||
comp.originalCurrentPage$.subscribe((event) => {
|
||||
expect(event).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when we press the pageChange button for license bundle', () => {
|
||||
beforeEach(() => {
|
||||
comp.switchLicensePage(2);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should give the value to the currentpage', () => {
|
||||
expect(comp.licenseOptions.currentPage).toBe(2);
|
||||
});
|
||||
it('should call the next function on the licenseCurrentPage', (done) => {
|
||||
comp.licenseCurrentPage$.subscribe((event) => {
|
||||
expect(event).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
@@ -13,6 +13,7 @@ import { switchMap, tap } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { hasValue, isEmpty } from '../../../../shared/empty.util';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
|
||||
/**
|
||||
* This component renders the file section of the item
|
||||
@@ -35,23 +36,22 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
||||
|
||||
pageSize = 5;
|
||||
originalOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'original-bitstreams-options',
|
||||
id: 'obo',
|
||||
currentPage: 1,
|
||||
pageSize: this.pageSize
|
||||
});
|
||||
originalCurrentPage$ = new BehaviorSubject<number>(1);
|
||||
|
||||
licenseOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'license-bitstreams-options',
|
||||
id: 'lbo',
|
||||
currentPage: 1,
|
||||
pageSize: this.pageSize
|
||||
});
|
||||
licenseCurrentPage$ = new BehaviorSubject<number>(1);
|
||||
|
||||
constructor(
|
||||
bitstreamDataService: BitstreamDataService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translateService: TranslateService
|
||||
protected translateService: TranslateService,
|
||||
protected paginationService: PaginationService
|
||||
) {
|
||||
super(bitstreamDataService, notificationsService, translateService);
|
||||
}
|
||||
@@ -61,11 +61,11 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
this.originals$ = this.originalCurrentPage$.pipe(
|
||||
switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
this.originals$ = this.paginationService.getCurrentPagination(this.originalOptions.id, this.originalOptions).pipe(
|
||||
switchMap((options: PaginationComponentOptions) => this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
this.item,
|
||||
'ORIGINAL',
|
||||
{elementsPerPage: this.pageSize, currentPage: pageNumber},
|
||||
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
||||
true,
|
||||
true,
|
||||
followLink('format')
|
||||
@@ -78,11 +78,11 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
||||
)
|
||||
);
|
||||
|
||||
this.licenses$ = this.licenseCurrentPage$.pipe(
|
||||
switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
this.licenses$ = this.paginationService.getCurrentPagination(this.licenseOptions.id, this.licenseOptions).pipe(
|
||||
switchMap((options: PaginationComponentOptions) => this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
this.item,
|
||||
'LICENSE',
|
||||
{elementsPerPage: this.pageSize, currentPage: pageNumber},
|
||||
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
||||
true,
|
||||
true,
|
||||
followLink('format')
|
||||
@@ -97,25 +97,13 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current page for the original bundle bitstreams
|
||||
* @param page
|
||||
*/
|
||||
switchOriginalPage(page: number) {
|
||||
this.originalOptions.currentPage = page;
|
||||
this.originalCurrentPage$.next(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current page for the license bundle bitstreams
|
||||
* @param page
|
||||
*/
|
||||
switchLicensePage(page: number) {
|
||||
this.licenseOptions.currentPage = page;
|
||||
this.licenseCurrentPage$.next(page);
|
||||
}
|
||||
|
||||
hasValuesInBundle(bundle: PaginatedList<Bitstream>) {
|
||||
return hasValue(bundle) && !isEmpty(bundle.page);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.originalOptions.id);
|
||||
this.paginationService.clearPagination(this.licenseOptions.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
||||
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
||||
<div class="file-section">
|
||||
<ds-file-download-link *ngFor="let file of bitstreams; let last=last;" [href]="file?._links.content.href" [download]="file?.name">
|
||||
<ds-file-download-link *ngFor="let file of bitstreams; let last=last;" [bitstream]="file">
|
||||
<span>{{file?.name}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||
|
@@ -9,6 +9,8 @@ import { ActivatedRouteStub } from '../shared/testing/active-router.stub';
|
||||
import { RoleServiceMock } from '../shared/mocks/role-service.mock';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
|
||||
import { PaginationServiceStub } from '../shared/testing/pagination-service.stub';
|
||||
import { PaginationService } from '../core/pagination/pagination.service';
|
||||
|
||||
describe('MyDSpaceConfigurationService', () => {
|
||||
let service: MyDSpaceConfigurationService;
|
||||
@@ -34,12 +36,13 @@ describe('MyDSpaceConfigurationService', () => {
|
||||
getRouteDataValue: observableOf({})
|
||||
});
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
const activatedRoute: any = new ActivatedRouteStub();
|
||||
|
||||
const roleService: any = new RoleServiceMock();
|
||||
|
||||
beforeEach(() => {
|
||||
service = new MyDSpaceConfigurationService(roleService, spy, activatedRoute);
|
||||
service = new MyDSpaceConfigurationService(roleService, spy, paginationService as any, activatedRoute);
|
||||
});
|
||||
|
||||
describe('when the scope is called', () => {
|
||||
@@ -102,25 +105,19 @@ describe('MyDSpaceConfigurationService', () => {
|
||||
|
||||
describe('when getCurrentSort is called', () => {
|
||||
beforeEach(() => {
|
||||
service.getCurrentSort({} as any);
|
||||
service.getCurrentSort('page-id', defaults.sort);
|
||||
});
|
||||
it('should call getQueryParameterValue on the routeService with parameter name \'sortDirection\'', () => {
|
||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortDirection');
|
||||
});
|
||||
it('should call getQueryParameterValue on the routeService with parameter name \'sortField\'', () => {
|
||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortField');
|
||||
it('should call getCurrentSort on the paginationService with the provided id and sort options', () => {
|
||||
expect((service as any).paginationService.getCurrentSort).toHaveBeenCalledWith('page-id', defaults.sort);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getCurrentPagination is called', () => {
|
||||
beforeEach(() => {
|
||||
service.getCurrentPagination({ currentPage: 1, pageSize: 10 } as any);
|
||||
service.getCurrentPagination('page-id', defaults.pagination);
|
||||
});
|
||||
it('should call getQueryParameterValue on the routeService with parameter name \'page\'', () => {
|
||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('page');
|
||||
});
|
||||
it('should call getQueryParameterValue on the routeService with parameter name \'pageSize\'', () => {
|
||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('pageSize');
|
||||
it('should call getCurrentPagination on the paginationService with the provided id and sort options', () => {
|
||||
expect((service as any).paginationService.getCurrentPagination).toHaveBeenCalledWith('page-id', defaults.pagination);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,7 +149,7 @@ describe('MyDSpaceConfigurationService', () => {
|
||||
|
||||
describe('when subscribeToPaginatedSearchOptions is called', () => {
|
||||
beforeEach(() => {
|
||||
(service as any).subscribeToPaginatedSearchOptions(defaults);
|
||||
(service as any).subscribeToPaginatedSearchOptions('id', defaults);
|
||||
});
|
||||
it('should call all getters it needs', () => {
|
||||
expect(service.getCurrentPagination).toHaveBeenCalled();
|
||||
|
@@ -11,6 +11,7 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { PaginationService } from '../core/pagination/pagination.service';
|
||||
|
||||
/**
|
||||
* Service that performs all actions that have to do with the current mydspace configuration
|
||||
@@ -55,13 +56,15 @@ export class MyDSpaceConfigurationService extends SearchConfigurationService {
|
||||
*
|
||||
* @param {roleService} roleService
|
||||
* @param {RouteService} routeService
|
||||
* @param {PaginationService} paginationService
|
||||
* @param {ActivatedRoute} route
|
||||
*/
|
||||
constructor(protected roleService: RoleService,
|
||||
protected routeService: RouteService,
|
||||
protected paginationService: PaginationService,
|
||||
protected route: ActivatedRoute) {
|
||||
|
||||
super(routeService, route);
|
||||
super(routeService, paginationService, route);
|
||||
|
||||
// override parent class initialization
|
||||
this._defaults = null;
|
||||
|
@@ -52,8 +52,7 @@
|
||||
[pageInfoState]="pageInfoState$"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||
|
@@ -25,6 +25,8 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
|
||||
import { RouterStub } from '../../shared/testing/router.stub';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('EPeopleRegistryComponent', () => {
|
||||
let component: EPeopleRegistryComponent;
|
||||
@@ -37,6 +39,8 @@ describe('EPeopleRegistryComponent', () => {
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let modalService;
|
||||
|
||||
let paginationService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
ePersonDataServiceStub = {
|
||||
@@ -115,6 +119,8 @@ describe('EPeopleRegistryComponent', () => {
|
||||
});
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
@@ -131,7 +137,8 @@ describe('EPeopleRegistryComponent', () => {
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) },
|
||||
{ provide: PaginationService, useValue: paginationService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -4,7 +4,7 @@ import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
@@ -14,15 +14,13 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
||||
import { EpersonDtoModel } from '../../core/eperson/models/eperson-dto.model';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getAllSucceededRemoteData
|
||||
} from '../../core/shared/operators';
|
||||
import { getAllSucceededRemoteData, getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { NoContent } from '../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-epeople-registry',
|
||||
@@ -60,7 +58,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
* Pagination config used to display the list of epeople
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'epeople-list-pagination',
|
||||
id: 'elp',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
@@ -78,9 +76,9 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
currentSearchScope: string;
|
||||
|
||||
/**
|
||||
* The subscription for the search method
|
||||
* FindListOptions
|
||||
*/
|
||||
searchSub: Subscription;
|
||||
findListOptionsSub: Subscription;
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
@@ -94,6 +92,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
private modalService: NgbModal,
|
||||
private paginationService: PaginationService,
|
||||
public requestService: RequestService) {
|
||||
this.currentSearchQuery = '';
|
||||
this.currentSearchScope = 'metadata';
|
||||
@@ -113,7 +112,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
initialisePage() {
|
||||
this.searching$.next(true);
|
||||
this.isEPersonFormShown = false;
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
if (eperson != null && eperson.id) {
|
||||
this.isEPersonFormShown = true;
|
||||
@@ -139,56 +138,51 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
return [epeople];
|
||||
}
|
||||
})).subscribe((value: PaginatedList<EpersonDtoModel>) => {
|
||||
this.searching$.next(false);
|
||||
this.ePeopleDto$.next(value);
|
||||
this.pageInfoState$.next(value.pageInfo);
|
||||
this.searching$.next(false);this.ePeopleDto$.next(value);
|
||||
this.pageInfoState$.next(value.pageInfo);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
if (this.config.currentPage !== event) {
|
||||
this.config.currentPage = event;
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in the EPeople by metadata (default) or email
|
||||
* @param data Contains scope and query param
|
||||
*/
|
||||
search(data: any) {
|
||||
this.searching$.next(true);
|
||||
const query: string = data.query;
|
||||
const scope: string = data.scope;
|
||||
if (query != null && this.currentSearchQuery !== query) {
|
||||
this.router.navigateByUrl(this.epersonService.getEPeoplePageRouterLink());
|
||||
this.currentSearchQuery = query;
|
||||
this.config.currentPage = 1;
|
||||
if (hasValue(this.findListOptionsSub)) {
|
||||
this.findListOptionsSub.unsubscribe();
|
||||
}
|
||||
if (scope != null && this.currentSearchScope !== scope) {
|
||||
this.router.navigateByUrl(this.epersonService.getEPeoplePageRouterLink());
|
||||
this.currentSearchScope = scope;
|
||||
this.config.currentPage = 1;
|
||||
}
|
||||
if (hasValue(this.searchSub)) {
|
||||
this.searchSub.unsubscribe();
|
||||
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
|
||||
}
|
||||
this.searchSub = this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize
|
||||
}).pipe(
|
||||
this.findListOptionsSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
switchMap((findListOptions) => {
|
||||
const query: string = data.query;
|
||||
const scope: string = data.scope;
|
||||
if (query != null && this.currentSearchQuery !== query) {
|
||||
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.currentSearchQuery = query;
|
||||
this.paginationService.resetPage(this.config.id);
|
||||
}
|
||||
if (scope != null && this.currentSearchScope !== scope) {
|
||||
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.currentSearchScope = scope;
|
||||
this.paginationService.resetPage(this.config.id);
|
||||
|
||||
}
|
||||
return this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||
currentPage: findListOptions.currentPage,
|
||||
elementsPerPage: findListOptions.pageSize
|
||||
});
|
||||
}
|
||||
),
|
||||
getAllSucceededRemoteData(),
|
||||
).subscribe((peopleRD) => {
|
||||
this.ePeople$.next(peopleRD.payload);
|
||||
this.pageInfoState$.next(peopleRD.payload.pageInfo);
|
||||
}
|
||||
);
|
||||
this.subs.push(this.searchSub);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,7 +237,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
if (hasValue(ePerson.id)) {
|
||||
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
||||
if (restResponse.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name }));
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name}));
|
||||
this.reset();
|
||||
} else {
|
||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||
@@ -260,8 +254,10 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.cleanupSubscribes();
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
|
||||
cleanupSubscribes() {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
@@ -283,7 +279,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
this.searchForm.patchValue({
|
||||
query: '',
|
||||
});
|
||||
this.search({ query: '' });
|
||||
this.search({query: ''});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -26,6 +26,8 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
|
||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('EPersonFormComponent', () => {
|
||||
let component: EPersonFormComponent;
|
||||
@@ -38,6 +40,10 @@ describe('EPersonFormComponent', () => {
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let groupsDataService: GroupDataService;
|
||||
|
||||
let paginationService;
|
||||
|
||||
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
ePersonDataServiceStub = {
|
||||
@@ -104,6 +110,8 @@ describe('EPersonFormComponent', () => {
|
||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
getGroupRegistryRouterLink: ''
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
@@ -121,6 +129,7 @@ describe('EPersonFormComponent', () => {
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -7,7 +7,7 @@ import {
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest, Observable, of, Subscription } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
@@ -16,9 +16,9 @@ import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import {
|
||||
getRemoteDataPayload,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstCompletedRemoteData
|
||||
getRemoteDataPayload
|
||||
} from '../../../core/shared/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
@@ -31,6 +31,7 @@ import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/c
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-eperson-form',
|
||||
@@ -118,7 +119,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* Observable whether or not the admin is allowed to reset the EPerson's password
|
||||
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
|
||||
*/
|
||||
canReset$: Observable<boolean> = of(false);
|
||||
canReset$: Observable<boolean> = observableOf(false);
|
||||
|
||||
/**
|
||||
* Observable whether or not the admin is allowed to delete the EPerson
|
||||
@@ -144,7 +145,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* Pagination config used to display the list of groups
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'groups-ePersonMemberOf-list-pagination',
|
||||
id: 'gem',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
@@ -167,6 +168,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
private authService: AuthService,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
private paginationService: PaginationService,
|
||||
public requestService: RequestService) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.epersonInitial = eperson;
|
||||
@@ -184,13 +186,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* This method will initialise the page
|
||||
*/
|
||||
initialisePage() {
|
||||
combineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||
this.translateService.get(`${this.messagePrefix}.email`),
|
||||
this.translateService.get(`${this.messagePrefix}.canLogIn`),
|
||||
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
|
||||
this.translateService.get(`${this.messagePrefix}.emailHint`),
|
||||
observableCombineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||
this.translateService.get(`${this.messagePrefix}.email`),
|
||||
this.translateService.get(`${this.messagePrefix}.canLogIn`),
|
||||
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
|
||||
this.translateService.get(`${this.messagePrefix}.emailHint`),
|
||||
).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
|
||||
this.firstName = new DynamicInputModel({
|
||||
id: 'firstName',
|
||||
@@ -222,19 +224,19 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
hint: emailHint
|
||||
});
|
||||
this.canLogIn = new DynamicCheckboxModel(
|
||||
{
|
||||
id: 'canLogIn',
|
||||
label: canLogIn,
|
||||
name: 'canLogIn',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
|
||||
});
|
||||
{
|
||||
id: 'canLogIn',
|
||||
label: canLogIn,
|
||||
name: 'canLogIn',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
|
||||
});
|
||||
this.requireCertificate = new DynamicCheckboxModel(
|
||||
{
|
||||
id: 'requireCertificate',
|
||||
label: requireCertificate,
|
||||
name: 'requireCertificate',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
|
||||
});
|
||||
{
|
||||
id: 'requireCertificate',
|
||||
label: requireCertificate,
|
||||
name: 'requireCertificate',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
|
||||
});
|
||||
this.formModel = [
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
@@ -258,11 +260,29 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
requireCertificate: eperson != null ? eperson.requireCertificate : false
|
||||
});
|
||||
}));
|
||||
this.canImpersonate$ = this.epersonService.getActiveEPerson().pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, hasValue(eperson) ? eperson.self : undefined))
|
||||
|
||||
const activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||
|
||||
this.groups = activeEPerson$.pipe(
|
||||
switchMap((eperson) => {
|
||||
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: this.config.pageSize
|
||||
})]);
|
||||
}),
|
||||
switchMap(([eperson, findListOptions]) => {
|
||||
if (eperson != null) {
|
||||
return this.groupsDataService.findAllByHref(eperson._links.groups.href, findListOptions);
|
||||
}
|
||||
return observableOf(undefined);
|
||||
})
|
||||
);
|
||||
this.canDelete$ = this.epersonService.getActiveEPerson().pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
||||
|
||||
this.canImpersonate$ = activeEPerson$.pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, hasValue(eperson) ? eperson.self : undefined))
|
||||
);
|
||||
this.canDelete$ = activeEPerson$.pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -322,10 +342,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd: RemoteData<EPerson>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name }));
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', {name: ePersonToCreate.name}));
|
||||
this.submitForm.emit(ePersonToCreate);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name }));
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', {name: ePersonToCreate.name}));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
@@ -361,10 +381,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
const response = this.epersonService.updateEPerson(editedEperson);
|
||||
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name }));
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', {name: editedEperson.name}));
|
||||
this.submitForm.emit(editedEperson);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name }));
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', {name: editedEperson.name}));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
@@ -469,8 +489,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method will ensure that the page gets reset and that the cache is cleared
|
||||
*/
|
||||
|
@@ -32,8 +32,7 @@
|
||||
[pageInfoState]="(ePeopleSearchDtos | async)"
|
||||
[collectionSize]="(ePeopleSearchDtos | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChangeSearch($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epersonsSearch" class="table table-striped table-hover table-bordered">
|
||||
@@ -86,8 +85,7 @@
|
||||
[pageInfoState]="(ePeopleMembersOfGroupDtos | async)"
|
||||
[collectionSize]="(ePeopleMembersOfGroupDtos | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
|
||||
|
@@ -26,6 +26,8 @@ import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder
|
||||
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { RouterMock } from '../../../../shared/mocks/router.mock';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('MembersListComponent', () => {
|
||||
let component: MembersListComponent;
|
||||
@@ -39,6 +41,7 @@ describe('MembersListComponent', () => {
|
||||
let allGroups;
|
||||
let epersonMembers;
|
||||
let subgroupMembers;
|
||||
let paginationService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
activeGroup = GroupMock;
|
||||
@@ -113,6 +116,8 @@ describe('MembersListComponent', () => {
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
@@ -129,6 +134,7 @@ describe('MembersListComponent', () => {
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -18,13 +18,13 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import {
|
||||
getRemoteDataPayload,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstCompletedRemoteData, getAllCompletedRemoteData
|
||||
getFirstCompletedRemoteData, getAllCompletedRemoteData, getRemoteDataPayload
|
||||
} from '../../../../core/shared/operators';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import {EpersonDtoModel} from '../../../../core/eperson/models/eperson-dto.model';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
|
||||
/**
|
||||
* Keys to keep track of specific subscriptions
|
||||
@@ -60,7 +60,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
* Pagination config used to display the list of EPeople that are result of EPeople search
|
||||
*/
|
||||
configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'search-members-list-pagination',
|
||||
id: 'sml',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
@@ -68,7 +68,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
* Pagination config used to display the list of EPerson Membes of active group being edited
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'members-list-pagination',
|
||||
id: 'ml',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
@@ -91,11 +91,15 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
// current active group being edited
|
||||
groupBeingEdited: Group;
|
||||
|
||||
paginationSub: Subscription;
|
||||
|
||||
|
||||
constructor(private groupDataService: GroupDataService,
|
||||
public ePersonDataService: EPersonDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private paginationService: PaginationService,
|
||||
private router: Router) {
|
||||
this.currentSearchQuery = '';
|
||||
this.currentSearchScope = 'metadata';
|
||||
@@ -114,23 +118,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page on search result
|
||||
* @param event
|
||||
*/
|
||||
onPageChangeSearch(event) {
|
||||
this.configSearch.currentPage = event;
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page on EPerson embers of active group
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.retrieveMembers(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the EPersons that are members of the group
|
||||
*
|
||||
@@ -139,10 +126,15 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
private retrieveMembers(page: number) {
|
||||
this.unsubFrom(SubKey.MembersDTO);
|
||||
this.subs.set(SubKey.MembersDTO, this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
|
||||
currentPage: page,
|
||||
elementsPerPage: this.config.pageSize
|
||||
}).pipe(
|
||||
this.subs.set(SubKey.MembersDTO,
|
||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
switchMap((currentPagination) => {
|
||||
return this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
|
||||
currentPage: currentPagination.currentPage,
|
||||
elementsPerPage: currentPagination.pageSize
|
||||
}
|
||||
);
|
||||
}),
|
||||
getAllCompletedRemoteData(),
|
||||
map((rd: RemoteData<any>) => {
|
||||
if (rd.hasFailed) {
|
||||
@@ -245,26 +237,34 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
* @param data Contains scope and query param
|
||||
*/
|
||||
search(data: any) {
|
||||
const query: string = data.query;
|
||||
const scope: string = data.scope;
|
||||
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
|
||||
this.currentSearchQuery = query;
|
||||
this.configSearch.currentPage = 1;
|
||||
}
|
||||
if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) {
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
|
||||
this.currentSearchScope = scope;
|
||||
this.configSearch.currentPage = 1;
|
||||
}
|
||||
this.searchDone = true;
|
||||
|
||||
this.unsubFrom(SubKey.SearchResultsDTO);
|
||||
this.subs.set(SubKey.SearchResultsDTO,
|
||||
this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||
currentPage: this.configSearch.currentPage,
|
||||
elementsPerPage: this.configSearch.pageSize
|
||||
}, false).pipe(
|
||||
this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
|
||||
switchMap((paginationOptions) => {
|
||||
|
||||
const query: string = data.query;
|
||||
const scope: string = data.scope;
|
||||
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
|
||||
this.router.navigate([], {
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.currentSearchQuery = query;
|
||||
this.paginationService.resetPage(this.configSearch.id);
|
||||
}
|
||||
if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) {
|
||||
this.router.navigate([], {
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.currentSearchScope = scope;
|
||||
this.paginationService.resetPage(this.configSearch.id);
|
||||
}
|
||||
this.searchDone = true;
|
||||
|
||||
return this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||
currentPage: paginationOptions.currentPage,
|
||||
elementsPerPage: paginationOptions.pageSize
|
||||
});
|
||||
}),
|
||||
getAllCompletedRemoteData(),
|
||||
map((rd: RemoteData<any>) => {
|
||||
if (rd.hasFailed) {
|
||||
@@ -300,6 +300,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
for (const key of this.subs.keys()) {
|
||||
this.unsubFrom(key);
|
||||
}
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
this.paginationService.clearPagination(this.configSearch.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -29,8 +29,7 @@
|
||||
[pageInfoState]="(searchResults$ | async)?.payload"
|
||||
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChangeSearch($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="groupsSearch" class="table table-striped table-hover table-bordered">
|
||||
@@ -83,8 +82,7 @@
|
||||
[pageInfoState]="(subGroups$ | async)?.payload"
|
||||
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
||||
|
@@ -35,6 +35,8 @@ import { getMockTranslateService } from '../../../../shared/mocks/translate.serv
|
||||
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('SubgroupsListComponent', () => {
|
||||
let component: SubgroupsListComponent;
|
||||
@@ -47,6 +49,7 @@ describe('SubgroupsListComponent', () => {
|
||||
let subgroups;
|
||||
let allGroups;
|
||||
let routerStub;
|
||||
let paginationService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
activeGroup = GroupMock;
|
||||
@@ -100,6 +103,8 @@ describe('SubgroupsListComponent', () => {
|
||||
routerStub = new RouterMock();
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
@@ -115,6 +120,7 @@ describe('SubgroupsListComponent', () => {
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -2,20 +2,21 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs';
|
||||
import { map, mergeMap, take } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import {
|
||||
getRemoteDataPayload,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstCompletedRemoteData
|
||||
getRemoteDataPayload
|
||||
} from '../../../../core/shared/operators';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { NoContent } from '../../../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
|
||||
/**
|
||||
* Keys to keep track of specific subscriptions
|
||||
@@ -56,7 +57,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
* Pagination config used to display the list of groups that are result of groups search
|
||||
*/
|
||||
configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'search-subgroups-list-pagination',
|
||||
id: 'ssgl',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
@@ -64,7 +65,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
* Pagination config used to display the list of subgroups of currently active group being edited
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'subgroups-list-pagination',
|
||||
id: 'sgl',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
@@ -85,6 +86,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private paginationService: PaginationService,
|
||||
private router: Router) {
|
||||
this.currentSearchQuery = '';
|
||||
}
|
||||
@@ -96,42 +98,27 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.retrieveSubGroups(this.config.currentPage);
|
||||
this.retrieveSubGroups();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page on search result
|
||||
* @param event
|
||||
*/
|
||||
onPageChangeSearch(event) {
|
||||
this.configSearch.currentPage = event;
|
||||
this.search({ query: this.currentSearchQuery });
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page on subgroups of active group
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.retrieveSubGroups(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Subgroups that are members of the group
|
||||
*
|
||||
* @param page the number of the page to retrieve
|
||||
* @private
|
||||
*/
|
||||
private retrieveSubGroups(page: number) {
|
||||
private retrieveSubGroups() {
|
||||
this.unsubFrom(SubKey.Members);
|
||||
this.subs.set(
|
||||
SubKey.Members,
|
||||
this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
|
||||
currentPage: page,
|
||||
elementsPerPage: this.config.pageSize
|
||||
}
|
||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
switchMap((config) => this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
|
||||
currentPage: config.currentPage,
|
||||
elementsPerPage: config.pageSize
|
||||
}
|
||||
))
|
||||
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||
this.subGroups$.next(rd);
|
||||
}));
|
||||
@@ -226,10 +213,12 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
this.searchDone = true;
|
||||
|
||||
this.unsubFrom(SubKey.SearchResults);
|
||||
this.subs.set(SubKey.SearchResults, this.groupDataService.searchGroups(this.currentSearchQuery, {
|
||||
currentPage: this.configSearch.currentPage,
|
||||
elementsPerPage: this.configSearch.pageSize
|
||||
}).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||
this.subs.set(SubKey.SearchResults, this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
|
||||
switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, {
|
||||
currentPage: config.currentPage,
|
||||
elementsPerPage: config.pageSize
|
||||
}))
|
||||
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||
this.searchResults$.next(rd);
|
||||
}));
|
||||
}
|
||||
@@ -255,6 +244,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
for (const key of this.subs.keys()) {
|
||||
this.unsubFrom(key);
|
||||
}
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
this.paginationService.clearPagination(this.configSearch.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -40,8 +40,7 @@
|
||||
[pageInfoState]="pageInfoState$"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
|
@@ -28,6 +28,8 @@ import { TranslateLoaderMock } from '../../shared/testing/translate-loader.mock'
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { routeServiceStub } from '../../shared/testing/route-service.stub';
|
||||
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('GroupRegistryComponent', () => {
|
||||
let component: GroupsRegistryComponent;
|
||||
@@ -39,6 +41,7 @@ describe('GroupRegistryComponent', () => {
|
||||
|
||||
let mockGroups;
|
||||
let mockEPeople;
|
||||
let paginationService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
mockGroups = [GroupMock, GroupMock2];
|
||||
@@ -131,6 +134,7 @@ describe('GroupRegistryComponent', () => {
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
@@ -149,6 +153,7 @@ describe('GroupRegistryComponent', () => {
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -5,15 +5,15 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest as observableCombineLatest,
|
||||
Subscription,
|
||||
Observable,
|
||||
of as observableOf
|
||||
of as observableOf,
|
||||
Subscription
|
||||
} from 'rxjs';
|
||||
import { catchError, map, switchMap, take } from 'rxjs/operators';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||
@@ -24,15 +24,17 @@ import { Group } from '../../core/eperson/models/group.model';
|
||||
import { RouteService } from '../../core/services/route.service';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import {
|
||||
getAllSucceededRemoteDataPayload,
|
||||
getAllSucceededRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData
|
||||
getFirstSucceededRemoteData,
|
||||
getRemoteDataPayload
|
||||
} from '../../core/shared/operators';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { NoContent } from '../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-groups-registry',
|
||||
@@ -50,7 +52,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
* Pagination config used to display the list of groups
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'groups-list-pagination',
|
||||
id: 'gl',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
@@ -83,6 +85,8 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
searchSub: Subscription;
|
||||
|
||||
paginationSub: Subscription;
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
@@ -97,6 +101,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
protected routeService: RouteService,
|
||||
private router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private paginationService: PaginationService,
|
||||
public requestService: RequestService) {
|
||||
this.currentSearchQuery = '';
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
@@ -108,37 +113,30 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
this.search({ query: this.currentSearchQuery });
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.search({ query: this.currentSearchQuery });
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in the groups (searches by group name and by uuid exact match)
|
||||
* @param data Contains query param
|
||||
*/
|
||||
search(data: any) {
|
||||
this.searching$.next(true);
|
||||
const query: string = data.query;
|
||||
if (query != null && this.currentSearchQuery !== query) {
|
||||
this.router.navigateByUrl(this.groupService.getGroupRegistryRouterLink());
|
||||
this.currentSearchQuery = query;
|
||||
this.config.currentPage = 1;
|
||||
}
|
||||
if (hasValue(this.searchSub)) {
|
||||
this.searchSub.unsubscribe();
|
||||
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
|
||||
}
|
||||
|
||||
this.searchSub = this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize
|
||||
}).pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
this.searchSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
switchMap((paginationOptions) => {
|
||||
const query: string = data.query;
|
||||
if (query != null && this.currentSearchQuery !== query) {
|
||||
this.currentSearchQuery = query;
|
||||
this.paginationService.updateRouteWithUrl(this.config.id, [], {page: 1});
|
||||
}
|
||||
return this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
||||
currentPage: paginationOptions.currentPage,
|
||||
elementsPerPage: paginationOptions.pageSize
|
||||
});
|
||||
}),
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
switchMap((groups: PaginatedList<Group>) => {
|
||||
if (groups.page.length === 0) {
|
||||
return observableOf(buildPaginatedList(groups.pageInfo, []));
|
||||
@@ -166,13 +164,15 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
})).pipe(map((dtos: GroupDtoModel[]) => {
|
||||
return buildPaginatedList(groups.pageInfo, dtos);
|
||||
}));
|
||||
})).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
||||
})
|
||||
).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
||||
this.groupsDto$.next(value);
|
||||
this.pageInfoState$.next(value.pageInfo);
|
||||
this.searching$.next(false);
|
||||
});
|
||||
|
||||
this.subs.push(this.searchSub);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Group
|
||||
@@ -248,9 +248,16 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.cleanupSubscribes();
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
|
||||
cleanupSubscribes() {
|
||||
if (hasValue(this.paginationSub)) {
|
||||
this.paginationSub.unsubscribe();
|
||||
}
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { getCommunityPageRoute } from './+community-page/community-page-routing-
|
||||
import { getCollectionPageRoute } from './+collection-page/collection-page-routing-paths';
|
||||
import { getItemPageRoute } from './+item-page/item-page-routing-paths';
|
||||
import { hasValue } from './shared/empty.util';
|
||||
import { URLCombiner } from './core/url-combiner/url-combiner';
|
||||
|
||||
export const BITSTREAM_MODULE_PATH = 'bitstreams';
|
||||
|
||||
@@ -13,6 +14,10 @@ export function getBitstreamModuleRoute() {
|
||||
return `/${BITSTREAM_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
export function getBitstreamDownloadRoute(bitstream): string {
|
||||
return new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString();
|
||||
}
|
||||
|
||||
export const ADMIN_MODULE_PATH = 'admin';
|
||||
|
||||
export function getAdminModuleRoute() {
|
||||
|
74
src/app/core/auth/auth-request.service.spec.ts
Normal file
74
src/app/core/auth/auth-request.service.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { ShortLivedToken } from './models/short-lived-token.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
|
||||
describe(`AuthRequestService`, () => {
|
||||
let halService: HALEndpointService;
|
||||
let endpointURL: string;
|
||||
let shortLivedToken: ShortLivedToken;
|
||||
let shortLivedTokenRD: RemoteData<ShortLivedToken>;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let service: AuthRequestService;
|
||||
let testScheduler;
|
||||
|
||||
class TestAuthRequestService extends AuthRequestService {
|
||||
constructor(
|
||||
hes: HALEndpointService,
|
||||
rs: RequestService,
|
||||
rdbs: RemoteDataBuildService
|
||||
) {
|
||||
super(hes, rs, rdbs);
|
||||
}
|
||||
|
||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
||||
}
|
||||
}
|
||||
|
||||
const init = (cold: typeof TestScheduler.prototype.createColdObservable) => {
|
||||
endpointURL = 'https://rest.api/auth';
|
||||
shortLivedToken = Object.assign(new ShortLivedToken(), {
|
||||
value: 'some-token'
|
||||
});
|
||||
shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken);
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
'getEndpoint': cold('a', { a: endpointURL })
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'send': null
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD })
|
||||
});
|
||||
|
||||
service = new TestAuthRequestService(halService, requestService, rdbService);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
testScheduler = new TestScheduler((actual, expected) => {
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`getShortlivedToken`, () => {
|
||||
it(`should call createShortLivedTokenRequest with the url for the endpoint`, () => {
|
||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||
init(cold);
|
||||
spyOn(service as any, 'createShortLivedTokenRequest');
|
||||
// expectObservable is needed to let testScheduler know to take it in to account, but since
|
||||
// we're not testing the outcome in this test, a .toBe(…) isn't necessary
|
||||
expectObservable(service.getShortlivedToken());
|
||||
flush();
|
||||
expect((service as any).createShortLivedTokenRequest).toHaveBeenCalledWith(`${endpointURL}/shortlivedtokens`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,14 +1,9 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import {
|
||||
GetRequest,
|
||||
PostRequest,
|
||||
RestRequest,
|
||||
} from '../data/request.models';
|
||||
import { GetRequest, PostRequest, RestRequest, } from '../data/request.models';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
@@ -17,8 +12,10 @@ import { AuthStatus } from './models/auth-status.model';
|
||||
import { ShortLivedToken } from './models/short-lived-token.model';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
|
||||
@Injectable()
|
||||
export class AuthRequestService {
|
||||
/**
|
||||
* Abstract service to send authentication requests
|
||||
*/
|
||||
export abstract class AuthRequestService {
|
||||
protected linkName = 'authn';
|
||||
protected browseEndpoint = '';
|
||||
protected shortlivedtokensEndpoint = 'shortlivedtokens';
|
||||
@@ -62,16 +59,26 @@ export class AuthRequestService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST request to retrieve a short-lived token which provides download access of restricted files
|
||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
||||
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
|
||||
* only the server IP to send a GET to this endpoint.
|
||||
*
|
||||
* @param href The href to send the request to
|
||||
* @protected
|
||||
*/
|
||||
protected abstract createShortLivedTokenRequest(href: string): GetRequest | PostRequest;
|
||||
|
||||
/**
|
||||
* Send a request to retrieve a short-lived token which provides download access of restricted files
|
||||
*/
|
||||
public getShortlivedToken(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkName).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
distinctUntilChanged(),
|
||||
map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()),
|
||||
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||
tap((request: PostRequest) => this.requestService.send(request)),
|
||||
switchMap((request: PostRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
|
||||
map((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)),
|
||||
tap((request: RestRequest) => this.requestService.send(request)),
|
||||
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
|
||||
getFirstCompletedRemoteData(),
|
||||
map((response: RemoteData<ShortLivedToken>) => {
|
||||
if (response.hasSucceeded) {
|
||||
|
29
src/app/core/auth/browser-auth-request.service.spec.ts
Normal file
29
src/app/core/auth/browser-auth-request.service.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { BrowserAuthRequestService } from './browser-auth-request.service';
|
||||
|
||||
describe(`BrowserAuthRequestService`, () => {
|
||||
let href: string;
|
||||
let requestService: RequestService;
|
||||
let service: AuthRequestService;
|
||||
|
||||
beforeEach(() => {
|
||||
href = 'https://rest.api/auth/shortlivedtokens';
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2'
|
||||
});
|
||||
service = new BrowserAuthRequestService(null, requestService, null);
|
||||
});
|
||||
|
||||
describe(`createShortLivedTokenRequest`, () => {
|
||||
it(`should return a PostRequest`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.constructor.name).toBe('PostRequest');
|
||||
});
|
||||
|
||||
it(`should return a request with the given href`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.href).toBe(href) ;
|
||||
});
|
||||
});
|
||||
});
|
34
src/app/core/auth/browser-auth-request.service.ts
Normal file
34
src/app/core/auth/browser-auth-request.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
|
||||
/**
|
||||
* Client side version of the service to send authentication requests
|
||||
*/
|
||||
@Injectable()
|
||||
export class BrowserAuthRequestService extends AuthRequestService {
|
||||
|
||||
constructor(
|
||||
halService: HALEndpointService,
|
||||
requestService: RequestService,
|
||||
rdbService: RemoteDataBuildService
|
||||
) {
|
||||
super(halService, requestService, rdbService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
||||
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
|
||||
* only the server IP to send a GET to this endpoint.
|
||||
*
|
||||
* @param href The href to send the request to
|
||||
* @protected
|
||||
*/
|
||||
protected createShortLivedTokenRequest(href: string): PostRequest {
|
||||
return new PostRequest(this.requestService.generateRequestId(), href);
|
||||
}
|
||||
|
||||
}
|
34
src/app/core/auth/server-auth-request.service.spec.ts
Normal file
34
src/app/core/auth/server-auth-request.service.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { ServerAuthRequestService } from './server-auth-request.service';
|
||||
|
||||
describe(`ServerAuthRequestService`, () => {
|
||||
let href: string;
|
||||
let requestService: RequestService;
|
||||
let service: AuthRequestService;
|
||||
|
||||
beforeEach(() => {
|
||||
href = 'https://rest.api/auth/shortlivedtokens';
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2'
|
||||
});
|
||||
service = new ServerAuthRequestService(null, requestService, null);
|
||||
});
|
||||
|
||||
describe(`createShortLivedTokenRequest`, () => {
|
||||
it(`should return a GetRequest`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.constructor.name).toBe('GetRequest');
|
||||
});
|
||||
|
||||
it(`should return a request with the given href`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.href).toBe(href) ;
|
||||
});
|
||||
|
||||
it(`should have a responseMsToLive of 2 seconds`, () => {
|
||||
const result = (service as any).createShortLivedTokenRequest(href);
|
||||
expect(result.responseMsToLive).toBe(2 * 1000) ;
|
||||
});
|
||||
});
|
||||
});
|
36
src/app/core/auth/server-auth-request.service.ts
Normal file
36
src/app/core/auth/server-auth-request.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AuthRequestService } from './auth-request.service';
|
||||
import { GetRequest } from '../data/request.models';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
|
||||
/**
|
||||
* Server side version of the service to send authentication requests
|
||||
*/
|
||||
@Injectable()
|
||||
export class ServerAuthRequestService extends AuthRequestService {
|
||||
|
||||
constructor(
|
||||
halService: HALEndpointService,
|
||||
requestService: RequestService,
|
||||
rdbService: RemoteDataBuildService
|
||||
) {
|
||||
super(halService, requestService, rdbService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create the request object to send. This needs to be a POST client side and
|
||||
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
|
||||
* only the server IP to send a GET to this endpoint.
|
||||
*
|
||||
* @param href The href to send the request to
|
||||
* @protected
|
||||
*/
|
||||
protected createShortLivedTokenRequest(href: string): GetRequest {
|
||||
return Object.assign(new GetRequest(this.requestService.generateRequestId(), href), {
|
||||
responseMsToLive: 2 * 1000 // A short lived token is only valid for 2 seconds.
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -31,7 +31,6 @@ import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||
import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service';
|
||||
import { AuthRequestService } from './auth/auth-request.service';
|
||||
import { AuthenticatedGuard } from './auth/authenticated.guard';
|
||||
import { AuthStatus } from './auth/models/auth-status.model';
|
||||
import { BrowseService } from './browse/browse.service';
|
||||
@@ -188,7 +187,6 @@ const EXPORTS = [];
|
||||
const PROVIDERS = [
|
||||
ApiService,
|
||||
AuthenticatedGuard,
|
||||
AuthRequestService,
|
||||
CommunityDataService,
|
||||
CollectionDataService,
|
||||
SiteDataService,
|
||||
|
@@ -12,4 +12,5 @@ export enum FeatureID {
|
||||
CanManageGroups = 'canManageGroups',
|
||||
IsCollectionAdmin = 'isCollectionAdmin',
|
||||
IsCommunityAdmin = 'isCommunityAdmin',
|
||||
CanDownload = 'canDownload',
|
||||
}
|
||||
|
@@ -181,11 +181,7 @@ describe('MetadataService', () => {
|
||||
Meta,
|
||||
Title,
|
||||
// tslint:disable-next-line:no-empty
|
||||
{ provide: ItemDataService, useValue: { findById: () => { } } },
|
||||
{
|
||||
provide: HardRedirectService,
|
||||
useValue: { rewriteDownloadURL: (a) => a, getRequestOrigin: () => environment.ui.baseUrl }
|
||||
},
|
||||
{ provide: ItemDataService, useValue: { findById: () => {} } },
|
||||
BrowseService,
|
||||
MetadataService
|
||||
],
|
||||
@@ -225,8 +221,8 @@ describe('MetadataService', () => {
|
||||
tick();
|
||||
expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document');
|
||||
expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher');
|
||||
expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual(new URLCombiner(environment.ui.baseUrl, router.url).toString());
|
||||
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content');
|
||||
expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([environment.ui.baseUrl, router.url].join(''));
|
||||
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download');
|
||||
}));
|
||||
|
||||
it('items page should set meta tags as published Technical Report', fakeAsync(() => {
|
||||
|
@@ -19,10 +19,13 @@ import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../shared/operators';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import {
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getFirstSucceededRemoteListPayload
|
||||
} from '../shared/operators';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataService {
|
||||
@@ -41,7 +44,6 @@ export class MetadataService {
|
||||
private dsoNameService: DSONameService,
|
||||
private bitstreamDataService: BitstreamDataService,
|
||||
private bitstreamFormatDataService: BitstreamFormatDataService,
|
||||
private redirectService: HardRedirectService,
|
||||
private rootService: RootDataService
|
||||
) {
|
||||
// TODO: determine what open graph meta tags are needed and whether
|
||||
@@ -262,7 +264,7 @@ export class MetadataService {
|
||||
*/
|
||||
private setCitationAbstractUrlTag(): void {
|
||||
if (this.currentObject.value instanceof Item) {
|
||||
const value = new URLCombiner(this.redirectService.getRequestOrigin(), this.router.url).toString();
|
||||
const value = [environment.ui.baseUrl, this.router.url].join('');
|
||||
this.addMetaTag('citation_abstract_html_url', value);
|
||||
}
|
||||
}
|
||||
@@ -287,8 +289,8 @@ export class MetadataService {
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
).subscribe((format: BitstreamFormat) => {
|
||||
if (format.mimetype === 'application/pdf') {
|
||||
const rewrittenURL= this.redirectService.rewriteDownloadURL(bitstream._links.content.href);
|
||||
this.addMetaTag('citation_pdf_url', rewrittenURL);
|
||||
const bitstreamLink = getBitstreamDownloadRoute(bitstream);
|
||||
this.addMetaTag('citation_pdf_url', bitstreamLink);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
158
src/app/core/pagination/pagination.service.spec.ts
Normal file
158
src/app/core/pagination/pagination.service.spec.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { PaginationService } from './pagination.service';
|
||||
import { RouterStub } from '../../shared/testing/router.stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
|
||||
|
||||
describe('PaginationService', () => {
|
||||
let service: PaginationService;
|
||||
let router;
|
||||
let routeService;
|
||||
|
||||
const defaultPagination = new PaginationComponentOptions();
|
||||
const defaultSort = new SortOptions('id', SortDirection.DESC);
|
||||
const defaultFindListOptions = new FindListOptions();
|
||||
|
||||
beforeEach(() => {
|
||||
router = new RouterStub();
|
||||
routeService = {
|
||||
getQueryParameterValue: (param) => {
|
||||
let value;
|
||||
if (param.endsWith('.page')) {
|
||||
value = 5;
|
||||
}
|
||||
if (param.endsWith('.rpp')) {
|
||||
value = 10;
|
||||
}
|
||||
if (param.endsWith('.sd')) {
|
||||
value = 'ASC';
|
||||
}
|
||||
if (param.endsWith('.sf')) {
|
||||
value = 'score';
|
||||
}
|
||||
return observableOf(value);
|
||||
}
|
||||
};
|
||||
|
||||
service = new PaginationService(routeService, router);
|
||||
});
|
||||
|
||||
|
||||
describe('getCurrentPagination', () => {
|
||||
it('should retrieve the current pagination info from the routerService', () => {
|
||||
service.getCurrentPagination('test-id', defaultPagination).subscribe((currentPagination) => {
|
||||
expect(currentPagination).toEqual(Object.assign(new PaginationComponentOptions(), {
|
||||
currentPage: 5,
|
||||
pageSize: 10
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('getCurrentSort', () => {
|
||||
it('should retrieve the current sort info from the routerService', () => {
|
||||
service.getCurrentSort('test-id', defaultSort).subscribe((currentSort) => {
|
||||
expect(currentSort).toEqual(Object.assign(new SortOptions('score', SortDirection.ASC )));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('getFindListOptions', () => {
|
||||
it('should retrieve the current findListOptions info from the routerService', () => {
|
||||
service.getFindListOptions('test-id', defaultFindListOptions).subscribe((findListOptions) => {
|
||||
expect(findListOptions).toEqual(Object.assign(new FindListOptions(),
|
||||
{
|
||||
sort: new SortOptions('score', SortDirection.ASC ),
|
||||
currentPage: 5,
|
||||
elementsPerPage: 10
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('resetPage', () => {
|
||||
it('should call the updateRoute method with the id and page 1', () => {
|
||||
spyOn(service, 'updateRoute');
|
||||
service.resetPage('test');
|
||||
|
||||
expect(service.updateRoute).toHaveBeenCalledWith('test', {page: 1});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRoute', () => {
|
||||
it('should update the route with the provided page params', () => {
|
||||
service.updateRoute('test', {page: 2, pageSize: 5, sortField: 'title', sortDirection: SortDirection.DESC});
|
||||
|
||||
const navigateParams = {};
|
||||
navigateParams[`test.page`] = `2`;
|
||||
navigateParams[`test.rpp`] = `5`;
|
||||
navigateParams[`test.sf`] = `title`;
|
||||
navigateParams[`test.sd`] = `DESC`;
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith([], {queryParams: navigateParams, queryParamsHandling: 'merge'});
|
||||
});
|
||||
it('should update the route with the provided page params while keeping the existing non provided ones', () => {
|
||||
service.updateRoute('test', {page: 2});
|
||||
|
||||
const navigateParams = {};
|
||||
navigateParams[`test.page`] = `2`;
|
||||
navigateParams[`test.rpp`] = `10`;
|
||||
navigateParams[`test.sf`] = `score`;
|
||||
navigateParams[`test.sd`] = `ASC`;
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith([], {queryParams: navigateParams, queryParamsHandling: 'merge'});
|
||||
});
|
||||
});
|
||||
describe('updateRouteWithUrl', () => {
|
||||
it('should update the route with the provided page params and url', () => {
|
||||
service.updateRouteWithUrl('test', ['someUrl'], {page: 2, pageSize: 5, sortField: 'title', sortDirection: SortDirection.DESC});
|
||||
|
||||
const navigateParams = {};
|
||||
navigateParams[`test.page`] = `2`;
|
||||
navigateParams[`test.rpp`] = `5`;
|
||||
navigateParams[`test.sf`] = `title`;
|
||||
navigateParams[`test.sd`] = `DESC`;
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(['someUrl'], {queryParams: navigateParams, queryParamsHandling: 'merge'});
|
||||
});
|
||||
it('should update the route with the provided page params and url while keeping the existing non provided ones', () => {
|
||||
service.updateRouteWithUrl('test',['someUrl'], {page: 2});
|
||||
|
||||
const navigateParams = {};
|
||||
navigateParams[`test.page`] = `2`;
|
||||
navigateParams[`test.rpp`] = `10`;
|
||||
navigateParams[`test.sf`] = `score`;
|
||||
navigateParams[`test.sd`] = `ASC`;
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(['someUrl'], {queryParams: navigateParams, queryParamsHandling: 'merge'});
|
||||
});
|
||||
|
||||
});
|
||||
describe('clearPagination', () => {
|
||||
it('should clear the pagination next time the updateRoute/updateRouteWithUrl method is called', () => {
|
||||
service.clearPagination('test');
|
||||
|
||||
const resetParams = {};
|
||||
resetParams[`test.page`] = null;
|
||||
resetParams[`test.rpp`] = null;
|
||||
resetParams[`test.sf`] = null;
|
||||
resetParams[`test.sd`] = null;
|
||||
|
||||
|
||||
const navigateParams = {};
|
||||
navigateParams[`another-id.page`] = `5`;
|
||||
navigateParams[`another-id.rpp`] = `10`;
|
||||
navigateParams[`another-id.sf`] = `score`;
|
||||
navigateParams[`another-id.sd`] = `ASC`;
|
||||
|
||||
service.updateRoute('another-id', {});
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith([], {queryParams: Object.assign({}, resetParams, navigateParams), queryParamsHandling: 'merge'});
|
||||
});
|
||||
});
|
||||
describe('getPageParam', () => {
|
||||
it('should return the name of the page param', () => {
|
||||
const pageParam = service.getPageParam('test');
|
||||
expect(pageParam).toEqual('test.page');
|
||||
});
|
||||
});
|
||||
});
|
242
src/app/core/pagination/pagination.service.ts
Normal file
242
src/app/core/pagination/pagination.service.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouteService } from '../services/route.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { filter, map, take } from 'rxjs/operators';
|
||||
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { difference } from '../../shared/object.util';
|
||||
import { isNumeric } from 'rxjs/internal-compatibility';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
/**
|
||||
* Service to manage the pagination of different components
|
||||
* The pagination information will be stored in the route based on a paginationID.
|
||||
* The following params are used for the different kind of pagination information:
|
||||
* - For the page: {paginationID}.p
|
||||
* - For the page size: {paginationID}.rpp
|
||||
* - For the sort direction: {paginationID}.sd
|
||||
* - For the sort field: {paginationID}.sf
|
||||
*/
|
||||
export class PaginationService {
|
||||
|
||||
private defaultSortOptions = new SortOptions('id', SortDirection.ASC);
|
||||
|
||||
private clearParams = {};
|
||||
|
||||
constructor(protected routeService: RouteService,
|
||||
protected router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to retrieve the current pagination settings for an ID based on the router params and default options
|
||||
* @param paginationId - The id to check the pagination for
|
||||
* @param defaultPagination - The default pagination values to be used when no route info is present
|
||||
* @returns {Observable<PaginationComponentOptions>} Retrieves the current pagination settings based on the router params
|
||||
*/
|
||||
getCurrentPagination(paginationId: string, defaultPagination: PaginationComponentOptions): Observable<PaginationComponentOptions> {
|
||||
const page$ = this.routeService.getQueryParameterValue(`${paginationId}.page`);
|
||||
const size$ = this.routeService.getQueryParameterValue(`${paginationId}.rpp`);
|
||||
return observableCombineLatest([page$, size$]).pipe(
|
||||
map(([page, size]) => {
|
||||
return Object.assign(new PaginationComponentOptions(), defaultPagination, {
|
||||
currentPage: this.convertToNumeric(page, defaultPagination.currentPage),
|
||||
pageSize: this.getBestMatchPageSize(size, defaultPagination)
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to retrieve the current sort options for an ID based on the router params and default options
|
||||
* @param paginationId - The id to check the sort options for
|
||||
* @param defaultSort - The default sort options to be used when no route info is present
|
||||
* @param ignoreDefault - Indicate whether the default should be ignored
|
||||
* @returns {Observable<SortOptions>} Retrieves the current sort options based on the router params
|
||||
*/
|
||||
getCurrentSort(paginationId: string, defaultSort: SortOptions, ignoreDefault?: boolean): Observable<SortOptions> {
|
||||
if (!ignoreDefault && (isEmpty(defaultSort) || !hasValue(defaultSort))) {
|
||||
defaultSort = this.defaultSortOptions;
|
||||
}
|
||||
const sortDirection$ = this.routeService.getQueryParameterValue(`${paginationId}.sd`);
|
||||
const sortField$ = this.routeService.getQueryParameterValue(`${paginationId}.sf`);
|
||||
return observableCombineLatest([sortDirection$, sortField$]).pipe(map(([sortDirection, sortField]) => {
|
||||
const field = sortField || defaultSort?.field;
|
||||
const direction = SortDirection[sortDirection] || defaultSort?.direction;
|
||||
return new SortOptions(field, direction);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to retrieve the current find list options for an ID based on the router params and default options
|
||||
* @param paginationId - The id to check the find list options for
|
||||
* @param defaultFindList - The default find list options to be used when no route info is present
|
||||
* @param ignoreDefault - Indicate whether the default should be ignored
|
||||
* @returns {Observable<FindListOptions>} Retrieves the current find list options based on the router params
|
||||
*/
|
||||
getFindListOptions(paginationId: string, defaultFindList: FindListOptions, ignoreDefault?: boolean): Observable<FindListOptions> {
|
||||
const paginationComponentOptions = new PaginationComponentOptions();
|
||||
paginationComponentOptions.currentPage = defaultFindList.currentPage;
|
||||
paginationComponentOptions.pageSize = defaultFindList.elementsPerPage;
|
||||
const currentPagination$ = this.getCurrentPagination(paginationId, paginationComponentOptions);
|
||||
const currentSortOptions$ = this.getCurrentSort(paginationId, defaultFindList.sort, ignoreDefault);
|
||||
|
||||
return observableCombineLatest([currentPagination$, currentSortOptions$]).pipe(
|
||||
filter(([currentPagination, currentSortOptions]) => hasValue(currentPagination) && hasValue(currentSortOptions)),
|
||||
map(([currentPagination, currentSortOptions]) => {
|
||||
return Object.assign(new FindListOptions(), defaultFindList, {
|
||||
sort: currentSortOptions,
|
||||
currentPage: currentPagination.currentPage,
|
||||
elementsPerPage: currentPagination.pageSize
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the current page for the provided pagination ID to 1.
|
||||
* @param paginationId - The pagination id for which to reset the page
|
||||
*/
|
||||
resetPage(paginationId: string) {
|
||||
this.updateRoute(paginationId, {page: 1});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the route with the provided information
|
||||
* @param paginationId - The pagination ID for which to update the route with info
|
||||
* @param params - The page related params to update in the route
|
||||
* @param extraParams - Addition params unrelated to the pagination that need to be added to the route
|
||||
* @param retainScrollPosition - Scroll to the pagination component after updating the route instead of the top of the page
|
||||
*/
|
||||
updateRoute(paginationId: string, params: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
sortField?: string
|
||||
sortDirection?: SortDirection
|
||||
}, extraParams?, retainScrollPosition?: boolean) {
|
||||
|
||||
this.updateRouteWithUrl(paginationId, [], params, extraParams, retainScrollPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the route with the provided information
|
||||
* @param paginationId - The pagination ID for which to update the route with info
|
||||
* @param url - The url to navigate to
|
||||
* @param params - The page related params to update in the route
|
||||
* @param extraParams - Addition params unrelated to the pagination that need to be added to the route
|
||||
* @param retainScrollPosition - Scroll to the pagination component after updating the route instead of the top of the page
|
||||
*/
|
||||
updateRouteWithUrl(paginationId: string, url: string[], params: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
sortField?: string
|
||||
sortDirection?: SortDirection
|
||||
}, extraParams?, retainScrollPosition?: boolean) {
|
||||
this.getCurrentRouting(paginationId).subscribe((currentFindListOptions) => {
|
||||
const currentParametersWithIdName = this.getParametersWithIdName(paginationId, currentFindListOptions);
|
||||
const parametersWithIdName = this.getParametersWithIdName(paginationId, params);
|
||||
if (isNotEmpty(difference(parametersWithIdName, currentParametersWithIdName)) || isNotEmpty(extraParams) || isNotEmpty(this.clearParams)) {
|
||||
const queryParams = Object.assign({}, this.clearParams, currentParametersWithIdName,
|
||||
parametersWithIdName, extraParams);
|
||||
if (retainScrollPosition) {
|
||||
this.router.navigate(url, {
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: `p-${paginationId}`
|
||||
});
|
||||
} else {
|
||||
this.router.navigate(url, {
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
}
|
||||
this.clearParams = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the params to be cleared to the clearParams variable.
|
||||
* When the updateRoute or updateRouteWithUrl these params will be removed from the route pagination
|
||||
* @param paginationId - The ID for which to clear the params
|
||||
*/
|
||||
clearPagination(paginationId: string) {
|
||||
const params = {};
|
||||
params[`${paginationId}.page`] = null;
|
||||
params[`${paginationId}.rpp`] = null;
|
||||
params[`${paginationId}.sf`] = null;
|
||||
params[`${paginationId}.sd`] = null;
|
||||
|
||||
Object.assign(this.clearParams, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the page parameter for the provided id
|
||||
* @param paginationId - The ID for which to retrieve the page param
|
||||
*/
|
||||
getPageParam(paginationId: string) {
|
||||
return `${paginationId}.page`;
|
||||
}
|
||||
|
||||
private getCurrentRouting(paginationId: string) {
|
||||
return this.getFindListOptions(paginationId, {}, true).pipe(
|
||||
take(1),
|
||||
map((findListoptions: FindListOptions) => {
|
||||
return {
|
||||
page: findListoptions.currentPage,
|
||||
pageSize: findListoptions.elementsPerPage,
|
||||
sortField: findListoptions.sort.field,
|
||||
sortDirection: findListoptions.sort.direction,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getParametersWithIdName(paginationId: string, params: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
sortField?: string
|
||||
sortDirection?: SortDirection
|
||||
}) {
|
||||
const paramsWithIdName = {};
|
||||
if (hasValue(params.page)) {
|
||||
paramsWithIdName[`${paginationId}.page`] = `${params.page}`;
|
||||
}
|
||||
if (hasValue(params.pageSize)) {
|
||||
paramsWithIdName[`${paginationId}.rpp`] = `${params.pageSize}`;
|
||||
}
|
||||
if (hasValue(params.sortField)) {
|
||||
paramsWithIdName[`${paginationId}.sf`] = `${params.sortField}`;
|
||||
}
|
||||
if (hasValue(params.sortDirection)) {
|
||||
paramsWithIdName[`${paginationId}.sd`] = `${params.sortDirection}`;
|
||||
}
|
||||
return paramsWithIdName;
|
||||
}
|
||||
|
||||
private convertToNumeric(param, defaultValue) {
|
||||
let result = defaultValue;
|
||||
if (isNumeric(param)) {
|
||||
result = +param;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private getBestMatchPageSize(pageSize: any, defaultPagination: PaginationComponentOptions): number {
|
||||
const numberPageSize = this.convertToNumeric(pageSize, defaultPagination.pageSize);
|
||||
const differenceList = defaultPagination.pageSizeOptions.map((pageSizeOption) => {
|
||||
return Math.abs(pageSizeOption - numberPageSize);
|
||||
});
|
||||
const minDifference = Math.min.apply(Math, differenceList);
|
||||
return defaultPagination.pageSizeOptions[differenceList.indexOf(minDifference)];
|
||||
}
|
||||
|
||||
}
|
@@ -18,7 +18,7 @@ import { AddUrlToHistoryAction } from '../history/history.actions';
|
||||
*/
|
||||
export const routeParametersSelector = createSelector(
|
||||
coreSelector,
|
||||
(state: CoreState) => state.route.params
|
||||
(state: CoreState) => hasValue(state) && hasValue(state.route) ? state.route.params : undefined
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ export const routeParametersSelector = createSelector(
|
||||
*/
|
||||
export const queryParametersSelector = createSelector(
|
||||
coreSelector,
|
||||
(state: CoreState) => state.route.queryParams
|
||||
(state: CoreState) => hasValue(state) && hasValue(state.route) ? state.route.queryParams : undefined
|
||||
);
|
||||
|
||||
/**
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
|
||||
/**
|
||||
* Provides utility methods to save files on the client-side.
|
||||
@@ -17,17 +18,16 @@ export class FileService {
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Combines an URL with a short-lived token and sets the current URL to the newly created one
|
||||
* Combines an URL with a short-lived token and sets the current URL to the newly created one and returns it
|
||||
*
|
||||
* @param url
|
||||
* file url
|
||||
*/
|
||||
downloadFile(url: string) {
|
||||
this.authService.getShortlivedToken().pipe(take(1)).subscribe((token) => {
|
||||
this._window.nativeWindow.location.href = hasValue(token) ? new URLCombiner(url, `?authentication-token=${token}`).toString() : url;
|
||||
});
|
||||
retrieveFileDownloadLink(url: string): Observable<string> {
|
||||
return this.authService.getShortlivedToken().pipe(take(1), map((token) =>
|
||||
hasValue(token) ? new URLCombiner(url, `?authentication-token=${token}`).toString() : url
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives file name from the http response
|
||||
* by looking inside content-disposition
|
||||
|
@@ -45,32 +45,32 @@ export const sendRequest = (requestService: RequestService) =>
|
||||
(source: Observable<RestRequest>): Observable<RestRequest> =>
|
||||
source.pipe(tap((request: RestRequest) => requestService.send(request)));
|
||||
|
||||
export const getRemoteDataPayload = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
export const getRemoteDataPayload = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload));
|
||||
|
||||
export const getPaginatedListPayload = () =>
|
||||
<T>(source: Observable<PaginatedList<T>>): Observable<T[]> =>
|
||||
export const getPaginatedListPayload = <T>() =>
|
||||
(source: Observable<PaginatedList<T>>): Observable<T[]> =>
|
||||
source.pipe(map((list: PaginatedList<T>) => list.page));
|
||||
|
||||
export const getAllCompletedRemoteData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
export const getAllCompletedRemoteData = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(filter((rd: RemoteData<T>) => hasValue(rd) && rd.hasCompleted));
|
||||
|
||||
export const getFirstCompletedRemoteData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
export const getFirstCompletedRemoteData = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(getAllCompletedRemoteData(), take(1));
|
||||
|
||||
export const takeUntilCompletedRemoteData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
export const takeUntilCompletedRemoteData = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(takeWhile((rd: RemoteData<T>) => hasNoValue(rd) || rd.isLoading, true));
|
||||
|
||||
export const getFirstSucceededRemoteData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
export const getFirstSucceededRemoteData = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(filter((rd: RemoteData<T>) => rd.hasSucceeded), take(1));
|
||||
|
||||
export const getFirstSucceededRemoteWithNotEmptyData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
export const getFirstSucceededRemoteWithNotEmptyData = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded && isNotEmpty(rd.payload)));
|
||||
|
||||
/**
|
||||
@@ -83,8 +83,8 @@ export const getFirstSucceededRemoteWithNotEmptyData = () =>
|
||||
* These operators were created as a first step in refactoring
|
||||
* out all the instances where this is used incorrectly.
|
||||
*/
|
||||
export const getFirstSucceededRemoteDataPayload = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
export const getFirstSucceededRemoteDataPayload = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
source.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload()
|
||||
@@ -100,8 +100,8 @@ export const getFirstSucceededRemoteDataPayload = () =>
|
||||
* These operators were created as a first step in refactoring
|
||||
* out all the instances where this is used incorrectly.
|
||||
*/
|
||||
export const getFirstSucceededRemoteDataWithNotEmptyPayload = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
export const getFirstSucceededRemoteDataWithNotEmptyPayload = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
source.pipe(
|
||||
getFirstSucceededRemoteWithNotEmptyData(),
|
||||
getRemoteDataPayload()
|
||||
@@ -117,8 +117,8 @@ export const getFirstSucceededRemoteDataWithNotEmptyPayload = () =>
|
||||
* These operators were created as a first step in refactoring
|
||||
* out all the instances where this is used incorrectly.
|
||||
*/
|
||||
export const getAllSucceededRemoteDataPayload = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
export const getAllSucceededRemoteDataPayload = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||
source.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload()
|
||||
@@ -138,8 +138,8 @@ export const getAllSucceededRemoteDataPayload = () =>
|
||||
* These operators were created as a first step in refactoring
|
||||
* out all the instances where this is used incorrectly.
|
||||
*/
|
||||
export const getFirstSucceededRemoteListPayload = () =>
|
||||
<T>(source: Observable<RemoteData<PaginatedList<T>>>): Observable<T[]> =>
|
||||
export const getFirstSucceededRemoteListPayload = <T>() =>
|
||||
(source: Observable<RemoteData<PaginatedList<T>>>): Observable<T[]> =>
|
||||
source.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
@@ -160,8 +160,8 @@ export const getFirstSucceededRemoteListPayload = () =>
|
||||
* These operators were created as a first step in refactoring
|
||||
* out all the instances where this is used incorrectly.
|
||||
*/
|
||||
export const getAllSucceededRemoteListPayload = () =>
|
||||
<T>(source: Observable<RemoteData<PaginatedList<T>>>): Observable<T[]> =>
|
||||
export const getAllSucceededRemoteListPayload = <T>() =>
|
||||
(source: Observable<RemoteData<PaginatedList<T>>>): Observable<T[]> =>
|
||||
source.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
@@ -174,8 +174,8 @@ export const getAllSucceededRemoteListPayload = () =>
|
||||
* @param router The router used to navigate to a new page
|
||||
* @param authService Service to check if the user is authenticated
|
||||
*/
|
||||
export const redirectOn4xx = (router: Router, authService: AuthService) =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
export const redirectOn4xx = <T>(router: Router, authService: AuthService) =>
|
||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
observableCombineLatest(source, authService.isAuthenticated()).pipe(
|
||||
map(([rd, isAuthenticated]: [RemoteData<T>, boolean]) => {
|
||||
if (rd.hasFailed) {
|
||||
@@ -229,16 +229,16 @@ export const returnEndUserAgreementUrlTreeOnFalse = (router: Router, redirect: s
|
||||
return hasAgreed ? hasAgreed : router.createUrlTree([getEndUserAgreementPath()], { queryParams });
|
||||
}));
|
||||
|
||||
export const getFinishedRemoteData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
export const getFinishedRemoteData = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(find((rd: RemoteData<T>) => !rd.isLoading));
|
||||
|
||||
export const getAllSucceededRemoteData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
export const getAllSucceededRemoteData = <T>() =>
|
||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(filter((rd: RemoteData<T>) => rd.hasSucceeded));
|
||||
|
||||
export const toDSpaceObjectListRD = () =>
|
||||
<T extends DSpaceObject>(source: Observable<RemoteData<PaginatedList<SearchResult<T>>>>): Observable<RemoteData<PaginatedList<T>>> =>
|
||||
export const toDSpaceObjectListRD = <T extends DSpaceObject>() =>
|
||||
(source: Observable<RemoteData<PaginatedList<SearchResult<T>>>>): Observable<RemoteData<PaginatedList<T>>> =>
|
||||
source.pipe(
|
||||
filter((rd: RemoteData<PaginatedList<SearchResult<T>>>) => rd.hasSucceeded),
|
||||
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
|
||||
|
@@ -5,6 +5,7 @@ import { SortDirection, SortOptions } from '../../cache/models/sort-options.mode
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { SearchFilter } from '../../../shared/search/search-filter.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('SearchConfigurationService', () => {
|
||||
let service: SearchConfigurationService;
|
||||
@@ -15,7 +16,7 @@ describe('SearchConfigurationService', () => {
|
||||
'f.date.max': ['2018']
|
||||
};
|
||||
const defaults = new PaginatedSearchOptions({
|
||||
pagination: Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 }),
|
||||
pagination: Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 }),
|
||||
sort: new SortOptions('score', SortDirection.DESC),
|
||||
configuration: 'default',
|
||||
query: '',
|
||||
@@ -30,10 +31,13 @@ describe('SearchConfigurationService', () => {
|
||||
getRouteParameterValue: observableOf('')
|
||||
});
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
|
||||
const activatedRoute: any = new ActivatedRouteStub();
|
||||
|
||||
beforeEach(() => {
|
||||
service = new SearchConfigurationService(routeService, activatedRoute);
|
||||
service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute);
|
||||
});
|
||||
describe('when the scope is called', () => {
|
||||
beforeEach(() => {
|
||||
@@ -95,25 +99,19 @@ describe('SearchConfigurationService', () => {
|
||||
|
||||
describe('when getCurrentSort is called', () => {
|
||||
beforeEach(() => {
|
||||
service.getCurrentSort({} as any);
|
||||
service.getCurrentSort(defaults.pagination.id, {} as any);
|
||||
});
|
||||
it('should call getQueryParameterValue on the routeService with parameter name \'sortDirection\'', () => {
|
||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortDirection');
|
||||
});
|
||||
it('should call getQueryParameterValue on the routeService with parameter name \'sortField\'', () => {
|
||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortField');
|
||||
it('should call getCurrentSort on the paginationService with the provided id and sort options', () => {
|
||||
expect((service as any).paginationService.getCurrentSort).toHaveBeenCalledWith(defaults.pagination.id, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getCurrentPagination is called', () => {
|
||||
beforeEach(() => {
|
||||
service.getCurrentPagination({ currentPage: 1, pageSize: 10 } as any);
|
||||
service.getCurrentPagination(defaults.pagination.id, defaults.pagination);
|
||||
});
|
||||
it('should call getQueryParameterValue on the routeService with parameter name \'page\'', () => {
|
||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('page');
|
||||
});
|
||||
it('should call getQueryParameterValue on the routeService with parameter name \'pageSize\'', () => {
|
||||
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('pageSize');
|
||||
it('should call getCurrentPagination on the paginationService with the provided id and sort options', () => {
|
||||
expect((service as any).paginationService.getCurrentPagination).toHaveBeenCalledWith(defaults.pagination.id, defaults.pagination);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -145,7 +143,7 @@ describe('SearchConfigurationService', () => {
|
||||
|
||||
describe('when subscribeToPaginatedSearchOptions is called', () => {
|
||||
beforeEach(() => {
|
||||
(service as any).subscribeToPaginatedSearchOptions(defaults);
|
||||
(service as any).subscribeToPaginatedSearchOptions(defaults.pagination.id, defaults);
|
||||
});
|
||||
it('should call all getters it needs', () => {
|
||||
expect(service.getCurrentPagination).toHaveBeenCalled();
|
||||
|
@@ -14,17 +14,20 @@ import { RouteService } from '../../services/route.service';
|
||||
import { getFirstSucceededRemoteData } from '../operators';
|
||||
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { PaginationService } from '../../pagination/pagination.service';
|
||||
|
||||
/**
|
||||
* Service that performs all actions that have to do with the current search configuration
|
||||
*/
|
||||
@Injectable()
|
||||
export class SearchConfigurationService implements OnDestroy {
|
||||
|
||||
public paginationID = 'spc';
|
||||
/**
|
||||
* Default pagination settings
|
||||
*/
|
||||
protected defaultPagination = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'search-page-configuration',
|
||||
id: this.paginationID,
|
||||
pageSize: 10,
|
||||
currentPage: 1
|
||||
});
|
||||
@@ -75,6 +78,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
* @param {ActivatedRoute} route
|
||||
*/
|
||||
constructor(protected routeService: RouteService,
|
||||
protected paginationService: PaginationService,
|
||||
protected route: ActivatedRoute) {
|
||||
|
||||
this.initDefaults();
|
||||
@@ -91,7 +95,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
this.paginatedSearchOptions = new BehaviorSubject<PaginatedSearchOptions>(defs);
|
||||
this.searchOptions = new BehaviorSubject<SearchOptions>(defs);
|
||||
this.subs.push(this.subscribeToSearchOptions(defs));
|
||||
this.subs.push(this.subscribeToPaginatedSearchOptions(defs));
|
||||
this.subs.push(this.subscribeToPaginatedSearchOptions(defs.pagination.id, defs));
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -140,34 +144,15 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
/**
|
||||
* @returns {Observable<string>} Emits the current pagination settings
|
||||
*/
|
||||
getCurrentPagination(defaultPagination: PaginationComponentOptions): Observable<PaginationComponentOptions> {
|
||||
const page$ = this.routeService.getQueryParameterValue('page');
|
||||
const size$ = this.routeService.getQueryParameterValue('pageSize');
|
||||
return observableCombineLatest(page$, size$).pipe(map(([page, size]) => {
|
||||
return Object.assign(new PaginationComponentOptions(), defaultPagination, {
|
||||
currentPage: page || defaultPagination.currentPage,
|
||||
pageSize: size || defaultPagination.pageSize
|
||||
});
|
||||
})
|
||||
);
|
||||
getCurrentPagination(paginationId: string, defaultPagination: PaginationComponentOptions): Observable<PaginationComponentOptions> {
|
||||
return this.paginationService.getCurrentPagination(paginationId, defaultPagination);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<string>} Emits the current sorting settings
|
||||
*/
|
||||
getCurrentSort(defaultSort: SortOptions): Observable<SortOptions> {
|
||||
const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection');
|
||||
const sortField$ = this.routeService.getQueryParameterValue('sortField');
|
||||
return observableCombineLatest(sortDirection$, sortField$).pipe(map(([sortDirection, sortField]) => {
|
||||
// Dirty fix because sometimes the observable value is null somehow
|
||||
sortField = this.route.snapshot.queryParamMap.get('sortField');
|
||||
|
||||
const field = sortField || defaultSort.field;
|
||||
const direction = SortDirection[sortDirection] || defaultSort.direction;
|
||||
return new SortOptions(field, direction);
|
||||
}
|
||||
)
|
||||
);
|
||||
getCurrentSort(paginationId: string, defaultSort: SortOptions): Observable<SortOptions> {
|
||||
return this.paginationService.getCurrentSort(paginationId, defaultSort);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,10 +219,10 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
* @param {PaginatedSearchOptions} defaults Default values for when no parameters are available
|
||||
* @returns {Subscription} The subscription to unsubscribe from
|
||||
*/
|
||||
private subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
|
||||
private subscribeToPaginatedSearchOptions(paginationId: string, defaults: PaginatedSearchOptions): Subscription {
|
||||
return observableMerge(
|
||||
this.getPaginationPart(defaults.pagination),
|
||||
this.getSortPart(defaults.sort),
|
||||
this.getPaginationPart(paginationId, defaults.pagination),
|
||||
this.getSortPart(paginationId, defaults.sort),
|
||||
this.getConfigurationPart(defaults.configuration),
|
||||
this.getScopePart(defaults.scope),
|
||||
this.getQueryPart(defaults.query),
|
||||
@@ -317,8 +302,8 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
/**
|
||||
* @returns {Observable<string>} Emits the current pagination settings as a partial SearchOptions object
|
||||
*/
|
||||
private getPaginationPart(defaultPagination: PaginationComponentOptions): Observable<any> {
|
||||
return this.getCurrentPagination(defaultPagination).pipe(map((pagination) => {
|
||||
private getPaginationPart(paginationId: string, defaultPagination: PaginationComponentOptions): Observable<any> {
|
||||
return this.getCurrentPagination(paginationId, defaultPagination).pipe(map((pagination) => {
|
||||
return { pagination };
|
||||
}));
|
||||
}
|
||||
@@ -326,8 +311,8 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
/**
|
||||
* @returns {Observable<string>} Emits the current sorting settings as a partial SearchOptions object
|
||||
*/
|
||||
private getSortPart(defaultSort: SortOptions): Observable<any> {
|
||||
return this.getCurrentSort(defaultSort).pipe(map((sort) => {
|
||||
private getSortPart(paginationId: string, defaultSort: SortOptions): Observable<any> {
|
||||
return this.getCurrentSort(paginationId, defaultSort).pipe(map((sort) => {
|
||||
return { sort };
|
||||
}));
|
||||
}
|
||||
|
@@ -22,6 +22,12 @@ import { routeServiceStub } from '../../../shared/testing/route-service.stub';
|
||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { SearchObjects } from '../../../shared/search/search-objects.model';
|
||||
import { PaginationService } from '../../pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../data/request.models';
|
||||
import { SearchConfigurationService } from './search-configuration.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
|
||||
@Component({ template: '' })
|
||||
class DummyComponent {
|
||||
@@ -32,6 +38,7 @@ describe('SearchService', () => {
|
||||
let searchService: SearchService;
|
||||
const router = new RouterStub();
|
||||
const route = new ActivatedRouteStub();
|
||||
const searchConfigService = {paginationID: 'page-id'};
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -51,6 +58,8 @@ describe('SearchService', () => {
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
{ provide: CommunityDataService, useValue: {} },
|
||||
{ provide: DSpaceObjectDataService, useValue: {} },
|
||||
{ provide: PaginationService, useValue: {} },
|
||||
{ provide: SearchConfigurationService, useValue: searchConfigService },
|
||||
SearchService
|
||||
],
|
||||
});
|
||||
@@ -94,6 +103,9 @@ describe('SearchService', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
const searchConfigService = {paginationID: 'page-id'};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -113,6 +125,8 @@ describe('SearchService', () => {
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: CommunityDataService, useValue: {} },
|
||||
{ provide: DSpaceObjectDataService, useValue: {} },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: SearchConfigurationService, useValue: searchConfigService },
|
||||
SearchService
|
||||
],
|
||||
});
|
||||
@@ -124,18 +138,14 @@ describe('SearchService', () => {
|
||||
|
||||
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
|
||||
searchService.setViewMode(ViewMode.ListElement);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
||||
queryParams: { view: ViewMode.ListElement, page: 1 },
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.ListElement }
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
|
||||
searchService.setViewMode(ViewMode.GridElement);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
||||
queryParams: { view: ViewMode.GridElement, page: 1 },
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.GridElement }
|
||||
);
|
||||
});
|
||||
|
||||
it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { NavigationExtras, Router } from '@angular/router';
|
||||
import { first, map, switchMap, take } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||
import { LinkService } from '../../cache/builders/link.service';
|
||||
import { PaginatedList } from '../../data/paginated-list.model';
|
||||
@@ -37,6 +37,9 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl
|
||||
import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator';
|
||||
import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model';
|
||||
import { FacetValues } from '../../../shared/search/facet-values.model';
|
||||
import { PaginationService } from '../../pagination/pagination.service';
|
||||
import { SearchConfigurationService } from './search-configuration.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
/**
|
||||
* Service that performs all general actions that have to do with the search page
|
||||
@@ -75,7 +78,9 @@ export class SearchService implements OnDestroy {
|
||||
private linkService: LinkService,
|
||||
private halService: HALEndpointService,
|
||||
private communityService: CommunityDataService,
|
||||
private dspaceObjectService: DSpaceObjectDataService
|
||||
private dspaceObjectService: DSpaceObjectDataService,
|
||||
private paginationService: PaginationService,
|
||||
private searchConfigurationService: SearchConfigurationService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -380,20 +385,16 @@ export class SearchService implements OnDestroy {
|
||||
* @param {ViewMode} viewMode Mode to switch to
|
||||
*/
|
||||
setViewMode(viewMode: ViewMode, searchLinkParts?: string[]) {
|
||||
this.routeService.getQueryParameterValue('pageSize').pipe(first())
|
||||
.subscribe((pageSize) => {
|
||||
let queryParams = { view: viewMode, page: 1 };
|
||||
this.paginationService.getCurrentPagination(this.searchConfigurationService.paginationID, new PaginationComponentOptions()).pipe(take(1))
|
||||
.subscribe((config) => {
|
||||
let pageParams = { page: 1 };
|
||||
const queryParams = { view: viewMode };
|
||||
if (viewMode === ViewMode.DetailedListElement) {
|
||||
queryParams = Object.assign(queryParams, {pageSize: '1'});
|
||||
} else if (pageSize === '1') {
|
||||
queryParams = Object.assign(queryParams, {pageSize: '10'});
|
||||
pageParams = Object.assign(pageParams, {pageSize: 1});
|
||||
} else if (config.pageSize === 1) {
|
||||
pageParams = Object.assign(pageParams, {pageSize: 10});
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: 'merge'
|
||||
};
|
||||
|
||||
this.router.navigate(hasValue(searchLinkParts) ? searchLinkParts : [this.getSearchLink()], navigationExtras);
|
||||
this.paginationService.updateRouteWithUrl(this.searchConfigurationService.paginationID, hasValue(searchLinkParts) ? searchLinkParts : [this.getSearchLink()], pageParams, queryParams);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -56,7 +56,7 @@
|
||||
<p class="m-0">
|
||||
<a class="text-white" href="http://www.dspace.org/">{{ 'footer.link.dspace' | translate}}</a>
|
||||
{{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }}
|
||||
<a class="text-white" href="http://www.duraspace.org/">{{ 'footer.link.duraspace' | translate}}</a>
|
||||
<a class="text-white" href="https://www.lyrasis.org/">{{ 'footer.link.lyrasis' | translate}}</a>
|
||||
</p>
|
||||
<ul class="footer-info list-unstyled small d-flex justify-content-center mb-0">
|
||||
<li>
|
||||
|
@@ -15,7 +15,7 @@
|
||||
|
||||
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files" [title]="'process.detail.output-files'">
|
||||
<ds-file-download-link *ngFor="let file of files; let last=last;" [href]="file?._links?.content?.href" [download]="getFileName(file)">
|
||||
<ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
||||
<span>{{getFileName(file)}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
</ds-file-download-link>
|
||||
|
@@ -8,8 +8,7 @@
|
||||
[pageInfoState]="(processesRD$ | async)?.payload"
|
||||
[collectionSize]="(processesRD$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
|
@@ -12,6 +12,12 @@ import { By } from '@angular/platform-browser';
|
||||
import { ProcessStatus } from '../processes/process-status.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('ProcessOverviewComponent', () => {
|
||||
let component: ProcessOverviewComponent;
|
||||
@@ -19,6 +25,7 @@ describe('ProcessOverviewComponent', () => {
|
||||
|
||||
let processService: ProcessDataService;
|
||||
let ePersonService: EPersonDataService;
|
||||
let paginationService;
|
||||
|
||||
let processes: Process[];
|
||||
let ePerson: EPerson;
|
||||
@@ -69,6 +76,8 @@ describe('ProcessOverviewComponent', () => {
|
||||
ePersonService = jasmine.createSpyObj('ePersonService', {
|
||||
findById: createSuccessfulRemoteDataObject$(ePerson)
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
}
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -78,7 +87,8 @@ describe('ProcessOverviewComponent', () => {
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: ProcessDataService, useValue: processService },
|
||||
{ provide: EPersonDataService, useValue: ePersonService }
|
||||
{ provide: EPersonDataService, useValue: ePersonService },
|
||||
{ provide: PaginationService, useValue: paginationService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -143,16 +153,4 @@ describe('ProcessOverviewComponent', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onPageChange', () => {
|
||||
const toPage = 2;
|
||||
|
||||
beforeEach(() => {
|
||||
component.onPageChange(toPage);
|
||||
});
|
||||
|
||||
it('should call a new findAll with the corresponding page', () => {
|
||||
expect(processService.findAll).toHaveBeenCalledWith(jasmine.objectContaining({ currentPage: toPage }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -8,8 +8,9 @@ import { FindListOptions } from '../../core/data/request.models';
|
||||
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-process-overview',
|
||||
@@ -36,7 +37,7 @@ export class ProcessOverviewComponent implements OnInit {
|
||||
* The current pagination configuration for the page
|
||||
*/
|
||||
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'process-overview-pagination',
|
||||
id: 'po',
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
@@ -46,6 +47,7 @@ export class ProcessOverviewComponent implements OnInit {
|
||||
dateFormat = 'yyyy-MM-dd HH:mm:ss';
|
||||
|
||||
constructor(protected processService: ProcessDataService,
|
||||
protected paginationService: PaginationService,
|
||||
protected ePersonService: EPersonDataService) {
|
||||
}
|
||||
|
||||
@@ -53,23 +55,13 @@ export class ProcessOverviewComponent implements OnInit {
|
||||
this.setProcesses();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the page is changed, make sure to update the list of processes to match the new page
|
||||
* @param event The page change event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config = Object.assign(new FindListOptions(), this.config, {
|
||||
currentPage: event,
|
||||
});
|
||||
this.pageConfig.currentPage = event;
|
||||
this.setProcesses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to fetch all processes for the current page
|
||||
*/
|
||||
setProcesses() {
|
||||
this.processesRD$ = this.processService.findAll(this.config);
|
||||
this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
||||
switchMap((config) => this.processService.findAll(config))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,5 +74,8 @@ export class ProcessOverviewComponent implements OnInit {
|
||||
map((eperson: EPerson) => eperson.name)
|
||||
);
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.pageConfig.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,6 +8,12 @@ import { SearchService } from '../core/shared/search/search.service';
|
||||
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
|
||||
|
||||
import { SearchNavbarComponent } from './search-navbar.component';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationService } from '../core/pagination/pagination.service';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
import { PaginationServiceStub } from '../shared/testing/pagination-service.stub';
|
||||
|
||||
describe('SearchNavbarComponent', () => {
|
||||
let component: SearchNavbarComponent;
|
||||
@@ -15,6 +21,7 @@ describe('SearchNavbarComponent', () => {
|
||||
let mockSearchService: any;
|
||||
let router: Router;
|
||||
let routerStub;
|
||||
let paginationService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
mockSearchService = {
|
||||
@@ -26,6 +33,9 @@ describe('SearchNavbarComponent', () => {
|
||||
routerStub = {
|
||||
navigate: (commands) => commands
|
||||
};
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
@@ -40,7 +50,9 @@ describe('SearchNavbarComponent', () => {
|
||||
declarations: [SearchNavbarComponent],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: mockSearchService },
|
||||
{ provide: Router, useValue: routerStub }
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: SearchConfigurationService, useValue: {paginationID: 'page-id'} }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
@@ -88,7 +100,7 @@ describe('SearchNavbarComponent', () => {
|
||||
}));
|
||||
it('to search page with empty query', () => {
|
||||
expect(component.onSubmit).toHaveBeenCalledWith({ query: '' });
|
||||
expect(router.navigate).toHaveBeenCalled();
|
||||
expect(paginationService.updateRouteWithUrl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -112,7 +124,7 @@ describe('SearchNavbarComponent', () => {
|
||||
}));
|
||||
it('to search page with query', async () => {
|
||||
expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' });
|
||||
expect(router.navigate).toHaveBeenCalled();
|
||||
expect(paginationService.updateRouteWithUrl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -3,6 +3,8 @@ import { FormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { expandSearchInput } from '../shared/animations/slide';
|
||||
import { PaginationService } from '../core/pagination/pagination.service';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
|
||||
/**
|
||||
* The search box in the header that expands on focus and collapses on focus out
|
||||
@@ -24,7 +26,9 @@ export class SearchNavbarComponent {
|
||||
// Search input field
|
||||
@ViewChild('searchInput') searchField: ElementRef;
|
||||
|
||||
constructor(private formBuilder: FormBuilder, private router: Router, private searchService: SearchService) {
|
||||
constructor(private formBuilder: FormBuilder, private router: Router, private searchService: SearchService,
|
||||
private paginationService: PaginationService,
|
||||
private searchConfig: SearchConfigurationService) {
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
query: '',
|
||||
}));
|
||||
@@ -63,9 +67,6 @@ export class SearchNavbarComponent {
|
||||
this.collapse();
|
||||
const linkToNavigateTo = this.searchService.getSearchLink().split('/');
|
||||
this.searchForm.reset();
|
||||
this.router.navigate(linkToNavigateTo, {
|
||||
queryParams: Object.assign({}, { page: 1 }, data),
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.paginationService.updateRouteWithUrl(this.searchConfig.paginationID, linkToNavigateTo, {page: 1}, data);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<div class="container">
|
||||
<h3>{{'bitstream.download.page' | translate:{bitstream: (bitstream$ | async)?.name} }}</h3>
|
||||
</div>
|
@@ -0,0 +1,158 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { FileService } from '../../core/shared/file.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { BitstreamDownloadPageComponent } from './bitstream-download-page.component';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
describe('BitstreamDownloadPageComponent', () => {
|
||||
let component: BitstreamDownloadPageComponent;
|
||||
let fixture: ComponentFixture<BitstreamDownloadPageComponent>;
|
||||
|
||||
let authService: AuthService;
|
||||
let fileService: FileService;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let hardRedirectService: HardRedirectService;
|
||||
let activatedRoute;
|
||||
let router;
|
||||
|
||||
let bitstream: Bitstream;
|
||||
|
||||
function init() {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
authorizationService = jasmine.createSpyObj('authorizationSerivice', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
|
||||
fileService = jasmine.createSpyObj('fileService', {
|
||||
retrieveFileDownloadLink: observableOf('content-url-with-headers')
|
||||
});
|
||||
|
||||
hardRedirectService = jasmine.createSpyObj('fileService', {
|
||||
redirect: {}
|
||||
});
|
||||
bitstream = Object.assign(new Bitstream(), {
|
||||
uuid: 'bitstreamUuid',
|
||||
_links: {
|
||||
content: {href: 'bitstream-content-link'},
|
||||
self: {href: 'bitstream-self-link'},
|
||||
}
|
||||
});
|
||||
|
||||
activatedRoute = {
|
||||
data: observableOf({
|
||||
bitstream: createSuccessfulRemoteDataObject(
|
||||
bitstream
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
router = jasmine.createSpyObj('router', ['navigateByUrl']);
|
||||
}
|
||||
|
||||
function initTestbed() {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, TranslateModule.forRoot()],
|
||||
declarations: [BitstreamDownloadPageComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: activatedRoute},
|
||||
{provide: Router, useValue: router},
|
||||
{provide: AuthorizationDataService, useValue: authorizationService},
|
||||
{provide: AuthService, useValue: authService},
|
||||
{provide: FileService, useValue: fileService},
|
||||
{provide: HardRedirectService, useValue: hardRedirectService},
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}
|
||||
|
||||
describe('init', () => {
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamDownloadPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should init the comp', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bitstream retrieval', () => {
|
||||
describe('when the user is authorized and not logged in', () => {
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamDownloadPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should redirect to the content link', () => {
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
|
||||
});
|
||||
});
|
||||
describe('when the user is authorized and logged in', () => {
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamDownloadPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should redirect to an updated content link', () => {
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers');
|
||||
});
|
||||
});
|
||||
describe('when the user is not authorized and logged in', () => {
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamDownloadPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should navigate to the forbidden route', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), {skipLocationChange: true});
|
||||
});
|
||||
});
|
||||
describe('when the user is not authorized and not logged in', () => {
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
initTestbed();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BitstreamDownloadPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should navigate to the login page', () => {
|
||||
expect(authService.setRedirectUrl).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('login');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,83 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { hasValue, isNotEmpty } from '../empty.util';
|
||||
import { getRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { FileService } from '../../core/shared/file.service';
|
||||
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-bitstream-download-page',
|
||||
templateUrl: './bitstream-download-page.component.html'
|
||||
})
|
||||
/**
|
||||
* Page component for downloading a bitstream
|
||||
*/
|
||||
export class BitstreamDownloadPageComponent implements OnInit {
|
||||
|
||||
bitstream$: Observable<Bitstream>;
|
||||
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
||||
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private auth: AuthService,
|
||||
private fileService: FileService,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.bitstreamRD$ = this.route.data.pipe(
|
||||
map((data) => data.bitstream));
|
||||
|
||||
this.bitstream$ = this.bitstreamRD$.pipe(
|
||||
redirectOn4xx(this.router, this.auth),
|
||||
getRemoteDataPayload()
|
||||
);
|
||||
|
||||
this.bitstream$.pipe(
|
||||
switchMap((bitstream: Bitstream) => {
|
||||
const isAuthorized$ = this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(bitstream) ? bitstream.self : undefined);
|
||||
const isLoggedIn$ = this.auth.isAuthenticated();
|
||||
return observableCombineLatest([isAuthorized$, isLoggedIn$, observableOf(bitstream)]);
|
||||
}),
|
||||
filter(([isAuthorized, isLoggedIn, bitstream]: [boolean, boolean, Bitstream]) => hasValue(isAuthorized) && hasValue(isLoggedIn)),
|
||||
take(1),
|
||||
switchMap(([isAuthorized, isLoggedIn, bitstream]: [boolean, boolean, Bitstream]) => {
|
||||
if (isAuthorized && isLoggedIn) {
|
||||
return this.fileService.retrieveFileDownloadLink(bitstream._links.content.href).pipe(
|
||||
filter((fileLink) => hasValue(fileLink)),
|
||||
take(1),
|
||||
map((fileLink) => {
|
||||
return [isAuthorized, isLoggedIn, bitstream, fileLink];
|
||||
}));
|
||||
} else {
|
||||
return [[isAuthorized, isLoggedIn, bitstream, '']];
|
||||
}
|
||||
})
|
||||
).subscribe(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) => {
|
||||
if (isAuthorized && isLoggedIn && isNotEmpty(fileLink)) {
|
||||
this.hardRedirectService.redirect(fileLink);
|
||||
} else if (isAuthorized && !isLoggedIn) {
|
||||
this.hardRedirectService.redirect(bitstream._links.content.href);
|
||||
} else if (!isAuthorized && isLoggedIn) {
|
||||
this.router.navigateByUrl(getForbiddenRoute(), {skipLocationChange: true});
|
||||
} else if (!isAuthorized && !isLoggedIn) {
|
||||
this.auth.setRedirectUrl(this.router.url);
|
||||
this.router.navigateByUrl('login');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -18,6 +18,9 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||
import { storeModuleConfig } from '../../app.reducer';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../testing/pagination-service.stub';
|
||||
|
||||
describe('BrowseByComponent', () => {
|
||||
let comp: BrowseByComponent;
|
||||
@@ -45,6 +48,14 @@ describe('BrowseByComponent', () => {
|
||||
];
|
||||
const mockItemsRD$ = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockItems));
|
||||
|
||||
const paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'test-pagination',
|
||||
currentPage: 1,
|
||||
pageSizeOptions: [5, 10, 15, 20],
|
||||
pageSize: 15
|
||||
});
|
||||
const paginationService = new PaginationServiceStub(paginationConfig);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -63,7 +74,9 @@ describe('BrowseByComponent', () => {
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
declarations: [],
|
||||
providers: [],
|
||||
providers: [
|
||||
{provide: PaginationService, useValue: paginationService}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
@@ -95,12 +108,8 @@ describe('BrowseByComponent', () => {
|
||||
beforeEach(() => {
|
||||
comp.enableArrows = true;
|
||||
comp.objects$ = mockItemsRD$;
|
||||
comp.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'test-pagination',
|
||||
currentPage: 1,
|
||||
pageSizeOptions: [5, 10, 15, 20],
|
||||
pageSize: 15
|
||||
});
|
||||
|
||||
comp.paginationConfig = paginationConfig;
|
||||
comp.sortConfig = Object.assign(new SortOptions('dc.title', SortDirection.ASC));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -136,8 +145,8 @@ describe('BrowseByComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a signal to the EventEmitter', () => {
|
||||
expect(comp.pageSizeChange.emit).toHaveBeenCalled();
|
||||
it('should call the updateRoute method from the paginationService', () => {
|
||||
expect(paginationService.updateRoute).toHaveBeenCalledWith('test-pagination', {pageSize: paginationConfig.pageSizeOptions[0]});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,8 +157,8 @@ describe('BrowseByComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a signal to the EventEmitter', () => {
|
||||
expect(comp.sortDirectionChange.emit).toHaveBeenCalled();
|
||||
it('should call the updateRoute method from the paginationService', () => {
|
||||
expect(paginationService.updateRoute).toHaveBeenCalledWith('test-pagination', {sortDirection: 'ASC'});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -7,6 +7,7 @@ import { fadeIn, fadeInOut } from '../animations/fade';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||
import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by',
|
||||
@@ -96,7 +97,9 @@ export class BrowseByComponent implements OnInit {
|
||||
*/
|
||||
public sortDirections = SortDirection;
|
||||
|
||||
public constructor(private injector: Injector) {
|
||||
public constructor(private injector: Injector,
|
||||
protected paginationService: PaginationService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
@@ -119,8 +122,7 @@ export class BrowseByComponent implements OnInit {
|
||||
* @param size
|
||||
*/
|
||||
doPageSizeChange(size) {
|
||||
this.paginationConfig.pageSize = size;
|
||||
this.pageSizeChange.emit(size);
|
||||
this.paginationService.updateRoute(this.paginationConfig.id,{pageSize: size});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,8 +130,7 @@ export class BrowseByComponent implements OnInit {
|
||||
* @param direction
|
||||
*/
|
||||
doSortDirectionChange(direction) {
|
||||
this.sortConfig.direction = direction;
|
||||
this.sortDirectionChange.emit(direction);
|
||||
this.paginationService.updateRoute(this.paginationConfig.id,{sortDirection: direction});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +142,10 @@ export class BrowseByComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.objectInjector = Injector.create({
|
||||
providers: [{ provide: 'startsWithOptions', useFactory: () => (this.startsWithOptions), deps:[] }],
|
||||
providers: [
|
||||
{ provide: 'startsWithOptions', useFactory: () => (this.startsWithOptions), deps:[] },
|
||||
{ provide: 'paginationId', useFactory: () => (this.paginationConfig?.id), deps:[] }
|
||||
],
|
||||
parent: this.injector
|
||||
});
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<a *ngIf="!(isAuthenticated$ | async)" [href]="href" [download]="download"><ng-container *ngTemplateOutlet="content"></ng-container></a>
|
||||
<a *ngIf="(isAuthenticated$ | async)" [href]="href" [download]="download" (click)="downloadFile()"><ng-container *ngTemplateOutlet="content"></ng-container></a>
|
||||
<a [href]="bitstreamPath"><ng-container *ngTemplateOutlet="content"></ng-container></a>
|
||||
|
||||
|
||||
<ng-template #content>
|
||||
<ng-content></ng-content>
|
||||
|
@@ -3,7 +3,10 @@ import { FileDownloadLinkComponent } from './file-download-link.component';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { FileService } from '../../core/shared/file.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getBitstreamModuleRoute } from '../../app-routing-paths';
|
||||
|
||||
describe('FileDownloadLinkComponent', () => {
|
||||
let component: FileDownloadLinkComponent;
|
||||
@@ -11,14 +14,16 @@ describe('FileDownloadLinkComponent', () => {
|
||||
|
||||
let authService: AuthService;
|
||||
let fileService: FileService;
|
||||
let href: string;
|
||||
let bitstream: Bitstream;
|
||||
|
||||
function init() {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true)
|
||||
});
|
||||
fileService = jasmine.createSpyObj('fileService', ['downloadFile']);
|
||||
href = 'test-download-file-link';
|
||||
bitstream = Object.assign(new Bitstream(), {
|
||||
uuid: 'bitstreamUuid',
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -28,7 +33,6 @@ describe('FileDownloadLinkComponent', () => {
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: FileService, useValue: fileService },
|
||||
{ provide: HardRedirectService, useValue: { rewriteDownloadURL: (a) => a } },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
@@ -37,23 +41,22 @@ describe('FileDownloadLinkComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FileDownloadLinkComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.href = href;
|
||||
component.bitstream = bitstream;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('downloadFile', () => {
|
||||
let result;
|
||||
describe('init', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
result = component.downloadFile();
|
||||
describe('getBitstreamPath', () => {
|
||||
it('should set the bitstreamPath based on the input bitstream', () => {
|
||||
expect(component.bitstreamPath).toEqual(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
||||
});
|
||||
});
|
||||
|
||||
it('should call fileService.downloadFile with the provided href', () => {
|
||||
expect(fileService.downloadFile).toHaveBeenCalledWith(href);
|
||||
it('should init the component', () => {
|
||||
const link = fixture.debugElement.query(By.css('a')).nativeElement;
|
||||
expect(link.href).toContain(new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString());
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FileService } from '../../core/shared/file.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-file-download-link',
|
||||
@@ -15,37 +13,18 @@ import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||
* ensuring the user is authorized to download the file.
|
||||
*/
|
||||
export class FileDownloadLinkComponent implements OnInit {
|
||||
/**
|
||||
* Href to link to
|
||||
*/
|
||||
@Input() href: string;
|
||||
|
||||
/**
|
||||
* Optional file name for the download
|
||||
* Optional bitstream instead of href and file name
|
||||
*/
|
||||
@Input() download: string;
|
||||
|
||||
/**
|
||||
* Whether or not the current user is authenticated
|
||||
*/
|
||||
isAuthenticated$: Observable<boolean>;
|
||||
|
||||
constructor(private fileService: FileService,
|
||||
private authService: AuthService,
|
||||
private redirectService: HardRedirectService) {
|
||||
}
|
||||
@Input() bitstream: Bitstream;
|
||||
bitstreamPath: string;
|
||||
|
||||
ngOnInit() {
|
||||
this.isAuthenticated$ = this.authService.isAuthenticated();
|
||||
this.href = this.redirectService.rewriteDownloadURL(this.href);
|
||||
this.bitstreamPath = this.getBitstreamPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a download of the file
|
||||
* Return false to ensure the original href is displayed when the user hovers over the link
|
||||
*/
|
||||
downloadFile(): boolean {
|
||||
this.fileService.downloadFile(this.href);
|
||||
return false;
|
||||
getBitstreamPath() {
|
||||
return getBitstreamDownloadRoute(this.bitstream);
|
||||
}
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@
|
||||
'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}">
|
||||
<div [ngClass]="getClass('grid', 'control')">
|
||||
<ng-container #componentViewContainer></ng-container>
|
||||
<small *ngIf="hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)"
|
||||
<small *ngIf="hasHint && ((model.repeatable === false && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
|
||||
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
|
||||
<!-- In case of repeatable fields show empty space for all elements except the first -->
|
||||
<div *ngIf="context?.index !== null
|
||||
@@ -35,7 +35,7 @@
|
||||
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="isRelationship && !isVirtual()" class="col-auto text-center">
|
||||
<div *ngIf="isRelationship" class="col-auto text-center">
|
||||
<button class="btn btn-secondary"
|
||||
type="button"
|
||||
ngbTooltip="{{'form.lookup-help' | translate}}"
|
||||
|
@@ -37,6 +37,7 @@ import {
|
||||
DynamicFormControl,
|
||||
DynamicFormControlContainerComponent,
|
||||
DynamicFormControlEvent,
|
||||
DynamicFormControlEventType,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormLayout,
|
||||
DynamicFormLayoutService,
|
||||
@@ -395,9 +396,25 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
});
|
||||
|
||||
if (hasValue(this.model.value)) {
|
||||
this.submissionService.dispatchSave(this.model.submissionId);
|
||||
this.focus.emit({
|
||||
$event: new Event('focus'),
|
||||
context: this.context,
|
||||
control: this.control,
|
||||
model: this.model,
|
||||
type: DynamicFormControlEventType.Focus
|
||||
} as DynamicFormControlEvent);
|
||||
|
||||
this.change.emit({
|
||||
$event: new Event('change'),
|
||||
context: this.context,
|
||||
control: this.control,
|
||||
model: this.model,
|
||||
type: DynamicFormControlEventType.Change
|
||||
} as DynamicFormControlEvent);
|
||||
}
|
||||
|
||||
this.submissionService.dispatchSave(this.model.submissionId);
|
||||
|
||||
const modalComp = this.modalRef.componentInstance;
|
||||
|
||||
if (hasValue(this.model.value) && !this.model.readOnly) {
|
||||
@@ -427,6 +444,9 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
const path = this.formBuilderService.getPath(arrayContext);
|
||||
const formArrayControl = this.group.root.get(path) as FormArray;
|
||||
this.formBuilderService.removeFormArrayGroup(this.context.index, formArrayControl, arrayContext);
|
||||
if (this.model.parent.context.groups.length === 0) {
|
||||
this.formBuilderService.addFormArrayGroup(formArrayControl, arrayContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -16,6 +16,8 @@ import { ItemSearchResult } from '../../../../object-collection/shared/item-sear
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../../../testing/translate-loader.mock';
|
||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
||||
import { SubmissionServiceStub } from '../../../../testing/submission-service.stub';
|
||||
|
||||
describe('ExistingMetadataListElementComponent', () => {
|
||||
let component: ExistingMetadataListElementComponent;
|
||||
@@ -36,6 +38,7 @@ describe('ExistingMetadataListElementComponent', () => {
|
||||
let relatedSearchResult;
|
||||
let submissionId;
|
||||
let relationshipService;
|
||||
let submissionServiceStub;
|
||||
|
||||
function init() {
|
||||
uuid1 = '91ce578d-2e63-4093-8c73-3faafd716000';
|
||||
@@ -62,6 +65,8 @@ describe('ExistingMetadataListElementComponent', () => {
|
||||
relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ });
|
||||
submissionId = '1234';
|
||||
reoRel = new ReorderableRelationship(relationship, true, {} as any, {} as any, submissionId);
|
||||
submissionServiceStub = new SubmissionServiceStub();
|
||||
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf({}));
|
||||
}
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -79,6 +84,7 @@ describe('ExistingMetadataListElementComponent', () => {
|
||||
providers: [
|
||||
{ provide: SelectableListService, useValue: selectionService },
|
||||
{ provide: Store, useValue: store },
|
||||
{ provide: SubmissionService, useValue: submissionServiceStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user