mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'main' into w2p-78243_edit-item-page-permission-checks
Conflicts: src/app/core/data/feature-authorization/feature-id.ts
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",
|
||||
|
@@ -160,6 +160,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)) {
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
|
@@ -33,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>
|
||||
@@ -74,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>
|
||||
|
@@ -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>
|
||||
|
@@ -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,6 +12,7 @@ export enum FeatureID {
|
||||
CanManageGroups = 'canManageGroups',
|
||||
IsCollectionAdmin = 'isCollectionAdmin',
|
||||
IsCommunityAdmin = 'isCommunityAdmin',
|
||||
CanDownload = 'canDownload',
|
||||
CanManageVersions = 'canManageVersions',
|
||||
CanManageBitstreams = 'canManageBitstreams',
|
||||
CanManageRelationships = 'canManageRelationships',
|
||||
|
@@ -182,10 +182,6 @@ describe('MetadataService', () => {
|
||||
Title,
|
||||
// tslint:disable-next-line:no-empty
|
||||
{ provide: ItemDataService, useValue: { findById: () => {} } },
|
||||
{
|
||||
provide: HardRedirectService,
|
||||
useValue: { rewriteDownloadURL: (a) => a, getRequestOrigin: () => environment.ui.baseUrl }
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -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) {
|
||||
@@ -242,16 +242,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>>>) => {
|
||||
|
@@ -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>
|
||||
|
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -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]
|
||||
})
|
||||
|
@@ -3,7 +3,7 @@ import { FormControl } from '@angular/forms';
|
||||
import { DynamicFormArrayGroupModel } from '@ng-dynamic-forms/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { filter, take } from 'rxjs/operators';
|
||||
import { AppState } from '../../../../../app.reducer';
|
||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
||||
@@ -19,6 +19,8 @@ import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-v
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { DynamicConcatModel } from '../models/ds-dynamic-concat.model';
|
||||
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
|
||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
||||
import { SubmissionObjectEntry } from '../../../../../submission/objects/submission-objects.reducer';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
/**
|
||||
@@ -155,7 +157,8 @@ export class ExistingMetadataListElementComponent implements OnInit, OnChanges,
|
||||
|
||||
constructor(
|
||||
private selectableListService: SelectableListService,
|
||||
private store: Store<AppState>
|
||||
private store: Store<AppState>,
|
||||
private submissionService: SubmissionService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -194,9 +197,14 @@ export class ExistingMetadataListElementComponent implements OnInit, OnChanges,
|
||||
* Removes the selected relationship from the list
|
||||
*/
|
||||
removeSelection() {
|
||||
this.submissionService.dispatchSave(this.submissionId);
|
||||
this.submissionService.getSubmissionObject(this.submissionId).pipe(
|
||||
filter((state: SubmissionObjectEntry) => !state.savePending && !state.isLoading),
|
||||
take(1)).subscribe(() => {
|
||||
this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem }));
|
||||
this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType, this.submissionId));
|
||||
this.remove.emit();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -12,6 +12,8 @@ import { ItemSearchResult } from '../../../../object-collection/shared/item-sear
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ReorderableRelationship } from '../existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
|
||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
||||
import { SubmissionServiceStub } from '../../../../testing/submission-service.stub';
|
||||
|
||||
describe('ExistingRelationListElementComponent', () => {
|
||||
let component: ExistingRelationListElementComponent;
|
||||
@@ -67,6 +69,7 @@ describe('ExistingRelationListElementComponent', () => {
|
||||
providers: [
|
||||
{ provide: SelectableListService, useValue: selectionService },
|
||||
{ provide: Store, useValue: store },
|
||||
{ provide: SubmissionService, useClass: SubmissionServiceStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
|
@@ -12,6 +12,7 @@ import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { ReorderableRelationship } from '../existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
/**
|
||||
@@ -72,6 +73,7 @@ export class ExistingRelationListElementComponent implements OnInit, OnChanges,
|
||||
|
||||
constructor(
|
||||
private selectableListService: SelectableListService,
|
||||
private submissionService: SubmissionService,
|
||||
private store: Store<AppState>
|
||||
) {
|
||||
}
|
||||
@@ -102,6 +104,7 @@ export class ExistingRelationListElementComponent implements OnInit, OnChanges,
|
||||
* Removes the selected relationship from the list
|
||||
*/
|
||||
removeSelection() {
|
||||
this.submissionService.dispatchSave(this.submissionId);
|
||||
this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem$.getValue() }));
|
||||
this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem$.getValue(), this.relationshipOptions.relationshipType, this.submissionId));
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ class CustomLoader implements TranslateLoader {
|
||||
'footer': {
|
||||
'copyright': 'copyright © 2002-{{ year }}',
|
||||
'link.dspace': 'DSpace software',
|
||||
'link.duraspace': 'DuraSpace'
|
||||
'link.lyrasis': 'LYRASIS'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@ export class ItemDetailPreviewComponent {
|
||||
first())
|
||||
.subscribe((url) => {
|
||||
const fileUrl = `${url}/${uuid}/content`;
|
||||
this.fileService.downloadFile(fileUrl);
|
||||
this.fileService.retrieveFileDownloadLink(fileUrl);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -223,6 +223,7 @@ import { SearchObjects } from './search/search-objects.model';
|
||||
import { SearchResult } from './search/search-result.model';
|
||||
import { FacetConfigResponse } from './search/facet-config-response.model';
|
||||
import { FacetValues } from './search/facet-values.model';
|
||||
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component';
|
||||
import { GenericItemPageFieldComponent } from '../+item-page/simple/field-components/specific-field/generic/generic-item-page-field.component';
|
||||
import { MetadataRepresentationListComponent } from '../+item-page/simple/metadata-representation-list/metadata-representation-list.component';
|
||||
import { RelatedItemsComponent } from '../+item-page/simple/related-items/related-items-component';
|
||||
@@ -432,6 +433,7 @@ const COMPONENTS = [
|
||||
EpersonSearchBoxComponent,
|
||||
GroupSearchBoxComponent,
|
||||
FileDownloadLinkComponent,
|
||||
BitstreamDownloadPageComponent,
|
||||
CollectionDropdownComponent,
|
||||
ExportMetadataSelectorComponent,
|
||||
ConfirmationModalComponent,
|
||||
@@ -510,6 +512,14 @@ const ENTRY_COMPONENTS = [
|
||||
ClaimedTaskActionsRejectComponent,
|
||||
ClaimedTaskActionsReturnToPoolComponent,
|
||||
ClaimedTaskActionsEditMetadataComponent,
|
||||
CollectionDropdownComponent,
|
||||
FileDownloadLinkComponent,
|
||||
BitstreamDownloadPageComponent,
|
||||
CurationFormComponent,
|
||||
ExportMetadataSelectorComponent,
|
||||
ConfirmationModalComponent,
|
||||
VocabularyTreeviewComponent,
|
||||
SidebarSearchListElementComponent,
|
||||
PublicationSidebarSearchListElementComponent,
|
||||
CollectionSidebarSearchListElementComponent,
|
||||
CommunitySidebarSearchListElementComponent,
|
||||
|
@@ -17,12 +17,14 @@ import { RouterStub } from '../../shared/testing/router.stub';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||
import { mockSubmissionObject } from '../../shared/mocks/submission.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
|
||||
describe('SubmissionEditComponent Component', () => {
|
||||
|
||||
let comp: SubmissionEditComponent;
|
||||
let fixture: ComponentFixture<SubmissionEditComponent>;
|
||||
let submissionServiceStub: SubmissionServiceStub;
|
||||
let itemDataService: ItemDataService;
|
||||
let router: RouterStub;
|
||||
|
||||
const submissionId = '826';
|
||||
@@ -30,6 +32,9 @@ describe('SubmissionEditComponent Component', () => {
|
||||
const submissionObject: any = mockSubmissionObject;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||
findByHref: createSuccessfulRemoteDataObject$(submissionObject.item),
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
@@ -41,6 +46,7 @@ describe('SubmissionEditComponent Component', () => {
|
||||
providers: [
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||
{ provide: SubmissionService, useClass: SubmissionServiceStub },
|
||||
{ provide: ItemDataService, useValue: itemDataService },
|
||||
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
@@ -63,7 +69,7 @@ describe('SubmissionEditComponent Component', () => {
|
||||
router = null;
|
||||
});
|
||||
|
||||
it('should init properly when a valid SubmissionObject has been retrieved', fakeAsync(() => {
|
||||
it('should init properly when a valid SubmissionObject has been retrieved',() => {
|
||||
|
||||
route.testParams = { id: submissionId };
|
||||
submissionServiceStub.retrieveSubmission.and.returnValue(
|
||||
@@ -78,9 +84,9 @@ describe('SubmissionEditComponent Component', () => {
|
||||
expect(comp.sections).toBe(submissionObject.sections);
|
||||
expect(comp.submissionDefinition).toBe(submissionObject.submissionDefinition);
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
it('should redirect to mydspace when an empty SubmissionObject has been retrieved', fakeAsync(() => {
|
||||
it('should redirect to mydspace when an empty SubmissionObject has been retrieved',() => {
|
||||
|
||||
route.testParams = { id: submissionId };
|
||||
submissionServiceStub.retrieveSubmission.and.returnValue(createSuccessfulRemoteDataObject$({})
|
||||
@@ -90,9 +96,9 @@ describe('SubmissionEditComponent Component', () => {
|
||||
|
||||
expect(router.navigate).toHaveBeenCalled();
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not has effects when an invalid SubmissionObject has been retrieved', fakeAsync(() => {
|
||||
it('should not has effects when an invalid SubmissionObject has been retrieved',() => {
|
||||
|
||||
route.testParams = { id: submissionId };
|
||||
submissionServiceStub.retrieveSubmission.and.returnValue(observableOf(null));
|
||||
@@ -104,6 +110,6 @@ describe('SubmissionEditComponent Component', () => {
|
||||
expect(comp.selfUrl).toBeUndefined();
|
||||
expect(comp.sections).toBeUndefined();
|
||||
expect(comp.submissionDefinition).toBeUndefined();
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -2,11 +2,11 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
import { filter, switchMap } from 'rxjs/operators';
|
||||
import { filter, switchMap, debounceTime } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model';
|
||||
import { hasValue, isEmpty, isNotNull } from '../../shared/empty.util';
|
||||
import { hasValue, isEmpty, isNotNull, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model';
|
||||
import { SubmissionService } from '../submission.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
@@ -14,6 +14,9 @@ import { SubmissionObject } from '../../core/submission/models/submission-object
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getAllSucceededRemoteData } from '../../core/shared/operators';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
|
||||
/**
|
||||
* This component allows to edit an existing workspaceitem/workflowitem.
|
||||
@@ -60,6 +63,16 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
|
||||
* @type {Array}
|
||||
*/
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* BehaviorSubject containing the self link to the item for this submission
|
||||
* @private
|
||||
*/
|
||||
private itemLink$: BehaviorSubject<string> = new BehaviorSubject(undefined);
|
||||
|
||||
/**
|
||||
* The item for this submission.
|
||||
*/
|
||||
public item: Item;
|
||||
|
||||
/**
|
||||
@@ -69,6 +82,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
|
||||
* @param {NotificationsService} notificationsService
|
||||
* @param {ActivatedRoute} route
|
||||
* @param {Router} router
|
||||
* @param {ItemDataService} itemDataService
|
||||
* @param {SubmissionService} submissionService
|
||||
* @param {TranslateService} translate
|
||||
*/
|
||||
@@ -76,6 +90,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
|
||||
private notificationsService: NotificationsService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private itemDataService: ItemDataService,
|
||||
private submissionService: SubmissionService,
|
||||
private translate: TranslateService) {
|
||||
}
|
||||
@@ -84,7 +99,8 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
|
||||
* Retrieve workspaceitem/workflowitem from server and initialize all instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.subs.push(this.route.paramMap.pipe(
|
||||
this.subs.push(
|
||||
this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))),
|
||||
// NOTE new submission is retrieved on the browser side only, so get null on server side rendering
|
||||
filter((submissionObjectRD: RemoteData<SubmissionObject>) => isNotNull(submissionObjectRD))
|
||||
@@ -98,9 +114,9 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
|
||||
this.collectionId = (submissionObjectRD.payload.collection as Collection).id;
|
||||
this.selfUrl = submissionObjectRD.payload._links.self.href;
|
||||
this.sections = submissionObjectRD.payload.sections;
|
||||
this.item = submissionObjectRD.payload.item as Item;
|
||||
this.itemLink$.next(submissionObjectRD.payload._links.item.href);
|
||||
this.item = submissionObjectRD.payload.item;
|
||||
this.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
} else {
|
||||
if (submissionObjectRD.statusCode === 404) {
|
||||
@@ -109,7 +125,21 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
// TODO handle generic error
|
||||
}
|
||||
}));
|
||||
}),
|
||||
this.itemLink$.pipe(
|
||||
isNotEmptyOperator(),
|
||||
switchMap((itemLink: string) =>
|
||||
this.itemDataService.findByHref(itemLink)
|
||||
),
|
||||
getAllSucceededRemoteData(),
|
||||
// Multiple sources can update the item in quick succession.
|
||||
// We only want to rerender the form if the item is unchanged for some time
|
||||
debounceTime(300),
|
||||
).subscribe((itemRd: RemoteData<Item>) => {
|
||||
this.item = itemRd.payload;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<span class="mb-5">{{ licenseText$ | async }}</span>
|
||||
<span class="mb-5" [innerHTML]="licenseText$ | async"></span>
|
||||
<br> <br>
|
||||
<ds-form *ngIf="formModel" #formRef="formComponent"
|
||||
[formId]="formId"
|
||||
|
@@ -41,7 +41,7 @@ import { FormBuilderService } from '../../../../shared/form/builder/form-builder
|
||||
|
||||
function getMockFileService(): FileService {
|
||||
return jasmine.createSpyObj('FileService', {
|
||||
downloadFile: jasmine.createSpy('downloadFile'),
|
||||
retrieveFileDownloadLink: jasmine.createSpy('retrieveFileDownloadLink'),
|
||||
getFileNameFromResponseContentDisposition: jasmine.createSpy('getFileNameFromResponseContentDisposition')
|
||||
});
|
||||
}
|
||||
@@ -232,7 +232,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
|
||||
|
||||
tick();
|
||||
|
||||
expect(fileService.downloadFile).toHaveBeenCalled();
|
||||
expect(fileService.retrieveFileDownloadLink).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should save Bitstream File data properly when form is valid', fakeAsync(() => {
|
||||
|
@@ -224,7 +224,7 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
|
||||
first())
|
||||
.subscribe((url) => {
|
||||
const fileUrl = `${url}/${this.fileData.uuid}/content`;
|
||||
this.fileService.downloadFile(fileUrl);
|
||||
this.fileService.retrieveFileDownloadLink(fileUrl);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -15,18 +15,24 @@ import { RouterStub } from '../../shared/testing/router.stub';
|
||||
import { mockSubmissionObject } from '../../shared/mocks/submission.mock';
|
||||
import { SubmissionSubmitComponent } from './submission-submit.component';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
|
||||
describe('SubmissionSubmitComponent Component', () => {
|
||||
|
||||
let comp: SubmissionSubmitComponent;
|
||||
let fixture: ComponentFixture<SubmissionSubmitComponent>;
|
||||
let submissionServiceStub: SubmissionServiceStub;
|
||||
let itemDataService: ItemDataService;
|
||||
let router: RouterStub;
|
||||
|
||||
const submissionId = '826';
|
||||
const submissionObject: any = mockSubmissionObject;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
itemDataService = jasmine.createSpyObj('itemDataService', {
|
||||
findByHref: createSuccessfulRemoteDataObject$(submissionObject.item),
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
@@ -38,6 +44,7 @@ describe('SubmissionSubmitComponent Component', () => {
|
||||
providers: [
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||
{ provide: SubmissionService, useClass: SubmissionServiceStub },
|
||||
{ provide: ItemDataService, useValue: itemDataService },
|
||||
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
@@ -60,7 +67,7 @@ describe('SubmissionSubmitComponent Component', () => {
|
||||
router = null;
|
||||
});
|
||||
|
||||
it('should init properly when a valid SubmissionObject has been retrieved', fakeAsync(() => {
|
||||
it('should init properly when a valid SubmissionObject has been retrieved',() => {
|
||||
|
||||
submissionServiceStub.createSubmission.and.returnValue(observableOf(submissionObject));
|
||||
|
||||
@@ -72,9 +79,9 @@ describe('SubmissionSubmitComponent Component', () => {
|
||||
expect(comp.sections).toBe(submissionObject.sections);
|
||||
expect(comp.submissionDefinition).toBe(submissionObject.submissionDefinition);
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
it('should redirect to mydspace when an empty SubmissionObject has been retrieved', fakeAsync(() => {
|
||||
it('should redirect to mydspace when an empty SubmissionObject has been retrieved',() => {
|
||||
|
||||
submissionServiceStub.createSubmission.and.returnValue(observableOf({}));
|
||||
|
||||
@@ -82,9 +89,9 @@ describe('SubmissionSubmitComponent Component', () => {
|
||||
|
||||
expect(router.navigate).toHaveBeenCalled();
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not has effects when an invalid SubmissionObject has been retrieved', fakeAsync(() => {
|
||||
it('should not has effects when an invalid SubmissionObject has been retrieved',() => {
|
||||
|
||||
submissionServiceStub.createSubmission.and.returnValue(observableOf(null));
|
||||
|
||||
@@ -94,6 +101,6 @@ describe('SubmissionSubmitComponent Component', () => {
|
||||
expect(comp.collectionId).toBeUndefined();
|
||||
expect(comp.selfUrl).toBeUndefined();
|
||||
expect(comp.submissionDefinition).toBeUndefined();
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { hasValue, isEmpty, isNotNull } from '../../shared/empty.util';
|
||||
import { hasValue, isEmpty, isNotNull, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
@@ -12,6 +12,11 @@ import { SubmissionObject } from '../../core/submission/models/submission-object
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { switchMap, debounceTime } from 'rxjs/operators';
|
||||
import { getAllSucceededRemoteData } from '../../core/shared/operators';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
|
||||
/**
|
||||
* This component allows to submit a new workspaceitem.
|
||||
@@ -28,6 +33,16 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit {
|
||||
* @type {string}
|
||||
*/
|
||||
public collectionId: string;
|
||||
|
||||
/**
|
||||
* BehaviorSubject containing the self link to the item for this submission
|
||||
* @private
|
||||
*/
|
||||
private itemLink$: BehaviorSubject<string> = new BehaviorSubject(undefined);
|
||||
|
||||
/**
|
||||
* The item for this submission.
|
||||
*/
|
||||
public item: Item;
|
||||
|
||||
/**
|
||||
@@ -71,6 +86,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit {
|
||||
*
|
||||
* @param {ChangeDetectorRef} changeDetectorRef
|
||||
* @param {NotificationsService} notificationsService
|
||||
* @param {ItemDataService} itemDataService
|
||||
* @param {SubmissionService} submissionService
|
||||
* @param {Router} router
|
||||
* @param {TranslateService} translate
|
||||
@@ -80,13 +96,16 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit {
|
||||
constructor(private changeDetectorRef: ChangeDetectorRef,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private itemDataService: ItemDataService,
|
||||
private submissionService: SubmissionService,
|
||||
private translate: TranslateService,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private route: ActivatedRoute) {
|
||||
this.route
|
||||
.queryParams
|
||||
.subscribe((params) => { this.collectionParam = (params.collection); });
|
||||
.subscribe((params) => {
|
||||
this.collectionParam = (params.collection);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,10 +127,23 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit {
|
||||
this.selfUrl = submissionObject._links.self.href;
|
||||
this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel);
|
||||
this.submissionId = submissionObject.id;
|
||||
this.itemLink$.next(submissionObject._links.item.href);
|
||||
this.item = submissionObject.item as Item;
|
||||
}
|
||||
}
|
||||
}),
|
||||
this.itemLink$.pipe(
|
||||
isNotEmptyOperator(),
|
||||
switchMap((itemLink: string) =>
|
||||
this.itemDataService.findByHref(itemLink)
|
||||
),
|
||||
getAllSucceededRemoteData(),
|
||||
// Multiple sources can update the item in quick succession.
|
||||
// We only want to rerender the form if the item is unchanged for some time
|
||||
debounceTime(300),
|
||||
).subscribe((itemRd: RemoteData<Item>) => {
|
||||
this.item = itemRd.payload;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -2287,9 +2287,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -2245,8 +2245,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "software DSpace",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -2014,8 +2014,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "DSpace Software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -526,6 +526,10 @@
|
||||
|
||||
|
||||
|
||||
"bitstream.download.page": "Now downloading {{bitstream}}..." ,
|
||||
|
||||
|
||||
|
||||
"bitstream.edit.bitstream": "Bitstream: ",
|
||||
|
||||
"bitstream.edit.form.description.hint": "Optionally, provide a brief description of the file, for example \"<i>Main article</i>\" or \"<i>Experiment data readings</i>\".",
|
||||
@@ -1237,7 +1241,7 @@
|
||||
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
"footer.link.cookies": "Cookie settings",
|
||||
|
||||
|
@@ -2081,8 +2081,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "Software DSpace",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -1891,8 +1891,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "DSpace-ohjelmisto",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -2082,8 +2082,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -1748,8 +1748,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "DSpace szoftver",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
"footer.link.cookies": "Süti beállítások",
|
||||
|
@@ -2287,9 +2287,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -1886,8 +1886,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -2080,8 +2080,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -2287,9 +2287,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -2037,8 +2037,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -2037,8 +2037,8 @@
|
||||
// "footer.link.dspace": "DSpace software",
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -2287,9 +2287,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -2287,9 +2287,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.dspace": "DSpace software",
|
||||
|
||||
// "footer.link.duraspace": "DuraSpace",
|
||||
// "footer.link.lyrasis": "LYRASIS",
|
||||
// TODO New key - Add a translation
|
||||
"footer.link.duraspace": "DuraSpace",
|
||||
"footer.link.lyrasis": "LYRASIS",
|
||||
|
||||
// "footer.link.cookies": "Cookie settings",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="231.893px" height="167.458px" viewBox="0 0 231.893 167.458" enable-background="new 0 0 231.893 167.458"
|
||||
xml:space="preserve">
|
||||
<path fill="#43515F" d="M51.733,143.32c0-1.94,1.65-3.202,4.562-3.202c3.303,0,6.798,1.115,9.419,3.543l3.835-5.146
|
||||
c-3.202-2.963-7.476-4.516-12.621-4.516c-7.622,0-12.284,4.467-12.284,9.855c0,12.188,18.644,8.254,18.644,13.887
|
||||
c0,1.893-1.797,3.592-5.632,3.592c-4.466,0-8.011-2.039-10.292-4.418l-3.787,5.39c3.058,3.059,7.525,5.153,13.788,5.153
|
||||
c8.691,0,12.964-4.474,12.964-10.396C70.329,144.971,51.733,148.418,51.733,143.32z M100.682,134.484H85.534v32.386h6.895v-11.557
|
||||
h8.254c6.99,0,10.875-4.759,10.875-10.391C111.558,139.243,107.722,134.484,100.682,134.484z M99.71,149.245h-7.283v-8.69h7.283
|
||||
c2.72,0,4.808,1.651,4.808,4.368C104.518,147.592,102.43,149.245,99.71,149.245z M180.759,140.067c3.302,0,6.215,2.09,7.573,4.71
|
||||
l5.923-2.913c-2.28-4.078-6.407-7.914-13.496-7.914c-9.759,0-17.283,6.75-17.283,16.75c0,9.954,7.524,16.76,17.283,16.76
|
||||
c7.089,0,11.216-3.94,13.496-7.971l-5.923-2.865c-1.358,2.623-4.271,4.711-7.573,4.711c-5.924,0-10.194-4.517-10.194-10.635
|
||||
C170.564,144.583,174.835,140.067,180.759,140.067z M131.958,134.484l-12.485,32.386h7.823l2.04-5.486h13.887l2.038,5.486h7.816
|
||||
l-12.479-32.386H131.958z M131.228,155.313l5.05-13.936l5.05,13.936H131.228z M231.892,140.553v-6.069h-22.916v32.386h22.916v-6.07
|
||||
H215.87v-7.379h15.684v-6.069H215.87v-6.797L231.892,140.553L231.892,140.553z"/>
|
||||
<path fill="#43515F" d="M29.956,150.652c0-9.71-7.04-16.168-17.187-16.168H0v32.386h12.817
|
||||
C22.916,166.87,29.956,160.458,29.956,150.652z M12.769,160.799H6.894v-20.246h5.924c6.603,0,10.098,4.418,10.098,10.099
|
||||
C22.916,156.187,19.177,160.799,12.769,160.799z"/>
|
||||
<path fill="#43515F" d="M120.726,58.569l0.109-0.006l0.116-0.01l0.106-0.013l0.11-0.01l0.11-0.023l0.109-0.019l0.106-0.023
|
||||
l0.106-0.029l0.105-0.023l0.106-0.033l0.103-0.034l0.097-0.035l0.104-0.04l0.101-0.042l0.1-0.042v-0.001l0.096-0.045l0,0
|
||||
l0.095-0.044l0.097-0.049l0.091-0.056v-0.001l0.094-0.05v-0.002l0.09-0.056v-0.001l0.093-0.06l0.083-0.056v-0.001l0.085-0.063
|
||||
l0.088-0.065v-0.002l0.087-0.062v-0.001c0.816-0.683,1.393-1.646,1.561-2.738l0.013-0.104V54.72l0.014-0.101v-0.011l0.009-0.098
|
||||
v-0.012l0.009-0.101V54.38l0.005-0.095v-0.016l0.002-0.105v-16.46l-0.002-0.105v-0.016l-0.005-0.095v-0.013l-0.009-0.101v-0.012
|
||||
l-0.009-0.098v-0.011l-0.014-0.1v-0.01l-0.013-0.104c-0.167-1.092-0.744-2.057-1.561-2.738V34.3l-0.087-0.063v-0.002l-0.088-0.065
|
||||
l-0.085-0.063v-0.001l-0.083-0.056l-0.093-0.061l0,0l-0.09-0.056V33.93l-0.094-0.05v-0.001l-0.091-0.056l-0.097-0.049l-0.095-0.043
|
||||
V33.73l-0.096-0.045v-0.001l-0.1-0.043l-0.101-0.042l-0.104-0.04l-0.097-0.035l-0.103-0.031l-0.106-0.036l-0.105-0.023l-0.106-0.028
|
||||
l-0.106-0.024l-0.109-0.019l-0.11-0.023l-0.11-0.009l-0.106-0.014l-0.116-0.01l-0.109-0.006l-0.114-0.005h-7.89
|
||||
c-9.716,0-15.858-7.838-15.858-17.15V6.92c0-3.812-3.102-6.915-6.914-6.915H74.085c-3.813,0-6.92,3.106-6.92,6.915v16.682
|
||||
c0,3.806,3.104,6.909,6.92,6.909h8.414c9.169,0,16.906,5.95,17.146,15.403v0.04c-0.24,9.453-7.978,15.402-17.146,15.402h-8.414
|
||||
c-3.815,0-6.92,3.103-6.92,6.909v16.682c0,3.81,3.106,6.915,6.92,6.915H89.95c3.812,0,6.914-3.104,6.914-6.915v-9.223
|
||||
c0-9.312,6.144-17.149,15.858-17.149h7.89L120.726,58.569z M154.772,9.956C148.631,3.814,140.15,0,130.816,0h-15.024v17.424h15.024
|
||||
c4.526,0,8.647,1.858,11.64,4.849c2.99,2.99,4.849,7.112,4.849,11.639v24.042c0,4.538-1.853,8.665-4.832,11.655l-0.017-0.016
|
||||
c-2.991,2.991-7.113,4.849-11.64,4.849h-15.024v17.424h15.024c9.333,0,17.814-3.814,23.956-9.956v-0.033
|
||||
c6.142-6.143,9.955-14.614,9.955-23.923V33.912C164.727,24.578,160.914,16.097,154.772,9.956z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -255,10 +255,14 @@ export const environment: GlobalConfig = {
|
||||
// // A theme with only a name will match every route
|
||||
// name: 'custom'
|
||||
// },
|
||||
// {
|
||||
// // This theme will use the default bootstrap styling for DSpace components
|
||||
// name: BASE_THEME_NAME
|
||||
// },
|
||||
|
||||
{
|
||||
// This theme will use the default bootstrap styling for DSpace components
|
||||
name: BASE_THEME_NAME
|
||||
// The default dspace theme
|
||||
name: 'dspace'
|
||||
},
|
||||
],
|
||||
// Whether the UI should rewrite file download URLs to match its domain. Only necessary to enable when running UI and REST API on separate domains
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<title>DSpace</title>
|
||||
<meta name="viewport" content="width=device-width,minimum-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" />
|
||||
<link class="theme-css" rel="stylesheet" href="/base-theme.css">
|
||||
<link class="theme-css" rel="stylesheet" href="/dspace-theme.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@@ -31,6 +31,8 @@ import {
|
||||
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||
import { RouterModule, NoPreloading } from '@angular/router';
|
||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
|
||||
|
||||
export const REQ_KEY = makeStateKey<string>('req');
|
||||
|
||||
@@ -105,6 +107,10 @@ export function getRequest(transferState: TransferState): any {
|
||||
provide: GoogleAnalyticsService,
|
||||
useClass: GoogleAnalyticsService,
|
||||
},
|
||||
{
|
||||
provide: AuthRequestService,
|
||||
useClass: BrowserAuthRequestService,
|
||||
},
|
||||
{
|
||||
provide: LocationToken,
|
||||
useFactory: locationProvider,
|
||||
|
@@ -31,6 +31,8 @@ import { ServerHardRedirectService } from '../../app/core/services/server-hard-r
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||
import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service';
|
||||
|
||||
export function createTranslateLoader() {
|
||||
return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5');
|
||||
@@ -82,6 +84,10 @@ export function createTranslateLoader() {
|
||||
provide: SubmissionService,
|
||||
useClass: ServerSubmissionService
|
||||
},
|
||||
{
|
||||
provide: AuthRequestService,
|
||||
useClass: ServerAuthRequestService,
|
||||
},
|
||||
{
|
||||
provide: LocaleService,
|
||||
useClass: ServerLocaleService
|
||||
|
@@ -1,4 +1,3 @@
|
||||
@import '../../node_modules/bootstrap/scss/functions.scss';
|
||||
@import '../../node_modules/bootstrap/scss/mixins.scss';
|
||||
|
||||
@mixin word-wrap() {
|
||||
|
@@ -0,0 +1,37 @@
|
||||
<div class="background-image">
|
||||
<div class="container">
|
||||
<div class="jumbotron jumbotron-fluid">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div>
|
||||
<h1 class="display-3">DSpace 7 - Beta 5</h1>
|
||||
<p class="lead">DSpace is the world leading open source repository platform that enables
|
||||
organisations to:</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li>easily ingest documents, audio, video, datasets and their corresponding Dublin Core
|
||||
metadata
|
||||
</li>
|
||||
<li>open up this content to local and global audiences, thanks to the OAI-PMH interface and
|
||||
Google Scholar optimizations
|
||||
</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.lyrasis.org/display/DSPACE/DSpace+Positioning" target="_blank">leading institutions using DSpace</a>.</p>
|
||||
<p>Participate in the <a href="https://wiki.lyrasis.org/display/DSPACE/DSpace+Release+7.0+Testathon+Page"
|
||||
target="_blank">official community Testathon</a>
|
||||
from <strong>April 19th through May 7th</strong>. The test user accounts below have their password set to the name of
|
||||
this
|
||||
software in lowercase.</p>
|
||||
<ul>
|
||||
<li>Demo Site Administrator = dspacedemo+admin@gmail.com</li>
|
||||
<li>Demo Community Administrator = dspacedemo+commadmin@gmail.com</li>
|
||||
<li>Demo Collection Administrator = dspacedemo+colladmin@gmail.com</li>
|
||||
<li>Demo Submitter = dspacedemo+submit@gmail.com</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<small class="credits">Photo by <a href="https://www.pexels.com/@inspiredimages">@inspiredimages</a></small>
|
||||
</div>
|
@@ -0,0 +1,69 @@
|
||||
:host {
|
||||
display: block;
|
||||
margin-top: calc(var(--ds-content-spacing) * -1);
|
||||
|
||||
div.background-image {
|
||||
color: white;
|
||||
background-color: var(--bs-info);
|
||||
position: relative;
|
||||
background-position-y: -200px;
|
||||
background-image: url('/assets/dspace/images/banner.jpg');
|
||||
background-size: cover;
|
||||
@media screen and (max-width: map-get($grid-breakpoints, lg)) {
|
||||
background-position-y: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
||||
|
||||
&:before, &:after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: var(--ds-banner-background-gradient-width);
|
||||
height: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&:before {
|
||||
background: linear-gradient(to left, var(--ds-banner-text-background), transparent);
|
||||
left: calc(-1 * var(--ds-banner-background-gradient-width));
|
||||
|
||||
}
|
||||
|
||||
&:after {
|
||||
background: linear-gradient(to right, var(--ds-banner-text-background), transparent);
|
||||
right: calc(-1 * var(--ds-banner-background-gradient-width));
|
||||
}
|
||||
|
||||
background-color: var(--ds-banner-text-background);
|
||||
}
|
||||
|
||||
|
||||
small.credits {
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
opacity: 0.3;
|
||||
position: absolute;
|
||||
right: var(--bs-spacer);
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--ds-home-news-link-color);
|
||||
|
||||
@include hover {
|
||||
color: var(--ds-home-news-link-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HomeNewsComponent as BaseComponent } from '../../../../../app/+home-page/home-news/home-news.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-home-news',
|
||||
styleUrls: ['./home-news.component.scss'],
|
||||
templateUrl: './home-news.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* Component to render the news section on the home page
|
||||
*/
|
||||
export class HomeNewsComponent extends BaseComponent {}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
@import 'src/app/navbar/navbar.component.scss';
|
||||
|
||||
nav.navbar {
|
||||
border-bottom: 5px $green solid;
|
||||
border-bottom: 5px var(--bs-green) solid;
|
||||
}
|
||||
|
||||
|
15
src/themes/dspace/app/navbar/navbar.component.ts
Normal file
15
src/themes/dspace/app/navbar/navbar.component.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { NavbarComponent as BaseComponent } from '../../../../app/navbar/navbar.component';
|
||||
import { slideMobileNav } from '../../../../app/shared/animations/slide';
|
||||
|
||||
/**
|
||||
* Component representing the public navbar
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-navbar',
|
||||
styleUrls: ['./navbar.component.scss'],
|
||||
templateUrl: '../../../../app/navbar/navbar.component.html',
|
||||
animations: [slideMobileNav]
|
||||
})
|
||||
export class NavbarComponent extends BaseComponent {
|
||||
}
|
0
src/themes/dspace/assets/fonts/.gitkeep
Normal file
0
src/themes/dspace/assets/fonts/.gitkeep
Normal file
0
src/themes/dspace/assets/images/.gitkeep
Normal file
0
src/themes/dspace/assets/images/.gitkeep
Normal file
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
2
src/themes/dspace/entry-components.ts
Normal file
2
src/themes/dspace/entry-components.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const ENTRY_COMPONENTS = [
|
||||
];
|
23
src/themes/dspace/styles/_global-styles.scss
Normal file
23
src/themes/dspace/styles/_global-styles.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
// Add any global css for the theme here
|
||||
|
||||
// imports the base global style
|
||||
@import '../../../styles/_global-styles.scss';
|
||||
|
||||
.facet-filter,.setting-option {
|
||||
background-color: var(--bs-light);
|
||||
border-radius: var(--bs-border-radius);
|
||||
|
||||
&.p-3 {
|
||||
// Needs !important because the original bootstrap class uses it
|
||||
padding-top: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
background-color: var(--bs-primary);
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.1rem
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
// Override or add CSS variables for your theme here
|
||||
|
||||
:root {
|
||||
--ds-banner-text-background: rgba(0, 0, 0, 0.45);
|
||||
--ds-banner-background-gradient-width: 300px;
|
||||
--ds-home-news-link-color: #{$green};
|
||||
--ds-home-news-link-hover-color: #{darken($green, 15%)};
|
||||
}
|
||||
|
30
src/themes/dspace/styles/_theme_sass_variable_overrides.scss
Normal file
30
src/themes/dspace/styles/_theme_sass_variable_overrides.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
// DSpace works with CSS variables for its own components, and has a mapping of all bootstrap Sass
|
||||
// variables to CSS equivalents (see src/styles/_bootstrap_variables_mapping.scss). However Bootstrap
|
||||
// still uses Sass variables internally. So if you want to override bootstrap (or other sass
|
||||
// variables) you can do so here. Their CSS counterparts will include the changes you make here
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;0,800;1,200;1,300;1,400;1,600;1,700;1,800&display=swap');
|
||||
|
||||
$font-family-sans-serif: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
$gray-100: #e8ebf3 !default;
|
||||
$gray-400: #ced4da !default;
|
||||
$gray-600: #959595 !default;
|
||||
$gray-800: #444444 !default;
|
||||
|
||||
$navbar-dark-color: #FFFFFF;
|
||||
|
||||
/* Reassign color vars to semantic color scheme */
|
||||
$blue: #43515f !default;
|
||||
$green: #92C642 !default;
|
||||
$cyan: #207698 !default;
|
||||
$yellow: #ec9433 !default;
|
||||
$red: #CF4444 !default;
|
||||
$dark: #43515f !default;
|
||||
|
||||
$body-color: $gray-800 !default;
|
||||
|
||||
$table-accent-bg: $gray-100 !default;
|
||||
$table-hover-bg: $gray-400 !default;
|
||||
|
||||
$yiq-contrasted-threshold: 170 !default;
|
||||
|
12
src/themes/dspace/styles/theme.scss
Normal file
12
src/themes/dspace/styles/theme.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
// This file combines the other scss files in to one. You usually shouldn't edit this file directly
|
||||
|
||||
@import './_theme_sass_variable_overrides.scss';
|
||||
@import '../../../styles/_variables.scss';
|
||||
@import '../../../styles/_mixins.scss';
|
||||
@import '../../../styles/helpers/font_awesome_imports.scss';
|
||||
@import '../../../../node_modules/bootstrap/scss/bootstrap.scss';
|
||||
@import '../../../../node_modules/nouislider/distribute/nouislider.min';
|
||||
@import '../../../styles/_custom_variables.scss';
|
||||
@import './_theme_css_variable_overrides.scss';
|
||||
@import '../../../styles/bootstrap_variables_mapping.scss';
|
||||
@import './_global-styles.scss';
|
105
src/themes/dspace/theme.module.ts
Normal file
105
src/themes/dspace/theme.module.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AdminRegistriesModule } from '../../app/+admin/admin-registries/admin-registries.module';
|
||||
import { AdminSearchModule } from '../../app/+admin/admin-search-page/admin-search.module';
|
||||
import { AdminWorkflowModuleModule } from '../../app/+admin/admin-workflow-page/admin-workflow.module';
|
||||
import { BitstreamFormatsModule } from '../../app/+admin/admin-registries/bitstream-formats/bitstream-formats.module';
|
||||
import { BrowseByModule } from '../../app/+browse-by/browse-by.module';
|
||||
import { CollectionFormModule } from '../../app/+collection-page/collection-form/collection-form.module';
|
||||
import { CommunityFormModule } from '../../app/+community-page/community-form/community-form.module';
|
||||
import { CoreModule } from '../../app/core/core.module';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { EditItemPageModule } from '../../app/+item-page/edit-item-page/edit-item-page.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { IdlePreloadModule } from 'angular-idle-preload';
|
||||
import { JournalEntitiesModule } from '../../app/entity-groups/journal-entities/journal-entities.module';
|
||||
import { MyDspaceSearchModule } from '../../app/+my-dspace-page/my-dspace-search.module';
|
||||
import { MenuModule } from '../../app/shared/menu/menu.module';
|
||||
import { NavbarModule } from '../../app/navbar/navbar.module';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ProfilePageModule } from '../../app/profile-page/profile-page.module';
|
||||
import { RegisterEmailFormModule } from '../../app/register-email-form/register-email-form.module';
|
||||
import { ResearchEntitiesModule } from '../../app/entity-groups/research-entities/research-entities.module';
|
||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||
import { SearchPageModule } from '../../app/+search-page/search-page.module';
|
||||
import { SharedModule } from '../../app/shared/shared.module';
|
||||
import { StatisticsModule } from '../../app/statistics/statistics.module';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { HomeNewsComponent } from './app/+home-page/home-news/home-news.component';
|
||||
import { HomePageModule } from '../../app/+home-page/home-page.module';
|
||||
import { AppModule } from '../../app/app.module';
|
||||
import { ItemPageModule } from '../../app/+item-page/item-page.module';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CommunityListPageModule } from '../../app/community-list-page/community-list-page.module';
|
||||
import { InfoModule } from '../../app/info/info.module';
|
||||
import { StatisticsPageModule } from '../../app/statistics-page/statistics-page.module';
|
||||
import { CommunityPageModule } from '../../app/+community-page/community-page.module';
|
||||
import { CollectionPageModule } from '../../app/+collection-page/collection-page.module';
|
||||
import { SubmissionModule } from '../../app/submission/submission.module';
|
||||
import { MyDSpacePageModule } from '../../app/+my-dspace-page/my-dspace-page.module';
|
||||
import { NavbarComponent } from './app/navbar/navbar.component';
|
||||
|
||||
const DECLARATIONS = [
|
||||
HomeNewsComponent,
|
||||
NavbarComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AdminRegistriesModule,
|
||||
AdminSearchModule,
|
||||
AdminWorkflowModuleModule,
|
||||
AppModule,
|
||||
BitstreamFormatsModule,
|
||||
BrowseByModule,
|
||||
CollectionFormModule,
|
||||
CollectionPageModule,
|
||||
CommonModule,
|
||||
CommunityFormModule,
|
||||
CommunityListPageModule,
|
||||
CommunityPageModule,
|
||||
CoreModule,
|
||||
DragDropModule,
|
||||
ItemPageModule,
|
||||
EditItemPageModule,
|
||||
FormsModule,
|
||||
HomePageModule,
|
||||
HttpClientModule,
|
||||
IdlePreloadModule,
|
||||
InfoModule,
|
||||
JournalEntitiesModule,
|
||||
MenuModule,
|
||||
MyDspaceSearchModule,
|
||||
NavbarModule,
|
||||
NgbModule,
|
||||
ProfilePageModule,
|
||||
RegisterEmailFormModule,
|
||||
ResearchEntitiesModule,
|
||||
RouterModule,
|
||||
ScrollToModule,
|
||||
SearchPageModule,
|
||||
SharedModule,
|
||||
StatisticsModule,
|
||||
StatisticsPageModule,
|
||||
StoreModule,
|
||||
StoreRouterConnectingModule,
|
||||
TranslateModule,
|
||||
SubmissionModule,
|
||||
MyDSpacePageModule,
|
||||
MyDspaceSearchModule,
|
||||
],
|
||||
declarations: DECLARATIONS
|
||||
})
|
||||
|
||||
/**
|
||||
* This module serves as an index for all the components in this theme.
|
||||
* It should import all other modules, so the compiler knows where to find any components referenced
|
||||
* from a component in this theme
|
||||
* It is purposefully not exported, it should never be imported anywhere else, its only purpose is
|
||||
* to give lazily loaded components a context in which they can be compiled successfully
|
||||
*/
|
||||
class ThemeModule {
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
<div class="jumbotron jumbotron-fluid">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div>
|
||||
<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>
|
||||
<ul>
|
||||
<li>easily ingest documents, audio, video, datasets and their corresponding Dublin Core
|
||||
metadata
|
||||
</li>
|
||||
<li>open up this content to local and global audiences, thanks to the OAI-PMH interface and
|
||||
Google Scholar optimizations
|
||||
</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>
|
||||
</div>
|
@@ -1,17 +0,0 @@
|
||||
@import 'src/app/+home-page/home-news/home-news.component.scss';
|
||||
:host {
|
||||
--ds-home-news-link-color: #{$green};
|
||||
--ds-home-news-link-hover-color: #{darken($green, 15%)};
|
||||
|
||||
.jumbotron {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--ds-home-news-link-color);
|
||||
|
||||
@include hover {
|
||||
color: var(--ds-home-news-link-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
<div class="background-image">
|
||||
<div class="container">
|
||||
<ds-home-news></ds-home-news>
|
||||
<ds-search-form class="d-block pb-3" [inPlaceSearch]="false" [large]="true" [brandColor]="'success'"></ds-search-form>
|
||||
</div>
|
||||
<small class="credits">Photo by <a href="https://www.pexels.com/@inspiredimages">@inspiredimages</a></small>
|
||||
</div>
|
||||
<div class="container pt-3">
|
||||
<ds-top-level-community-list></ds-top-level-community-list>
|
||||
</div>
|
@@ -1,52 +0,0 @@
|
||||
@import 'src/app/+home-page/home-page.component.scss';
|
||||
|
||||
div.background-image {
|
||||
color: white;
|
||||
background-color: var(--bs-info);
|
||||
position: relative;
|
||||
background-position-y: -200px;
|
||||
background-image: url('/assets/images/banner.jpg');
|
||||
background-size: cover;
|
||||
@media screen and (max-width: map-get($grid-breakpoints, lg)) {
|
||||
background-position-y: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
||||
|
||||
&:before, &:after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: var(--ds-banner-background-gradient-width);
|
||||
height: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&:before {
|
||||
background: linear-gradient(to left, var(--ds-banner-text-background), transparent);
|
||||
left: calc(-1 * var(--ds-banner-background-gradient-width));
|
||||
|
||||
}
|
||||
|
||||
&:after {
|
||||
background: linear-gradient(to right, var(--ds-banner-text-background), transparent);
|
||||
right: calc(-1 * var(--ds-banner-background-gradient-width));
|
||||
}
|
||||
|
||||
background-color: var(--ds-banner-text-background);
|
||||
}
|
||||
|
||||
|
||||
small.credits {
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
opacity: 0.3;
|
||||
position: absolute;
|
||||
right: var(--bs-spacer);
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
<div *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="itemRD?.isLoading" message="{{'loading.item' | translate}}"></ds-loading>
|
||||
</div>
|
@@ -1,87 +0,0 @@
|
||||
a<div class="top-item-page">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-12 col-md-2 d-flex flex-md-column justify-content-between">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
<div>
|
||||
<a class="btn btn-secondary"
|
||||
[routerLink]="[itemPageRoute + '/full']">
|
||||
{{"item.page.link.full" | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-10">
|
||||
<ds-item-page-title-field [item]="object"></ds-item-page-title-field>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<ds-metadata-representation-list
|
||||
[parentItem]="object"
|
||||
[itemType]="'Person'"
|
||||
[metadataField]="'dc.contributor.author'"
|
||||
[label]="'relationships.isAuthorOf' | translate">
|
||||
</ds-metadata-representation-list>
|
||||
<ds-item-page-file-section [item]="object"></ds-item-page-file-section>
|
||||
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
||||
<ds-item-page-author-field [item]="object"></ds-item-page-author-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['journal.title']"
|
||||
[label]="'publication.page.journal-title'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['journal.identifier.issn']"
|
||||
[label]="'publication.page.journal-issn'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['journalvolume.identifier.name']"
|
||||
[label]="'publication.page.volume-title'">
|
||||
</ds-generic-item-page-field>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['dc.subject']"
|
||||
[separator]="','"
|
||||
[label]="'item.page.subject'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['dc.identifier.citation']"
|
||||
[label]="'item.page.citation'">
|
||||
</ds-generic-item-page-field>
|
||||
<<ds-item-page-uri-field [item]="object"
|
||||
[fields]="['dc.identifier.uri']"
|
||||
[label]="'item.page.uri'">
|
||||
</ds-item-page-uri-field>
|
||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relationships-item-page">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ds-related-items
|
||||
class="col-12 col-md-4"
|
||||
[parentItem]="object"
|
||||
[relationType]="'isProjectOfPublication'"
|
||||
[label]="'relationships.isProjectOf' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
class="col-12 col-md-4"
|
||||
[parentItem]="object"
|
||||
[relationType]="'isOrgUnitOfPublication'"
|
||||
[label]="'relationships.isOrgUnitOf' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
class="col-12 col-md-4"
|
||||
[parentItem]="object"
|
||||
[relationType]="'isJournalIssueOfPublication'"
|
||||
[label]="'relationships.isJournalIssueOf' | translate">
|
||||
</ds-related-items>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,30 +0,0 @@
|
||||
@import 'src/app/+item-page/simple/item-types/publication/publication.component.scss';
|
||||
|
||||
:host {
|
||||
> * {
|
||||
display: block;
|
||||
padding-top: var(--ds-content-spacing);
|
||||
padding-bottom: var(--ds-content-spacing);
|
||||
}
|
||||
|
||||
.top-item-page {
|
||||
background-color: var(--bs-gray-100);
|
||||
margin-top: calc(-1 * var(--ds-content-spacing));
|
||||
}
|
||||
|
||||
.relationships-item-page {
|
||||
padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer));
|
||||
}
|
||||
|
||||
ds-metadata-field-wrapper {
|
||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||
flex: 1;
|
||||
padding-right: calc(var(--bs-spacer) / 2);
|
||||
}
|
||||
|
||||
ds-thumbnail {
|
||||
display: block;
|
||||
max-width: var(--ds-thumbnail-max-width);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,77 +0,0 @@
|
||||
<div class="top-item-page">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-12 col-md-2 d-flex flex-md-column justify-content-between">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
<div>
|
||||
<a class="btn btn-secondary"
|
||||
[routerLink]="[itemPageRoute + '/full']">
|
||||
{{"item.page.link.full" | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-10">
|
||||
<h2 class="item-page-title-field">
|
||||
{{'journalissue.page.titleprefix' | translate}}
|
||||
<ds-metadata-values
|
||||
[mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['journal.title']"
|
||||
[label]="'journalissue.page.journal-title'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['publicationvolume.volumeNumber']"
|
||||
[label]="'journalvolume.page.volume'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['publicationissue.issueNumber']"
|
||||
[label]="'journalissue.page.number'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['creativeworkseries.issn']"
|
||||
[label]="'journalissue.page.journal-issn'">
|
||||
</ds-generic-item-page-field>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['creativework.datePublished']"
|
||||
[label]="'journalissue.page.issuedate'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['dc.description']"
|
||||
[label]="'journalissue.page.description'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['creativework.keywords']"
|
||||
[label]="'journalissue.page.keyword'">
|
||||
</ds-generic-item-page-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relationships-item-page">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ds-related-items
|
||||
class="col-12 col-md-4"
|
||||
[parentItem]="object"
|
||||
[relationType]="'isJournalVolumeOfIssue'"
|
||||
[label]="'relationships.isSingleVolumeOf' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
class="mb-1 mt-1 col-12 col-md-4"
|
||||
[parentItem]="object"
|
||||
[relationType]="'isPublicationOfJournalIssue'"
|
||||
[label]="'relationships.isPublicationOfJournalIssue' | translate">
|
||||
</ds-related-items>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,30 +0,0 @@
|
||||
@import 'src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss';
|
||||
|
||||
:host {
|
||||
> * {
|
||||
display: block;
|
||||
padding-top: var(--ds-content-spacing);
|
||||
padding-bottom: var(--ds-content-spacing);
|
||||
}
|
||||
|
||||
.top-item-page {
|
||||
background-color: var(--bs-gray-100);
|
||||
margin-top: calc(-1 * var(--ds-content-spacing));
|
||||
}
|
||||
|
||||
.relationships-item-page {
|
||||
padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer));
|
||||
}
|
||||
|
||||
ds-metadata-field-wrapper {
|
||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||
flex: 1;
|
||||
padding-right: calc(var(--bs-spacer) / 2);
|
||||
}
|
||||
|
||||
ds-thumbnail {
|
||||
display: block;
|
||||
max-width: var(--ds-thumbnail-max-width);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,62 +0,0 @@
|
||||
<div class="top-item-page">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-12 col-md-2 d-flex flex-md-column justify-content-between">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
<div>
|
||||
<a class="btn btn-secondary"
|
||||
[routerLink]="[itemPageRoute + '/full']">
|
||||
{{"item.page.link.full" | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-10">
|
||||
<h2 class="item-page-title-field">
|
||||
{{'journalvolume.page.titleprefix' | translate}}
|
||||
<ds-metadata-values
|
||||
[mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['publicationvolume.volumeNumber']"
|
||||
[label]="'journalvolume.page.volume'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['creativework.datePublished']"
|
||||
[label]="'journalvolume.page.issuedate'">
|
||||
</ds-generic-item-page-field>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['dc.description']"
|
||||
[label]="'journalvolume.page.description'">
|
||||
</ds-generic-item-page-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relationships-item-page">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ds-related-items
|
||||
class="col-12 col-md-4"
|
||||
[parentItem]="object"
|
||||
[relationType]="'isJournalOfVolume'"
|
||||
[label]="'relationships.isSingleJournalOf' | translate">
|
||||
</ds-related-items>
|
||||
<ds-related-items
|
||||
class="col-12 col-md-4"
|
||||
[parentItem]="object"
|
||||
[relationType]="'isIssueOfJournalVolume'"
|
||||
[label]="'relationships.isIssueOf' | translate">
|
||||
</ds-related-items>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,30 +0,0 @@
|
||||
@import 'src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss';
|
||||
|
||||
:host {
|
||||
> * {
|
||||
display: block;
|
||||
padding-top: var(--ds-content-spacing);
|
||||
padding-bottom: var(--ds-content-spacing);
|
||||
}
|
||||
|
||||
.top-item-page {
|
||||
background-color: var(--bs-gray-100);
|
||||
margin-top: calc(-1 * var(--ds-content-spacing));
|
||||
}
|
||||
|
||||
.relationships-item-page {
|
||||
padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer));
|
||||
}
|
||||
|
||||
ds-metadata-field-wrapper {
|
||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||
flex: 1;
|
||||
padding-right: calc(var(--bs-spacer) / 2);
|
||||
}
|
||||
|
||||
ds-thumbnail {
|
||||
display: block;
|
||||
max-width: var(--ds-thumbnail-max-width);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
<div class="top-item-page">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-2 d-flex flex-md-column justify-content-between">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
<div>
|
||||
<a class="btn btn-secondary"
|
||||
[routerLink]="[itemPageRoute + '/full']">
|
||||
{{"item.page.link.full" | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-10">
|
||||
<h2 class="item-page-title-field">
|
||||
{{'journal.page.titleprefix' | translate}}
|
||||
<ds-metadata-values
|
||||
[mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
|
||||
<ds-generic-item-page-field class="item-page-fields" [item]="object"
|
||||
[fields]="['creativeworkseries.issn']"
|
||||
[label]="'journal.page.issn'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field class="item-page-fields" [item]="object"
|
||||
[fields]="['creativework.publisher']"
|
||||
[label]="'journal.page.publisher'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-generic-item-page-field [item]="object"
|
||||
[fields]="['creativework.editor']"
|
||||
[label]="'journal.page.editor'">
|
||||
</ds-generic-item-page-field>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ds-generic-item-page-field class="item-page-fields" [item]="object"
|
||||
[fields]="['dc.description']"
|
||||
[label]="'journal.page.description'">
|
||||
</ds-generic-item-page-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relationships-item-page">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ds-related-items
|
||||
class="col-12 col-md-4"
|
||||
[parentItem]="object"
|
||||
[relationType]="'isVolumeOfJournal'"
|
||||
[label]="'relationships.isVolumeOf' | translate">
|
||||
</ds-related-items>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="container search-container">
|
||||
<h3 class="h2">{{"item.page.journal.search.title" | translate}}</h3>
|
||||
</div>
|
||||
<ds-tabbed-related-entities-search [item]="object"
|
||||
[relationTypes]="[{
|
||||
label: 'isJournalOfPublication',
|
||||
filter: 'isJournalOfPublication'
|
||||
}]">
|
||||
</ds-tabbed-related-entities-search>
|
||||
</div>
|
@@ -1,38 +0,0 @@
|
||||
@import 'src/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss';
|
||||
|
||||
:host {
|
||||
> * {
|
||||
display: block;
|
||||
padding-top: var(--ds-content-spacing);
|
||||
padding-bottom: var(--ds-content-spacing);
|
||||
}
|
||||
|
||||
.top-item-page {
|
||||
background-color: var(--bs-gray-100);
|
||||
margin-top: calc(-1 * var(--ds-content-spacing));
|
||||
}
|
||||
|
||||
.relationships-item-page {
|
||||
padding-bottom: calc(var(--ds-content-spacing) - var(--bs-spacer));
|
||||
}
|
||||
|
||||
ds-metadata-field-wrapper {
|
||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||
flex: 1;
|
||||
padding-right: calc(var(--bs-spacer) / 2);
|
||||
}
|
||||
|
||||
ds-thumbnail {
|
||||
display: block;
|
||||
max-width: var(--ds-thumbnail-max-width);
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: var(--bs-spacer);
|
||||
@media screen and (max-width: map-get($grid-breakpoints, lg)) {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user