diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b5b3f9d8c..d2e8b9fe5e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/angular.json b/angular.json index 19cbe94be6..00799dc33c 100644 --- a/angular.json +++ b/angular.json @@ -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": [] diff --git a/docker/cli.assetstore.yml b/docker/cli.assetstore.yml index 592194a752..c2846286d7 100644 --- a/docker/cli.assetstore.yml +++ b/docker/cli.assetstore.yml @@ -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' diff --git a/docker/db.entities.yml b/docker/db.entities.yml index d39eedc5c6..818d14877c 100644 --- a/docker/db.entities.yml +++ b/docker/db.entities.yml @@ -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/null 2>&1; do sleep 1; done; + /dspace/bin/dspace database migrate + sed -i '/name-map collection-handle="default".*/a \\n \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + ' /dspace/config/item-submission.xml + catalina.sh run \ No newline at end of file diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index ec2d0912cf..d2d02f0a55 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -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/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: \ No newline at end of file diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index e6668f3f58..c99e469941 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -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/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: diff --git a/docs/Configuration.md b/docs/Configuration.md index f918622568..f4fff1166c 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -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 `` 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` `` 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" -``` - - - - - diff --git a/e2e/protractor-ci.conf.js b/e2e/protractor-ci.conf.js index 63173e44e3..0cfc1f9eaf 100644 --- a/e2e/protractor-ci.conf.js +++ b/e2e/protractor-ci.conf.js @@ -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; diff --git a/package.json b/package.json index 6032910c4c..09e3e19caf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server.ts b/server.ts index 8829fd10be..e9fb1a7fd0 100644 --- a/server.ts +++ b/server.ts @@ -166,6 +166,11 @@ function ngApp(req, res) { }, (err, data) => { if (hasNoValue(err) && hasValue(data)) { res.send(data); + } else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { + // When this error occurs we can't fall back to CSR because the response has already been + // sent. These errors occur for various reasons in universal, not all of which are in our + // control to solve. + console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client'); } else { console.warn('Error in SSR, serving for direct CSR.'); if (hasValue(err)) { diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html index e5cf7cf5ec..6569b2d4c8 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html @@ -14,8 +14,7 @@ [pageInfoState]="(bitstreamFormats | async)?.payload" [collectionSize]="(bitstreamFormats | async)?.payload?.totalElements" [hideGear]="true" - [hidePagerWhenSinglePage]="true" - (pageChange)="onPageChange($event)"> + [hidePagerWhenSinglePage]="true">
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index 74ca566029..8cfba1d37b 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -23,6 +23,11 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; describe('BitstreamFormatsComponent', () => { let comp: BitstreamFormatsComponent; @@ -30,6 +35,7 @@ describe('BitstreamFormatsComponent', () => { let bitstreamFormatService; let scheduler: TestScheduler; let notificationsServiceStub; + let paginationService; const bitstreamFormat1 = new BitstreamFormat(); bitstreamFormat1.uuid = 'test-uuid-1'; @@ -79,6 +85,10 @@ describe('BitstreamFormatsComponent', () => { ]; const mockFormatsRD = createSuccessfulRemoteDataObject(createPaginatedList(mockFormatsList)); + const pagination = Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 }); + const sort = new SortOptions('score', SortDirection.DESC); + const findlistOptions = Object.assign(new FindListOptions(), { currentPage: 1, elementsPerPage: 20 }); + const initAsync = () => { notificationsServiceStub = new NotificationsServiceStub(); @@ -95,13 +105,16 @@ describe('BitstreamFormatsComponent', () => { clearBitStreamFormatRequests: observableOf('cleared') }); + paginationService = new PaginationServiceStub(); + TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe], providers: [ { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: NotificationsService, useValue: notificationsServiceStub } + { provide: NotificationsService, useValue: notificationsServiceStub }, + { provide: PaginationService, useValue: paginationService } ] }).compileComponents(); }; @@ -217,13 +230,16 @@ describe('BitstreamFormatsComponent', () => { clearBitStreamFormatRequests: observableOf('cleared') }); - TestBed.configureTestingModule({ + paginationService = new PaginationServiceStub(); + + TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe], providers: [ { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: NotificationsService, useValue: notificationsServiceStub } + { provide: NotificationsService, useValue: notificationsServiceStub }, + { provide: PaginationService, useValue: paginationService } ] }).compileComponents(); } @@ -263,13 +279,16 @@ describe('BitstreamFormatsComponent', () => { clearBitStreamFormatRequests: observableOf('cleared') }); - TestBed.configureTestingModule({ + paginationService = new PaginationServiceStub(); + + TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe], providers: [ { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: NotificationsService, useValue: notificationsServiceStub } + { provide: NotificationsService, useValue: notificationsServiceStub }, + { provide: PaginationService, useValue: paginationService } ] }).compileComponents(); } diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts index 869e57ef89..cbbcbe07a4 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit } from '@angular/core'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, zip } from 'rxjs'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { combineLatest as observableCombineLatest, Observable, zip } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; @@ -12,6 +12,7 @@ import { NotificationsService } from '../../../shared/notifications/notification import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { NoContent } from '../../../core/shared/NoContent.model'; +import { PaginationService } from '../../../core/pagination/pagination.service'; /** * This component renders a list of bitstream formats @@ -20,18 +21,13 @@ import { NoContent } from '../../../core/shared/NoContent.model'; selector: 'ds-bitstream-formats', templateUrl: './bitstream-formats.component.html' }) -export class BitstreamFormatsComponent implements OnInit { +export class BitstreamFormatsComponent implements OnInit, OnDestroy { /** * A paginated list of bitstream formats to be shown on the page */ bitstreamFormats: Observable>>; - /** - * A BehaviourSubject that keeps track of the pageState used to update the currently displayed bitstreamFormats - */ - pageState: BehaviorSubject; - /** * The current pagination configuration for the page used by the FindAll method * Currently simply renders all bitstream formats @@ -45,16 +41,19 @@ export class BitstreamFormatsComponent implements OnInit { * Currently simply renders all bitstream formats */ pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'registry-bitstreamformats-pagination', + id: 'rbp', pageSize: 20 }); constructor(private notificationsService: NotificationsService, private router: Router, private translateService: TranslateService, - private bitstreamFormatService: BitstreamFormatDataService) { + private bitstreamFormatService: BitstreamFormatDataService, + private paginationService: PaginationService, + ) { } + /** * Deletes the currently selected formats from the registry and updates the presented list */ @@ -80,10 +79,8 @@ export class BitstreamFormatsComponent implements OnInit { this.deselectAll(); - this.router.navigate([], { - queryParams: Object.assign({}, { page: 1 }), - queryParamsHandling: 'merge' - }); }); + this.paginationService.resetPage(this.pageConfig.id); + }); } ); } @@ -141,31 +138,17 @@ export class BitstreamFormatsComponent implements OnInit { }); } - /** - * When the page is changed, make sure to update the list of bitstreams to match the new page - * @param event The page change event - */ - onPageChange(event) { - this.config = Object.assign(new FindListOptions(), this.config, { - currentPage: event, - }); - this.pageConfig.currentPage = event; - this.pageState.next('pageChange'); - } - ngOnInit(): void { - this.pageState = new BehaviorSubject('init'); - this.bitstreamFormats = this.pageState.pipe( - switchMap(() => { - return this.updateFormats() - ; - })); + + this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe( + switchMap((findListOptions: FindListOptions) => { + return this.bitstreamFormatService.findAll(findListOptions); + }) + ); } - /** - * Finds all formats based on the current config - */ - private updateFormats() { - return this.bitstreamFormatService.findAll(this.config); + + ngOnDestroy(): void { + this.paginationService.clearPagination(this.pageConfig.id); } } diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html index 42b7558397..771f18ba4b 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html @@ -13,8 +13,7 @@ [paginationOptions]="config" [collectionSize]="(metadataSchemas | async)?.payload?.totalElements" [hideGear]="true" - [hidePagerWhenSinglePage]="true" - (pageChange)="onPageChange($event)"> + [hidePagerWhenSinglePage]="true">
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts index a5a65198af..0253725cb9 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts @@ -18,11 +18,17 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications- import { RestResponse } from '../../../core/cache/response.models'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; describe('MetadataRegistryComponent', () => { let comp: MetadataRegistryComponent; let fixture: ComponentFixture; let registryService: RegistryService; + let paginationService; const mockSchemasList = [ { id: 1, @@ -62,6 +68,8 @@ describe('MetadataRegistryComponent', () => { }; /* tslint:enable:no-empty */ + paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], @@ -69,6 +77,7 @@ describe('MetadataRegistryComponent', () => { providers: [ { provide: RegistryService, useValue: registryServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: PaginationService, useValue: paginationService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts index 41efb7a578..8574c4678b 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts @@ -13,6 +13,7 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; import { NoContent } from '../../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { PaginationService } from '../../../core/pagination/pagination.service'; @Component({ selector: 'ds-metadata-registry', @@ -34,7 +35,7 @@ export class MetadataRegistryComponent { * Pagination config used to display the list of metadata schemas */ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'registry-metadataschemas-pagination', + id: 'rm', pageSize: 25 }); @@ -46,26 +47,20 @@ export class MetadataRegistryComponent { constructor(private registryService: RegistryService, private notificationsService: NotificationsService, private router: Router, + private paginationService: PaginationService, private translateService: TranslateService) { this.updateSchemas(); } - /** - * Event triggered when the user changes page - * @param event - */ - onPageChange(event) { - this.config.currentPage = event; - this.forceUpdateSchemas(); - } - /** * Update the list of schemas by fetching it from the rest api or cache */ private updateSchemas() { + this.metadataSchemas = this.needsUpdate$.pipe( filter((update) => update === true), - switchMap(() => this.registryService.getMetadataSchemas(toFindListOptions(this.config))) + switchMap(() => this.paginationService.getCurrentPagination(this.config.id, this.config)), + switchMap((currentPagination) => this.registryService.getMetadataSchemas(toFindListOptions(currentPagination))) ); } @@ -169,7 +164,7 @@ export class MetadataRegistryComponent { const suffix = success ? 'success' : 'failure'; const messages = observableCombineLatest( this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), - this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount }) + this.translateService.get(`${prefix}.deleted.${suffix}`, {amount: amount}) ); messages.subscribe(([head, content]) => { if (success) { @@ -179,4 +174,8 @@ export class MetadataRegistryComponent { } }); } + ngOnDestroy(): void { + this.paginationService.clearPagination(this.config.id); + } + } diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html index 49ef748349..49fed0c847 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html @@ -19,8 +19,7 @@ [pageInfoState]="fields" [collectionSize]="fields?.totalElements" [hideGear]="false" - [hidePagerWhenSinglePage]="true" - (pageChange)="onPageChange($event)"> + [hidePagerWhenSinglePage]="true">
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts index 8c685c7012..6eb3c5b1a4 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts @@ -23,6 +23,11 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { VarDirective } from '../../../shared/utils/var.directive'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; describe('MetadataSchemaComponent', () => { let comp: MetadataSchemaComponent; @@ -125,6 +130,8 @@ describe('MetadataSchemaComponent', () => { }) }); + const paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], @@ -134,6 +141,7 @@ describe('MetadataSchemaComponent', () => { { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: Router, useValue: new RouterStub() }, + { provide: PaginationService, useValue: paginationService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts index 572bcc8a51..8a2086d5e2 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -6,6 +6,7 @@ import { combineLatest as observableCombineLatest, combineLatest, Observable, + of as observableOf, zip } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; @@ -17,12 +18,10 @@ import { NotificationsService } from '../../../shared/notifications/notification import { TranslateService } from '@ngx-translate/core'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; -import { - getFirstCompletedRemoteData, - getFirstSucceededRemoteDataPayload -} from '../../../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; import { NoContent } from '../../../core/shared/NoContent.model'; +import { PaginationService } from '../../../core/pagination/pagination.service'; @Component({ selector: 'ds-metadata-schema', @@ -48,7 +47,7 @@ export class MetadataSchemaComponent implements OnInit { * Pagination config used to display the list of metadata fields */ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'registry-metadatafields-pagination', + id: 'rm', pageSize: 25, pageSizeOptions: [25, 50, 100, 200] }); @@ -62,6 +61,7 @@ export class MetadataSchemaComponent implements OnInit { private route: ActivatedRoute, private notificationsService: NotificationsService, private router: Router, + private paginationService: PaginationService, private translateService: TranslateService) { } @@ -81,25 +81,17 @@ export class MetadataSchemaComponent implements OnInit { this.updateFields(); } - /** - * Event triggered when the user changes page - * @param event - */ - onPageChange(event) { - this.config.currentPage = event; - this.forceUpdateFields(); - } - /** * Update the list of fields by fetching it from the rest api or cache */ private updateFields() { - this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe( - switchMap(([schema, update]: [MetadataSchema, boolean]) => { + this.metadataFields$ = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( + switchMap((currentPagination) => combineLatest(this.metadataSchema$, this.needsUpdate$, observableOf(currentPagination))), + switchMap(([schema, update, currentPagination]: [MetadataSchema, boolean, PaginationComponentOptions]) => { if (update) { this.needsUpdate$.next(false); } - return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), !update, true); + return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(currentPagination), !update, true); }) ); } @@ -216,4 +208,8 @@ export class MetadataSchemaComponent implements OnInit { } }); } + ngOnDestroy(): void { + this.paginationService.clearPagination(this.config.id); + } + } diff --git a/src/app/+bitstream-page/bitstream-page-routing.module.ts b/src/app/+bitstream-page/bitstream-page-routing.module.ts index 14d688064c..bbbd65f279 100644 --- a/src/app/+bitstream-page/bitstream-page-routing.module.ts +++ b/src/app/+bitstream-page/bitstream-page-routing.module.ts @@ -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, diff --git a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.spec.ts b/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.spec.ts index 87473a876b..a5cc69e430 100644 --- a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.spec.ts +++ b/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.spec.ts @@ -18,11 +18,17 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search- import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec'; import { VarDirective } from '../../shared/utils/var.directive'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../core/data/request.models'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; describe('BrowseByDatePageComponent', () => { let comp: BrowseByDatePageComponent; let fixture: ComponentFixture; let route: ActivatedRoute; + let paginationService; const mockCommunity = Object.assign(new Community(), { id: 'test-uuid', @@ -65,6 +71,8 @@ describe('BrowseByDatePageComponent', () => { detectChanges: () => fixture.detectChanges() }); + paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], @@ -74,6 +82,7 @@ describe('BrowseByDatePageComponent', () => { { provide: BrowseService, useValue: mockBrowseService }, { provide: DSpaceObjectDataService, useValue: mockDsoService }, { provide: Router, useValue: new RouterMock() }, + { provide: PaginationService, useValue: paginationService }, { provide: ChangeDetectorRef, useValue: mockCdRef } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts b/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts index 03c50dd051..a9eaa09e2f 100644 --- a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts @@ -8,12 +8,16 @@ import { combineLatest as observableCombineLatest } from 'rxjs'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Params, Router } from '@angular/router'; import { BrowseService } from '../../core/browse/browse.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator'; import { environment } from '../../../environments/environment'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { map } from 'rxjs/operators'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; @Component({ selector: 'ds-browse-by-date-page', @@ -37,30 +41,32 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { protected browseService: BrowseService, protected dsoService: DSpaceObjectDataService, protected router: Router, + protected paginationService: PaginationService, protected cdRef: ChangeDetectorRef) { - super(route, browseService, dsoService, router); + super(route, browseService, dsoService, paginationService, router); } ngOnInit(): void { + const sortConfig = new SortOptions('default', SortDirection.ASC); this.startsWithType = StartsWithType.date; - this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, this.sortConfig)); + this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig)); + this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); + this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.subs.push( - observableCombineLatest( - this.route.params, - this.route.queryParams, - this.route.data, - (params, queryParams, data ) => { - return Object.assign({}, params, queryParams, data); + observableCombineLatest([this.route.params, this.route.queryParams, this.route.data, + this.currentPagination$, this.currentSort$]).pipe( + map(([routeParams, queryParams, data, currentPage, currentSort]) => { + return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort]; }) - .subscribe((params) => { - const metadataField = params.metadataField || this.defaultMetadataField; - this.browseId = params.id || this.defaultBrowseId; - this.startsWith = +params.startsWith || params.startsWith; - const searchOptions = browseParamsToOptions(params, Object.assign({}), this.sortConfig, this.browseId); - this.updatePageWithItems(searchOptions, this.value); - this.updateParent(params.scope); - this.updateStartsWithOptions(this.browseId, metadataField, params.scope); - })); + ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { + const metadataField = params.metadataField || this.defaultMetadataField; + this.browseId = params.id || this.defaultBrowseId; + this.startsWith = +params.startsWith || params.startsWith; + const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); + this.updatePageWithItems(searchOptions, this.value); + this.updateParent(params.scope); + this.updateStartsWithOptions(this.browseId, metadataField, params.scope); + })); } /** diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.html b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.html index 45f2ef3b2a..2321da0204 100644 --- a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.html +++ b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.html @@ -27,15 +27,13 @@ title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + browseId | translate, value: (value)? '"' + value + '"': ''} }}" parentname="{{(parent$ | async)?.payload?.name || ''}}" [objects$]="(items$ !== undefined)? items$ : browseEntries$" - [paginationConfig]="paginationConfig" - [sortConfig]="sortConfig" + [paginationConfig]="(currentPagination$ |async)" + [sortConfig]="(currentSort$ |async)" [type]="startsWithType" [startsWithOptions]="startsWithOptions" [enableArrows]="true" (prev)="goPrev()" - (next)="goNext()" - (pageSizeChange)="pageSizeChange($event)" - (sortDirectionChange)="sortDirectionChange($event)"> + (next)="goNext()"> diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts index faa75af2f2..60d2fa549b 100644 --- a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts +++ b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts @@ -14,7 +14,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model'; -import { SortDirection } from '../../core/cache/models/sort-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { Item } from '../../core/shared/item.model'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { Community } from '../../core/shared/community.model'; @@ -22,12 +22,16 @@ import { RouterMock } from '../../shared/mocks/router.mock'; import { BrowseEntry } from '../../core/shared/browse-entry.model'; import { VarDirective } from '../../shared/utils/var.directive'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; describe('BrowseByMetadataPageComponent', () => { let comp: BrowseByMetadataPageComponent; let fixture: ComponentFixture; let browseService: BrowseService; let route: ActivatedRoute; + let paginationService; const mockCommunity = Object.assign(new Community(), { id: 'test-uuid', @@ -82,6 +86,8 @@ describe('BrowseByMetadataPageComponent', () => { params: observableOf({}) }); + paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], @@ -90,6 +96,7 @@ describe('BrowseByMetadataPageComponent', () => { { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: BrowseService, useValue: mockBrowseService }, { provide: DSpaceObjectDataService, useValue: mockDsoService }, + { provide: PaginationService, useValue: paginationService }, { provide: Router, useValue: new RouterMock() } ], schemas: [NO_ERRORS_SCHEMA] @@ -133,18 +140,23 @@ describe('BrowseByMetadataPageComponent', () => { let result: BrowseEntrySearchOptions; beforeEach(() => { - const paramsWithPaginationAndScope = { - page: 5, - pageSize: 10, - sortDirection: SortDirection.ASC, - sortField: 'fake-field', + const paramsScope = { scope: 'fake-scope' }; + const paginationOptions = Object.assign(new PaginationComponentOptions(), { + currentPage: 5, + pageSize: 10, + }); + const sortOptions = { + direction: SortDirection.ASC, + field: 'fake-field', + }; - result = browseParamsToOptions(paramsWithPaginationAndScope, Object.assign({}), Object.assign({}), 'author'); + result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author'); }); it('should return BrowseEntrySearchOptions with the correct properties', () => { + expect(result.metadataDefinition).toEqual('author'); expect(result.pagination.currentPage).toEqual(5); expect(result.pagination.pageSize).toEqual(10); diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts index 3b67d2e3d0..f5adefc779 100644 --- a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts @@ -4,7 +4,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Params, Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { BrowseService } from '../../core/browse/browse.service'; import { BrowseEntry } from '../../core/shared/browse-entry.model'; @@ -15,6 +15,8 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { map } from 'rxjs/operators'; @Component({ selector: 'ds-browse-by-metadata-page', @@ -48,15 +50,20 @@ export class BrowseByMetadataPageComponent implements OnInit { * The pagination config used to display the values */ paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'browse-by-metadata-pagination', + id: 'bbm', currentPage: 1, pageSize: 20 }); /** - * The sorting config used to sort the values (defaults to Ascending) + * The pagination observable */ - sortConfig: SortOptions = new SortOptions('default', SortDirection.ASC); + currentPagination$: Observable; + + /** + * The sorting config observable + */ + currentSort$: Observable; /** * List of subscriptions @@ -100,23 +107,25 @@ export class BrowseByMetadataPageComponent implements OnInit { public constructor(protected route: ActivatedRoute, protected browseService: BrowseService, protected dsoService: DSpaceObjectDataService, + protected paginationService: PaginationService, protected router: Router) { } ngOnInit(): void { - this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, this.sortConfig)); + const sortConfig = new SortOptions('default', SortDirection.ASC); + this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig)); + this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); + this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.subs.push( - observableCombineLatest( - this.route.params, - this.route.queryParams, - (params, queryParams, ) => { - return Object.assign({}, params, queryParams); + observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe( + map(([routeParams, queryParams, currentPage, currentSort]) => { + return [Object.assign({}, routeParams, queryParams),currentPage,currentSort]; }) - .subscribe((params) => { + ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { this.browseId = params.id || this.defaultBrowseId; this.value = +params.value || params.value || ''; this.startsWith = +params.startsWith || params.startsWith; - const searchOptions = browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.browseId); + const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); if (isNotEmpty(this.value)) { this.updatePageWithItems(searchOptions, this.value); } else { @@ -158,6 +167,7 @@ export class BrowseByMetadataPageComponent implements OnInit { * @param value The value of the browse-entry to display items for */ updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) { + console.log('updatePAge', searchOptions); this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions); } @@ -203,32 +213,12 @@ export class BrowseByMetadataPageComponent implements OnInit { } } - /** - * Change the page size - * @param size - */ - pageSizeChange(size) { - this.router.navigate([], { - queryParams: Object.assign({ pageSize: size }), - queryParamsHandling: 'merge' - }); - } - - /** - * Change the sorting direction - * @param direction - */ - sortDirectionChange(direction) { - this.router.navigate([], { - queryParams: Object.assign({ sortDirection: direction }), - queryParamsHandling: 'merge' - }); - } - ngOnDestroy(): void { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + this.paginationService.clearPagination(this.paginationConfig.id); } + } /** @@ -244,20 +234,8 @@ export function browseParamsToOptions(params: any, metadata?: string): BrowseEntrySearchOptions { return new BrowseEntrySearchOptions( metadata, - Object.assign({}, - paginationConfig, - { - currentPage: +params.page || paginationConfig.currentPage, - pageSize: +params.pageSize || paginationConfig.pageSize - } - ), - Object.assign({}, - sortConfig, - { - direction: params.sortDirection || sortConfig.direction, - field: params.sortField || sortConfig.field - } - ), + paginationConfig, + sortConfig, +params.startsWith || params.startsWith, params.scope ); diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts index 4f7b2d5255..d44c667044 100644 --- a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts +++ b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts @@ -18,6 +18,11 @@ import { BrowseService } from '../../core/browse/browse.service'; import { RouterMock } from '../../shared/mocks/router.mock'; import { VarDirective } from '../../shared/utils/var.directive'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../core/data/request.models'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; describe('BrowseByTitlePageComponent', () => { let comp: BrowseByTitlePageComponent; @@ -61,6 +66,8 @@ describe('BrowseByTitlePageComponent', () => { data: observableOf({ metadata: 'title' }) }); + const paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], @@ -69,6 +76,7 @@ describe('BrowseByTitlePageComponent', () => { { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: BrowseService, useValue: mockBrowseService }, { provide: DSpaceObjectDataService, useValue: mockDsoService }, + { provide: PaginationService, useValue: paginationService }, { provide: Router, useValue: new RouterMock() } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts index 3b3c29c3fd..381684f9f0 100644 --- a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts +++ b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest } from 'rxjs'; import { Component } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Params, Router } from '@angular/router'; import { hasValue } from '../../shared/empty.util'; import { BrowseByMetadataPageComponent, @@ -11,6 +11,9 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv import { BrowseService } from '../../core/browse/browse.service'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { map } from 'rxjs/operators'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; @Component({ selector: 'ds-browse-by-title-page', @@ -26,26 +29,26 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { public constructor(protected route: ActivatedRoute, protected browseService: BrowseService, protected dsoService: DSpaceObjectDataService, + protected paginationService: PaginationService, protected router: Router) { - super(route, browseService, dsoService, router); + super(route, browseService, dsoService, paginationService, router); } ngOnInit(): void { - this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); - this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, this.sortConfig)); + const sortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig)); + this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); + this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.subs.push( - observableCombineLatest( - this.route.params, - this.route.queryParams, - this.route.data, - (params, queryParams, data ) => { - return Object.assign({}, params, queryParams, data); + observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe( + map(([routeParams, queryParams, currentPage, currentSort]) => { + return [Object.assign({}, routeParams, queryParams),currentPage,currentSort]; }) - .subscribe((params) => { - this.browseId = params.id || this.defaultBrowseId; - this.updatePageWithItems(browseParamsToOptions(params, this.paginationConfig, this.sortConfig, this.browseId), undefined); - this.updateParent(params.scope); - })); + ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { + this.browseId = params.id || this.defaultBrowseId; + this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined); + this.updateParent(params.scope); + })); this.updateStartsWithTextOptions(); } diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index ed0ad1f021..9d598a3b69 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -52,8 +52,7 @@ [config]="paginationConfig" [sortConfig]="sortConfig" [objects]="itemRD" - [hideGear]="true" - (paginationChange)="onPaginationChange($event)"> + [hideGear]="true"> this.collectionRD$.pipe( + const currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); + const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig); + + this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe( + switchMap(([currentPagination, currentSort ]) => this.collectionRD$.pipe( getFirstSucceededRemoteData(), map((rd) => rd.payload.id), switchMap((id: string) => { return this.searchService.search( new PaginatedSearchOptions({ scope: id, - pagination: dto.paginationConfig, - sort: dto.sortConfig, + pagination: currentPagination, + sort: currentSort, dsoTypes: [DSpaceObjectType.ITEM] })).pipe(toDSpaceObjectListRD()) as Observable>>; }), @@ -128,19 +132,9 @@ export class CollectionPageComponent implements OnInit { return isNotEmpty(object); } - onPaginationChange(event: PaginationChangeEvent) { - this.paginationConfig = Object.assign(new PaginationComponentOptions(), { - currentPage: event.pagination.currentPage || this.paginationConfig.currentPage, - pageSize: event.pagination.pageSize || this.paginationConfig.pageSize, - id: 'collection-page-pagination' - }); - this.sortConfig = Object.assign(new SortOptions('dc.date.accessioned', SortDirection.DESC), { - direction: event.sort.direction || this.sortConfig.direction, - field: event.sort.field || this.sortConfig.field - }); - this.paginationChanges$.next({ - paginationConfig: this.paginationConfig, - sortConfig: this.sortConfig - }); + ngOnDestroy(): void { + this.paginationService.clearPagination(this.paginationConfig.id); } + + } diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html index bf6ce7fd57..9928ebd18a 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html @@ -5,8 +5,7 @@ [config]="config" [sortConfig]="sortConfig" [objects]="subCollectionsRD" - [hideGear]="false" - (paginationChange)="onPaginationChange($event)"> + [hideGear]="false"> diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts index 13a91563c8..93a6c6fbb1 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -18,8 +18,13 @@ import { PageInfo } from '../../core/shared/page-info.model'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { of as observableOf } from 'rxjs'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { ThemeService } from '../../shared/theme-support/theme.service'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; describe('CommunityPageSubCollectionList Component', () => { let comp: CommunityPageSubCollectionListComponent; @@ -113,6 +118,8 @@ describe('CommunityPageSubCollectionList Component', () => { } }; + const paginationService = new PaginationServiceStub(); + themeService = getMockThemeService(); beforeEach(waitForAsync(() => { @@ -128,6 +135,7 @@ describe('CommunityPageSubCollectionList Component', () => { providers: [ { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: PaginationService, useValue: paginationService }, { provide: SelectableListService, useValue: {} }, { provide: ThemeService, useValue: themeService }, ], @@ -161,28 +169,4 @@ describe('CommunityPageSubCollectionList Component', () => { const subComHead = fixture.debugElement.queryAll(By.css('h2')); expect(subComHead.length).toEqual(0); }); - - it('should update list of collections on pagination change', () => { - subCollList = collections; - fixture.detectChanges(); - - const pagination = Object.create({ - pagination:{ - id: comp.pageId, - currentPage: 2, - pageSize: 5 - }, - sort: { - field: 'dc.title', - direction: 'ASC' - } - }); - comp.onPaginationChange(pagination); - fixture.detectChanges(); - - const collList = fixture.debugElement.queryAll(By.css('li')); - expect(collList.length).toEqual(2); - expect(collList[0].nativeElement.textContent).toContain('Collection 6'); - expect(collList[1].nativeElement.textContent).toContain('Collection 7'); - }); }); diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts index 261ae41aa2..adb4c32a32 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs'; import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; @@ -10,7 +10,8 @@ import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; -import { takeUntilCompletedRemoteData } from '../../core/shared/operators'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { switchMap } from 'rxjs/operators'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -29,7 +30,7 @@ export class CommunityPageSubCollectionListComponent implements OnInit { /** * The pagination id */ - pageId = 'community-collections-pagination'; + pageId = 'cmcl'; /** * The sorting configuration @@ -41,7 +42,10 @@ export class CommunityPageSubCollectionListComponent implements OnInit { */ subCollectionsRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); - constructor(private cds: CollectionDataService) {} + constructor(private cds: CollectionDataService, + private paginationService: PaginationService, + + ) {} ngOnInit(): void { this.config = new PaginationComponentOptions(); @@ -49,31 +53,31 @@ export class CommunityPageSubCollectionListComponent implements OnInit { this.config.pageSize = 5; this.config.currentPage = 1; this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); - this.updatePage(); + this.initPage(); } /** - * Called when one of the pagination settings is changed - * @param event The new pagination data + * Initialise the list of collections */ - onPaginationChange(event) { - this.config.currentPage = event.pagination.currentPage; - this.config.pageSize = event.pagination.pageSize; - this.sortConfig.field = event.sort.field; - this.sortConfig.direction = event.sort.direction; - this.updatePage(); - } + initPage() { + const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config); + const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig); - /** - * Update the list of collections - */ - updatePage() { - this.cds.findByParent(this.community.id,{ - currentPage: this.config.currentPage, - elementsPerPage: this.config.pageSize, - sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } - }).pipe(takeUntilCompletedRemoteData()).subscribe((results) => { + observableCombineLatest([pagination$, sort$]).pipe( + switchMap(([currentPagination, currentSort]) => { + return this.cds.findByParent(this.community.id, { + currentPage: currentPagination.currentPage, + elementsPerPage: currentPagination.pageSize, + sort: {field: currentSort.field, direction: currentSort.direction} + }); + }) + ).subscribe((results) => { this.subCollectionsRDObs.next(results); }); } + + ngOnDestroy(): void { + this.paginationService.clearPagination(this.config.id); + } + } diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html index 880ea9cc8e..2d14dce60a 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html @@ -5,8 +5,7 @@ [config]="config" [sortConfig]="sortConfig" [objects]="subCommunitiesRD" - [hideGear]="false" - (paginationChange)="onPaginationChange($event)"> + [hideGear]="false"> diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index 21ba1b28b0..e573259b63 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -18,8 +18,13 @@ import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { CommunityDataService } from '../../core/data/community-data.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { of as observableOf } from 'rxjs'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { ThemeService } from '../../shared/theme-support/theme.service'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; describe('CommunityPageSubCommunityListComponent Component', () => { let comp: CommunityPageSubCommunityListComponent; @@ -114,6 +119,8 @@ describe('CommunityPageSubCommunityListComponent Component', () => { } }; + const paginationService = new PaginationServiceStub(); + themeService = getMockThemeService(); beforeEach(waitForAsync(() => { @@ -129,6 +136,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => { providers: [ { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: PaginationService, useValue: paginationService }, { provide: SelectableListService, useValue: {} }, { provide: ThemeService, useValue: themeService }, ], @@ -163,28 +171,4 @@ describe('CommunityPageSubCommunityListComponent Component', () => { const subComHead = fixture.debugElement.queryAll(By.css('h2')); expect(subComHead.length).toEqual(0); }); - - it('should update list of sub-communities on pagination change', () => { - subCommList = subcommunities; - fixture.detectChanges(); - - const pagination = Object.create({ - pagination:{ - id: comp.pageId, - currentPage: 2, - pageSize: 5 - }, - sort: { - field: 'dc.title', - direction: 'ASC' - } - }); - comp.onPaginationChange(pagination); - fixture.detectChanges(); - - const collList = fixture.debugElement.queryAll(By.css('li')); - expect(collList.length).toEqual(2); - expect(collList[0].nativeElement.textContent).toContain('SubCommunity 6'); - expect(collList[1].nativeElement.textContent).toContain('SubCommunity 7'); - }); }); diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts index c9f72fbc04..2c30ede554 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; @@ -10,6 +10,8 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { takeUntilCompletedRemoteData } from '../../core/shared/operators'; +import { switchMap } from 'rxjs/operators'; +import { PaginationService } from '../../core/pagination/pagination.service'; @Component({ selector: 'ds-community-page-sub-community-list', @@ -31,7 +33,7 @@ export class CommunityPageSubCommunityListComponent implements OnInit { /** * The pagination id */ - pageId = 'community-subCommunities-pagination'; + pageId = 'cmscm'; /** * The sorting configuration @@ -43,7 +45,9 @@ export class CommunityPageSubCommunityListComponent implements OnInit { */ subCommunitiesRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); - constructor(private cds: CommunityDataService) { + constructor(private cds: CommunityDataService, + private paginationService: PaginationService + ) { } ngOnInit(): void { @@ -52,25 +56,29 @@ export class CommunityPageSubCommunityListComponent implements OnInit { this.config.pageSize = 5; this.config.currentPage = 1; this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); - this.updatePage(); - } - - /** - * Called when one of the pagination settings is changed - * @param event The new pagination data - */ - onPaginationChange(event) { - this.config.currentPage = event.pagination.currentPage; - this.config.pageSize = event.pagination.pageSize; - this.sortConfig.field = event.sort.field; - this.sortConfig.direction = event.sort.direction; - this.updatePage(); + this.initPage(); } /** * Update the list of sub-communities */ - updatePage() { + initPage() { + const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config); + const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig); + + observableCombineLatest([pagination$, sort$]).pipe( + switchMap(([currentPagination, currentSort]) => { + return this.cds.findByParent(this.community.id, { + currentPage: currentPagination.currentPage, + elementsPerPage: currentPagination.pageSize, + sort: { field: currentSort.field, direction: currentSort.direction } + }); + }) + ).subscribe((results) => { + this.subCommunitiesRDObs.next(results); + }); + + this.cds.findByParent(this.community.id, { currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize, @@ -79,4 +87,9 @@ export class CommunityPageSubCommunityListComponent implements OnInit { this.subCommunitiesRDObs.next(results); }); } + + ngOnDestroy(): void { + this.paginationService.clearPagination(this.config.id); + } + } diff --git a/src/app/+home-page/home-news/home-news.component.html b/src/app/+home-page/home-news/home-news.component.html index 812c38f798..6bee3cd76f 100644 --- a/src/app/+home-page/home-news/home-news.component.html +++ b/src/app/+home-page/home-news/home-news.component.html @@ -2,7 +2,7 @@
-

Welcome to the DSpace 7 Preview

+

DSpace 7

DSpace is the world leading open source repository platform that enables organisations to:

@@ -13,6 +13,8 @@
  • issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI
  • -

    Join an international community of leading institutions using DSpace.

    +

    Join an international community of leading institutions using DSpace. +

    diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.html b/src/app/+home-page/top-level-community-list/top-level-community-list.component.html index f318a04f38..dbea87a175 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.html +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.html @@ -6,8 +6,7 @@ [config]="config" [sortConfig]="sortConfig" [objects]="communitiesRD$ | async" - [hideGear]="true" - (paginationChange)="onPaginationChange($event)"> + [hideGear]="true"> diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts index 0daa0a0ae0..00408e4696 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts @@ -18,13 +18,19 @@ import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { CommunityDataService } from '../../core/data/community-data.service'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { of as observableOf } from 'rxjs'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { ThemeService } from '../../shared/theme-support/theme.service'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; describe('TopLevelCommunityList Component', () => { let comp: TopLevelCommunityListComponent; let fixture: ComponentFixture; let communityDataServiceStub: any; + let paginationService; let themeService; const topCommList = [Object.assign(new Community(), { @@ -104,6 +110,8 @@ describe('TopLevelCommunityList Component', () => { } }; + paginationService = new PaginationServiceStub(); + themeService = getMockThemeService(); beforeEach(waitForAsync(() => { @@ -119,6 +127,7 @@ describe('TopLevelCommunityList Component', () => { providers: [ { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: PaginationService, useValue: paginationService }, { provide: SelectableListService, useValue: {} }, { provide: ThemeService, useValue: themeService }, ], @@ -143,25 +152,4 @@ describe('TopLevelCommunityList Component', () => { expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4'); expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5'); }); - - it('should update list of top-communities on pagination change', () => { - const pagination = Object.create({ - pagination: { - id: comp.pageId, - currentPage: 2, - pageSize: 5 - }, - sort: { - field: 'dc.title', - direction: 'ASC' - } - }); - comp.onPaginationChange(pagination); - fixture.detectChanges(); - - const collList = fixture.debugElement.queryAll(By.css('li')); - expect(collList.length).toEqual(2); - expect(collList[0].nativeElement.textContent).toContain('TopCommunity 6'); - expect(collList[1].nativeElement.textContent).toContain('TopCommunity 7'); - }); }); diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index b089244008..5f6306649f 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; @@ -10,6 +10,8 @@ import { Community } from '../../core/shared/community.model'; import { fadeInOut } from '../../shared/animations/fade'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { hasValue } from '../../shared/empty.util'; +import { switchMap } from 'rxjs/operators'; +import { PaginationService } from '../../core/pagination/pagination.service'; /** * this component renders the Top-Level Community list @@ -36,7 +38,7 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy { /** * The pagination id */ - pageId = 'top-level-pagination'; + pageId = 'tl'; /** * The sorting configuration @@ -48,7 +50,8 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy { */ currentPageSubscription: Subscription; - constructor(private cds: CommunityDataService) { + constructor(private cds: CommunityDataService, + private paginationService: PaginationService) { this.config = new PaginationComponentOptions(); this.config.id = this.pageId; this.config.pageSize = 5; @@ -57,31 +60,26 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy { } ngOnInit() { - this.updatePage(); + this.initPage(); } - /** - * Called when one of the pagination settings is changed - * @param event The new pagination data - */ - onPaginationChange(event) { - this.config.currentPage = event.pagination.currentPage; - this.config.pageSize = event.pagination.pageSize; - this.sortConfig.field = event.sort.field; - this.sortConfig.direction = event.sort.direction; - this.updatePage(); - } /** * Update the list of top communities */ - updatePage() { - this.unsubscribe(); - this.currentPageSubscription = this.cds.findTop({ - currentPage: this.config.currentPage, - elementsPerPage: this.config.pageSize, - sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } - }).subscribe((results) => { + initPage() { + const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config); + const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig); + + this.currentPageSubscription = observableCombineLatest([pagination$, sort$]).pipe( + switchMap(([currentPagination, currentSort]) => { + return this.cds.findTop({ + currentPage: currentPagination.currentPage, + elementsPerPage: currentPagination.pageSize, + sort: {field: currentSort.field, direction: currentSort.direction} + }); + }) + ).subscribe((results) => { this.communitiesRD$.next(results); }); } @@ -100,5 +98,7 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy { */ ngOnDestroy() { this.unsubscribe(); + this.paginationService.clearPagination(this.config.id); } + } diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html index 9197b89796..8f0d83bd1f 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html @@ -4,9 +4,7 @@ [hidePaginationDetail]="true" [paginationOptions]="options" [pageInfoState]="(objectsRD$ | async)?.payload" - [collectionSize]="(objectsRD$ | async)?.payload?.totalElements" - [disableRouteParameterUpdate]="true" - (pageChange)="switchPage($event)"> + [collectionSize]="(objectsRD$ | async)?.payload?.totalElements">
    { let comp: PaginatedDragAndDropBitstreamListComponent; @@ -24,6 +29,7 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => { let bundleService: BundleDataService; let objectValuesPipe: ObjectValuesPipe; let requestService: RequestService; + let paginationService; const columnSizes = new ResponsiveTableSizes([ new ResponsiveColumnSizes(2, 2, 3, 4, 4), @@ -109,6 +115,8 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => { hasByHref$: observableOf(true) }); + paginationService = new PaginationServiceStub(); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective], @@ -116,7 +124,8 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => { { provide: ObjectUpdatesService, useValue: objectUpdatesService }, { provide: BundleDataService, useValue: bundleService }, { provide: ObjectValuesPipe, useValue: objectValuesPipe }, - { provide: RequestService, useValue: requestService } + { provide: RequestService, useValue: requestService }, + { provide: PaginationService, useValue: paginationService } ], schemas: [ NO_ERRORS_SCHEMA ] diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts index bb77bfc0c4..f3f00abf92 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts @@ -10,6 +10,8 @@ import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-siz import { followLink } from '../../../../../shared/utils/follow-link-config.model'; import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; import { RequestService } from '../../../../../core/data/request.service'; +import { PaginationService } from '../../../../../core/pagination/pagination.service'; +import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; @Component({ selector: 'ds-paginated-drag-and-drop-bitstream-list', @@ -37,8 +39,9 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate protected elRef: ElementRef, protected objectValuesPipe: ObjectValuesPipe, protected bundleService: BundleDataService, + protected paginationService: PaginationService, protected requestService: RequestService) { - super(objectUpdatesService, elRef, objectValuesPipe); + super(objectUpdatesService, elRef, objectValuesPipe, paginationService); } ngOnInit() { @@ -50,8 +53,8 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate */ initializeObjectsRD(): void { this.objectsRD$ = this.currentPage$.pipe( - switchMap((page: number) => { - const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}); + switchMap((page: PaginationComponentOptions) => { + const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, page)}); return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe( switchMap((href) => this.requestService.hasByHref$(href)), switchMap(() => this.bundleService.getBitstreams( diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html index 00218b66d1..bc1c63cc32 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html @@ -8,8 +8,7 @@ [paginationOptions]="originalOptions" [pageInfoState]="originals" [collectionSize]="originals?.totalElements" - [disableRouteParameterUpdate]="true" - (pageChange)="switchOriginalPage($event)"> + [retainScrollPosition]="true">
    @@ -34,7 +33,7 @@
    - + {{"item.page.filesection.download" | translate}}
    @@ -51,8 +50,7 @@ [paginationOptions]="licenseOptions" [pageInfoState]="licenses" [collectionSize]="licenses?.totalElements" - [disableRouteParameterUpdate]="true" - (pageChange)="switchLicensePage($event)"> + [retainScrollPosition]="true">
    @@ -76,7 +74,7 @@
    - + {{"item.page.filesection.download" | translate}}
    diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts index 1773a0fe74..9b225632df 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts @@ -16,6 +16,11 @@ import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock'; import { By } from '@angular/platform-browser'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../../core/data/request.models'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; describe('FullFileSectionComponent', () => { let comp: FullFileSectionComponent; @@ -52,6 +57,8 @@ describe('FullFileSectionComponent', () => { findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream, mockBitstream, mockBitstream])) }); + const paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -64,7 +71,8 @@ describe('FullFileSectionComponent', () => { declarations: [FullFileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent], providers: [ { provide: BitstreamDataService, useValue: bitstreamDataService }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() } + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: PaginationService, useValue: paginationService } ], schemas: [NO_ERRORS_SCHEMA] @@ -82,39 +90,5 @@ describe('FullFileSectionComponent', () => { const fileSection = fixture.debugElement.queryAll(By.css('.file-section')); expect(fileSection.length).toEqual(6); }); - - describe('when we press the pageChange button for original bundle', () => { - beforeEach(() => { - comp.switchOriginalPage(2); - fixture.detectChanges(); - }); - - it('should give the value to the currentpage', () => { - expect(comp.originalOptions.currentPage).toBe(2); - }); - it('should call the next function on the originalCurrentPage', (done) => { - comp.originalCurrentPage$.subscribe((event) => { - expect(event).toEqual(2); - done(); - }); - }); - }); - - describe('when we press the pageChange button for license bundle', () => { - beforeEach(() => { - comp.switchLicensePage(2); - fixture.detectChanges(); - }); - - it('should give the value to the currentpage', () => { - expect(comp.licenseOptions.currentPage).toBe(2); - }); - it('should call the next function on the licenseCurrentPage', (done) => { - comp.licenseCurrentPage$.subscribe((event) => { - expect(event).toEqual(2); - done(); - }); - }); - }); }); }); diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts index ca3d5e65c7..214484120e 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { Observable } from 'rxjs'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; @@ -13,6 +13,7 @@ import { switchMap, tap } from 'rxjs/operators'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { hasValue, isEmpty } from '../../../../shared/empty.util'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; /** * This component renders the file section of the item @@ -35,23 +36,22 @@ export class FullFileSectionComponent extends FileSectionComponent implements On pageSize = 5; originalOptions = Object.assign(new PaginationComponentOptions(), { - id: 'original-bitstreams-options', + id: 'obo', currentPage: 1, pageSize: this.pageSize }); - originalCurrentPage$ = new BehaviorSubject(1); licenseOptions = Object.assign(new PaginationComponentOptions(), { - id: 'license-bitstreams-options', + id: 'lbo', currentPage: 1, pageSize: this.pageSize }); - licenseCurrentPage$ = new BehaviorSubject(1); constructor( bitstreamDataService: BitstreamDataService, protected notificationsService: NotificationsService, - protected translateService: TranslateService + protected translateService: TranslateService, + protected paginationService: PaginationService ) { super(bitstreamDataService, notificationsService, translateService); } @@ -61,11 +61,11 @@ export class FullFileSectionComponent extends FileSectionComponent implements On } initialize(): void { - this.originals$ = this.originalCurrentPage$.pipe( - switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName( + this.originals$ = this.paginationService.getCurrentPagination(this.originalOptions.id, this.originalOptions).pipe( + switchMap((options: PaginationComponentOptions) => this.bitstreamDataService.findAllByItemAndBundleName( this.item, 'ORIGINAL', - {elementsPerPage: this.pageSize, currentPage: pageNumber}, + {elementsPerPage: options.pageSize, currentPage: options.currentPage}, true, true, followLink('format') @@ -78,11 +78,11 @@ export class FullFileSectionComponent extends FileSectionComponent implements On ) ); - this.licenses$ = this.licenseCurrentPage$.pipe( - switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName( + this.licenses$ = this.paginationService.getCurrentPagination(this.licenseOptions.id, this.licenseOptions).pipe( + switchMap((options: PaginationComponentOptions) => this.bitstreamDataService.findAllByItemAndBundleName( this.item, 'LICENSE', - {elementsPerPage: this.pageSize, currentPage: pageNumber}, + {elementsPerPage: options.pageSize, currentPage: options.currentPage}, true, true, followLink('format') @@ -97,25 +97,13 @@ export class FullFileSectionComponent extends FileSectionComponent implements On } - /** - * Update the current page for the original bundle bitstreams - * @param page - */ - switchOriginalPage(page: number) { - this.originalOptions.currentPage = page; - this.originalCurrentPage$.next(page); - } - - /** - * Update the current page for the license bundle bitstreams - * @param page - */ - switchLicensePage(page: number) { - this.licenseOptions.currentPage = page; - this.licenseCurrentPage$.next(page); - } - hasValuesInBundle(bundle: PaginatedList) { return hasValue(bundle) && !isEmpty(bundle.page); } + + ngOnDestroy(): void { + this.paginationService.clearPagination(this.originalOptions.id); + this.paginationService.clearPagination(this.licenseOptions.id); + } + } diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.html b/src/app/+item-page/simple/field-components/file-section/file-section.component.html index 1fdee6dc4d..0fa5daa012 100644 --- a/src/app/+item-page/simple/field-components/file-section/file-section.component.html +++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.html @@ -1,7 +1,7 @@
    - + {{file?.name}} ({{(file?.sizeBytes) | dsFileSize }}) diff --git a/src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts b/src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts index 4154a09f15..87a2f8a9dd 100644 --- a/src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts +++ b/src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts @@ -9,6 +9,8 @@ import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; import { RoleServiceMock } from '../shared/mocks/role-service.mock'; import { cold, hot } from 'jasmine-marbles'; import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type'; +import { PaginationServiceStub } from '../shared/testing/pagination-service.stub'; +import { PaginationService } from '../core/pagination/pagination.service'; describe('MyDSpaceConfigurationService', () => { let service: MyDSpaceConfigurationService; @@ -34,12 +36,13 @@ describe('MyDSpaceConfigurationService', () => { getRouteDataValue: observableOf({}) }); + const paginationService = new PaginationServiceStub(); const activatedRoute: any = new ActivatedRouteStub(); const roleService: any = new RoleServiceMock(); beforeEach(() => { - service = new MyDSpaceConfigurationService(roleService, spy, activatedRoute); + service = new MyDSpaceConfigurationService(roleService, spy, paginationService as any, activatedRoute); }); describe('when the scope is called', () => { @@ -102,25 +105,19 @@ describe('MyDSpaceConfigurationService', () => { describe('when getCurrentSort is called', () => { beforeEach(() => { - service.getCurrentSort({} as any); + service.getCurrentSort('page-id', defaults.sort); }); - it('should call getQueryParameterValue on the routeService with parameter name \'sortDirection\'', () => { - expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortDirection'); - }); - it('should call getQueryParameterValue on the routeService with parameter name \'sortField\'', () => { - expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortField'); + it('should call getCurrentSort on the paginationService with the provided id and sort options', () => { + expect((service as any).paginationService.getCurrentSort).toHaveBeenCalledWith('page-id', defaults.sort); }); }); describe('when getCurrentPagination is called', () => { beforeEach(() => { - service.getCurrentPagination({ currentPage: 1, pageSize: 10 } as any); + service.getCurrentPagination('page-id', defaults.pagination); }); - it('should call getQueryParameterValue on the routeService with parameter name \'page\'', () => { - expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('page'); - }); - it('should call getQueryParameterValue on the routeService with parameter name \'pageSize\'', () => { - expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('pageSize'); + it('should call getCurrentPagination on the paginationService with the provided id and sort options', () => { + expect((service as any).paginationService.getCurrentPagination).toHaveBeenCalledWith('page-id', defaults.pagination); }); }); @@ -152,7 +149,7 @@ describe('MyDSpaceConfigurationService', () => { describe('when subscribeToPaginatedSearchOptions is called', () => { beforeEach(() => { - (service as any).subscribeToPaginatedSearchOptions(defaults); + (service as any).subscribeToPaginatedSearchOptions('id', defaults); }); it('should call all getters it needs', () => { expect(service.getCurrentPagination).toHaveBeenCalled(); diff --git a/src/app/+my-dspace-page/my-dspace-configuration.service.ts b/src/app/+my-dspace-page/my-dspace-configuration.service.ts index 58946c9c16..82f76eb776 100644 --- a/src/app/+my-dspace-page/my-dspace-configuration.service.ts +++ b/src/app/+my-dspace-page/my-dspace-configuration.service.ts @@ -11,6 +11,7 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { RouteService } from '../core/services/route.service'; +import { PaginationService } from '../core/pagination/pagination.service'; /** * Service that performs all actions that have to do with the current mydspace configuration @@ -55,13 +56,15 @@ export class MyDSpaceConfigurationService extends SearchConfigurationService { * * @param {roleService} roleService * @param {RouteService} routeService + * @param {PaginationService} paginationService * @param {ActivatedRoute} route */ constructor(protected roleService: RoleService, protected routeService: RouteService, + protected paginationService: PaginationService, protected route: ActivatedRoute) { - super(routeService, route); + super(routeService, paginationService, route); // override parent class initialization this._defaults = null; diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html index 1a2e8e1e13..7ef02a76cf 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -52,8 +52,7 @@ [pageInfoState]="pageInfoState$" [collectionSize]="(pageInfoState$ | async)?.totalElements" [hideGear]="true" - [hidePagerWhenSinglePage]="true" - (pageChange)="onPageChange($event)"> + [hidePagerWhenSinglePage]="true">
    diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts index 2fb955bb02..bcf7e8f1d9 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts @@ -25,6 +25,8 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser import { RouterStub } from '../../shared/testing/router.stub'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { RequestService } from '../../core/data/request.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; describe('EPeopleRegistryComponent', () => { let component: EPeopleRegistryComponent; @@ -37,6 +39,8 @@ describe('EPeopleRegistryComponent', () => { let authorizationService: AuthorizationDataService; let modalService; + let paginationService; + beforeEach(waitForAsync(() => { mockEPeople = [EPersonMock, EPersonMock2]; ePersonDataServiceStub = { @@ -115,6 +119,8 @@ describe('EPeopleRegistryComponent', () => { }); builderService = getMockFormBuilderService(); translateService = getMockTranslateService(); + + paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ @@ -131,7 +137,8 @@ describe('EPeopleRegistryComponent', () => { { provide: AuthorizationDataService, useValue: authorizationService }, { provide: FormBuilderService, useValue: builderService }, { provide: Router, useValue: new RouterStub() }, - { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) } + { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }, + { provide: PaginationService, useValue: paginationService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.ts b/src/app/access-control/epeople-registry/epeople-registry.component.ts index f76c56853c..b99304d037 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts @@ -4,7 +4,7 @@ import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; import { map, switchMap, take } from 'rxjs/operators'; -import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model'; +import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPerson } from '../../core/eperson/models/eperson.model'; @@ -14,15 +14,13 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c import { EpersonDtoModel } from '../../core/eperson/models/eperson-dto.model'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { - getFirstCompletedRemoteData, - getAllSucceededRemoteData -} from '../../core/shared/operators'; +import { getAllSucceededRemoteData, getFirstCompletedRemoteData } from '../../core/shared/operators'; import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { RequestService } from '../../core/data/request.service'; import { PageInfo } from '../../core/shared/page-info.model'; import { NoContent } from '../../core/shared/NoContent.model'; +import { PaginationService } from '../../core/pagination/pagination.service'; @Component({ selector: 'ds-epeople-registry', @@ -60,7 +58,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { * Pagination config used to display the list of epeople */ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'epeople-list-pagination', + id: 'elp', pageSize: 5, currentPage: 1 }); @@ -78,9 +76,9 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { currentSearchScope: string; /** - * The subscription for the search method + * FindListOptions */ - searchSub: Subscription; + findListOptionsSub: Subscription; /** * List of subscriptions @@ -94,6 +92,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { private formBuilder: FormBuilder, private router: Router, private modalService: NgbModal, + private paginationService: PaginationService, public requestService: RequestService) { this.currentSearchQuery = ''; this.currentSearchScope = 'metadata'; @@ -113,7 +112,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { initialisePage() { this.searching$.next(true); this.isEPersonFormShown = false; - this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery }); + this.search({scope: this.currentSearchScope, query: this.currentSearchQuery}); this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { if (eperson != null && eperson.id) { this.isEPersonFormShown = true; @@ -139,56 +138,51 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { return [epeople]; } })).subscribe((value: PaginatedList) => { - this.searching$.next(false); - this.ePeopleDto$.next(value); - this.pageInfoState$.next(value.pageInfo); + this.searching$.next(false);this.ePeopleDto$.next(value); + this.pageInfoState$.next(value.pageInfo); })); } - /** - * Event triggered when the user changes page - * @param event - */ - onPageChange(event) { - if (this.config.currentPage !== event) { - this.config.currentPage = event; - this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery }); - } - } - /** * Search in the EPeople by metadata (default) or email * @param data Contains scope and query param */ search(data: any) { this.searching$.next(true); - const query: string = data.query; - const scope: string = data.scope; - if (query != null && this.currentSearchQuery !== query) { - this.router.navigateByUrl(this.epersonService.getEPeoplePageRouterLink()); - this.currentSearchQuery = query; - this.config.currentPage = 1; + if (hasValue(this.findListOptionsSub)) { + this.findListOptionsSub.unsubscribe(); } - if (scope != null && this.currentSearchScope !== scope) { - this.router.navigateByUrl(this.epersonService.getEPeoplePageRouterLink()); - this.currentSearchScope = scope; - this.config.currentPage = 1; - } - if (hasValue(this.searchSub)) { - this.searchSub.unsubscribe(); - this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub); - } - this.searchSub = this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { - currentPage: this.config.currentPage, - elementsPerPage: this.config.pageSize - }).pipe( + this.findListOptionsSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( + switchMap((findListOptions) => { + const query: string = data.query; + const scope: string = data.scope; + if (query != null && this.currentSearchQuery !== query) { + this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], { + queryParamsHandling: 'merge' + }); + this.currentSearchQuery = query; + this.paginationService.resetPage(this.config.id); + } + if (scope != null && this.currentSearchScope !== scope) { + this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], { + queryParamsHandling: 'merge' + }); + this.currentSearchScope = scope; + this.paginationService.resetPage(this.config.id); + + } + return this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + currentPage: findListOptions.currentPage, + elementsPerPage: findListOptions.pageSize + }); + } + ), getAllSucceededRemoteData(), ).subscribe((peopleRD) => { this.ePeople$.next(peopleRD.payload); this.pageInfoState$.next(peopleRD.payload.pageInfo); } ); - this.subs.push(this.searchSub); } /** @@ -243,7 +237,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { if (hasValue(ePerson.id)) { this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => { if (restResponse.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name })); + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name})); this.reset(); } else { this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); @@ -260,8 +254,10 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.cleanupSubscribes(); + this.paginationService.clearPagination(this.config.id); } + cleanupSubscribes() { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } @@ -283,7 +279,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.searchForm.patchValue({ query: '', }); - this.search({ query: '' }); + this.search({query: ''}); } /** diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index 454dad0018..832f4f6ce5 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -26,6 +26,8 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati import { GroupDataService } from '../../../core/eperson/group-data.service'; import { createPaginatedList } from '../../../shared/testing/utils.test'; import { RequestService } from '../../../core/data/request.service'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; describe('EPersonFormComponent', () => { let component: EPersonFormComponent; @@ -38,6 +40,10 @@ describe('EPersonFormComponent', () => { let authorizationService: AuthorizationDataService; let groupsDataService: GroupDataService; + let paginationService; + + + beforeEach(waitForAsync(() => { mockEPeople = [EPersonMock, EPersonMock2]; ePersonDataServiceStub = { @@ -104,6 +110,8 @@ describe('EPersonFormComponent', () => { findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), getGroupRegistryRouterLink: '' }); + + paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ @@ -121,6 +129,7 @@ describe('EPersonFormComponent', () => { { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: PaginationService, useValue: paginationService }, { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index 285cbbefa6..11c117ef55 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -7,7 +7,7 @@ import { DynamicInputModel } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; -import { combineLatest, Observable, of, Subscription } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { switchMap, take } from 'rxjs/operators'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; @@ -16,9 +16,9 @@ import { GroupDataService } from '../../../core/eperson/group-data.service'; import { EPerson } from '../../../core/eperson/models/eperson.model'; import { Group } from '../../../core/eperson/models/group.model'; import { - getRemoteDataPayload, + getFirstCompletedRemoteData, getFirstSucceededRemoteData, - getFirstCompletedRemoteData + getRemoteDataPayload } from '../../../core/shared/operators'; import { hasValue } from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; @@ -31,6 +31,7 @@ import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/c import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { RequestService } from '../../../core/data/request.service'; import { NoContent } from '../../../core/shared/NoContent.model'; +import { PaginationService } from '../../../core/pagination/pagination.service'; @Component({ selector: 'ds-eperson-form', @@ -118,7 +119,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Observable whether or not the admin is allowed to reset the EPerson's password * TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false) */ - canReset$: Observable = of(false); + canReset$: Observable = observableOf(false); /** * Observable whether or not the admin is allowed to delete the EPerson @@ -144,7 +145,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Pagination config used to display the list of groups */ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'groups-ePersonMemberOf-list-pagination', + id: 'gem', pageSize: 5, currentPage: 1 }); @@ -167,6 +168,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { private authService: AuthService, private authorizationService: AuthorizationDataService, private modalService: NgbModal, + private paginationService: PaginationService, public requestService: RequestService) { this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { this.epersonInitial = eperson; @@ -184,13 +186,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * This method will initialise the page */ initialisePage() { - combineLatest( - this.translateService.get(`${this.messagePrefix}.firstName`), - this.translateService.get(`${this.messagePrefix}.lastName`), - this.translateService.get(`${this.messagePrefix}.email`), - this.translateService.get(`${this.messagePrefix}.canLogIn`), - this.translateService.get(`${this.messagePrefix}.requireCertificate`), - this.translateService.get(`${this.messagePrefix}.emailHint`), + observableCombineLatest( + this.translateService.get(`${this.messagePrefix}.firstName`), + this.translateService.get(`${this.messagePrefix}.lastName`), + this.translateService.get(`${this.messagePrefix}.email`), + this.translateService.get(`${this.messagePrefix}.canLogIn`), + this.translateService.get(`${this.messagePrefix}.requireCertificate`), + this.translateService.get(`${this.messagePrefix}.emailHint`), ).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => { this.firstName = new DynamicInputModel({ id: 'firstName', @@ -222,19 +224,19 @@ export class EPersonFormComponent implements OnInit, OnDestroy { hint: emailHint }); this.canLogIn = new DynamicCheckboxModel( - { - id: 'canLogIn', - label: canLogIn, - name: 'canLogIn', - value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true) - }); + { + id: 'canLogIn', + label: canLogIn, + name: 'canLogIn', + value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true) + }); this.requireCertificate = new DynamicCheckboxModel( - { - id: 'requireCertificate', - label: requireCertificate, - name: 'requireCertificate', - value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false) - }); + { + id: 'requireCertificate', + label: requireCertificate, + name: 'requireCertificate', + value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false) + }); this.formModel = [ this.firstName, this.lastName, @@ -258,11 +260,29 @@ export class EPersonFormComponent implements OnInit, OnDestroy { requireCertificate: eperson != null ? eperson.requireCertificate : false }); })); - this.canImpersonate$ = this.epersonService.getActiveEPerson().pipe( - switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, hasValue(eperson) ? eperson.self : undefined)) + + const activeEPerson$ = this.epersonService.getActiveEPerson(); + + this.groups = activeEPerson$.pipe( + switchMap((eperson) => { + return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, { + currentPage: 1, + elementsPerPage: this.config.pageSize + })]); + }), + switchMap(([eperson, findListOptions]) => { + if (eperson != null) { + return this.groupsDataService.findAllByHref(eperson._links.groups.href, findListOptions); + } + return observableOf(undefined); + }) ); - this.canDelete$ = this.epersonService.getActiveEPerson().pipe( - switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)) + + this.canImpersonate$ = activeEPerson$.pipe( + switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, hasValue(eperson) ? eperson.self : undefined)) + ); + this.canDelete$ = activeEPerson$.pipe( + switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)) ); }); } @@ -322,10 +342,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy { getFirstCompletedRemoteData() ).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name })); + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', {name: ePersonToCreate.name})); this.submitForm.emit(ePersonToCreate); } else { - this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name })); + this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', {name: ePersonToCreate.name})); this.cancelForm.emit(); } }); @@ -361,10 +381,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy { const response = this.epersonService.updateEPerson(editedEperson); response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name })); + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', {name: editedEperson.name})); this.submitForm.emit(editedEperson); } else { - this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name })); + this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', {name: editedEperson.name})); this.cancelForm.emit(); } }); @@ -469,8 +489,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.onCancel(); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + this.paginationService.clearPagination(this.config.id); } + /** * This method will ensure that the page gets reset and that the cache is cleared */ diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html index f54936148c..51282b49c0 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html @@ -32,8 +32,7 @@ [pageInfoState]="(ePeopleSearchDtos | async)" [collectionSize]="(ePeopleSearchDtos | async)?.totalElements" [hideGear]="true" - [hidePagerWhenSinglePage]="true" - (pageChange)="onPageChangeSearch($event)"> + [hidePagerWhenSinglePage]="true">
    @@ -86,8 +85,7 @@ [pageInfoState]="(ePeopleMembersOfGroupDtos | async)" [collectionSize]="(ePeopleMembersOfGroupDtos | async)?.totalElements" [hideGear]="true" - [hidePagerWhenSinglePage]="true" - (pageChange)="onPageChange($event)"> + [hidePagerWhenSinglePage]="true">
    diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts index 20419fb49d..0b19b17100 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts @@ -26,6 +26,8 @@ import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { RouterMock } from '../../../../shared/mocks/router.mock'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; describe('MembersListComponent', () => { let component: MembersListComponent; @@ -39,6 +41,7 @@ describe('MembersListComponent', () => { let allGroups; let epersonMembers; let subgroupMembers; + let paginationService; beforeEach(waitForAsync(() => { activeGroup = GroupMock; @@ -113,6 +116,8 @@ describe('MembersListComponent', () => { }; builderService = getMockFormBuilderService(); translateService = getMockTranslateService(); + + paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ @@ -129,6 +134,7 @@ describe('MembersListComponent', () => { { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: FormBuilderService, useValue: builderService }, { provide: Router, useValue: new RouterMock() }, + { provide: PaginationService, useValue: paginationService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts index 6513881fbf..54d144da51 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts @@ -18,13 +18,13 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { EPerson } from '../../../../core/eperson/models/eperson.model'; import { Group } from '../../../../core/eperson/models/group.model'; import { - getRemoteDataPayload, getFirstSucceededRemoteData, - getFirstCompletedRemoteData, getAllCompletedRemoteData + getFirstCompletedRemoteData, getAllCompletedRemoteData, getRemoteDataPayload } from '../../../../core/shared/operators'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import {EpersonDtoModel} from '../../../../core/eperson/models/eperson-dto.model'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; /** * Keys to keep track of specific subscriptions @@ -60,7 +60,7 @@ export class MembersListComponent implements OnInit, OnDestroy { * Pagination config used to display the list of EPeople that are result of EPeople search */ configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'search-members-list-pagination', + id: 'sml', pageSize: 5, currentPage: 1 }); @@ -68,7 +68,7 @@ export class MembersListComponent implements OnInit, OnDestroy { * Pagination config used to display the list of EPerson Membes of active group being edited */ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'members-list-pagination', + id: 'ml', pageSize: 5, currentPage: 1 }); @@ -91,11 +91,15 @@ export class MembersListComponent implements OnInit, OnDestroy { // current active group being edited groupBeingEdited: Group; + paginationSub: Subscription; + + constructor(private groupDataService: GroupDataService, public ePersonDataService: EPersonDataService, private translateService: TranslateService, private notificationsService: NotificationsService, private formBuilder: FormBuilder, + private paginationService: PaginationService, private router: Router) { this.currentSearchQuery = ''; this.currentSearchScope = 'metadata'; @@ -114,23 +118,6 @@ export class MembersListComponent implements OnInit, OnDestroy { })); } - /** - * Event triggered when the user changes page on search result - * @param event - */ - onPageChangeSearch(event) { - this.configSearch.currentPage = event; - this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery }); - } - - /** - * Event triggered when the user changes page on EPerson embers of active group - * @param event - */ - onPageChange(event) { - this.retrieveMembers(event); - } - /** * Retrieve the EPersons that are members of the group * @@ -139,10 +126,15 @@ export class MembersListComponent implements OnInit, OnDestroy { */ private retrieveMembers(page: number) { this.unsubFrom(SubKey.MembersDTO); - this.subs.set(SubKey.MembersDTO, this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, { - currentPage: page, - elementsPerPage: this.config.pageSize - }).pipe( + this.subs.set(SubKey.MembersDTO, + this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( + switchMap((currentPagination) => { + return this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, { + currentPage: currentPagination.currentPage, + elementsPerPage: currentPagination.pageSize + } + ); + }), getAllCompletedRemoteData(), map((rd: RemoteData) => { if (rd.hasFailed) { @@ -245,26 +237,34 @@ export class MembersListComponent implements OnInit, OnDestroy { * @param data Contains scope and query param */ search(data: any) { - const query: string = data.query; - const scope: string = data.scope; - if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) { - this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); - this.currentSearchQuery = query; - this.configSearch.currentPage = 1; - } - if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) { - this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); - this.currentSearchScope = scope; - this.configSearch.currentPage = 1; - } - this.searchDone = true; - this.unsubFrom(SubKey.SearchResultsDTO); this.subs.set(SubKey.SearchResultsDTO, - this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { - currentPage: this.configSearch.currentPage, - elementsPerPage: this.configSearch.pageSize - }, false).pipe( + this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe( + switchMap((paginationOptions) => { + + const query: string = data.query; + const scope: string = data.scope; + if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) { + this.router.navigate([], { + queryParamsHandling: 'merge' + }); + this.currentSearchQuery = query; + this.paginationService.resetPage(this.configSearch.id); + } + if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) { + this.router.navigate([], { + queryParamsHandling: 'merge' + }); + this.currentSearchScope = scope; + this.paginationService.resetPage(this.configSearch.id); + } + this.searchDone = true; + + return this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + currentPage: paginationOptions.currentPage, + elementsPerPage: paginationOptions.pageSize + }); + }), getAllCompletedRemoteData(), map((rd: RemoteData) => { if (rd.hasFailed) { @@ -300,6 +300,8 @@ export class MembersListComponent implements OnInit, OnDestroy { for (const key of this.subs.keys()) { this.unsubFrom(key); } + this.paginationService.clearPagination(this.config.id); + this.paginationService.clearPagination(this.configSearch.id); } /** diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html index 293d9e469a..5b87045e78 100644 --- a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html @@ -29,8 +29,7 @@ [pageInfoState]="(searchResults$ | async)?.payload" [collectionSize]="(searchResults$ | async)?.payload?.totalElements" [hideGear]="true" - [hidePagerWhenSinglePage]="true" - (pageChange)="onPageChangeSearch($event)"> + [hidePagerWhenSinglePage]="true">
    @@ -83,8 +82,7 @@ [pageInfoState]="(subGroups$ | async)?.payload" [collectionSize]="(subGroups$ | async)?.payload?.totalElements" [hideGear]="true" - [hidePagerWhenSinglePage]="true" - (pageChange)="onPageChange($event)"> + [hidePagerWhenSinglePage]="true">
    diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts index e839d77a6a..bee5126e09 100644 --- a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts @@ -35,6 +35,8 @@ import { getMockTranslateService } from '../../../../shared/mocks/translate.serv import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { map } from 'rxjs/operators'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; describe('SubgroupsListComponent', () => { let component: SubgroupsListComponent; @@ -47,6 +49,7 @@ describe('SubgroupsListComponent', () => { let subgroups; let allGroups; let routerStub; + let paginationService; beforeEach(waitForAsync(() => { activeGroup = GroupMock; @@ -100,6 +103,8 @@ describe('SubgroupsListComponent', () => { routerStub = new RouterMock(); builderService = getMockFormBuilderService(); translateService = getMockTranslateService(); + + paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ @@ -115,6 +120,7 @@ describe('SubgroupsListComponent', () => { { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: FormBuilderService, useValue: builderService }, { provide: Router, useValue: routerStub }, + { provide: PaginationService, useValue: paginationService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index d9f03963d6..6d8285f10b 100644 --- a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -2,20 +2,21 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs'; -import { map, mergeMap, take } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; +import { map, mergeMap, switchMap, take } from 'rxjs/operators'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { Group } from '../../../../core/eperson/models/group.model'; import { - getRemoteDataPayload, + getFirstCompletedRemoteData, getFirstSucceededRemoteData, - getFirstCompletedRemoteData + getRemoteDataPayload } from '../../../../core/shared/operators'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { NoContent } from '../../../../core/shared/NoContent.model'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; /** * Keys to keep track of specific subscriptions @@ -56,7 +57,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { * Pagination config used to display the list of groups that are result of groups search */ configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'search-subgroups-list-pagination', + id: 'ssgl', pageSize: 5, currentPage: 1 }); @@ -64,7 +65,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { * Pagination config used to display the list of subgroups of currently active group being edited */ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'subgroups-list-pagination', + id: 'sgl', pageSize: 5, currentPage: 1 }); @@ -85,6 +86,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { private translateService: TranslateService, private notificationsService: NotificationsService, private formBuilder: FormBuilder, + private paginationService: PaginationService, private router: Router) { this.currentSearchQuery = ''; } @@ -96,42 +98,27 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { if (activeGroup != null) { this.groupBeingEdited = activeGroup; - this.retrieveSubGroups(this.config.currentPage); + this.retrieveSubGroups(); } })); } - /** - * Event triggered when the user changes page on search result - * @param event - */ - onPageChangeSearch(event) { - this.configSearch.currentPage = event; - this.search({ query: this.currentSearchQuery }); - } - - /** - * Event triggered when the user changes page on subgroups of active group - * @param event - */ - onPageChange(event) { - this.retrieveSubGroups(event); - } - /** * Retrieve the Subgroups that are members of the group * * @param page the number of the page to retrieve * @private */ - private retrieveSubGroups(page: number) { + private retrieveSubGroups() { this.unsubFrom(SubKey.Members); this.subs.set( SubKey.Members, - this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, { - currentPage: page, - elementsPerPage: this.config.pageSize - } + this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( + switchMap((config) => this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, { + currentPage: config.currentPage, + elementsPerPage: config.pageSize + } + )) ).subscribe((rd: RemoteData>) => { this.subGroups$.next(rd); })); @@ -226,10 +213,12 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { this.searchDone = true; this.unsubFrom(SubKey.SearchResults); - this.subs.set(SubKey.SearchResults, this.groupDataService.searchGroups(this.currentSearchQuery, { - currentPage: this.configSearch.currentPage, - elementsPerPage: this.configSearch.pageSize - }).subscribe((rd: RemoteData>) => { + this.subs.set(SubKey.SearchResults, this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe( + switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, { + currentPage: config.currentPage, + elementsPerPage: config.pageSize + })) + ).subscribe((rd: RemoteData>) => { this.searchResults$.next(rd); })); } @@ -255,6 +244,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { for (const key of this.subs.keys()) { this.unsubFrom(key); } + this.paginationService.clearPagination(this.config.id); + this.paginationService.clearPagination(this.configSearch.id); } /** diff --git a/src/app/access-control/group-registry/groups-registry.component.html b/src/app/access-control/group-registry/groups-registry.component.html index 219c236ef5..e5e25ae944 100644 --- a/src/app/access-control/group-registry/groups-registry.component.html +++ b/src/app/access-control/group-registry/groups-registry.component.html @@ -40,8 +40,7 @@ [pageInfoState]="pageInfoState$" [collectionSize]="(pageInfoState$ | async)?.totalElements" [hideGear]="true" - [hidePagerWhenSinglePage]="true" - (pageChange)="onPageChange($event)"> + [hidePagerWhenSinglePage]="true">
    diff --git a/src/app/access-control/group-registry/groups-registry.component.spec.ts b/src/app/access-control/group-registry/groups-registry.component.spec.ts index b5b5a1b209..10064800e1 100644 --- a/src/app/access-control/group-registry/groups-registry.component.spec.ts +++ b/src/app/access-control/group-registry/groups-registry.component.spec.ts @@ -28,6 +28,8 @@ import { TranslateLoaderMock } from '../../shared/testing/translate-loader.mock' import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { routeServiceStub } from '../../shared/testing/route-service.stub'; import { RouterMock } from '../../shared/mocks/router.mock'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; describe('GroupRegistryComponent', () => { let component: GroupsRegistryComponent; @@ -39,6 +41,7 @@ describe('GroupRegistryComponent', () => { let mockGroups; let mockEPeople; + let paginationService; beforeEach(waitForAsync(() => { mockGroups = [GroupMock, GroupMock2]; @@ -131,6 +134,7 @@ describe('GroupRegistryComponent', () => { authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: observableOf(true) }); + paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ @@ -149,6 +153,7 @@ describe('GroupRegistryComponent', () => { { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: new RouterMock() }, { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: PaginationService, useValue: paginationService }, { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/access-control/group-registry/groups-registry.component.ts b/src/app/access-control/group-registry/groups-registry.component.ts index 19dcc3c552..b28ac043d9 100644 --- a/src/app/access-control/group-registry/groups-registry.component.ts +++ b/src/app/access-control/group-registry/groups-registry.component.ts @@ -5,15 +5,15 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest as observableCombineLatest, - Subscription, Observable, - of as observableOf + of as observableOf, + Subscription } from 'rxjs'; import { catchError, map, switchMap, take } from 'rxjs/operators'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; -import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model'; +import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { RequestService } from '../../core/data/request.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; @@ -24,15 +24,17 @@ import { Group } from '../../core/eperson/models/group.model'; import { RouteService } from '../../core/services/route.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { - getAllSucceededRemoteDataPayload, + getAllSucceededRemoteData, getFirstCompletedRemoteData, - getFirstSucceededRemoteData + getFirstSucceededRemoteData, + getRemoteDataPayload } from '../../core/shared/operators'; import { PageInfo } from '../../core/shared/page-info.model'; import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { NoContent } from '../../core/shared/NoContent.model'; +import { PaginationService } from '../../core/pagination/pagination.service'; @Component({ selector: 'ds-groups-registry', @@ -50,7 +52,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { * Pagination config used to display the list of groups */ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'groups-list-pagination', + id: 'gl', pageSize: 5, currentPage: 1 }); @@ -83,6 +85,8 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { */ searchSub: Subscription; + paginationSub: Subscription; + /** * List of subscriptions */ @@ -97,6 +101,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { protected routeService: RouteService, private router: Router, private authorizationService: AuthorizationDataService, + private paginationService: PaginationService, public requestService: RequestService) { this.currentSearchQuery = ''; this.searchForm = this.formBuilder.group(({ @@ -108,37 +113,30 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { this.search({ query: this.currentSearchQuery }); } - /** - * Event triggered when the user changes page - * @param event - */ - onPageChange(event) { - this.config.currentPage = event; - this.search({ query: this.currentSearchQuery }); - } - /** * Search in the groups (searches by group name and by uuid exact match) * @param data Contains query param */ search(data: any) { this.searching$.next(true); - const query: string = data.query; - if (query != null && this.currentSearchQuery !== query) { - this.router.navigateByUrl(this.groupService.getGroupRegistryRouterLink()); - this.currentSearchQuery = query; - this.config.currentPage = 1; - } if (hasValue(this.searchSub)) { this.searchSub.unsubscribe(); this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub); } - - this.searchSub = this.groupService.searchGroups(this.currentSearchQuery.trim(), { - currentPage: this.config.currentPage, - elementsPerPage: this.config.pageSize - }).pipe( - getAllSucceededRemoteDataPayload(), + this.searchSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( + switchMap((paginationOptions) => { + const query: string = data.query; + if (query != null && this.currentSearchQuery !== query) { + this.currentSearchQuery = query; + this.paginationService.updateRouteWithUrl(this.config.id, [], {page: 1}); + } + return this.groupService.searchGroups(this.currentSearchQuery.trim(), { + currentPage: paginationOptions.currentPage, + elementsPerPage: paginationOptions.pageSize + }); + }), + getAllSucceededRemoteData(), + getRemoteDataPayload(), switchMap((groups: PaginatedList) => { if (groups.page.length === 0) { return observableOf(buildPaginatedList(groups.pageInfo, [])); @@ -166,13 +164,15 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { })).pipe(map((dtos: GroupDtoModel[]) => { return buildPaginatedList(groups.pageInfo, dtos); })); - })).subscribe((value: PaginatedList) => { + }) + ).subscribe((value: PaginatedList) => { this.groupsDto$.next(value); this.pageInfoState$.next(value.pageInfo); this.searching$.next(false); }); + this.subs.push(this.searchSub); - } + } /** * Delete Group @@ -248,9 +248,16 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.cleanupSubscribes(); + this.paginationService.clearPagination(this.config.id); } + cleanupSubscribes() { + if (hasValue(this.paginationSub)) { + this.paginationSub.unsubscribe(); + } this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + this.paginationService.clearPagination(this.config.id); } + } diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 08f7b9585f..7dfdbd2c49 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -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() { diff --git a/src/app/core/auth/auth-request.service.spec.ts b/src/app/core/auth/auth-request.service.spec.ts new file mode 100644 index 0000000000..707daf9e30 --- /dev/null +++ b/src/app/core/auth/auth-request.service.spec.ts @@ -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; + 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`); + }); + }); + }); +}); diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 4315ddfea8..00a94822d3 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -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 { 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(request.uuid)), + map((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)), + tap((request: RestRequest) => this.requestService.send(request)), + switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), getFirstCompletedRemoteData(), map((response: RemoteData) => { if (response.hasSucceeded) { diff --git a/src/app/core/auth/browser-auth-request.service.spec.ts b/src/app/core/auth/browser-auth-request.service.spec.ts new file mode 100644 index 0000000000..18d27340af --- /dev/null +++ b/src/app/core/auth/browser-auth-request.service.spec.ts @@ -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) ; + }); + }); +}); diff --git a/src/app/core/auth/browser-auth-request.service.ts b/src/app/core/auth/browser-auth-request.service.ts new file mode 100644 index 0000000000..85d5f54340 --- /dev/null +++ b/src/app/core/auth/browser-auth-request.service.ts @@ -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); + } + +} diff --git a/src/app/core/auth/server-auth-request.service.spec.ts b/src/app/core/auth/server-auth-request.service.spec.ts new file mode 100644 index 0000000000..69053fbb3a --- /dev/null +++ b/src/app/core/auth/server-auth-request.service.spec.ts @@ -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) ; + }); + }); +}); diff --git a/src/app/core/auth/server-auth-request.service.ts b/src/app/core/auth/server-auth-request.service.ts new file mode 100644 index 0000000000..751389f71d --- /dev/null +++ b/src/app/core/auth/server-auth-request.service.ts @@ -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. + }); + } + +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index f73bfd0bdf..619a7dbadc 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -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, diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index e3473a895e..dbdd794665 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -12,4 +12,5 @@ export enum FeatureID { CanManageGroups = 'canManageGroups', IsCollectionAdmin = 'isCollectionAdmin', IsCommunityAdmin = 'isCommunityAdmin', + CanDownload = 'canDownload', } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 25165aba79..18421dd489 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -181,11 +181,7 @@ describe('MetadataService', () => { Meta, Title, // tslint:disable-next-line:no-empty - { provide: ItemDataService, useValue: { findById: () => { } } }, - { - provide: HardRedirectService, - useValue: { rewriteDownloadURL: (a) => a, getRequestOrigin: () => environment.ui.baseUrl } - }, + { provide: ItemDataService, useValue: { findById: () => {} } }, BrowseService, MetadataService ], @@ -225,8 +221,8 @@ describe('MetadataService', () => { tick(); expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document'); expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher'); - expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual(new URLCombiner(environment.ui.baseUrl, router.url).toString()); - expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content'); + expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([environment.ui.baseUrl, router.url].join('')); + expect(tagStore.get('citation_pdf_url')[0].content).toEqual('/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download'); })); it('items page should set meta tags as published Technical Report', fakeAsync(() => { diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index dac0a787fb..807f7a42ab 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -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); } }); } diff --git a/src/app/core/pagination/pagination.service.spec.ts b/src/app/core/pagination/pagination.service.spec.ts new file mode 100644 index 0000000000..18f94cc84c --- /dev/null +++ b/src/app/core/pagination/pagination.service.spec.ts @@ -0,0 +1,158 @@ +import { PaginationService } from './pagination.service'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { of as observableOf } from 'rxjs'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../cache/models/sort-options.model'; +import { FindListOptions } from '../data/request.models'; + + +describe('PaginationService', () => { + let service: PaginationService; + let router; + let routeService; + + const defaultPagination = new PaginationComponentOptions(); + const defaultSort = new SortOptions('id', SortDirection.DESC); + const defaultFindListOptions = new FindListOptions(); + + beforeEach(() => { + router = new RouterStub(); + routeService = { + getQueryParameterValue: (param) => { + let value; + if (param.endsWith('.page')) { + value = 5; + } + if (param.endsWith('.rpp')) { + value = 10; + } + if (param.endsWith('.sd')) { + value = 'ASC'; + } + if (param.endsWith('.sf')) { + value = 'score'; + } + return observableOf(value); + } + }; + + service = new PaginationService(routeService, router); + }); + + + describe('getCurrentPagination', () => { + it('should retrieve the current pagination info from the routerService', () => { + service.getCurrentPagination('test-id', defaultPagination).subscribe((currentPagination) => { + expect(currentPagination).toEqual(Object.assign(new PaginationComponentOptions(), { + currentPage: 5, + pageSize: 10 + })); + }); + }); + }); + describe('getCurrentSort', () => { + it('should retrieve the current sort info from the routerService', () => { + service.getCurrentSort('test-id', defaultSort).subscribe((currentSort) => { + expect(currentSort).toEqual(Object.assign(new SortOptions('score', SortDirection.ASC ))); + }); + }); + }); + describe('getFindListOptions', () => { + it('should retrieve the current findListOptions info from the routerService', () => { + service.getFindListOptions('test-id', defaultFindListOptions).subscribe((findListOptions) => { + expect(findListOptions).toEqual(Object.assign(new FindListOptions(), + { + sort: new SortOptions('score', SortDirection.ASC ), + currentPage: 5, + elementsPerPage: 10 + })); + }); + }); + }); + describe('resetPage', () => { + it('should call the updateRoute method with the id and page 1', () => { + spyOn(service, 'updateRoute'); + service.resetPage('test'); + + expect(service.updateRoute).toHaveBeenCalledWith('test', {page: 1}); + }); + }); + + describe('updateRoute', () => { + it('should update the route with the provided page params', () => { + service.updateRoute('test', {page: 2, pageSize: 5, sortField: 'title', sortDirection: SortDirection.DESC}); + + const navigateParams = {}; + navigateParams[`test.page`] = `2`; + navigateParams[`test.rpp`] = `5`; + navigateParams[`test.sf`] = `title`; + navigateParams[`test.sd`] = `DESC`; + + expect(router.navigate).toHaveBeenCalledWith([], {queryParams: navigateParams, queryParamsHandling: 'merge'}); + }); + it('should update the route with the provided page params while keeping the existing non provided ones', () => { + service.updateRoute('test', {page: 2}); + + const navigateParams = {}; + navigateParams[`test.page`] = `2`; + navigateParams[`test.rpp`] = `10`; + navigateParams[`test.sf`] = `score`; + navigateParams[`test.sd`] = `ASC`; + + expect(router.navigate).toHaveBeenCalledWith([], {queryParams: navigateParams, queryParamsHandling: 'merge'}); + }); + }); + describe('updateRouteWithUrl', () => { + it('should update the route with the provided page params and url', () => { + service.updateRouteWithUrl('test', ['someUrl'], {page: 2, pageSize: 5, sortField: 'title', sortDirection: SortDirection.DESC}); + + const navigateParams = {}; + navigateParams[`test.page`] = `2`; + navigateParams[`test.rpp`] = `5`; + navigateParams[`test.sf`] = `title`; + navigateParams[`test.sd`] = `DESC`; + + expect(router.navigate).toHaveBeenCalledWith(['someUrl'], {queryParams: navigateParams, queryParamsHandling: 'merge'}); + }); + it('should update the route with the provided page params and url while keeping the existing non provided ones', () => { + service.updateRouteWithUrl('test',['someUrl'], {page: 2}); + + const navigateParams = {}; + navigateParams[`test.page`] = `2`; + navigateParams[`test.rpp`] = `10`; + navigateParams[`test.sf`] = `score`; + navigateParams[`test.sd`] = `ASC`; + + expect(router.navigate).toHaveBeenCalledWith(['someUrl'], {queryParams: navigateParams, queryParamsHandling: 'merge'}); + }); + + }); + describe('clearPagination', () => { + it('should clear the pagination next time the updateRoute/updateRouteWithUrl method is called', () => { + service.clearPagination('test'); + + const resetParams = {}; + resetParams[`test.page`] = null; + resetParams[`test.rpp`] = null; + resetParams[`test.sf`] = null; + resetParams[`test.sd`] = null; + + + const navigateParams = {}; + navigateParams[`another-id.page`] = `5`; + navigateParams[`another-id.rpp`] = `10`; + navigateParams[`another-id.sf`] = `score`; + navigateParams[`another-id.sd`] = `ASC`; + + service.updateRoute('another-id', {}); + + expect(router.navigate).toHaveBeenCalledWith([], {queryParams: Object.assign({}, resetParams, navigateParams), queryParamsHandling: 'merge'}); + }); + }); + describe('getPageParam', () => { + it('should return the name of the page param', () => { + const pageParam = service.getPageParam('test'); + expect(pageParam).toEqual('test.page'); + }); + }); +}); diff --git a/src/app/core/pagination/pagination.service.ts b/src/app/core/pagination/pagination.service.ts new file mode 100644 index 0000000000..dae6991834 --- /dev/null +++ b/src/app/core/pagination/pagination.service.ts @@ -0,0 +1,242 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { RouteService } from '../services/route.service'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; +import { filter, map, take } from 'rxjs/operators'; +import { SortDirection, SortOptions } from '../cache/models/sort-options.model'; +import { FindListOptions } from '../data/request.models'; +import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { difference } from '../../shared/object.util'; +import { isNumeric } from 'rxjs/internal-compatibility'; + + +@Injectable({ + providedIn: 'root', +}) +/** + * Service to manage the pagination of different components + * The pagination information will be stored in the route based on a paginationID. + * The following params are used for the different kind of pagination information: + * - For the page: {paginationID}.p + * - For the page size: {paginationID}.rpp + * - For the sort direction: {paginationID}.sd + * - For the sort field: {paginationID}.sf + */ +export class PaginationService { + + private defaultSortOptions = new SortOptions('id', SortDirection.ASC); + + private clearParams = {}; + + constructor(protected routeService: RouteService, + protected router: Router + ) { + } + + /** + * Method to retrieve the current pagination settings for an ID based on the router params and default options + * @param paginationId - The id to check the pagination for + * @param defaultPagination - The default pagination values to be used when no route info is present + * @returns {Observable} Retrieves the current pagination settings based on the router params + */ + getCurrentPagination(paginationId: string, defaultPagination: PaginationComponentOptions): Observable { + const page$ = this.routeService.getQueryParameterValue(`${paginationId}.page`); + const size$ = this.routeService.getQueryParameterValue(`${paginationId}.rpp`); + return observableCombineLatest([page$, size$]).pipe( + map(([page, size]) => { + return Object.assign(new PaginationComponentOptions(), defaultPagination, { + currentPage: this.convertToNumeric(page, defaultPagination.currentPage), + pageSize: this.getBestMatchPageSize(size, defaultPagination) + }); + }) + ); + } + + /** + * Method to retrieve the current sort options for an ID based on the router params and default options + * @param paginationId - The id to check the sort options for + * @param defaultSort - The default sort options to be used when no route info is present + * @param ignoreDefault - Indicate whether the default should be ignored + * @returns {Observable} Retrieves the current sort options based on the router params + */ + getCurrentSort(paginationId: string, defaultSort: SortOptions, ignoreDefault?: boolean): Observable { + if (!ignoreDefault && (isEmpty(defaultSort) || !hasValue(defaultSort))) { + defaultSort = this.defaultSortOptions; + } + const sortDirection$ = this.routeService.getQueryParameterValue(`${paginationId}.sd`); + const sortField$ = this.routeService.getQueryParameterValue(`${paginationId}.sf`); + return observableCombineLatest([sortDirection$, sortField$]).pipe(map(([sortDirection, sortField]) => { + const field = sortField || defaultSort?.field; + const direction = SortDirection[sortDirection] || defaultSort?.direction; + return new SortOptions(field, direction); + }) + ); + } + + /** + * Method to retrieve the current find list options for an ID based on the router params and default options + * @param paginationId - The id to check the find list options for + * @param defaultFindList - The default find list options to be used when no route info is present + * @param ignoreDefault - Indicate whether the default should be ignored + * @returns {Observable} Retrieves the current find list options based on the router params + */ + getFindListOptions(paginationId: string, defaultFindList: FindListOptions, ignoreDefault?: boolean): Observable { + const paginationComponentOptions = new PaginationComponentOptions(); + paginationComponentOptions.currentPage = defaultFindList.currentPage; + paginationComponentOptions.pageSize = defaultFindList.elementsPerPage; + const currentPagination$ = this.getCurrentPagination(paginationId, paginationComponentOptions); + const currentSortOptions$ = this.getCurrentSort(paginationId, defaultFindList.sort, ignoreDefault); + + return observableCombineLatest([currentPagination$, currentSortOptions$]).pipe( + filter(([currentPagination, currentSortOptions]) => hasValue(currentPagination) && hasValue(currentSortOptions)), + map(([currentPagination, currentSortOptions]) => { + return Object.assign(new FindListOptions(), defaultFindList, { + sort: currentSortOptions, + currentPage: currentPagination.currentPage, + elementsPerPage: currentPagination.pageSize + }); + })); + } + + /** + * Reset the current page for the provided pagination ID to 1. + * @param paginationId - The pagination id for which to reset the page + */ + resetPage(paginationId: string) { + this.updateRoute(paginationId, {page: 1}); + } + + + /** + * Update the route with the provided information + * @param paginationId - The pagination ID for which to update the route with info + * @param params - The page related params to update in the route + * @param extraParams - Addition params unrelated to the pagination that need to be added to the route + * @param retainScrollPosition - Scroll to the pagination component after updating the route instead of the top of the page + */ + updateRoute(paginationId: string, params: { + page?: number + pageSize?: number + sortField?: string + sortDirection?: SortDirection + }, extraParams?, retainScrollPosition?: boolean) { + + this.updateRouteWithUrl(paginationId, [], params, extraParams, retainScrollPosition); + } + + /** + * Update the route with the provided information + * @param paginationId - The pagination ID for which to update the route with info + * @param url - The url to navigate to + * @param params - The page related params to update in the route + * @param extraParams - Addition params unrelated to the pagination that need to be added to the route + * @param retainScrollPosition - Scroll to the pagination component after updating the route instead of the top of the page + */ + updateRouteWithUrl(paginationId: string, url: string[], params: { + page?: number + pageSize?: number + sortField?: string + sortDirection?: SortDirection + }, extraParams?, retainScrollPosition?: boolean) { + this.getCurrentRouting(paginationId).subscribe((currentFindListOptions) => { + const currentParametersWithIdName = this.getParametersWithIdName(paginationId, currentFindListOptions); + const parametersWithIdName = this.getParametersWithIdName(paginationId, params); + if (isNotEmpty(difference(parametersWithIdName, currentParametersWithIdName)) || isNotEmpty(extraParams) || isNotEmpty(this.clearParams)) { + const queryParams = Object.assign({}, this.clearParams, currentParametersWithIdName, + parametersWithIdName, extraParams); + if (retainScrollPosition) { + this.router.navigate(url, { + queryParams: queryParams, + queryParamsHandling: 'merge', + fragment: `p-${paginationId}` + }); + } else { + this.router.navigate(url, { + queryParams: queryParams, + queryParamsHandling: 'merge' + }); + } + this.clearParams = {}; + } + }); + } + + /** + * Add the params to be cleared to the clearParams variable. + * When the updateRoute or updateRouteWithUrl these params will be removed from the route pagination + * @param paginationId - The ID for which to clear the params + */ + clearPagination(paginationId: string) { + const params = {}; + params[`${paginationId}.page`] = null; + params[`${paginationId}.rpp`] = null; + params[`${paginationId}.sf`] = null; + params[`${paginationId}.sd`] = null; + + Object.assign(this.clearParams, params); + } + + /** + * Retrieve the page parameter for the provided id + * @param paginationId - The ID for which to retrieve the page param + */ + getPageParam(paginationId: string) { + return `${paginationId}.page`; + } + + private getCurrentRouting(paginationId: string) { + return this.getFindListOptions(paginationId, {}, true).pipe( + take(1), + map((findListoptions: FindListOptions) => { + return { + page: findListoptions.currentPage, + pageSize: findListoptions.elementsPerPage, + sortField: findListoptions.sort.field, + sortDirection: findListoptions.sort.direction, + }; + }) + ); + } + + private getParametersWithIdName(paginationId: string, params: { + page?: number + pageSize?: number + sortField?: string + sortDirection?: SortDirection + }) { + const paramsWithIdName = {}; + if (hasValue(params.page)) { + paramsWithIdName[`${paginationId}.page`] = `${params.page}`; + } + if (hasValue(params.pageSize)) { + paramsWithIdName[`${paginationId}.rpp`] = `${params.pageSize}`; + } + if (hasValue(params.sortField)) { + paramsWithIdName[`${paginationId}.sf`] = `${params.sortField}`; + } + if (hasValue(params.sortDirection)) { + paramsWithIdName[`${paginationId}.sd`] = `${params.sortDirection}`; + } + return paramsWithIdName; + } + + private convertToNumeric(param, defaultValue) { + let result = defaultValue; + if (isNumeric(param)) { + result = +param; + } + return result; + } + + + private getBestMatchPageSize(pageSize: any, defaultPagination: PaginationComponentOptions): number { + const numberPageSize = this.convertToNumeric(pageSize, defaultPagination.pageSize); + const differenceList = defaultPagination.pageSizeOptions.map((pageSizeOption) => { + return Math.abs(pageSizeOption - numberPageSize); + }); + const minDifference = Math.min.apply(Math, differenceList); + return defaultPagination.pageSizeOptions[differenceList.indexOf(minDifference)]; + } + +} diff --git a/src/app/core/services/route.service.ts b/src/app/core/services/route.service.ts index f53b24c1c8..23b7ccec85 100644 --- a/src/app/core/services/route.service.ts +++ b/src/app/core/services/route.service.ts @@ -18,7 +18,7 @@ import { AddUrlToHistoryAction } from '../history/history.actions'; */ export const routeParametersSelector = createSelector( coreSelector, - (state: CoreState) => state.route.params + (state: CoreState) => hasValue(state) && hasValue(state.route) ? state.route.params : undefined ); /** @@ -26,7 +26,7 @@ export const routeParametersSelector = createSelector( */ export const queryParametersSelector = createSelector( coreSelector, - (state: CoreState) => state.route.queryParams + (state: CoreState) => hasValue(state) && hasValue(state.route) ? state.route.queryParams : undefined ); /** diff --git a/src/app/core/shared/file.service.ts b/src/app/core/shared/file.service.ts index fcbd86161a..98c468e9a3 100644 --- a/src/app/core/shared/file.service.ts +++ b/src/app/core/shared/file.service.ts @@ -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 { + 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 diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index dd610b6ca7..3128538ea9 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -45,32 +45,32 @@ export const sendRequest = (requestService: RequestService) => (source: Observable): Observable => source.pipe(tap((request: RestRequest) => requestService.send(request))); -export const getRemoteDataPayload = () => - (source: Observable>): Observable => +export const getRemoteDataPayload = () => + (source: Observable>): Observable => source.pipe(map((remoteData: RemoteData) => remoteData.payload)); -export const getPaginatedListPayload = () => - (source: Observable>): Observable => +export const getPaginatedListPayload = () => + (source: Observable>): Observable => source.pipe(map((list: PaginatedList) => list.page)); -export const getAllCompletedRemoteData = () => - (source: Observable>): Observable> => +export const getAllCompletedRemoteData = () => + (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => hasValue(rd) && rd.hasCompleted)); -export const getFirstCompletedRemoteData = () => - (source: Observable>): Observable> => +export const getFirstCompletedRemoteData = () => + (source: Observable>): Observable> => source.pipe(getAllCompletedRemoteData(), take(1)); -export const takeUntilCompletedRemoteData = () => - (source: Observable>): Observable> => +export const takeUntilCompletedRemoteData = () => + (source: Observable>): Observable> => source.pipe(takeWhile((rd: RemoteData) => hasNoValue(rd) || rd.isLoading, true)); -export const getFirstSucceededRemoteData = () => - (source: Observable>): Observable> => +export const getFirstSucceededRemoteData = () => + (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => rd.hasSucceeded), take(1)); -export const getFirstSucceededRemoteWithNotEmptyData = () => - (source: Observable>): Observable> => +export const getFirstSucceededRemoteWithNotEmptyData = () => + (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => 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 = () => - (source: Observable>): Observable => +export const getFirstSucceededRemoteDataPayload = () => + (source: Observable>): Observable => 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 = () => - (source: Observable>): Observable => +export const getFirstSucceededRemoteDataWithNotEmptyPayload = () => + (source: Observable>): Observable => 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 = () => - (source: Observable>): Observable => +export const getAllSucceededRemoteDataPayload = () => + (source: Observable>): Observable => 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 = () => - (source: Observable>>): Observable => +export const getFirstSucceededRemoteListPayload = () => + (source: Observable>>): Observable => 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 = () => - (source: Observable>>): Observable => +export const getAllSucceededRemoteListPayload = () => + (source: Observable>>): Observable => 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) => - (source: Observable>): Observable> => +export const redirectOn4xx = (router: Router, authService: AuthService) => + (source: Observable>): Observable> => observableCombineLatest(source, authService.isAuthenticated()).pipe( map(([rd, isAuthenticated]: [RemoteData, boolean]) => { if (rd.hasFailed) { @@ -229,16 +229,16 @@ export const returnEndUserAgreementUrlTreeOnFalse = (router: Router, redirect: s return hasAgreed ? hasAgreed : router.createUrlTree([getEndUserAgreementPath()], { queryParams }); })); -export const getFinishedRemoteData = () => - (source: Observable>): Observable> => +export const getFinishedRemoteData = () => + (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => !rd.isLoading)); -export const getAllSucceededRemoteData = () => - (source: Observable>): Observable> => +export const getAllSucceededRemoteData = () => + (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => rd.hasSucceeded)); -export const toDSpaceObjectListRD = () => - (source: Observable>>>): Observable>> => +export const toDSpaceObjectListRD = () => + (source: Observable>>>): Observable>> => source.pipe( filter((rd: RemoteData>>) => rd.hasSucceeded), map((rd: RemoteData>>) => { diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts index 43c2c54427..061182c2fc 100644 --- a/src/app/core/shared/search/search-configuration.service.spec.ts +++ b/src/app/core/shared/search/search-configuration.service.spec.ts @@ -5,6 +5,7 @@ import { SortDirection, SortOptions } from '../../cache/models/sort-options.mode import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; import { SearchFilter } from '../../../shared/search/search-filter.model'; import { of as observableOf } from 'rxjs'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; describe('SearchConfigurationService', () => { let service: SearchConfigurationService; @@ -15,7 +16,7 @@ describe('SearchConfigurationService', () => { 'f.date.max': ['2018'] }; const defaults = new PaginatedSearchOptions({ - pagination: Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 }), + pagination: Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 }), sort: new SortOptions('score', SortDirection.DESC), configuration: 'default', query: '', @@ -30,10 +31,13 @@ describe('SearchConfigurationService', () => { getRouteParameterValue: observableOf('') }); + const paginationService = new PaginationServiceStub(); + + const activatedRoute: any = new ActivatedRouteStub(); beforeEach(() => { - service = new SearchConfigurationService(routeService, activatedRoute); + service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute); }); describe('when the scope is called', () => { beforeEach(() => { @@ -95,25 +99,19 @@ describe('SearchConfigurationService', () => { describe('when getCurrentSort is called', () => { beforeEach(() => { - service.getCurrentSort({} as any); + service.getCurrentSort(defaults.pagination.id, {} as any); }); - it('should call getQueryParameterValue on the routeService with parameter name \'sortDirection\'', () => { - expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortDirection'); - }); - it('should call getQueryParameterValue on the routeService with parameter name \'sortField\'', () => { - expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('sortField'); + it('should call getCurrentSort on the paginationService with the provided id and sort options', () => { + expect((service as any).paginationService.getCurrentSort).toHaveBeenCalledWith(defaults.pagination.id, {}); }); }); describe('when getCurrentPagination is called', () => { beforeEach(() => { - service.getCurrentPagination({ currentPage: 1, pageSize: 10 } as any); + service.getCurrentPagination(defaults.pagination.id, defaults.pagination); }); - it('should call getQueryParameterValue on the routeService with parameter name \'page\'', () => { - expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('page'); - }); - it('should call getQueryParameterValue on the routeService with parameter name \'pageSize\'', () => { - expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('pageSize'); + it('should call getCurrentPagination on the paginationService with the provided id and sort options', () => { + expect((service as any).paginationService.getCurrentPagination).toHaveBeenCalledWith(defaults.pagination.id, defaults.pagination); }); }); @@ -145,7 +143,7 @@ describe('SearchConfigurationService', () => { describe('when subscribeToPaginatedSearchOptions is called', () => { beforeEach(() => { - (service as any).subscribeToPaginatedSearchOptions(defaults); + (service as any).subscribeToPaginatedSearchOptions(defaults.pagination.id, defaults); }); it('should call all getters it needs', () => { expect(service.getCurrentPagination).toHaveBeenCalled(); diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index edd3982319..798a0de287 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -14,17 +14,20 @@ import { RouteService } from '../../services/route.service'; import { getFirstSucceededRemoteData } from '../operators'; import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { PaginationService } from '../../pagination/pagination.service'; /** * Service that performs all actions that have to do with the current search configuration */ @Injectable() export class SearchConfigurationService implements OnDestroy { + + public paginationID = 'spc'; /** * Default pagination settings */ protected defaultPagination = Object.assign(new PaginationComponentOptions(), { - id: 'search-page-configuration', + id: this.paginationID, pageSize: 10, currentPage: 1 }); @@ -75,6 +78,7 @@ export class SearchConfigurationService implements OnDestroy { * @param {ActivatedRoute} route */ constructor(protected routeService: RouteService, + protected paginationService: PaginationService, protected route: ActivatedRoute) { this.initDefaults(); @@ -91,7 +95,7 @@ export class SearchConfigurationService implements OnDestroy { this.paginatedSearchOptions = new BehaviorSubject(defs); this.searchOptions = new BehaviorSubject(defs); this.subs.push(this.subscribeToSearchOptions(defs)); - this.subs.push(this.subscribeToPaginatedSearchOptions(defs)); + this.subs.push(this.subscribeToPaginatedSearchOptions(defs.pagination.id, defs)); } ); } @@ -140,34 +144,15 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current pagination settings */ - getCurrentPagination(defaultPagination: PaginationComponentOptions): Observable { - const page$ = this.routeService.getQueryParameterValue('page'); - const size$ = this.routeService.getQueryParameterValue('pageSize'); - return observableCombineLatest(page$, size$).pipe(map(([page, size]) => { - return Object.assign(new PaginationComponentOptions(), defaultPagination, { - currentPage: page || defaultPagination.currentPage, - pageSize: size || defaultPagination.pageSize - }); - }) - ); + getCurrentPagination(paginationId: string, defaultPagination: PaginationComponentOptions): Observable { + return this.paginationService.getCurrentPagination(paginationId, defaultPagination); } /** * @returns {Observable} Emits the current sorting settings */ - getCurrentSort(defaultSort: SortOptions): Observable { - const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection'); - const sortField$ = this.routeService.getQueryParameterValue('sortField'); - return observableCombineLatest(sortDirection$, sortField$).pipe(map(([sortDirection, sortField]) => { - // Dirty fix because sometimes the observable value is null somehow - sortField = this.route.snapshot.queryParamMap.get('sortField'); - - const field = sortField || defaultSort.field; - const direction = SortDirection[sortDirection] || defaultSort.direction; - return new SortOptions(field, direction); - } - ) - ); + getCurrentSort(paginationId: string, defaultSort: SortOptions): Observable { + return this.paginationService.getCurrentSort(paginationId, defaultSort); } /** @@ -234,10 +219,10 @@ export class SearchConfigurationService implements OnDestroy { * @param {PaginatedSearchOptions} defaults Default values for when no parameters are available * @returns {Subscription} The subscription to unsubscribe from */ - private subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription { + private subscribeToPaginatedSearchOptions(paginationId: string, defaults: PaginatedSearchOptions): Subscription { return observableMerge( - this.getPaginationPart(defaults.pagination), - this.getSortPart(defaults.sort), + this.getPaginationPart(paginationId, defaults.pagination), + this.getSortPart(paginationId, defaults.sort), this.getConfigurationPart(defaults.configuration), this.getScopePart(defaults.scope), this.getQueryPart(defaults.query), @@ -317,8 +302,8 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current pagination settings as a partial SearchOptions object */ - private getPaginationPart(defaultPagination: PaginationComponentOptions): Observable { - return this.getCurrentPagination(defaultPagination).pipe(map((pagination) => { + private getPaginationPart(paginationId: string, defaultPagination: PaginationComponentOptions): Observable { + return this.getCurrentPagination(paginationId, defaultPagination).pipe(map((pagination) => { return { pagination }; })); } @@ -326,8 +311,8 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current sorting settings as a partial SearchOptions object */ - private getSortPart(defaultSort: SortOptions): Observable { - return this.getCurrentSort(defaultSort).pipe(map((sort) => { + private getSortPart(paginationId: string, defaultSort: SortOptions): Observable { + return this.getCurrentSort(paginationId, defaultSort).pipe(map((sort) => { return { sort }; })); } diff --git a/src/app/core/shared/search/search.service.spec.ts b/src/app/core/shared/search/search.service.spec.ts index 06208094bd..60cb0a87b9 100644 --- a/src/app/core/shared/search/search.service.spec.ts +++ b/src/app/core/shared/search/search.service.spec.ts @@ -22,6 +22,12 @@ import { routeServiceStub } from '../../../shared/testing/route-service.stub'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { SearchObjects } from '../../../shared/search/search-objects.model'; +import { PaginationService } from '../../pagination/pagination.service'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; +import { FindListOptions } from '../../data/request.models'; +import { SearchConfigurationService } from './search-configuration.service'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; @Component({ template: '' }) class DummyComponent { @@ -32,6 +38,7 @@ describe('SearchService', () => { let searchService: SearchService; const router = new RouterStub(); const route = new ActivatedRouteStub(); + const searchConfigService = {paginationID: 'page-id'}; beforeEach(() => { TestBed.configureTestingModule({ imports: [ @@ -51,6 +58,8 @@ describe('SearchService', () => { { provide: HALEndpointService, useValue: {} }, { provide: CommunityDataService, useValue: {} }, { provide: DSpaceObjectDataService, useValue: {} }, + { provide: PaginationService, useValue: {} }, + { provide: SearchConfigurationService, useValue: searchConfigService }, SearchService ], }); @@ -94,6 +103,9 @@ describe('SearchService', () => { } }; + const paginationService = new PaginationServiceStub(); + const searchConfigService = {paginationID: 'page-id'}; + beforeEach(() => { TestBed.configureTestingModule({ imports: [ @@ -113,6 +125,8 @@ describe('SearchService', () => { { provide: HALEndpointService, useValue: halService }, { provide: CommunityDataService, useValue: {} }, { provide: DSpaceObjectDataService, useValue: {} }, + { provide: PaginationService, useValue: paginationService }, + { provide: SearchConfigurationService, useValue: searchConfigService }, SearchService ], }); @@ -124,18 +138,14 @@ describe('SearchService', () => { it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => { searchService.setViewMode(ViewMode.ListElement); - expect(router.navigate).toHaveBeenCalledWith(['/search'], { - queryParams: { view: ViewMode.ListElement, page: 1 }, - queryParamsHandling: 'merge' - }); + expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.ListElement } + ); }); it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => { searchService.setViewMode(ViewMode.GridElement); - expect(router.navigate).toHaveBeenCalledWith(['/search'], { - queryParams: { view: ViewMode.GridElement, page: 1 }, - queryParamsHandling: 'merge' - }); + expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.GridElement } + ); }); it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => { diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index b380a70d44..054bde4c08 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -1,7 +1,7 @@ import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { Injectable, OnDestroy } from '@angular/core'; -import { NavigationExtras, Router } from '@angular/router'; -import { first, map, switchMap, take } from 'rxjs/operators'; +import { Router } from '@angular/router'; +import { map, switchMap, take } from 'rxjs/operators'; import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { LinkService } from '../../cache/builders/link.service'; import { PaginatedList } from '../../data/paginated-list.model'; @@ -37,6 +37,9 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator'; import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model'; import { FacetValues } from '../../../shared/search/facet-values.model'; +import { PaginationService } from '../../pagination/pagination.service'; +import { SearchConfigurationService } from './search-configuration.service'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; /** * Service that performs all general actions that have to do with the search page @@ -75,7 +78,9 @@ export class SearchService implements OnDestroy { private linkService: LinkService, private halService: HALEndpointService, private communityService: CommunityDataService, - private dspaceObjectService: DSpaceObjectDataService + private dspaceObjectService: DSpaceObjectDataService, + private paginationService: PaginationService, + private searchConfigurationService: SearchConfigurationService ) { } @@ -380,20 +385,16 @@ export class SearchService implements OnDestroy { * @param {ViewMode} viewMode Mode to switch to */ setViewMode(viewMode: ViewMode, searchLinkParts?: string[]) { - this.routeService.getQueryParameterValue('pageSize').pipe(first()) - .subscribe((pageSize) => { - let queryParams = { view: viewMode, page: 1 }; + this.paginationService.getCurrentPagination(this.searchConfigurationService.paginationID, new PaginationComponentOptions()).pipe(take(1)) + .subscribe((config) => { + let pageParams = { page: 1 }; + const queryParams = { view: viewMode }; if (viewMode === ViewMode.DetailedListElement) { - queryParams = Object.assign(queryParams, {pageSize: '1'}); - } else if (pageSize === '1') { - queryParams = Object.assign(queryParams, {pageSize: '10'}); + pageParams = Object.assign(pageParams, {pageSize: 1}); + } else if (config.pageSize === 1) { + pageParams = Object.assign(pageParams, {pageSize: 10}); } - const navigationExtras: NavigationExtras = { - queryParams: queryParams, - queryParamsHandling: 'merge' - }; - - this.router.navigate(hasValue(searchLinkParts) ? searchLinkParts : [this.getSearchLink()], navigationExtras); + this.paginationService.updateRouteWithUrl(this.searchConfigurationService.paginationID, hasValue(searchLinkParts) ? searchLinkParts : [this.getSearchLink()], pageParams, queryParams); }); } diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index 3756bce188..bc407c2a97 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -56,7 +56,7 @@

    {{ 'footer.link.dspace' | translate}} {{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }} - {{ 'footer.link.duraspace' | translate}} + {{ 'footer.link.lyrasis' | translate}}

    diff --git a/src/app/process-page/overview/process-overview.component.spec.ts b/src/app/process-page/overview/process-overview.component.spec.ts index ffbcbb10cf..98e78f6b36 100644 --- a/src/app/process-page/overview/process-overview.component.spec.ts +++ b/src/app/process-page/overview/process-overview.component.spec.ts @@ -12,6 +12,12 @@ import { By } from '@angular/platform-browser'; import { ProcessStatus } from '../processes/process-status.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createPaginatedList } from '../../shared/testing/utils.test'; +import { of as observableOf } from 'rxjs'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../core/data/request.models'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; describe('ProcessOverviewComponent', () => { let component: ProcessOverviewComponent; @@ -19,6 +25,7 @@ describe('ProcessOverviewComponent', () => { let processService: ProcessDataService; let ePersonService: EPersonDataService; + let paginationService; let processes: Process[]; let ePerson: EPerson; @@ -69,6 +76,8 @@ describe('ProcessOverviewComponent', () => { ePersonService = jasmine.createSpyObj('ePersonService', { findById: createSuccessfulRemoteDataObject$(ePerson) }); + + paginationService = new PaginationServiceStub(); } beforeEach(waitForAsync(() => { @@ -78,7 +87,8 @@ describe('ProcessOverviewComponent', () => { imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ { provide: ProcessDataService, useValue: processService }, - { provide: EPersonDataService, useValue: ePersonService } + { provide: EPersonDataService, useValue: ePersonService }, + { provide: PaginationService, useValue: paginationService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -143,16 +153,4 @@ describe('ProcessOverviewComponent', () => { }); }); }); - - describe('onPageChange', () => { - const toPage = 2; - - beforeEach(() => { - component.onPageChange(toPage); - }); - - it('should call a new findAll with the corresponding page', () => { - expect(processService.findAll).toHaveBeenCalledWith(jasmine.objectContaining({ currentPage: toPage })); - }); - }); }); diff --git a/src/app/process-page/overview/process-overview.component.ts b/src/app/process-page/overview/process-overview.component.ts index 541d6c212e..03fcf27222 100644 --- a/src/app/process-page/overview/process-overview.component.ts +++ b/src/app/process-page/overview/process-overview.component.ts @@ -8,8 +8,9 @@ import { FindListOptions } from '../../core/data/request.models'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { EPerson } from '../../core/eperson/models/eperson.model'; -import { map } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { ProcessDataService } from '../../core/data/processes/process-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; @Component({ selector: 'ds-process-overview', @@ -36,7 +37,7 @@ export class ProcessOverviewComponent implements OnInit { * The current pagination configuration for the page */ pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'process-overview-pagination', + id: 'po', pageSize: 20 }); @@ -46,6 +47,7 @@ export class ProcessOverviewComponent implements OnInit { dateFormat = 'yyyy-MM-dd HH:mm:ss'; constructor(protected processService: ProcessDataService, + protected paginationService: PaginationService, protected ePersonService: EPersonDataService) { } @@ -53,23 +55,13 @@ export class ProcessOverviewComponent implements OnInit { this.setProcesses(); } - /** - * When the page is changed, make sure to update the list of processes to match the new page - * @param event The page change event - */ - onPageChange(event) { - this.config = Object.assign(new FindListOptions(), this.config, { - currentPage: event, - }); - this.pageConfig.currentPage = event; - this.setProcesses(); - } - /** * Send a request to fetch all processes for the current page */ setProcesses() { - this.processesRD$ = this.processService.findAll(this.config); + this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe( + switchMap((config) => this.processService.findAll(config)) + ); } /** @@ -82,5 +74,8 @@ export class ProcessOverviewComponent implements OnInit { map((eperson: EPerson) => eperson.name) ); } + ngOnDestroy(): void { + this.paginationService.clearPagination(this.pageConfig.id); + } } diff --git a/src/app/search-navbar/search-navbar.component.spec.ts b/src/app/search-navbar/search-navbar.component.spec.ts index 9eef81a42e..ba08c7ca75 100644 --- a/src/app/search-navbar/search-navbar.component.spec.ts +++ b/src/app/search-navbar/search-navbar.component.spec.ts @@ -8,6 +8,12 @@ import { SearchService } from '../core/shared/search/search.service'; import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { SearchNavbarComponent } from './search-navbar.component'; +import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; +import { of as observableOf } from 'rxjs'; +import { PaginationService } from '../core/pagination/pagination.service'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; +import { PaginationServiceStub } from '../shared/testing/pagination-service.stub'; describe('SearchNavbarComponent', () => { let component: SearchNavbarComponent; @@ -15,6 +21,7 @@ describe('SearchNavbarComponent', () => { let mockSearchService: any; let router: Router; let routerStub; + let paginationService; beforeEach(waitForAsync(() => { mockSearchService = { @@ -26,6 +33,9 @@ describe('SearchNavbarComponent', () => { routerStub = { navigate: (commands) => commands }; + + paginationService = new PaginationServiceStub(); + TestBed.configureTestingModule({ imports: [ FormsModule, @@ -40,7 +50,9 @@ describe('SearchNavbarComponent', () => { declarations: [SearchNavbarComponent], providers: [ { provide: SearchService, useValue: mockSearchService }, - { provide: Router, useValue: routerStub } + { provide: PaginationService, useValue: paginationService }, + { provide: Router, useValue: routerStub }, + { provide: SearchConfigurationService, useValue: {paginationID: 'page-id'} } ] }) .compileComponents(); @@ -88,7 +100,7 @@ describe('SearchNavbarComponent', () => { })); it('to search page with empty query', () => { expect(component.onSubmit).toHaveBeenCalledWith({ query: '' }); - expect(router.navigate).toHaveBeenCalled(); + expect(paginationService.updateRouteWithUrl).toHaveBeenCalled(); }); }); }); @@ -112,7 +124,7 @@ describe('SearchNavbarComponent', () => { })); it('to search page with query', async () => { expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' }); - expect(router.navigate).toHaveBeenCalled(); + expect(paginationService.updateRouteWithUrl).toHaveBeenCalled(); }); }); }); diff --git a/src/app/search-navbar/search-navbar.component.ts b/src/app/search-navbar/search-navbar.component.ts index 1bedfb73ef..1e509a180b 100644 --- a/src/app/search-navbar/search-navbar.component.ts +++ b/src/app/search-navbar/search-navbar.component.ts @@ -3,6 +3,8 @@ import { FormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { SearchService } from '../core/shared/search/search.service'; import { expandSearchInput } from '../shared/animations/slide'; +import { PaginationService } from '../core/pagination/pagination.service'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; /** * The search box in the header that expands on focus and collapses on focus out @@ -24,7 +26,9 @@ export class SearchNavbarComponent { // Search input field @ViewChild('searchInput') searchField: ElementRef; - constructor(private formBuilder: FormBuilder, private router: Router, private searchService: SearchService) { + constructor(private formBuilder: FormBuilder, private router: Router, private searchService: SearchService, + private paginationService: PaginationService, + private searchConfig: SearchConfigurationService) { this.searchForm = this.formBuilder.group(({ query: '', })); @@ -63,9 +67,6 @@ export class SearchNavbarComponent { this.collapse(); const linkToNavigateTo = this.searchService.getSearchLink().split('/'); this.searchForm.reset(); - this.router.navigate(linkToNavigateTo, { - queryParams: Object.assign({}, { page: 1 }, data), - queryParamsHandling: 'merge' - }); + this.paginationService.updateRouteWithUrl(this.searchConfig.paginationID, linkToNavigateTo, {page: 1}, data); } } diff --git a/src/app/shared/bitstream-download-page/bitstream-download-page.component.html b/src/app/shared/bitstream-download-page/bitstream-download-page.component.html new file mode 100644 index 0000000000..183449fc5e --- /dev/null +++ b/src/app/shared/bitstream-download-page/bitstream-download-page.component.html @@ -0,0 +1,3 @@ +
    +

    {{'bitstream.download.page' | translate:{bitstream: (bitstream$ | async)?.name} }}

    +
    diff --git a/src/app/shared/bitstream-download-page/bitstream-download-page.component.spec.ts b/src/app/shared/bitstream-download-page/bitstream-download-page.component.spec.ts new file mode 100644 index 0000000000..2803a7f789 --- /dev/null +++ b/src/app/shared/bitstream-download-page/bitstream-download-page.component.spec.ts @@ -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; + + 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'); + }); + }); + }); +}); diff --git a/src/app/shared/bitstream-download-page/bitstream-download-page.component.ts b/src/app/shared/bitstream-download-page/bitstream-download-page.component.ts new file mode 100644 index 0000000000..a09d7e8b3e --- /dev/null +++ b/src/app/shared/bitstream-download-page/bitstream-download-page.component.ts @@ -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; + bitstreamRD$: Observable>; + + + 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'); + } + }); + } +} diff --git a/src/app/shared/browse-by/browse-by.component.spec.ts b/src/app/shared/browse-by/browse-by.component.spec.ts index 9f3ceade1a..806f4bdb6f 100644 --- a/src/app/shared/browse-by/browse-by.component.spec.ts +++ b/src/app/shared/browse-by/browse-by.component.spec.ts @@ -18,6 +18,9 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { storeModuleConfig } from '../../app.reducer'; +import { FindListOptions } from '../../core/data/request.models'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../testing/pagination-service.stub'; describe('BrowseByComponent', () => { let comp: BrowseByComponent; @@ -45,6 +48,14 @@ describe('BrowseByComponent', () => { ]; const mockItemsRD$ = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockItems)); + const paginationConfig = Object.assign(new PaginationComponentOptions(), { + id: 'test-pagination', + currentPage: 1, + pageSizeOptions: [5, 10, 15, 20], + pageSize: 15 + }); + const paginationService = new PaginationServiceStub(paginationConfig); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -63,7 +74,9 @@ describe('BrowseByComponent', () => { BrowserAnimationsModule ], declarations: [], - providers: [], + providers: [ + {provide: PaginationService, useValue: paginationService} + ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -95,12 +108,8 @@ describe('BrowseByComponent', () => { beforeEach(() => { comp.enableArrows = true; comp.objects$ = mockItemsRD$; - comp.paginationConfig = Object.assign(new PaginationComponentOptions(), { - id: 'test-pagination', - currentPage: 1, - pageSizeOptions: [5, 10, 15, 20], - pageSize: 15 - }); + + comp.paginationConfig = paginationConfig; comp.sortConfig = Object.assign(new SortOptions('dc.title', SortDirection.ASC)); fixture.detectChanges(); }); @@ -136,8 +145,8 @@ describe('BrowseByComponent', () => { fixture.detectChanges(); }); - it('should emit a signal to the EventEmitter', () => { - expect(comp.pageSizeChange.emit).toHaveBeenCalled(); + it('should call the updateRoute method from the paginationService', () => { + expect(paginationService.updateRoute).toHaveBeenCalledWith('test-pagination', {pageSize: paginationConfig.pageSizeOptions[0]}); }); }); @@ -148,8 +157,8 @@ describe('BrowseByComponent', () => { fixture.detectChanges(); }); - it('should emit a signal to the EventEmitter', () => { - expect(comp.sortDirectionChange.emit).toHaveBeenCalled(); + it('should call the updateRoute method from the paginationService', () => { + expect(paginationService.updateRoute).toHaveBeenCalledWith('test-pagination', {sortDirection: 'ASC'}); }); }); }); diff --git a/src/app/shared/browse-by/browse-by.component.ts b/src/app/shared/browse-by/browse-by.component.ts index 8706c39e54..1f05ad2258 100644 --- a/src/app/shared/browse-by/browse-by.component.ts +++ b/src/app/shared/browse-by/browse-by.component.ts @@ -7,6 +7,7 @@ import { fadeIn, fadeInOut } from '../animations/fade'; import { Observable } from 'rxjs'; import { ListableObject } from '../object-collection/shared/listable-object.model'; import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator'; +import { PaginationService } from '../../core/pagination/pagination.service'; @Component({ selector: 'ds-browse-by', @@ -96,7 +97,9 @@ export class BrowseByComponent implements OnInit { */ public sortDirections = SortDirection; - public constructor(private injector: Injector) { + public constructor(private injector: Injector, + protected paginationService: PaginationService, + ) { } @@ -119,8 +122,7 @@ export class BrowseByComponent implements OnInit { * @param size */ doPageSizeChange(size) { - this.paginationConfig.pageSize = size; - this.pageSizeChange.emit(size); + this.paginationService.updateRoute(this.paginationConfig.id,{pageSize: size}); } /** @@ -128,8 +130,7 @@ export class BrowseByComponent implements OnInit { * @param direction */ doSortDirectionChange(direction) { - this.sortConfig.direction = direction; - this.sortDirectionChange.emit(direction); + this.paginationService.updateRoute(this.paginationConfig.id,{sortDirection: direction}); } /** @@ -141,7 +142,10 @@ export class BrowseByComponent implements OnInit { ngOnInit(): void { this.objectInjector = Injector.create({ - providers: [{ provide: 'startsWithOptions', useFactory: () => (this.startsWithOptions), deps:[] }], + providers: [ + { provide: 'startsWithOptions', useFactory: () => (this.startsWithOptions), deps:[] }, + { provide: 'paginationId', useFactory: () => (this.paginationConfig?.id), deps:[] } + ], parent: this.injector }); } diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index 06624c8b40..f1843da5c6 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -1,5 +1,5 @@ - - + + diff --git a/src/app/shared/file-download-link/file-download-link.component.spec.ts b/src/app/shared/file-download-link/file-download-link.component.spec.ts index 4f3c17402f..6f7f50e585 100644 --- a/src/app/shared/file-download-link/file-download-link.component.spec.ts +++ b/src/app/shared/file-download-link/file-download-link.component.spec.ts @@ -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); - }); }); }); diff --git a/src/app/shared/file-download-link/file-download-link.component.ts b/src/app/shared/file-download-link/file-download-link.component.ts index f9861cd997..4423b6f5b7 100644 --- a/src/app/shared/file-download-link/file-download-link.component.ts +++ b/src/app/shared/file-download-link/file-download-link.component.ts @@ -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; - - 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); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index bfa9c214e9..bfbd2b4677 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -11,7 +11,7 @@ 'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}">
    -
    {{lang.display}}
    -
    +
    diff --git a/src/app/shared/item/item-versions/item-versions.component.spec.ts b/src/app/shared/item/item-versions/item-versions.component.spec.ts index f35ec1993d..cc28779537 100644 --- a/src/app/shared/item/item-versions/item-versions.component.spec.ts +++ b/src/app/shared/item/item-versions/item-versions.component.spec.ts @@ -11,6 +11,11 @@ import { VersionHistoryDataService } from '../../../core/data/version-history-da import { By } from '@angular/platform-browser'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { createPaginatedList } from '../../testing/utils.test'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { of as observableOf } from 'rxjs'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../testing/pagination-service.stub'; describe('ItemVersionsComponent', () => { let component: ItemVersionsComponent; @@ -52,12 +57,15 @@ describe('ItemVersionsComponent', () => { getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions)) }); + const paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ItemVersionsComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ - { provide: VersionHistoryDataService, useValue: versionHistoryService } + { provide: VersionHistoryDataService, useValue: versionHistoryService }, + { provide: PaginationService, useValue: paginationService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -107,16 +115,4 @@ describe('ItemVersionsComponent', () => { expect(summary.nativeElement.textContent).toEqual(version.summary); }); }); - - describe('switchPage', () => { - const page = 5; - - beforeEach(() => { - component.switchPage(page); - }); - - it('should set the option\'s currentPage to the new page', () => { - expect(component.options.currentPage).toEqual(page); - }); - }); }); diff --git a/src/app/shared/item/item-versions/item-versions.component.ts b/src/app/shared/item/item-versions/item-versions.component.ts index d25fdaa4f1..752cd55b5b 100644 --- a/src/app/shared/item/item-versions/item-versions.component.ts +++ b/src/app/shared/item/item-versions/item-versions.component.ts @@ -17,6 +17,7 @@ import { PaginatedSearchOptions } from '../../search/paginated-search-options.mo import { AlertType } from '../../alert/aletr-type'; import { followLink } from '../../utils/follow-link-config.model'; import { hasValueOperator } from '../../empty.util'; +import { PaginationService } from '../../../core/pagination/pagination.service'; import { getItemPageRoute } from '../../../+item-page/item-page-routing-paths'; @Component({ @@ -81,7 +82,7 @@ export class ItemVersionsComponent implements OnInit { * Start at page 1 and always use the set page size */ options = Object.assign(new PaginationComponentOptions(),{ - id: 'item-versions-options', + id: 'ivo', currentPage: 1, pageSize: this.pageSize }); @@ -100,7 +101,9 @@ export class ItemVersionsComponent implements OnInit { [itemId: string]: string }>; - constructor(private versionHistoryService: VersionHistoryDataService) { + constructor(private versionHistoryService: VersionHistoryDataService, + private paginationService: PaginationService + ) { } /** @@ -119,10 +122,11 @@ export class ItemVersionsComponent implements OnInit { getRemoteDataPayload(), hasValueOperator(), ); - this.versionsRD$ = observableCombineLatest(versionHistory$, this.currentPage$).pipe( - switchMap(([versionHistory, page]: [VersionHistory, number]) => + const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options); + this.versionsRD$ = observableCombineLatest(versionHistory$, currentPagination).pipe( + switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => this.versionHistoryService.getVersions(versionHistory.id, - new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}), + new PaginatedSearchOptions({pagination: Object.assign({}, options, { currentPage: options.currentPage })}), true, true, followLink('item'), followLink('eperson'))) ); this.hasEpersons$ = this.versionsRD$.pipe( @@ -143,13 +147,9 @@ export class ItemVersionsComponent implements OnInit { ); } - /** - * Update the current page - * @param page - */ - switchPage(page: number) { - this.options.currentPage = page; - this.currentPage$.next(page); + ngOnDestroy(): void { + this.paginationService.clearPagination(this.options.id); } + } diff --git a/src/app/shared/lang-switch/lang-switch.component.spec.ts b/src/app/shared/lang-switch/lang-switch.component.spec.ts index a110893904..61fd70d284 100644 --- a/src/app/shared/lang-switch/lang-switch.component.spec.ts +++ b/src/app/shared/lang-switch/lang-switch.component.spec.ts @@ -23,7 +23,7 @@ class CustomLoader implements TranslateLoader { 'footer': { 'copyright': 'copyright © 2002-{{ year }}', 'link.dspace': 'DSpace software', - 'link.duraspace': 'DuraSpace' + 'link.lyrasis': 'LYRASIS' } }); } diff --git a/src/app/shared/mocks/submission.mock.ts b/src/app/shared/mocks/submission.mock.ts index 16cc9b6262..1ee097af71 100644 --- a/src/app/shared/mocks/submission.mock.ts +++ b/src/app/shared/mocks/submission.mock.ts @@ -1101,6 +1101,91 @@ export const mockSubmissionState: SubmissionObjectState = Object.assign({}, { } }); +export const mockSubmissionStateWithoutUpload: SubmissionObjectState = Object.assign({}, { + 826: { + collection: mockSubmissionCollectionId, + definition: 'traditional', + selfUrl: mockSubmissionSelfUrl, + activeSection: null, + sections: { + extraction: { + config: '', + mandatory: true, + sectionType: 'utils', + visibility: { + main: 'HIDDEN', + other: 'HIDDEN' + }, + collapsed: false, + enabled: true, + data: {}, + errors: [], + isLoading: false, + isValid: false + } as any, + collection: { + config: '', + mandatory: true, + sectionType: 'collection', + visibility: { + main: 'HIDDEN', + other: 'HIDDEN' + }, + collapsed: false, + enabled: true, + data: {}, + errors: [], + isLoading: false, + isValid: false + } as any, + traditionalpageone: { + header: 'submit.progressbar.describe.stepone', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone', + mandatory: true, + sectionType: 'submission-form', + collapsed: false, + enabled: true, + data: {}, + errors: [], + formId: '2_traditionalpageone', + isLoading: false, + isValid: false + } as any, + traditionalpagetwo: { + header: 'submit.progressbar.describe.steptwo', + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpagetwo', + mandatory: false, + sectionType: 'submission-form', + collapsed: false, + enabled: false, + data: {}, + errors: [], + isLoading: false, + isValid: false + } as any, + license: { + header: 'submit.progressbar.license', + config: '', + mandatory: true, + sectionType: 'license', + visibility: { + main: null, + other: 'READONLY' + }, + collapsed: false, + enabled: true, + data: {}, + errors: [], + isLoading: false, + isValid: false + } as any + }, + isLoading: false, + savePending: false, + depositPending: false + } +}); + export const mockSectionsState = Object.assign({}, { extraction: { config: '', diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts index b7e67ff2ec..a4dc0a1d3d 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts @@ -82,7 +82,7 @@ export class ItemDetailPreviewComponent { first()) .subscribe((url) => { const fileUrl = `${url}/${uuid}/content`; - this.fileService.downloadFile(fileUrl); + this.fileService.retrieveFileDownloadLink(fileUrl); }); } diff --git a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts index fd4a239cc4..199bc56647 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts +++ b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts @@ -13,6 +13,11 @@ import { CollectionSelectComponent } from './collection-select.component'; import { Collection } from '../../../core/shared/collection.model'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { createPaginatedList } from '../../testing/utils.test'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { of as observableOf } from 'rxjs'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../testing/pagination-service.stub'; describe('CollectionSelectComponent', () => { let comp: CollectionSelectComponent; @@ -36,13 +41,15 @@ describe('CollectionSelectComponent', () => { currentPage: 1 }); + const paginationService = new PaginationServiceStub(); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], declarations: [], providers: [ { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub([mockCollectionList[1].id]) }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: PaginationService, useValue: paginationService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/object-select/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts index f99991f391..224fb764b6 100644 --- a/src/app/shared/object-select/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -11,14 +11,19 @@ import { HostWindowService } from '../../host-window.service'; import { HostWindowServiceStub } from '../../testing/host-window-service.stub'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; -import { of } from 'rxjs'; +import { of as observableOf, of } from 'rxjs'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { createPaginatedList } from '../../testing/utils.test'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { PaginationServiceStub } from '../../testing/pagination-service.stub'; describe('ItemSelectComponent', () => { let comp: ItemSelectComponent; let fixture: ComponentFixture; let objectSelectService: ObjectSelectService; + let paginationService; const mockItemList = [ Object.assign(new Item(), { @@ -59,13 +64,18 @@ describe('ItemSelectComponent', () => { currentPage: 1 }); + paginationService = new PaginationServiceStub(mockPaginationOptions); + + + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], declarations: [], providers: [ { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub([mockItemList[1].id]) }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: PaginationService, useValue: paginationService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/page-size-selector/page-size-selector.component.spec.ts b/src/app/shared/page-size-selector/page-size-selector.component.spec.ts index f75b5841af..77931400a2 100644 --- a/src/app/shared/page-size-selector/page-size-selector.component.spec.ts +++ b/src/app/shared/page-size-selector/page-size-selector.component.spec.ts @@ -12,6 +12,8 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options import { EnumKeysPipe } from '../utils/enum-keys-pipe'; import { VarDirective } from '../utils/var.directive'; import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../testing/pagination-service.stub'; describe('PageSizeSelectorComponent', () => { @@ -33,6 +35,8 @@ describe('PageSizeSelectorComponent', () => { sort }; + const paginationService = new PaginationServiceStub(pagination, sort); + const activatedRouteStub = { queryParams: observableOf({ query: queryParam, @@ -46,6 +50,7 @@ describe('PageSizeSelectorComponent', () => { declarations: [PageSizeSelectorComponent, EnumKeysPipe, VarDirective], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: PaginationService, useValue: paginationService }, { provide: SEARCH_CONFIG_SERVICE, useValue: { diff --git a/src/app/shared/page-size-selector/page-size-selector.component.ts b/src/app/shared/page-size-selector/page-size-selector.component.ts index dfea7d423f..764a8063db 100644 --- a/src/app/shared/page-size-selector/page-size-selector.component.ts +++ b/src/app/shared/page-size-selector/page-size-selector.component.ts @@ -1,11 +1,12 @@ import { Component, Inject, OnInit } from '@angular/core'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { Observable } from 'rxjs'; -import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { PaginatedSearchOptions } from '../search/paginated-search-options.model'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; +import { PaginationService } from '../../core/pagination/pagination.service'; @Component({ selector: 'ds-page-size-selector', @@ -22,8 +23,10 @@ export class PageSizeSelectorComponent implements OnInit { */ paginationOptions$: Observable; + constructor(private route: ActivatedRoute, private router: Router, + private paginationService: PaginationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) { } @@ -40,13 +43,10 @@ export class PageSizeSelectorComponent implements OnInit { */ reloadRPP(event: Event) { const value = (event.target as HTMLInputElement).value; - const navigationExtras: NavigationExtras = { - queryParams: { - pageSize: value, - page: 1 - }, - queryParamsHandling: 'merge' - }; - this.router.navigate([], navigationExtras); + this.paginationOptions$.pipe( + take(1) + ).subscribe((pagination: PaginationComponentOptions) => { + this.paginationService.updateRoute(pagination.id, {page: 1, pageSize: +value}); + }) ; } } diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts index 905b739270..0c6cd80a62 100644 --- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts +++ b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts @@ -11,6 +11,10 @@ import { PaginationComponent } from '../pagination/pagination.component'; import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; import { createPaginatedList } from '../testing/utils.test'; import { ObjectValuesPipe } from '../utils/object-values-pipe'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { PaginationServiceStub } from '../testing/pagination-service.stub'; @Component({ selector: 'ds-mock-paginated-drag-drop-abstract', @@ -22,8 +26,9 @@ class MockAbstractPaginatedDragAndDropListComponent extends AbstractPaginatedDra protected elRef: ElementRef, protected objectValuesPipe: ObjectValuesPipe, protected mockUrl: string, - protected mockObjectsRD$: Observable>>) { - super(objectUpdatesService, elRef, objectValuesPipe); + protected paginationService: PaginationService, + protected mockObjectsRD$: Observable>>) { + super(objectUpdatesService, elRef, objectValuesPipe, paginationService); } initializeObjectsRD(): void { @@ -43,10 +48,12 @@ describe('AbstractPaginatedDragAndDropListComponent', () => { const url = 'mock-abstract-paginated-drag-and-drop-list-component'; + const object1 = Object.assign(new DSpaceObject(), { uuid: 'object-1' }); const object2 = Object.assign(new DSpaceObject(), { uuid: 'object-2' }); const objectsRD = createSuccessfulRemoteDataObject(createPaginatedList([object1, object2])); let objectsRD$: BehaviorSubject>>; + let paginationService; const updates = { [object1.uuid]: { field: object1, changeType: undefined }, @@ -69,8 +76,9 @@ describe('AbstractPaginatedDragAndDropListComponent', () => { paginationComponent = jasmine.createSpyObj('paginationComponent', { doPageChange: {} }); + paginationService = new PaginationServiceStub(); objectsRD$ = new BehaviorSubject(objectsRD); - component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, objectValuesPipe, url, objectsRD$); + component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, objectValuesPipe, url, paginationService, objectsRD$); component.paginationComponent = paginationComponent; component.ngOnInit(); }); @@ -86,18 +94,6 @@ describe('AbstractPaginatedDragAndDropListComponent', () => { }); }); - describe('switchPage', () => { - const page = 3; - - beforeEach(() => { - component.switchPage(page); - }); - - it('should set currentPage$ to the new page', () => { - expect(component.currentPage$.value).toEqual(page); - }); - }); - describe('drop', () => { const event = { previousIndex: 0, @@ -117,7 +113,7 @@ describe('AbstractPaginatedDragAndDropListComponent', () => { it('should send out a dropObject event with the expected processed paginated indexes', () => { expect(component.dropObject.emit).toHaveBeenCalledWith(Object.assign({ - fromIndex: ((component.currentPage$.value - 1) * component.pageSize) + event.previousIndex, + fromIndex: ((component.currentPage$.value.currentPage - 1) * component.pageSize) + event.previousIndex, toIndex: ((hoverPage - 1) * component.pageSize), finish: jasmine.anything() })); diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts index 4cc647d091..f9b9ff8e90 100644 --- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts +++ b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts @@ -17,6 +17,7 @@ import { Component, ElementRef, EventEmitter, OnDestroy, Output, ViewChild } fro import { PaginationComponent } from '../pagination/pagination.component'; import { ObjectValuesPipe } from '../utils/object-values-pipe'; import { compareArraysUsing } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { PaginationService } from '../../core/pagination/pagination.service'; /** * Operator used for comparing {@link FieldUpdate}s by their field's UUID @@ -88,7 +89,7 @@ export abstract class AbstractPaginatedDragAndDropListComponent(1); + currentPage$ = new BehaviorSubject(this.options); /** * Whether or not we should display a loading animation @@ -113,7 +114,9 @@ export abstract class AbstractPaginatedDragAndDropListComponent { + this.currentPage$.next(currentPagination); + }); + } + /** * Initialize the field-updates in the store */ @@ -164,14 +177,6 @@ export abstract class AbstractPaginatedDragAndDropListComponent) { const dragIndex = event.previousIndex; let dropIndex = event.currentIndex; - const dragPage = this.currentPage$.value - 1; - let dropPage = this.currentPage$.value - 1; + const dragPage = this.currentPage$.value.currentPage - 1; + let dropPage = this.currentPage$.value.currentPage - 1; // Check if the user is hovering over any of the pagination's pages at the time of dropping the object const droppedOnElement = this.elRef.nativeElement.querySelector('.page-item:hover'); @@ -228,5 +233,6 @@ export abstract class AbstractPaginatedDragAndDropListComponent hasValue(sub)).forEach((sub) => sub.unsubscribe()); + this.paginationService.clearPagination(this.options.id); } } diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index 6c67b58a90..5f002e55d3 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -1,18 +1,18 @@ -
    +
    {{ 'pagination.showing.label' | translate }} - {{ 'pagination.showing.detail' | translate:getShowingDetails(collectionSize)}} + {{ 'pagination.showing.detail' | translate:(getShowingDetails(collectionSize)|async)}}
    - + - +
    @@ -20,15 +20,15 @@
    -
    diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts index 037edbc905..73aaab5170 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts @@ -19,6 +19,10 @@ import { PaginationComponentOptions } from '../../../pagination/pagination-compo import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../../core/data/request.models'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../testing/pagination-service.stub'; describe('EpersonGroupListComponent test suite', () => { let comp: EpersonGroupListComponent; @@ -27,6 +31,7 @@ describe('EpersonGroupListComponent test suite', () => { let de; let groupService: any; let epersonService: any; + let paginationService; const paginationOptions: PaginationComponentOptions = new PaginationComponentOptions(); paginationOptions.id = uniqueId('eperson-group-list-pagination-test'); @@ -60,6 +65,8 @@ describe('EpersonGroupListComponent test suite', () => { const groupPaginatedList = buildPaginatedList(new PageInfo(), [GroupMock, GroupMock]); const groupPaginatedListRD = createSuccessfulRemoteDataObject(groupPaginatedList); + paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -74,6 +81,7 @@ describe('EpersonGroupListComponent test suite', () => { { provide: EPersonDataService, useValue: mockEpersonService }, { provide: GroupDataService, useValue: mockGroupService }, { provide: RequestService, useValue: getMockRequestService() }, + { provide: PaginationService, useValue: paginationService }, EpersonGroupListComponent, ChangeDetectorRef, Injector @@ -177,13 +185,6 @@ describe('EpersonGroupListComponent test suite', () => { a: false })); }); - - it('should update list on page change', () => { - spyOn(comp, 'updateList'); - comp.onPageChange(2); - - expect(compAsAny.updateList).toHaveBeenCalled(); - }); }); describe('when is list of group', () => { @@ -255,13 +256,6 @@ describe('EpersonGroupListComponent test suite', () => { })); }); - it('should update list on page change', () => { - spyOn(comp, 'updateList'); - comp.onPageChange(2); - - expect(compAsAny.updateList).toHaveBeenCalled(); - }); - it('should update list on search triggered', () => { const options: PaginationComponentOptions = comp.paginationOptions; const event: SearchEvent = { @@ -271,7 +265,7 @@ describe('EpersonGroupListComponent test suite', () => { spyOn(comp, 'updateList'); comp.onSearch(event); - expect(compAsAny.updateList).toHaveBeenCalledWith(options, 'metadata', 'test'); + expect(compAsAny.updateList).toHaveBeenCalledWith('metadata', 'test'); }); }); }); diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts index dfd9ea65ab..11b5b5e7b3 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts @@ -20,6 +20,7 @@ import { EPersonDataService } from '../../../../core/eperson/eperson-data.servic import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { fadeInOut } from '../../../animations/fade'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; export interface SearchEvent { scope: string; @@ -93,13 +94,16 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { */ private subs: Subscription[] = []; + private pageConfigSub: Subscription; + /** * Initialize instance variables and inject the properly DataService * * @param {DSONameService} dsoNameService * @param {Injector} parentInjector */ - constructor(public dsoNameService: DSONameService, private parentInjector: Injector) { + constructor(public dsoNameService: DSONameService, private parentInjector: Injector, + private paginationService: PaginationService) { } /** @@ -112,14 +116,14 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { providers: [], parent: this.parentInjector }).get(provider); - this.paginationOptions.id = uniqueId('eperson-group-list-pagination'); + this.paginationOptions.id = uniqueId('egl'); this.paginationOptions.pageSize = 5; if (this.initSelected) { this.entrySelectedId.next(this.initSelected); } - this.updateList(this.paginationOptions, this.currentSearchScope, this.currentSearchQuery); + this.updateList(this.currentSearchScope, this.currentSearchQuery); } /** @@ -151,13 +155,6 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { ); } - /** - * Method called on page change - */ - onPageChange(page: number): void { - this.paginationOptions.currentPage = page; - this.updateList(this.paginationOptions, this.currentSearchScope, this.currentSearchQuery); - } /** * Method called on search @@ -165,17 +162,22 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { onSearch(searchEvent: SearchEvent) { this.currentSearchQuery = searchEvent.query; this.currentSearchScope = searchEvent.scope; - this.paginationOptions.currentPage = 1; - this.updateList(this.paginationOptions, this.currentSearchScope, this.currentSearchQuery); + this.paginationService.resetPage(this.paginationOptions.id); + this.updateList(this.currentSearchScope, this.currentSearchQuery); } /** * Retrieve a paginate list of eperson or group */ - updateList(config: PaginationComponentOptions, scope: string, query: string): void { + updateList(scope: string, query: string): void { + if (hasValue(this.pageConfigSub)) { + this.pageConfigSub.unsubscribe(); + } + this.pageConfigSub = this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) + .subscribe((paginationOptions) => { const options: FindListOptions = Object.assign({}, new FindListOptions(), { - elementsPerPage: config.pageSize, - currentPage: config.currentPage + elementsPerPage: paginationOptions.pageSize, + currentPage: paginationOptions.currentPage }); const search$: Observable>> = this.isListOfEPerson ? @@ -184,9 +186,12 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { this.subs.push(search$.pipe(getFirstCompletedRemoteData()) .subscribe((list: RemoteData>) => { - this.list$.next(list); + if (hasValue(this.list$)) { + this.list$.next(list); + } }) ); + }); } /** @@ -197,6 +202,8 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { this.subs .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()); + this.paginationService.clearPagination(this.paginationOptions.id); } + } diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index f2293afeca..1469eac566 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -8,6 +8,13 @@ import { Community } from '../../core/shared/community.model'; import { TranslateModule } from '@ngx-translate/core'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { SearchService } from '../../core/shared/search/search.service'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../core/data/request.models'; +import { of as observableOf } from 'rxjs'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { PaginationServiceStub } from '../testing/pagination-service.stub'; describe('SearchFormComponent', () => { let comp: SearchFormComponent; @@ -15,6 +22,10 @@ describe('SearchFormComponent', () => { let de: DebugElement; let el: HTMLElement; + const paginationService = new PaginationServiceStub(); + + const searchConfigService = {paginationID: 'test-id'}; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [FormsModule, RouterTestingModule, TranslateModule.forRoot()], @@ -22,7 +33,9 @@ describe('SearchFormComponent', () => { { provide: SearchService, useValue: {} - } + }, + { provide: PaginationService, useValue: paginationService }, + { provide: SearchConfigurationService, useValue: searchConfigService } ], declarations: [SearchFormComponent] }).compileComponents(); diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index 7bd7af2f02..2791aee378 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -4,6 +4,8 @@ import { Router } from '@angular/router'; import { isNotEmpty } from '../empty.util'; import { SearchService } from '../../core/shared/search/search.service'; import { currentPath } from '../utils/route.utils'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; /** * This component renders a simple item page. @@ -64,7 +66,10 @@ export class SearchFormComponent { */ @Output() submitSearch = new EventEmitter(); - constructor(private router: Router, private searchService: SearchService) { + constructor(private router: Router, private searchService: SearchService, + private paginationService: PaginationService, + private searchConfig: SearchConfigurationService + ) { } /** @@ -89,10 +94,15 @@ export class SearchFormComponent { * @param data Updated parameters */ updateSearch(data: any) { - this.router.navigate(this.getSearchLinkParts(), { - queryParams: Object.assign({}, { page: 1 }, data), + const queryParams = Object.assign({}, data); + const pageParam = this.paginationService.getPageParam(this.searchConfig.paginationID); + queryParams[pageParam] = 1; + + this.router.navigate(this.getSearchLinkParts(), { + queryParams: queryParams, queryParamsHandling: 'merge' }); + this.paginationService.updateRouteWithUrl(this.searchConfig.paginationID, this.getSearchLinkParts(), { page: 1 }, data); } /** diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.spec.ts index 4b2e5d2344..7299a39c32 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.spec.ts @@ -15,6 +15,11 @@ import { FacetValue } from '../../../../facet-value.model'; import { FilterType } from '../../../../filter-type.model'; import { SearchFilterConfig } from '../../../../search-filter-config.model'; import { SearchFacetOptionComponent } from './search-facet-option.component'; +import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../../../../core/data/request.models'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; describe('SearchFacetOptionComponent', () => { let comp: SearchFacetOptionComponent; @@ -81,6 +86,8 @@ describe('SearchFacetOptionComponent', () => { let router; const page = observableOf(0); + const pagination = Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 }); + const paginationService = new PaginationServiceStub(pagination); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], @@ -88,8 +95,10 @@ describe('SearchFacetOptionComponent', () => { providers: [ { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: Router, useValue: new RouterStub() }, + { provide: PaginationService, useValue: paginationService }, { provide: SearchConfigurationService, useValue: { + paginationID: 'page-id', searchOptions: observableOf({}) } }, @@ -131,7 +140,7 @@ describe('SearchFacetOptionComponent', () => { (comp as any).updateAddParams(selectedValues); expect(comp.addQueryParams).toEqual({ [mockFilterConfig.paramName]: [`${value1},${operator}`, value.value + ',equals'], - page: 1 + ['page-id.page']: 1 }); }); }); @@ -146,7 +155,7 @@ describe('SearchFacetOptionComponent', () => { (comp as any).updateAddParams(selectedValues); expect(comp.addQueryParams).toEqual({ [mockAuthorityFilterConfig.paramName]: [value1 + ',equals', `${value2},${operator}`], - page: 1 + ['page-id.page']: 1 }); }); }); diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts index c6d7f4ac7c..11056a232c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts @@ -10,6 +10,7 @@ import { SearchConfigurationService } from '../../../../../../core/shared/search import { hasValue } from '../../../../../empty.util'; import { currentPath } from '../../../../../utils/route.utils'; import { getFacetValueForType } from '../../../../search.utils'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; @Component({ selector: 'ds-search-facet-option', @@ -60,10 +61,13 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy { */ sub: Subscription; + paginationId: string; + constructor(protected searchService: SearchService, protected filterService: SearchFilterService, protected searchConfigService: SearchConfigurationService, - protected router: Router + protected router: Router, + protected paginationService: PaginationService ) { } @@ -71,6 +75,7 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy { * Initializes all observable instance variables and starts listening to them */ ngOnInit(): void { + this.paginationId = this.searchConfigService.paginationID; this.searchLink = this.getSearchLink(); this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked)); this.sub = observableCombineLatest(this.selectedValues$, this.searchConfigService.searchOptions) @@ -101,9 +106,10 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy { * @param {string[]} selectedValues The values that are currently selected for this filter */ private updateAddParams(selectedValues: FacetValue[]): void { + const page = this.paginationService.getPageParam(this.searchConfigService.paginationID); this.addQueryParams = { [this.filterConfig.paramName]: [...selectedValues.map((facetValue: FacetValue) => getFacetValueForType(facetValue, this.filterConfig)), this.getFacetValue()], - page: 1 + [page]: 1 }; } diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts index f5df1f038a..9ed8dee0ea 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts @@ -19,6 +19,11 @@ import { RANGE_FILTER_MAX_SUFFIX, RANGE_FILTER_MIN_SUFFIX } from '../../search-range-filter/search-range-filter.component'; +import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../../../../core/data/request.models'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; describe('SearchFacetRangeOptionComponent', () => { let comp: SearchFacetRangeOptionComponent; @@ -54,6 +59,9 @@ describe('SearchFacetRangeOptionComponent', () => { let router; const page = observableOf(0); + const pagination = Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 }); + const paginationService = new PaginationServiceStub(pagination); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], @@ -61,9 +69,11 @@ describe('SearchFacetRangeOptionComponent', () => { providers: [ { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: Router, useValue: new RouterStub() }, + { provide: PaginationService, useValue: paginationService }, { provide: SearchConfigurationService, useValue: { - searchOptions: observableOf({}) + searchOptions: observableOf({}), + paginationId: 'page-id' } }, { @@ -116,7 +126,7 @@ describe('SearchFacetRangeOptionComponent', () => { expect(comp.changeQueryParams).toEqual({ [mockFilterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: ['50'], [mockFilterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: ['60'], - page: 1 + ['page-id.page']: 1 }); }); }); diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts index 56a075d333..3d8215b210 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts @@ -13,6 +13,7 @@ import { import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { hasValue } from '../../../../../empty.util'; import { currentPath } from '../../../../../utils/route.utils'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; const rangeDelimiter = '-'; @@ -65,7 +66,8 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy { constructor(protected searchService: SearchService, protected filterService: SearchFilterService, protected searchConfigService: SearchConfigurationService, - protected router: Router + protected router: Router, + protected paginationService: PaginationService ) { } @@ -104,10 +106,11 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy { const parts = this.filterValue.value.split(rangeDelimiter); const min = parts.length > 1 ? parts[0].trim() : this.filterValue.value; const max = parts.length > 1 ? parts[1].trim() : this.filterValue.value; + const page = this.paginationService.getPageParam(this.searchConfigService.paginationID); this.changeQueryParams = { [this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: [min], [this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: [max], - page: 1 + [page]: 1 }; } diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.spec.ts index 69945d86f2..8f422b41bf 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.spec.ts @@ -14,6 +14,11 @@ import { FacetValue } from '../../../../facet-value.model'; import { FilterType } from '../../../../filter-type.model'; import { SearchFilterConfig } from '../../../../search-filter-config.model'; import { SearchFacetSelectedOptionComponent } from './search-facet-selected-option.component'; +import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../../../../core/data/request.models'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; describe('SearchFacetSelectedOptionComponent', () => { let comp: SearchFacetSelectedOptionComponent; @@ -106,6 +111,9 @@ describe('SearchFacetSelectedOptionComponent', () => { let router; const page = observableOf(0); + const pagination = Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 }); + const paginationService = new PaginationServiceStub(pagination); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], @@ -113,6 +121,7 @@ describe('SearchFacetSelectedOptionComponent', () => { providers: [ { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: Router, useValue: new RouterStub() }, + { provide: PaginationService, useValue: paginationService }, { provide: SearchConfigurationService, useValue: { searchOptions: observableOf({}) @@ -156,7 +165,7 @@ describe('SearchFacetSelectedOptionComponent', () => { (comp as any).updateRemoveParams(selectedValues); expect(comp.removeQueryParams).toEqual({ [mockFilterConfig.paramName]: [value1], - page: 1 + ['page-id.page']: 1 }); }); }); @@ -172,7 +181,7 @@ describe('SearchFacetSelectedOptionComponent', () => { (comp as any).updateRemoveParams(selectedAuthorityValues); expect(comp.removeQueryParams).toEqual({ [mockAuthorityFilterConfig.paramName]: [`${value1},${operator}`], - page: 1 + ['page-id.page']: 1 }); }); }); diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts index 159effe751..d92455fdd9 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts @@ -9,6 +9,7 @@ import { SearchConfigurationService } from '../../../../../../core/shared/search import { FacetValue } from '../../../../facet-value.model'; import { currentPath } from '../../../../../utils/route.utils'; import { getFacetValueForType } from '../../../../search.utils'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; @Component({ selector: 'ds-search-facet-selected-option', @@ -58,7 +59,8 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy { constructor(protected searchService: SearchService, protected filterService: SearchFilterService, protected searchConfigService: SearchConfigurationService, - protected router: Router + protected router: Router, + protected paginationService: PaginationService ) { } @@ -88,14 +90,14 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy { * @param {string[]} selectedValues The values that are currently selected for this filter */ private updateRemoveParams(selectedValues: FacetValue[]): void { + const page = this.paginationService.getPageParam(this.searchConfigService.paginationID); this.removeQueryParams = { [this.filterConfig.paramName]: selectedValues .filter((facetValue: FacetValue) => facetValue.label !== this.selectedValue.label) .map((facetValue: FacetValue) => this.getFacetValue(facetValue)), - page: 1 + [page]: 1 }; } - /** * TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved * Retrieve facet value related to facet type diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts index 00868a86a2..35de768977 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts +++ b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts @@ -11,6 +11,12 @@ import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-pag import { SearchServiceStub } from '../../../testing/search-service.stub'; import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub'; import { SearchService } from '../../../../core/shared/search/search.service'; +import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../../core/data/request.models'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +import { PaginationServiceStub } from '../../../testing/pagination-service.stub'; describe('SearchLabelComponent', () => { let comp: SearchLabelComponent; @@ -33,6 +39,9 @@ describe('SearchLabelComponent', () => { filter2 ]; + const pagination = Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 }); + const paginationService = new PaginationServiceStub(pagination); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], @@ -40,6 +49,8 @@ describe('SearchLabelComponent', () => { providers: [ { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, + { provide: PaginationService, useValue: paginationService }, { provide: Router, useValue: {} } // { provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} } ], diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.ts b/src/app/shared/search/search-labels/search-label/search-label.component.ts index 8ae1a8dd1b..74526ad2ad 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.ts +++ b/src/app/shared/search/search-labels/search-label/search-label.component.ts @@ -5,6 +5,8 @@ import { map } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../../empty.util'; import { SearchService } from '../../../../core/shared/search/search.service'; import { currentPath } from '../../../utils/route.utils'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; @Component({ selector: 'ds-search-label', @@ -32,6 +34,8 @@ export class SearchLabelComponent implements OnInit { */ constructor( private searchService: SearchService, + private paginationService: PaginationService, + private searchConfigurationService: SearchConfigurationService, private router: Router) { } @@ -50,9 +54,10 @@ export class SearchLabelComponent implements OnInit { map((filters) => { const field: string = Object.keys(filters).find((f) => f === this.key); const newValues = hasValue(filters[field]) ? filters[field].filter((v) => v !== this.value) : null; + const page = this.paginationService.getPageParam(this.searchConfigurationService.paginationID); return { [field]: isNotEmpty(newValues) ? newValues : null, - page: 1 + [page]: 1 }; }) ); diff --git a/src/app/shared/search/search-settings/search-settings.component.spec.ts b/src/app/shared/search/search-settings/search-settings.component.spec.ts index cd4a872815..dff8935ef5 100644 --- a/src/app/shared/search/search-settings/search-settings.component.spec.ts +++ b/src/app/shared/search/search-settings/search-settings.component.spec.ts @@ -16,6 +16,8 @@ import { take } from 'rxjs/operators'; import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component'; import { SidebarService } from '../../sidebar/sidebar.service'; import { SidebarServiceStub } from '../../testing/sidebar-service.stub'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../testing/pagination-service.stub'; describe('SearchSettingsComponent', () => { @@ -32,6 +34,8 @@ describe('SearchSettingsComponent', () => { let scopeParam; let paginatedSearchOptions; + let paginationService; + let activatedRouteStub; beforeEach(waitForAsync(() => { @@ -62,6 +66,8 @@ describe('SearchSettingsComponent', () => { }), }; + paginationService = new PaginationServiceStub(pagination, sort); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], declarations: [SearchSettingsComponent, EnumKeysPipe, VarDirective], @@ -77,6 +83,10 @@ describe('SearchSettingsComponent', () => { provide: SearchFilterService, useValue: {}, }, + { + provide: PaginationService, + useValue: paginationService, + }, { provide: SEARCH_CONFIG_SERVICE, useValue: { diff --git a/src/app/shared/search/search-settings/search-settings.component.ts b/src/app/shared/search/search-settings/search-settings.component.ts index 45d7c7b432..2241fc6c6a 100644 --- a/src/app/shared/search/search-settings/search-settings.component.ts +++ b/src/app/shared/search/search-settings/search-settings.component.ts @@ -1,11 +1,12 @@ import { Component, Inject, OnInit } from '@angular/core'; import { SearchService } from '../../../core/shared/search/search.service'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; -import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; import { Observable } from 'rxjs'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component'; +import { PaginationService } from '../../../core/pagination/pagination.service'; @Component({ selector: 'ds-search-settings', @@ -30,6 +31,7 @@ export class SearchSettingsComponent implements OnInit { constructor(private service: SearchService, private route: ActivatedRoute, private router: Router, + private paginationService: PaginationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) { } @@ -46,14 +48,10 @@ export class SearchSettingsComponent implements OnInit { */ reloadOrder(event: Event) { const values = (event.target as HTMLInputElement).value.split(','); - const navigationExtras: NavigationExtras = { - queryParams: { - sortDirection: values[1], - sortField: values[0], - page: 1 - }, - queryParamsHandling: 'merge' - }; - this.router.navigate([], navigationExtras); + this.paginationService.updateRoute(this.searchConfigurationService.paginationID, { + sortField: values[0], + sortDirection: values[1] as SortDirection, + page: 1 + }); } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 70e4f1a716..4d5f1eaad3 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -224,6 +224,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'; @@ -434,6 +435,7 @@ const COMPONENTS = [ EpersonSearchBoxComponent, GroupSearchBoxComponent, FileDownloadLinkComponent, + BitstreamDownloadPageComponent, CollectionDropdownComponent, ExportMetadataSelectorComponent, ConfirmationModalComponent, @@ -512,6 +514,14 @@ const ENTRY_COMPONENTS = [ ClaimedTaskActionsRejectComponent, ClaimedTaskActionsReturnToPoolComponent, ClaimedTaskActionsEditMetadataComponent, + CollectionDropdownComponent, + FileDownloadLinkComponent, + BitstreamDownloadPageComponent, + CurationFormComponent, + ExportMetadataSelectorComponent, + ConfirmationModalComponent, + VocabularyTreeviewComponent, + SidebarSearchListElementComponent, PublicationSidebarSearchListElementComponent, CollectionSidebarSearchListElementComponent, CommunitySidebarSearchListElementComponent, diff --git a/src/app/shared/starts-with/date/starts-with-date.component.spec.ts b/src/app/shared/starts-with/date/starts-with-date.component.spec.ts index 33edcfa5fd..dfee88c955 100644 --- a/src/app/shared/starts-with/date/starts-with-date.component.spec.ts +++ b/src/app/shared/starts-with/date/starts-with-date.component.spec.ts @@ -11,12 +11,18 @@ import { StartsWithDateComponent } from './starts-with-date.component'; import { ActivatedRouteStub } from '../../testing/active-router.stub'; import { EnumKeysPipe } from '../../utils/enum-keys-pipe'; import { RouterStub } from '../../testing/router.stub'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../testing/pagination-service.stub'; describe('StartsWithDateComponent', () => { let comp: StartsWithDateComponent; let fixture: ComponentFixture; let route: ActivatedRoute; let router: Router; + let paginationService; const options = [2019, 2018, 2017, 2016, 2015]; @@ -25,13 +31,17 @@ describe('StartsWithDateComponent', () => { queryParams: observableOf({}) }); + paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [StartsWithDateComponent, EnumKeysPipe], providers: [ { provide: 'startsWithOptions', useValue: options }, + { provide: 'paginationId', useValue: 'page-id' }, { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: PaginationService, useValue: paginationService }, { provide: Router, useValue: new RouterStub() } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/starts-with/date/starts-with-date.component.ts b/src/app/shared/starts-with/date/starts-with-date.component.ts index 75173212f9..e947fb76a2 100644 --- a/src/app/shared/starts-with/date/starts-with-date.component.ts +++ b/src/app/shared/starts-with/date/starts-with-date.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { renderStartsWithFor, StartsWithType } from '../starts-with-decorator'; import { StartsWithAbstractComponent } from '../starts-with-abstract.component'; import { hasValue } from '../../empty.util'; +import { PaginationService } from '../../../core/pagination/pagination.service'; /** * A switchable component rendering StartsWith options for the type "Date". @@ -33,9 +34,11 @@ export class StartsWithDateComponent extends StartsWithAbstractComponent { startsWithYear: number; public constructor(@Inject('startsWithOptions') public startsWithOptions: any[], + @Inject('paginationId') public paginationId: string, + protected paginationService: PaginationService, protected route: ActivatedRoute, protected router: Router) { - super(startsWithOptions, route, router); + super(startsWithOptions, paginationId, paginationService, route, router); } ngOnInit() { diff --git a/src/app/shared/starts-with/starts-with-abstract.component.ts b/src/app/shared/starts-with/starts-with-abstract.component.ts index 229777e96d..b0c7015a12 100644 --- a/src/app/shared/starts-with/starts-with-abstract.component.ts +++ b/src/app/shared/starts-with/starts-with-abstract.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; import { FormControl, FormGroup } from '@angular/forms'; import { hasValue } from '../empty.util'; +import { PaginationService } from '../../core/pagination/pagination.service'; /** * An abstract component to render StartsWith options @@ -28,6 +29,8 @@ export abstract class StartsWithAbstractComponent implements OnInit, OnDestroy { subs: Subscription[] = []; public constructor(@Inject('startsWithOptions') public startsWithOptions: any[], + @Inject('paginationId') public paginationId: string, + protected paginationService: PaginationService, protected route: ActivatedRoute, protected router: Router) { } diff --git a/src/app/shared/starts-with/text/starts-with-text.component.spec.ts b/src/app/shared/starts-with/text/starts-with-text.component.spec.ts index 780c221294..9f9d9d6d42 100644 --- a/src/app/shared/starts-with/text/starts-with-text.component.spec.ts +++ b/src/app/shared/starts-with/text/starts-with-text.component.spec.ts @@ -8,6 +8,12 @@ import { EnumKeysPipe } from '../../utils/enum-keys-pipe'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { StartsWithTextComponent } from './starts-with-text.component'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { of as observableOf } from 'rxjs'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../testing/pagination-service.stub'; describe('StartsWithTextComponent', () => { let comp: StartsWithTextComponent; @@ -17,12 +23,16 @@ describe('StartsWithTextComponent', () => { const options = ['0-9', 'A', 'B', 'C']; + const paginationService = new PaginationServiceStub(); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [StartsWithTextComponent, EnumKeysPipe], providers: [ - { provide: 'startsWithOptions', useValue: options } + { provide: 'startsWithOptions', useValue: options }, + { provide: 'paginationId', useValue: 'page-id' }, + { provide: PaginationService, useValue: paginationService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/testing/pagination-service.stub.ts b/src/app/shared/testing/pagination-service.stub.ts new file mode 100644 index 0000000000..985a5bfc4a --- /dev/null +++ b/src/app/shared/testing/pagination-service.stub.ts @@ -0,0 +1,25 @@ +import { of as observableOf } from 'rxjs'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../core/data/request.models'; + +export class PaginationServiceStub { + + constructor( + public pagination = Object.assign(new PaginationComponentOptions(), {currentPage: 1, pageSize: 20}), + public sort = new SortOptions('score', SortDirection.DESC), + public findlistOptions = Object.assign(new FindListOptions(), {currentPage: 1, elementsPerPage: 20}), + ) { + } + + getCurrentPagination = jasmine.createSpy('getCurrentPagination').and.returnValue(observableOf(this.pagination)); + getCurrentSort = jasmine.createSpy('getCurrentSort').and.returnValue(observableOf(this.sort)); + getFindListOptions = jasmine.createSpy('getFindListOptions').and.returnValue(observableOf(this.findlistOptions)); + resetPage = jasmine.createSpy('resetPage'); + updateRoute = jasmine.createSpy('updateRoute'); + updateRouteWithUrl = jasmine.createSpy('updateRouteWithUrl'); + clearPagination = jasmine.createSpy('clearPagination'); + getRouteParameterValue = jasmine.createSpy('getRouteParameterValue').and.returnValue(observableOf('')); + getPageParam = jasmine.createSpy('getPageParam').and.returnValue(`${this.pagination.id}.page`); + +} diff --git a/src/app/shared/testing/search-configuration-service.stub.ts b/src/app/shared/testing/search-configuration-service.stub.ts index 4c9402afb1..4b8f1d6f12 100644 --- a/src/app/shared/testing/search-configuration-service.stub.ts +++ b/src/app/shared/testing/search-configuration-service.stub.ts @@ -2,6 +2,8 @@ import { BehaviorSubject, of as observableOf } from 'rxjs'; export class SearchConfigurationServiceStub { + public paginationID = 'test-id'; + private searchOptions: BehaviorSubject = new BehaviorSubject({}); private paginatedSearchOptions: BehaviorSubject = new BehaviorSubject({}); diff --git a/src/app/shared/uploader/uploader.component.html b/src/app/shared/uploader/uploader.component.html index 36078fbeb4..4f7d507404 100644 --- a/src/app/shared/uploader/uploader.component.html +++ b/src/app/shared/uploader/uploader.component.html @@ -1,5 +1,5 @@
    { diff --git a/src/app/submission/edit/submission-edit.component.spec.ts b/src/app/submission/edit/submission-edit.component.spec.ts index 828edd00e8..7b730b7a73 100644 --- a/src/app/submission/edit/submission-edit.component.spec.ts +++ b/src/app/submission/edit/submission-edit.component.spec.ts @@ -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; 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(); - })); + }); }); diff --git a/src/app/submission/edit/submission-edit.component.ts b/src/app/submission/edit/submission-edit.component.ts index 7908a052b7..34fdcba104 100644 --- a/src/app/submission/edit/submission-edit.component.ts +++ b/src/app/submission/edit/submission-edit.component.ts @@ -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 = 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,32 +99,47 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { * Retrieve workspaceitem/workflowitem from server and initialize all instance variables */ ngOnInit() { - 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) => isNotNull(submissionObjectRD)) - ).subscribe((submissionObjectRD: RemoteData) => { - if (submissionObjectRD.hasSucceeded) { - if (isEmpty(submissionObjectRD.payload)) { - this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); - this.router.navigate(['/mydspace']); + 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) => isNotNull(submissionObjectRD)) + ).subscribe((submissionObjectRD: RemoteData) => { + if (submissionObjectRD.hasSucceeded) { + if (isEmpty(submissionObjectRD.payload)) { + this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); + this.router.navigate(['/mydspace']); + } else { + this.submissionId = submissionObjectRD.payload.id.toString(); + this.collectionId = (submissionObjectRD.payload.collection as Collection).id; + this.selfUrl = submissionObjectRD.payload._links.self.href; + this.sections = submissionObjectRD.payload.sections; + this.itemLink$.next(submissionObjectRD.payload._links.item.href); + this.item = submissionObjectRD.payload.item; + this.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel); + } } else { - this.submissionId = submissionObjectRD.payload.id.toString(); - 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.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel); - this.changeDetectorRef.detectChanges(); + if (submissionObjectRD.statusCode === 404) { + // redirect to not found page + this.router.navigate(['/404'], { skipLocationChange: true }); + } + // TODO handle generic error } - } else { - if (submissionObjectRD.statusCode === 404) { - // redirect to not found page - this.router.navigate(['/404'], { skipLocationChange: true }); - } - // 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) => { + this.item = itemRd.payload; + this.changeDetectorRef.detectChanges(); + }), + ); } /** diff --git a/src/app/submission/form/submission-form.component.html b/src/app/submission/form/submission-form.component.html index 7376b1e10b..33b5d4be12 100644 --- a/src/app/submission/form/submission-form.component.html +++ b/src/app/submission/form/submission-form.component.html @@ -1,35 +1,36 @@
    -
    - -
    -
    -
    -
    - - -
    -
    - - -
    -
    +
    +
    + +
    +
    -
    - - - - -
    - +
    + + +
    +
    + + +
    +
    + +
    + + + + +
    +
    diff --git a/src/app/submission/form/submission-form.component.spec.ts b/src/app/submission/form/submission-form.component.spec.ts index d719472adf..dd8e6d0ea3 100644 --- a/src/app/submission/form/submission-form.component.spec.ts +++ b/src/app/submission/form/submission-form.component.spec.ts @@ -24,7 +24,7 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic import { createTestComponent } from '../../shared/testing/utils.test'; import { Item } from '../../core/shared/item.model'; import { TestScheduler } from 'rxjs/testing'; - +import { SectionsService } from '../sections/sections.service'; describe('SubmissionFormComponent Component', () => { @@ -55,6 +55,7 @@ describe('SubmissionFormComponent Component', () => { { provide: AuthService, useClass: AuthServiceStub }, { provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') }, { provide: SubmissionService, useValue: submissionServiceStub }, + { provide: SectionsService, useValue: { isSectionTypeAvailable: () => observableOf(true) } }, ChangeDetectorRef, SubmissionFormComponent ], @@ -115,7 +116,7 @@ describe('SubmissionFormComponent Component', () => { expect(compAsAny.submissionSections).toBeUndefined(); expect(compAsAny.subs).toEqual([]); expect(submissionServiceStub.startAutoSave).not.toHaveBeenCalled(); - expect(comp.loading).toBeObservable(cold('(a|)', {a: true})); + expect(comp.loading).toBeObservable(cold('(a|)', { a: true })); done(); }); @@ -140,7 +141,7 @@ describe('SubmissionFormComponent Component', () => { }); scheduler.flush(); - expect(comp.submissionSections).toBeObservable(cold('(a|)', {a: sectionsList})); + expect(comp.submissionSections).toBeObservable(cold('(a|)', { a: sectionsList })); expect(submissionServiceStub.dispatchInit).toHaveBeenCalledWith( collectionId, @@ -201,7 +202,7 @@ describe('SubmissionFormComponent Component', () => { submissionDefinition: { name: 'traditional' } - } as any); + } as any); fixture.detectChanges(); }); scheduler.flush(); diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index dc6f264d9b..8df0ab1658 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -15,6 +15,8 @@ import { SubmissionObjectEntry } from '../objects/submission-objects.reducer'; import { SectionDataObject } from '../sections/models/section-data.model'; import { SubmissionService } from '../submission.service'; import { Item } from '../../core/shared/item.model'; +import { SectionsType } from '../sections/sections-type'; +import { SectionsService } from '../sections/sections.service'; /** * This component represents the submission form. @@ -69,6 +71,11 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { */ public loading: Observable = observableOf(true); + /** + * Emits true when the submission config has bitstream uploading enabled in submission + */ + public uploadEnabled$: Observable; + /** * Observable of the list of submission's sections * @type {Observable} @@ -100,12 +107,14 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { * @param {ChangeDetectorRef} changeDetectorRef * @param {HALEndpointService} halService * @param {SubmissionService} submissionService + * @param {SectionsService} sectionsService */ constructor( private authService: AuthService, private changeDetectorRef: ChangeDetectorRef, private halService: HALEndpointService, - private submissionService: SubmissionService) { + private submissionService: SubmissionService, + private sectionsService: SectionsService) { this.isActive = true; } @@ -129,6 +138,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { return observableOf([]); } })); + this.uploadEnabled$ = this.sectionsService.isSectionTypeAvailable(this.submissionId, SectionsType.Upload); // check if is submission loading this.loading = this.submissionService.getSubmissionObject(this.submissionId).pipe( diff --git a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts index 5e26df829c..30f5184d57 100644 --- a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts +++ b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts @@ -18,7 +18,7 @@ import { RemoteData } from '../../../core/data/remote-data'; import { PageInfo } from '../../../core/shared/page-info.model'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { FindListOptions } from '../../../core/data/request.models'; -import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { HostWindowService } from '../../../shared/host-window.service'; import { hasValue } from '../../../shared/empty.util'; @@ -124,7 +124,7 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); return observableOf(paginatedListRD); }), - getFirstSucceededRemoteDataPayload() + getFirstSucceededRemoteDataPayload(), ).subscribe((externalSource: PaginatedList) => { externalSource.page.forEach((element) => { this.sourceList.push({ id: element.id, name: element.name }); @@ -166,6 +166,7 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); return observableOf(paginatedListRD); }), + getFirstSucceededRemoteData(), tap(() => this.sourceListLoading = false) ).subscribe((externalSource: RemoteData>) => { externalSource.payload.page.forEach((element) => { diff --git a/src/app/submission/import-external/submission-import-external.component.html b/src/app/submission/import-external/submission-import-external.component.html index bee5f5d872..3c7ed3cd64 100644 --- a/src/app/submission/import-external/submission-import-external.component.html +++ b/src/app/submission/import-external/submission-import-external.component.html @@ -20,8 +20,7 @@ [context]="context" [importable]="true" [importConfig]="importConfig" - (importObject)="import($event)" - (pageChange)="paginationChange();"> + (importObject)="import($event)"> diff --git a/src/app/submission/import-external/submission-import-external.component.spec.ts b/src/app/submission/import-external/submission-import-external.component.spec.ts index 3a69a3727e..c0b85b696b 100644 --- a/src/app/submission/import-external/submission-import-external.component.spec.ts +++ b/src/app/submission/import-external/submission-import-external.component.spec.ts @@ -115,15 +115,22 @@ describe('SubmissionImportExternalComponent test suite', () => { spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValues(observableOf('source'), observableOf('dummy')); fixture.detectChanges(); - expect(compAsAny.retrieveExternalSources).toHaveBeenCalledWith('source', 'dummy'); + expect(compAsAny.retrieveExternalSources).toHaveBeenCalled(); }); it('Should call \'getExternalSourceEntries\' properly', () => { - comp.routeData = { sourceId: '', query: '' }; - scheduler.schedule(() => compAsAny.retrieveExternalSources('orcidV2', 'test')); - scheduler.flush(); + spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => { + if (param === 'source') { + return observableOf('orcidV2'); + } else if (param === 'query') { + return observableOf('test'); + } + return observableOf({}); + }); + + fixture.detectChanges(); + - expect(comp.routeData).toEqual({ sourceId: 'orcidV2', query: 'test' }); expect(comp.isLoading$.value).toBe(false); expect(compAsAny.externalService.getExternalSourceEntries).toHaveBeenCalled(); }); diff --git a/src/app/submission/import-external/submission-import-external.component.ts b/src/app/submission/import-external/submission-import-external.component.ts index 7a7ec2a4a3..e8370a9be0 100644 --- a/src/app/submission/import-external/submission-import-external.component.ts +++ b/src/app/submission/import-external/submission-import-external.component.ts @@ -2,13 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; -import { filter, mergeMap, take } from 'rxjs/operators'; +import { filter, mergeMap, switchMap, take, tap } from 'rxjs/operators'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { ExternalSourceService } from '../../core/data/external-source.service'; import { ExternalSourceData } from './import-external-searchbar/submission-import-external-searchbar.component'; import { RemoteData } from '../../core/data/remote-data'; -import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model'; +import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { Context } from '../../core/shared/context.model'; @@ -44,6 +44,11 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { * TRUE if the REST service is called to retrieve the external source items */ public isLoading$: BehaviorSubject = new BehaviorSubject(false); + + public reload$: BehaviorSubject<{ query: string, source: string }> = new BehaviorSubject<{ query: string; source: string }>({ + query: '', + source: '' + }); /** * Configuration to use for the import buttons */ @@ -64,7 +69,7 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { * The initial pagination options */ public initialPagination = Object.assign(new PaginationComponentOptions(), { - id: 'submission-external-source-relation-list', + id: 'spc', pageSize: 10 }); /** @@ -81,6 +86,8 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { */ protected subs: Subscription[] = []; + private retrieveExternalSourcesSub: Subscription; + /** * Initialize the component variables. * @param {SearchConfigurationService} searchConfigService @@ -106,7 +113,7 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { this.listId = 'list-submission-external-sources'; this.context = Context.EntitySearchModalWithNameVariants; this.repeatable = false; - this.routeData = { sourceId: '', query: '' }; + this.routeData = {sourceId: '', query: ''}; this.importConfig = { buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label }; @@ -119,7 +126,8 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { ]).pipe( take(1) ).subscribe(([source, query]: [string, string]) => { - this.retrieveExternalSources(source, query); + this.reload$.next({query: query, source: source}); + this.retrieveExternalSources(); })); } @@ -130,10 +138,13 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { this.router.navigate( [], { - queryParams: { source: event.sourceId, query: event.query }, + queryParams: {source: event.sourceId, query: event.query}, replaceUrl: true } - ).then(() => this.retrieveExternalSources(event.sourceId, event.query)); + ).then(() => { + this.reload$.next({source: event.sourceId, query: event.query}); + this.retrieveExternalSources(); + }); } /** @@ -148,13 +159,6 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { modalComp.externalSourceEntry = entry; } - /** - * Retrieve external sources on pagination change - */ - paginationChange() { - this.retrieveExternalSources(this.routeData.sourceId, this.routeData.query); - } - /** * Unsubscribe from all subscriptions */ @@ -162,6 +166,10 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { this.subs .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); + if (hasValue(this.retrieveExternalSourcesSub)) { + this.retrieveExternalSourcesSub.unsubscribe(); + } + } /** @@ -170,26 +178,30 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { * @param source The source tupe * @param query The query string to search */ - private retrieveExternalSources(source: string, query: string): void { - if (isNotEmpty(source) && isNotEmpty(query)) { - this.routeData.sourceId = source; - this.routeData.query = query; - this.isLoading$.next(true); - this.subs.push( - this.searchConfigService.paginatedSearchOptions.pipe( - filter((searchOptions) => searchOptions.query === query), - take(1), - mergeMap((searchOptions) => this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions).pipe( - getFinishedRemoteData(), - take(1) - )), - take(1) - ).subscribe((rdData) => { - this.entriesRD$.next(rdData); - this.isLoading$.next(false); - }) - ); + private retrieveExternalSources(): void { + if (hasValue(this.retrieveExternalSourcesSub)) { + this.retrieveExternalSourcesSub.unsubscribe(); } + this.retrieveExternalSourcesSub = this.reload$.pipe( + filter((sourceQueryObject: { source: string, query: string }) => isNotEmpty(sourceQueryObject.source) && isNotEmpty(sourceQueryObject.query)), + switchMap((sourceQueryObject: { source: string, query: string }) => { + const source = sourceQueryObject.source; + const query = sourceQueryObject.query; + this.routeData.sourceId = source; + this.routeData.query = query; + return this.searchConfigService.paginatedSearchOptions.pipe( + tap((v) => this.isLoading$.next(true)), + filter((searchOptions) => searchOptions.query === query), + mergeMap((searchOptions) => this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions).pipe( + getFinishedRemoteData(), + )), + ); + } + ), + ).subscribe((rdData) => { + this.entriesRD$.next(rdData); + this.isLoading$.next(false); + }); } } diff --git a/src/app/submission/sections/license/section-license.component.html b/src/app/submission/sections/license/section-license.component.html index b8d0f601d2..5ee8a2db6b 100644 --- a/src/app/submission/sections/license/section-license.component.html +++ b/src/app/submission/sections/license/section-license.component.html @@ -1,4 +1,4 @@ -{{ licenseText$ | async }} +

    { const sectionErrors: any = parseSectionErrors(mockSectionsErrors); const sectionData: any = mockSectionsData; const submissionState: any = Object.assign({}, mockSubmissionState[submissionId]); + const submissionStateWithoutUpload: any = Object.assign({}, mockSubmissionStateWithoutUpload[submissionId]); const sectionState: any = Object.assign({}, mockSubmissionState['826'].sections[sectionId]); const store: any = jasmine.createSpyObj('store', { @@ -314,6 +312,28 @@ describe('SectionsService test suite', () => { }); }); + describe('isSectionTypeAvailable', () => { + it('should return an observable of true when section is available', () => { + store.select.and.returnValue(observableOf(submissionState)); + + const expected = cold('(b|)', { + b: true + }); + + expect(service.isSectionTypeAvailable(submissionId, SectionsType.Upload)).toBeObservable(expected); + }); + + it('should return an observable of false when section is not available', () => { + store.select.and.returnValue(observableOf(submissionStateWithoutUpload)); + + const expected = cold('(b|)', { + b: false + }); + + expect(service.isSectionAvailable(submissionId, SectionsType.Upload)).toBeObservable(expected); + }); + }); + describe('addSection', () => { it('should dispatch a new EnableSectionAction a move target to new section', () => { diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index b2bc0e4f98..d8d1491cb7 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -5,7 +5,7 @@ import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; -import { isEqual } from 'lodash'; +import { findKey, isEqual } from 'lodash'; import { SubmissionState } from '../submission.reducers'; import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; @@ -291,14 +291,14 @@ export class SectionsService { } /** - * Check if a given section is a read only available + * Check if a given section id is present in the list of sections * * @param submissionId * The submission id * @param sectionId * The section id * @return Observable - * Emits true whenever a given section should be available + * Emits true whenever a given section id should be available */ public isSectionAvailable(submissionId: string, sectionId: string): Observable { return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe( @@ -309,6 +309,25 @@ export class SectionsService { distinctUntilChanged()); } + /** + * Check if a given section type is present in the list of sections + * + * @param submissionId + * The submission id + * @param sectionType + * The section type + * @return Observable + * Emits true whenever a given section type should be available + */ + public isSectionTypeAvailable(submissionId: string, sectionType: SectionsType): Observable { + return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe( + filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)), + map((submissionState: SubmissionObjectEntry) => { + return isNotUndefined(submissionState.sections) && isNotUndefined(findKey(submissionState.sections, {sectionType: sectionType})); + }), + distinctUntilChanged()); + } + /** * Dispatch a new [EnableSectionAction] to add a new section and move page target to it * diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts index 730a456faa..ac91e5eb3c 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts @@ -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(() => { diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.ts b/src/app/submission/sections/upload/file/section-upload-file.component.ts index 80945bc1fd..5a97140a70 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.ts @@ -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); }); } diff --git a/src/app/submission/submit/submission-submit.component.spec.ts b/src/app/submission/submit/submission-submit.component.spec.ts index 5a446f8b6b..16e26e3b33 100644 --- a/src/app/submission/submit/submission-submit.component.spec.ts +++ b/src/app/submission/submit/submission-submit.component.spec.ts @@ -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; 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(); - })); + }); }); diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index 003f5280a8..0c2172368a 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -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 = 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,11 +127,24 @@ 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.changeDetectorRef.detectChanges(); } } - }) + }), + 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) => { + this.item = itemRd.payload; + this.changeDetectorRef.detectChanges(); + }) ); } diff --git a/src/assets/i18n/ar.json5 b/src/assets/i18n/ar.json5 index 8383c440cd..0d036ff7d3 100644 --- a/src/assets/i18n/ar.json5 +++ b/src/assets/i18n/ar.json5 @@ -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 diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 2c7ca09930..0be4403ec9 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -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 diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index 306f1fcc51..18b0cae03d 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -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 diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 6823b10da1..7fa6978bef 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -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 \"Main article\" or \"Experiment data readings\".", @@ -1237,7 +1241,7 @@ "footer.link.dspace": "DSpace software", - "footer.link.duraspace": "DuraSpace", + "footer.link.lyrasis": "LYRASIS", "footer.link.cookies": "Cookie settings", diff --git a/src/assets/i18n/es.json5 b/src/assets/i18n/es.json5 index 5edaaf0cf1..480fc5d1ae 100644 --- a/src/assets/i18n/es.json5 +++ b/src/assets/i18n/es.json5 @@ -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 diff --git a/src/assets/i18n/fi.json5 b/src/assets/i18n/fi.json5 index 89d5955725..0348b906c2 100644 --- a/src/assets/i18n/fi.json5 +++ b/src/assets/i18n/fi.json5 @@ -1,5635 +1,5136 @@ { - + // "401.help": "You're not authorized to access this page. You can use the button below to get back to the home page.", - // TODO New key - Add a translation - "401.help": "You're not authorized to access this page. You can use the button below to get back to the home page.", - + "401.help": "Sinulla ei ole valtuuksia nähdä sivua. Voit palata etusivulle alla olevalla painikkeella.", + // "401.link.home-page": "Take me to the home page", - // TODO New key - Add a translation - "401.link.home-page": "Take me to the home page", - + "401.link.home-page": "Palaa etusivulle", + // "401.unauthorized": "unauthorized", - // TODO New key - Add a translation - "401.unauthorized": "unauthorized", - - - + "401.unauthorized": "valtuuttamaton", + + + // "403.help": "You don't have permission to access this page. You can use the button below to get back to the home page.", - // TODO New key - Add a translation - "403.help": "You don't have permission to access this page. You can use the button below to get back to the home page.", - + "403.help": "Sinulla ei ole oikeutta nähdä sivua. Voit palata etusivulle alla olevalla painikkeella.", + // "403.link.home-page": "Take me to the home page", - // TODO New key - Add a translation - "403.link.home-page": "Take me to the home page", - + "403.link.home-page": "Palaa etusivulle", + // "403.forbidden": "forbidden", - // TODO New key - Add a translation - "403.forbidden": "forbidden", - - - + "403.forbidden": "kielletty", + + + // "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", - "404.help": "Hakemaasi sivua ei löytynyt. Sivu on voitu siirtää tai poistaa. Painamalla alapuolella olevaa nappia palaat kotisivulle. ", - + "404.help": "Hakemaasi sivua ei löytynyt. Sivu on voitu siirtää tai poistaa. Painamalla alapuolella olevaa nappia palaat etusivulle. ", + // "404.link.home-page": "Take me to the home page", - "404.link.home-page": "Palaa kotisivulle", - + "404.link.home-page": "Palaa etusivulle", + // "404.page-not-found": "page not found", "404.page-not-found": "sivua ei löytynyt", - + // "admin.curation-tasks.breadcrumbs": "System curation tasks", - // TODO New key - Add a translation - "admin.curation-tasks.breadcrumbs": "System curation tasks", - + "admin.curation-tasks.breadcrumbs": "Järjestelmän kuratointitehtävät", + // "admin.curation-tasks.title": "System curation tasks", - // TODO New key - Add a translation - "admin.curation-tasks.title": "System curation tasks", - + "admin.curation-tasks.title": "Järjestelmän kuratointitehtävät", + // "admin.curation-tasks.header": "System curation tasks", - // TODO New key - Add a translation - "admin.curation-tasks.header": "System curation tasks", - + "admin.curation-tasks.header": "Järjestelmän kuratointitehtävät", + // "admin.registries.bitstream-formats.breadcrumbs": "Format registry", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.breadcrumbs": "Format registry", - + "admin.registries.bitstream-formats.breadcrumbs": "Formaattirekisteri", + // "admin.registries.bitstream-formats.create.breadcrumbs": "Bitstream format", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.breadcrumbs": "Bitstream format", - + "admin.registries.bitstream-formats.create.breadcrumbs": "Tiedostoformaatti", + // "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", "admin.registries.bitstream-formats.create.failure.content": "Virhe uutta tiedostoformaattia luotaessa.", - + // "admin.registries.bitstream-formats.create.failure.head": "Failure", "admin.registries.bitstream-formats.create.failure.head": "Virhe", - + // "admin.registries.bitstream-formats.create.head": "Create Bitstream format", "admin.registries.bitstream-formats.create.head": "Luo tiedostoformaatti", - + // "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", "admin.registries.bitstream-formats.create.new": "Lisää uusi tiedostoformaatti", - + // "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", "admin.registries.bitstream-formats.create.success.content": "Uusi tiedostoformaatti luotu.", - + // "admin.registries.bitstream-formats.create.success.head": "Success", "admin.registries.bitstream-formats.create.success.head": "Valmis", - + // "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", "admin.registries.bitstream-formats.delete.failure.amount": "{{ amount }} formaatin poisto epäonnistui", - + // "admin.registries.bitstream-formats.delete.failure.head": "Failure", "admin.registries.bitstream-formats.delete.failure.head": "Virhe", - + // "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", "admin.registries.bitstream-formats.delete.success.amount": "Poistettu {{ amount }} formaatti(a)", - + // "admin.registries.bitstream-formats.delete.success.head": "Success", "admin.registries.bitstream-formats.delete.success.head": "Valmis", - + // "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", "admin.registries.bitstream-formats.description": "Tässä luetellaan tiedostoformaatit ja niiden tukitasot.", - + // "admin.registries.bitstream-formats.edit.breadcrumbs": "Bitstream format", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.breadcrumbs": "Bitstream format", - + "admin.registries.bitstream-formats.edit.breadcrumbs": "Tiedostoformaatti", + // "admin.registries.bitstream-formats.edit.description.hint": "", "admin.registries.bitstream-formats.edit.description.hint": "", - + // "admin.registries.bitstream-formats.edit.description.label": "Description", "admin.registries.bitstream-formats.edit.description.label": "Kuvaus", - + // "admin.registries.bitstream-formats.edit.extensions.hint": "Extensions are file extensions that are used to automatically identify the format of uploaded files. You can enter several extensions for each format.", "admin.registries.bitstream-formats.edit.extensions.hint": "Tarkenteet ovat tiedostopäätteitä, joita käytetään tallennettujen tiedostojen formaatin automaattiseen tunnistamiseen. Yhtä formaattia voi vastata useampi tiedostopääte.", - + // "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", "admin.registries.bitstream-formats.edit.extensions.label": "Tiedostopäätteet", - + // "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extension without the dot", "admin.registries.bitstream-formats.edit.extensions.placeholder": "Lisää tiedostopääte ilman pistettä", - + // "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", "admin.registries.bitstream-formats.edit.failure.content": "Virhe tiedostoformaattia muokattaessa.", - + // "admin.registries.bitstream-formats.edit.failure.head": "Failure", "admin.registries.bitstream-formats.edit.failure.head": "Virhe", - + // "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", "admin.registries.bitstream-formats.edit.head": "Tiedostoformaatti: {{ format }}", - + // "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are hidden from the user, and used for administrative purposes.", "admin.registries.bitstream-formats.edit.internal.hint": "Sisäisiksi merkittyjä formaatteja käytetään hallinnollisiin tarkoituksiin, ja ne on piilotettu käyttäjiltä.", - + // "admin.registries.bitstream-formats.edit.internal.label": "Internal", "admin.registries.bitstream-formats.edit.internal.label": "Sisäinen", - + // "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", "admin.registries.bitstream-formats.edit.mimetype.hint": "Tiedostoformaatin MIME-tyyppi. MIME-tyypin ei tarvitse olla yksilöllinen.", - + // "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", "admin.registries.bitstream-formats.edit.mimetype.label": "MIME-tyyppi", - + // "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", "admin.registries.bitstream-formats.edit.shortDescription.hint": "Formaatin yksilöllinen nimi (esim. Microsoft Word XP tai Microsoft Word 2000)", - + // "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", "admin.registries.bitstream-formats.edit.shortDescription.label": "Nimi", - + // "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", "admin.registries.bitstream-formats.edit.success.content": "Tiedostoformaattia muokattu.", - + // "admin.registries.bitstream-formats.edit.success.head": "Success", "admin.registries.bitstream-formats.edit.success.head": "Valmis", - + // "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", "admin.registries.bitstream-formats.edit.supportLevel.hint": "Tukitaso, jonka järjestelmää ylläpitävä instituutio takaa tiedostoformaatille.", - + // "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", "admin.registries.bitstream-formats.edit.supportLevel.label": "Tukitaso", - + // "admin.registries.bitstream-formats.head": "Bitstream Format Registry", "admin.registries.bitstream-formats.head": "Tiedostoformaattirekisteri", - + // "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", "admin.registries.bitstream-formats.no-items": "Ei tiedostoformaatteja.", - + // "admin.registries.bitstream-formats.table.delete": "Delete selected", "admin.registries.bitstream-formats.table.delete": "Poista valittu", - + // "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", "admin.registries.bitstream-formats.table.deselect-all": "Poista kaikkien valinta", - + // "admin.registries.bitstream-formats.table.internal": "internal", "admin.registries.bitstream-formats.table.internal": "sisäinen", - + // "admin.registries.bitstream-formats.table.mimetype": "MIME Type", "admin.registries.bitstream-formats.table.mimetype": "MIME-tyyppi", - + // "admin.registries.bitstream-formats.table.name": "Name", "admin.registries.bitstream-formats.table.name": "Nimi", - + // "admin.registries.bitstream-formats.table.return": "Return", "admin.registries.bitstream-formats.table.return": "Palaa", - + // "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Tunnettu", - + // "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Tuettu", - + // "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Tuntematon", - + // "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", "admin.registries.bitstream-formats.table.supportLevel.head": "Tukitaso", - + // "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", "admin.registries.bitstream-formats.title": "DSpace Angular :: Tiedostoformaattirekisteri", - - - + + + // "admin.registries.metadata.breadcrumbs": "Metadata registry", - // TODO New key - Add a translation - "admin.registries.metadata.breadcrumbs": "Metadata registry", - + "admin.registries.metadata.breadcrumbs": "Metadatarekisteri", + // "admin.registries.metadata.description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.", "admin.registries.metadata.description": "Metadatarekisteriin on koottu tässä julkaisuarkistossa käytössä olevat metadatakentät. Kentät voivat jakautua eri skeemoihin. DSpace-alusta edellyttää Qualified Dublin Core -skeeman käyttöä.", - + // "admin.registries.metadata.form.create": "Create metadata schema", "admin.registries.metadata.form.create": "Luo metadataskeema", - + // "admin.registries.metadata.form.edit": "Edit metadata schema", "admin.registries.metadata.form.edit": "Muokkaa metadataskeemaa", - + // "admin.registries.metadata.form.name": "Name", "admin.registries.metadata.form.name": "Nimi", - + // "admin.registries.metadata.form.namespace": "Namespace", "admin.registries.metadata.form.namespace": "Nimiavaruus", - + // "admin.registries.metadata.head": "Metadata Registry", "admin.registries.metadata.head": "Metadatarekisteri", - + // "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", "admin.registries.metadata.schemas.no-items": "Ei metadataskeemoja.", - + // "admin.registries.metadata.schemas.table.delete": "Delete selected", "admin.registries.metadata.schemas.table.delete": "Poista valittu", - + // "admin.registries.metadata.schemas.table.id": "ID", "admin.registries.metadata.schemas.table.id": "ID-tunnus", - + // "admin.registries.metadata.schemas.table.name": "Name", "admin.registries.metadata.schemas.table.name": "Nimi", - + // "admin.registries.metadata.schemas.table.namespace": "Namespace", "admin.registries.metadata.schemas.table.namespace": "Nimiavaruus", - + // "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", "admin.registries.metadata.title": "DSpace Angular :: Metadatarekisteri", - - - + + + // "admin.registries.schema.breadcrumbs": "Metadata schema", - // TODO New key - Add a translation - "admin.registries.schema.breadcrumbs": "Metadata schema", - + "admin.registries.schema.breadcrumbs": "Metadataskeema", + // "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", "admin.registries.schema.description": "Tämä on metadataskeema \"{{namespace}}\"-nimiavaruudelle.", - + // "admin.registries.schema.fields.head": "Schema metadata fields", "admin.registries.schema.fields.head": "Skeeman metadatakentät", - + // "admin.registries.schema.fields.no-items": "No metadata fields to show.", "admin.registries.schema.fields.no-items": "Ei metadatakenttiä.", - + // "admin.registries.schema.fields.table.delete": "Delete selected", "admin.registries.schema.fields.table.delete": "Poista valittu", - + // "admin.registries.schema.fields.table.field": "Field", "admin.registries.schema.fields.table.field": "Kenttä", - + // "admin.registries.schema.fields.table.scopenote": "Scope Note", "admin.registries.schema.fields.table.scopenote": "Soveltamisala", - + // "admin.registries.schema.form.create": "Create metadata field", "admin.registries.schema.form.create": "Luo metadatakenttä", - + // "admin.registries.schema.form.edit": "Edit metadata field", "admin.registries.schema.form.edit": "Muokkaa metadatakenttää", - + // "admin.registries.schema.form.element": "Element", "admin.registries.schema.form.element": "Elementti", - + // "admin.registries.schema.form.qualifier": "Qualifier", "admin.registries.schema.form.qualifier": "Tarkenne", - + // "admin.registries.schema.form.scopenote": "Scope Note", "admin.registries.schema.form.scopenote": "Soveltamisala", - + // "admin.registries.schema.head": "Metadata Schema", "admin.registries.schema.head": "Metadataskeema", - + // "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", "admin.registries.schema.notification.created": "\"{{prefix}}\"-metadataskeema luotu", - + // "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", "admin.registries.schema.notification.deleted.failure": "{{amount}} metadataskeeman poisto epäonnistui ", - + // "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", "admin.registries.schema.notification.deleted.success": "{{amount}} metadataskeemaa poistettu", - + // "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", "admin.registries.schema.notification.edited": "\"{{prefix}}\"-metadaskeemaa muokattu", - + // "admin.registries.schema.notification.failure": "Error", "admin.registries.schema.notification.failure": "Virhe", - + // "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", "admin.registries.schema.notification.field.created": "\"{{field}}\"-metadatakenttä luotu", - + // "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", "admin.registries.schema.notification.field.deleted.failure": "{{amount}} metadatakentän poisto epäonnistui", - + // "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", "admin.registries.schema.notification.field.deleted.success": "{{amount}} metadatakenttää poistettu", - + // "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", "admin.registries.schema.notification.field.edited": "\"{{field}}\"-metadatakentää muokattu", - + // "admin.registries.schema.notification.success": "Success", "admin.registries.schema.notification.success": "Valmis", - + // "admin.registries.schema.return": "Return", "admin.registries.schema.return": "Palaa", - + // "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", "admin.registries.schema.title": "DSpace Angular :: Metadataskeemarekisteri", - - - + + + // "admin.access-control.epeople.actions.delete": "Delete EPerson", - // TODO New key - Add a translation - "admin.access-control.epeople.actions.delete": "Delete EPerson", - + "admin.access-control.epeople.actions.delete": "Poista käyttäjä", + // "admin.access-control.epeople.actions.impersonate": "Impersonate EPerson", - // TODO New key - Add a translation - "admin.access-control.epeople.actions.impersonate": "Impersonate EPerson", - + "admin.access-control.epeople.actions.impersonate": "Esiinny käyttäjänä", + // "admin.access-control.epeople.actions.reset": "Reset password", - // TODO New key - Add a translation - "admin.access-control.epeople.actions.reset": "Reset password", - + "admin.access-control.epeople.actions.reset": "Nollaa salasana", + // "admin.access-control.epeople.actions.stop-impersonating": "Stop impersonating EPerson", - // TODO New key - Add a translation - "admin.access-control.epeople.actions.stop-impersonating": "Stop impersonating EPerson", - + "admin.access-control.epeople.actions.stop-impersonating": "Lopeta käyttäjänä esiintyminen", + // "admin.access-control.epeople.title": "DSpace Angular :: EPeople", "admin.access-control.epeople.title": "DSpace Angular :: Käyttäjät", - + // "admin.access-control.epeople.head": "EPeople", "admin.access-control.epeople.head": "Käyttäjät", - + // "admin.access-control.epeople.search.head": "Search", "admin.access-control.epeople.search.head": "Hae", - + // "admin.access-control.epeople.button.see-all": "Browse All", "admin.access-control.epeople.button.see-all": "Selaa kaikkia", - + // "admin.access-control.epeople.search.scope.metadata": "Metadata", "admin.access-control.epeople.search.scope.metadata": "Metadata", - + // "admin.access-control.epeople.search.scope.email": "E-mail (exact)", "admin.access-control.epeople.search.scope.email": "Sähköpostiosoite (oikea)", - + // "admin.access-control.epeople.search.button": "Search", "admin.access-control.epeople.search.button": "Hae", - + // "admin.access-control.epeople.button.add": "Add EPerson", "admin.access-control.epeople.button.add": "Lisää käyttäjä", - + // "admin.access-control.epeople.table.id": "ID", "admin.access-control.epeople.table.id": "ID", - + // "admin.access-control.epeople.table.name": "Name", "admin.access-control.epeople.table.name": "Nimi", - + // "admin.access-control.epeople.table.email": "E-mail (exact)", "admin.access-control.epeople.table.email": "Sähköpostiosoite (oikea)", - + // "admin.access-control.epeople.table.edit": "Edit", "admin.access-control.epeople.table.edit": "Muokkaa", - + // "admin.access-control.epeople.table.edit.buttons.edit": "Edit \"{{name}}\"", "admin.access-control.epeople.table.edit.buttons.edit": "Muokkaa \"{{name}}\"", - + // "admin.access-control.epeople.table.edit.buttons.remove": "Delete \"{{name}}\"", "admin.access-control.epeople.table.edit.buttons.remove": "Poista \"{{name}}\"", - + // "admin.access-control.epeople.no-items": "No EPeople to show.", "admin.access-control.epeople.no-items": "Ei näytettäviä käyttäjiä.", - + // "admin.access-control.epeople.form.create": "Create EPerson", "admin.access-control.epeople.form.create": "Luo käyttäjä", - + // "admin.access-control.epeople.form.edit": "Edit EPerson", "admin.access-control.epeople.form.edit": "Muokkaa käyttäjää", - + // "admin.access-control.epeople.form.firstName": "First name", "admin.access-control.epeople.form.firstName": "Etunimi", - + // "admin.access-control.epeople.form.lastName": "Last name", "admin.access-control.epeople.form.lastName": "Sukunimi", - + // "admin.access-control.epeople.form.email": "E-mail", "admin.access-control.epeople.form.email": "Sähköpostiosoite", - + // "admin.access-control.epeople.form.emailHint": "Must be valid e-mail address", "admin.access-control.epeople.form.emailHint": "Sähköpostiosoitteen on oltava toimiva osoite", - + // "admin.access-control.epeople.form.canLogIn": "Can log in", "admin.access-control.epeople.form.canLogIn": "Voi kirjautua", - + // "admin.access-control.epeople.form.requireCertificate": "Requires certificate", "admin.access-control.epeople.form.requireCertificate": "Vaatii sertifikaatin", - + // "admin.access-control.epeople.form.notification.created.success": "Successfully created EPerson \"{{name}}\"", "admin.access-control.epeople.form.notification.created.success": "Käyttäjä \"{{name}}\" luotu", - + // "admin.access-control.epeople.form.notification.created.failure": "Failed to create EPerson \"{{name}}\"", "admin.access-control.epeople.form.notification.created.failure": "Käyttäjän \"{{name}}\" luonti epäonnistui", - + // "admin.access-control.epeople.form.notification.created.failure.emailInUse": "Failed to create EPerson \"{{name}}\", email \"{{email}}\" already in use.", "admin.access-control.epeople.form.notification.created.failure.emailInUse": "Käyttäjän \"{{name}}\" luonti epäonnistui, sähköpostiosoite \"{{email}}\" on jo käytössä.", - + // "admin.access-control.epeople.form.notification.edited.failure.emailInUse": "Failed to edit EPerson \"{{name}}\", email \"{{email}}\" already in use.", "admin.access-control.epeople.form.notification.edited.failure.emailInUse": "Käyttäjän \"{{name}}\" muokkaus epäonnistui, sähköpostiosoite \"{{email}}\" on jo käytössä.", - + // "admin.access-control.epeople.form.notification.edited.success": "Successfully edited EPerson \"{{name}}\"", "admin.access-control.epeople.form.notification.edited.success": "Käyttäjä \"{{name}}\" muokattu", - + // "admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"", "admin.access-control.epeople.form.notification.edited.failure": "Käyttäjän \"{{name}}\" muokkaus epäonnistui", - + // "admin.access-control.epeople.form.notification.deleted.success": "Successfully deleted EPerson \"{{name}}\"", - // TODO New key - Add a translation - "admin.access-control.epeople.form.notification.deleted.success": "Successfully deleted EPerson \"{{name}}\"", - + "admin.access-control.epeople.form.notification.deleted.success": "Käyttäjä \"{{name}}\" poistettu", + // "admin.access-control.epeople.form.notification.deleted.failure": "Failed to delete EPerson \"{{name}}\"", - // TODO New key - Add a translation - "admin.access-control.epeople.form.notification.deleted.failure": "Failed to delete EPerson \"{{name}}\"", - + "admin.access-control.epeople.form.notification.deleted.failure": "Käyttäjän \"{{name}}\" poisto epäonnistui", + // "admin.access-control.epeople.form.groupsEPersonIsMemberOf": "Member of these groups:", "admin.access-control.epeople.form.groupsEPersonIsMemberOf": "Jäsenenä näissä ryhmissä:", - + // "admin.access-control.epeople.form.table.id": "ID", - "admin.access-control.epeople.form.table.id": "ID", - + "admin.access-control.epeople.form.table.id": "ID-tunnus", + // "admin.access-control.epeople.form.table.name": "Name", "admin.access-control.epeople.form.table.name": "Nimi", - + // "admin.access-control.epeople.form.memberOfNoGroups": "This EPerson is not a member of any groups", "admin.access-control.epeople.form.memberOfNoGroups": "Tämä käyttäjä ei kuulu mihinkään ryhmään", - + // "admin.access-control.epeople.form.goToGroups": "Add to groups", "admin.access-control.epeople.form.goToGroups": "Lisää ryhmiin", - + // "admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"", "admin.access-control.epeople.notification.deleted.failure": "Käyttäjän poisto epäonnistui: \"{{name}}\"", - + // "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"", "admin.access-control.epeople.notification.deleted.success": "Käyttäjä poistettu: \"{{name}}\"", - - - + + + // "admin.access-control.groups.title": "DSpace Angular :: Groups", "admin.access-control.groups.title": "DSpace Angular :: Ryhmät", - + // "admin.access-control.groups.title.singleGroup": "DSpace Angular :: Edit Group", - // TODO New key - Add a translation - "admin.access-control.groups.title.singleGroup": "DSpace Angular :: Edit Group", - + "admin.access-control.groups.title.singleGroup": "DSpace Angular :: Ryhmän muokkaus", + // "admin.access-control.groups.title.addGroup": "DSpace Angular :: New Group", - // TODO New key - Add a translation - "admin.access-control.groups.title.addGroup": "DSpace Angular :: New Group", - + "admin.access-control.groups.title.addGroup": "DSpace Angular :: Uusi ryhmä", + // "admin.access-control.groups.head": "Groups", "admin.access-control.groups.head": "Ryhmät", - + // "admin.access-control.groups.button.add": "Add group", "admin.access-control.groups.button.add": "Lisää ryhmä", - + // "admin.access-control.groups.search.head": "Search groups", "admin.access-control.groups.search.head": "Hae ryhmiä", - + // "admin.access-control.groups.button.see-all": "Browse all", "admin.access-control.groups.button.see-all": "Selaa kaikkia", - + // "admin.access-control.groups.search.button": "Search", "admin.access-control.groups.search.button": "Hae", - + // "admin.access-control.groups.table.id": "ID", "admin.access-control.groups.table.id": "ID", - + // "admin.access-control.groups.table.name": "Name", "admin.access-control.groups.table.name": "Nimi", - + // "admin.access-control.groups.table.members": "Members", "admin.access-control.groups.table.members": "Jäsenet", - + // "admin.access-control.groups.table.edit": "Edit", "admin.access-control.groups.table.edit": "Muokkaa", - + // "admin.access-control.groups.table.edit.buttons.edit": "Edit \"{{name}}\"", "admin.access-control.groups.table.edit.buttons.edit": "Muokkaa ryhmää \"{{name}}\"", - + // "admin.access-control.groups.table.edit.buttons.remove": "Delete \"{{name}}\"", "admin.access-control.groups.table.edit.buttons.remove": "Poista \"{{name}}\"", - + // "admin.access-control.groups.no-items": "No groups found with this in their name or this as UUID", "admin.access-control.groups.no-items": "Ei ryhmiä, joiden nimi tai UUID täsmää", - + // "admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"", "admin.access-control.groups.notification.deleted.success": "Ryhmä \"{{name}}\" poistettu", - + // "admin.access-control.groups.notification.deleted.failure.title": "Failed to delete group \"{{name}}\"", - // TODO New key - Add a translation - "admin.access-control.groups.notification.deleted.failure.title": "Failed to delete group \"{{name}}\"", - + "admin.access-control.groups.notification.deleted.failure.title": "Ryhmän \"{{name}}\" poisto epäonnistui", + // "admin.access-control.groups.notification.deleted.failure.content": "Cause: \"{{cause}}\"", - // TODO New key - Add a translation - "admin.access-control.groups.notification.deleted.failure.content": "Cause: \"{{cause}}\"", - - - + "admin.access-control.groups.notification.deleted.failure.content": "Syy: \"{{cause}}\"", + + + // "admin.access-control.groups.form.alert.permanent": "This group is permanent, so it can't be edited or deleted. You can still add and remove group members using this page.", - // TODO New key - Add a translation - "admin.access-control.groups.form.alert.permanent": "This group is permanent, so it can't be edited or deleted. You can still add and remove group members using this page.", - + "admin.access-control.groups.form.alert.permanent": "Tämä ryhmä on pysyvä, eikä sitä voi muokata tai poistaa. Ryhmään voi kuitenkin lisätä ja poistaa jäseniä tällä sivulla.", + // "admin.access-control.groups.form.alert.workflowGroup": "This group can’t be modified or deleted because it corresponds to a role in the submission and workflow process in the \"{{name}}\" {{comcol}}. You can delete it from the \"assign roles\" tab on the edit {{comcol}} page. You can still add and remove group members using this page.", - // TODO New key - Add a translation - "admin.access-control.groups.form.alert.workflowGroup": "This group can’t be modified or deleted because it corresponds to a role in the submission and workflow process in the \"{{name}}\" {{comcol}}. You can delete it from the \"assign roles\" tab on the edit {{comcol}} page. You can still add and remove group members using this page.", - + "admin.access-control.groups.form.alert.workflowGroup": "Tätä ryhmää ei voi muokata eikä poistaa, koska siihen liittyy rooli tallennus- ja työnkulkuprosessissa \"{{name}}\" {{comcol}}. Voit poistaa sen \"Määritä rooleja\" -välilehdellä {{comcol}}-muokkaussivulla. Ryhmään voi kuitenkin lisätä ja poistaa jäseniä tällä sivulla.", + // "admin.access-control.groups.form.head.create": "Create group", "admin.access-control.groups.form.head.create": "Luo ryhmä", - + // "admin.access-control.groups.form.head.edit": "Edit group", "admin.access-control.groups.form.head.edit": "Muokkaa ryhmää", - + // "admin.access-control.groups.form.groupName": "Group name", "admin.access-control.groups.form.groupName": "Ryhmän nimi", - + // "admin.access-control.groups.form.groupDescription": "Description", "admin.access-control.groups.form.groupDescription": "Kuvaus", - + // "admin.access-control.groups.form.notification.created.success": "Successfully created Group \"{{name}}\"", "admin.access-control.groups.form.notification.created.success": "Ryhmä \"{{name}}\" luotu", - + // "admin.access-control.groups.form.notification.created.failure": "Failed to create Group \"{{name}}\"", "admin.access-control.groups.form.notification.created.failure": "Ryhmän \"{{name}}\" luonti epäonnistui", - + // "admin.access-control.groups.form.notification.created.failure.groupNameInUse": "Failed to create Group with name: \"{{name}}\", make sure the name is not already in use.", "admin.access-control.groups.form.notification.created.failure.groupNameInUse": "Ryhmän \"{{name}}\" luonti epäonnistui. Tarkista, että nimi ei ole käytössä.", - + // "admin.access-control.groups.form.notification.edited.failure": "Failed to edit Group \"{{name}}\"", - // TODO New key - Add a translation - "admin.access-control.groups.form.notification.edited.failure": "Failed to edit Group \"{{name}}\"", - + "admin.access-control.groups.form.notification.edited.failure": "Ryhmän \"{{name}}\" muokkaus epäonnistui", + // "admin.access-control.groups.form.notification.edited.failure.groupNameInUse": "Name \"{{name}}\" already in use!", - // TODO New key - Add a translation - "admin.access-control.groups.form.notification.edited.failure.groupNameInUse": "Name \"{{name}}\" already in use!", - + "admin.access-control.groups.form.notification.edited.failure.groupNameInUse": "Nimi \"{{name}}\" on jo käytössä!", + // "admin.access-control.groups.form.notification.edited.success": "Successfully edited Group \"{{name}}\"", - // TODO New key - Add a translation - "admin.access-control.groups.form.notification.edited.success": "Successfully edited Group \"{{name}}\"", - + "admin.access-control.groups.form.notification.edited.success": "Ryhmää \"{{name}}\" muokattu", + // "admin.access-control.groups.form.actions.delete": "Delete Group", - // TODO New key - Add a translation - "admin.access-control.groups.form.actions.delete": "Delete Group", - + "admin.access-control.groups.form.actions.delete": "Poista ryhmä", + // "admin.access-control.groups.form.delete-group.modal.header": "Delete Group \"{{ dsoName }}\"", - // TODO New key - Add a translation - "admin.access-control.groups.form.delete-group.modal.header": "Delete Group \"{{ dsoName }}\"", - + "admin.access-control.groups.form.delete-group.modal.header": "Poista ryhmä \"{{ dsoName }}\"", + // "admin.access-control.groups.form.delete-group.modal.info": "Are you sure you want to delete Group \"{{ dsoName }}\"", - // TODO New key - Add a translation - "admin.access-control.groups.form.delete-group.modal.info": "Are you sure you want to delete Group \"{{ dsoName }}\"", - + "admin.access-control.groups.form.delete-group.modal.info": "Haluatko varmasti poistaa ryhmän \"{{ dsoName }}\"", + // "admin.access-control.groups.form.delete-group.modal.cancel": "Cancel", - // TODO New key - Add a translation - "admin.access-control.groups.form.delete-group.modal.cancel": "Cancel", - + "admin.access-control.groups.form.delete-group.modal.cancel": "Peruuta", + // "admin.access-control.groups.form.delete-group.modal.confirm": "Delete", - // TODO New key - Add a translation - "admin.access-control.groups.form.delete-group.modal.confirm": "Delete", - + "admin.access-control.groups.form.delete-group.modal.confirm": "Poista", + // "admin.access-control.groups.form.notification.deleted.success": "Successfully deleted group \"{{ name }}\"", - // TODO New key - Add a translation - "admin.access-control.groups.form.notification.deleted.success": "Successfully deleted group \"{{ name }}\"", - + "admin.access-control.groups.form.notification.deleted.success": "Ryhmä \"{{ name }}\" poistettu", + // "admin.access-control.groups.form.notification.deleted.failure.title": "Failed to delete group \"{{ name }}\"", - // TODO New key - Add a translation - "admin.access-control.groups.form.notification.deleted.failure.title": "Failed to delete group \"{{ name }}\"", - + "admin.access-control.groups.form.notification.deleted.failure.title": "Ryhmän \"{{ name }}\" poisto epäonnistui", + // "admin.access-control.groups.form.notification.deleted.failure.content": "Cause: \"{{ cause }}\"", - // TODO New key - Add a translation - "admin.access-control.groups.form.notification.deleted.failure.content": "Cause: \"{{ cause }}\"", - + "admin.access-control.groups.form.notification.deleted.failure.content": "Syy: \"{{ cause }}\"", + // "admin.access-control.groups.form.members-list.head": "EPeople", "admin.access-control.groups.form.members-list.head": "Käyttäjät", - + // "admin.access-control.groups.form.members-list.search.head": "Add EPeople", "admin.access-control.groups.form.members-list.search.head": "Lisää käyttäjiä", - + // "admin.access-control.groups.form.members-list.button.see-all": "Browse All", "admin.access-control.groups.form.members-list.button.see-all": "Selaa kaikkia", - + // "admin.access-control.groups.form.members-list.headMembers": "Current Members", "admin.access-control.groups.form.members-list.headMembers": "Nykyiset jäsenet", - + // "admin.access-control.groups.form.members-list.search.scope.metadata": "Metadata", "admin.access-control.groups.form.members-list.search.scope.metadata": "Metadata", - + // "admin.access-control.groups.form.members-list.search.scope.email": "E-mail (exact)", "admin.access-control.groups.form.members-list.search.scope.email": "Sähköpostiosoite (oikea)", - + // "admin.access-control.groups.form.members-list.search.button": "Search", "admin.access-control.groups.form.members-list.search.button": "Hae", - + // "admin.access-control.groups.form.members-list.table.id": "ID", "admin.access-control.groups.form.members-list.table.id": "ID", - + // "admin.access-control.groups.form.members-list.table.name": "Name", "admin.access-control.groups.form.members-list.table.name": "Nimi", - + // "admin.access-control.groups.form.members-list.table.edit": "Remove / Add", "admin.access-control.groups.form.members-list.table.edit": "Poista / Lisää", - + // "admin.access-control.groups.form.members-list.table.edit.buttons.remove": "Remove member with name \"{{name}}\"", "admin.access-control.groups.form.members-list.table.edit.buttons.remove": "Poista jäsen \"{{name}}\"", - + // "admin.access-control.groups.form.members-list.notification.success.addMember": "Successfully added member: \"{{name}}\"", "admin.access-control.groups.form.members-list.notification.success.addMember": "Jäsen lisätty: \"{{name}}\"", - + // "admin.access-control.groups.form.members-list.notification.failure.addMember": "Failed to add member: \"{{name}}\"", "admin.access-control.groups.form.members-list.notification.failure.addMember": "Jäsenen lisäys epäonnistui: \"{{name}}\"", - + // "admin.access-control.groups.form.members-list.notification.success.deleteMember": "Successfully deleted member: \"{{name}}\"", "admin.access-control.groups.form.members-list.notification.success.deleteMember": "Jäsen poistettu: \"{{name}}\"", - + // "admin.access-control.groups.form.members-list.notification.failure.deleteMember": "Failed to delete member: \"{{name}}\"", "admin.access-control.groups.form.members-list.notification.failure.deleteMember": "Jäsenen poisto epäonnistui: \"{{name}}\"", - + // "admin.access-control.groups.form.members-list.table.edit.buttons.add": "Add member with name \"{{name}}\"", "admin.access-control.groups.form.members-list.table.edit.buttons.add": "Lisää jäsen \"{{name}}\"", - + // "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "Ei aktiivista ryhmää, syötä ensin nimi.", - + // "admin.access-control.groups.form.members-list.no-members-yet": "No members in group yet, search and add.", "admin.access-control.groups.form.members-list.no-members-yet": "Ryhmässä ei ole vielä jäseniä, hae ja lisää.", - + // "admin.access-control.groups.form.members-list.no-items": "No EPeople found in that search", "admin.access-control.groups.form.members-list.no-items": "Haku ei palauttanut käyttäjiä", - + // "admin.access-control.groups.form.subgroups-list.notification.failure": "Something went wrong: \"{{cause}}\"", - // TODO New key - Add a translation - "admin.access-control.groups.form.subgroups-list.notification.failure": "Something went wrong: \"{{cause}}\"", - + "admin.access-control.groups.form.subgroups-list.notification.failure": "Tapahtui virhe: \"{{cause}}\"", + // "admin.access-control.groups.form.subgroups-list.head": "Groups", "admin.access-control.groups.form.subgroups-list.head": "Ryhmät", - + // "admin.access-control.groups.form.subgroups-list.search.head": "Add Subgroup", "admin.access-control.groups.form.subgroups-list.search.head": "Lisää alaryhmä", - + // "admin.access-control.groups.form.subgroups-list.button.see-all": "Browse All", "admin.access-control.groups.form.subgroups-list.button.see-all": "Selaa kaikkia", - + // "admin.access-control.groups.form.subgroups-list.headSubgroups": "Current Subgroups", "admin.access-control.groups.form.subgroups-list.headSubgroups": "Nykyiset alaryhmät", - + // "admin.access-control.groups.form.subgroups-list.search.button": "Search", "admin.access-control.groups.form.subgroups-list.search.button": "Hae", - + // "admin.access-control.groups.form.subgroups-list.table.id": "ID", "admin.access-control.groups.form.subgroups-list.table.id": "ID", - + // "admin.access-control.groups.form.subgroups-list.table.name": "Name", "admin.access-control.groups.form.subgroups-list.table.name": "Nimi", - + // "admin.access-control.groups.form.subgroups-list.table.edit": "Remove / Add", "admin.access-control.groups.form.subgroups-list.table.edit": "Poista / Lisää", - + // "admin.access-control.groups.form.subgroups-list.table.edit.buttons.remove": "Remove subgroup with name \"{{name}}\"", "admin.access-control.groups.form.subgroups-list.table.edit.buttons.remove": "Poista alaryhmä nimeltään \"{{name}}\"", - + // "admin.access-control.groups.form.subgroups-list.table.edit.buttons.add": "Add subgroup with name \"{{name}}\"", "admin.access-control.groups.form.subgroups-list.table.edit.buttons.add": "Lisää alarymä nimellä \"{{name}}\"", - + // "admin.access-control.groups.form.subgroups-list.table.edit.currentGroup": "Current group", "admin.access-control.groups.form.subgroups-list.table.edit.currentGroup": "Nykyinen ryhmä", - + // "admin.access-control.groups.form.subgroups-list.notification.success.addSubgroup": "Successfully added subgroup: \"{{name}}\"", "admin.access-control.groups.form.subgroups-list.notification.success.addSubgroup": "Lisätty alaryhmä: \"{{name}}\"", - + // "admin.access-control.groups.form.subgroups-list.notification.failure.addSubgroup": "Failed to add subgroup: \"{{name}}\"", "admin.access-control.groups.form.subgroups-list.notification.failure.addSubgroup": "Alaryhmän lisäys epäonnistui: \"{{name}}\"", - + // "admin.access-control.groups.form.subgroups-list.notification.success.deleteSubgroup": "Successfully deleted subgroup: \"{{name}}\"", "admin.access-control.groups.form.subgroups-list.notification.success.deleteSubgroup": "Alaryhmä poistettu: \"{{name}}\"", - + // "admin.access-control.groups.form.subgroups-list.notification.failure.deleteSubgroup": "Failed to delete subgroup: \"{{name}}\"", "admin.access-control.groups.form.subgroups-list.notification.failure.deleteSubgroup": "Alaryhmän poisto epäonnistui: \"{{name}}\"", - + // "admin.access-control.groups.form.subgroups-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", "admin.access-control.groups.form.subgroups-list.notification.failure.noActiveGroup": "Ei nykyistä aktiivista ryhmää, tallenna ensin nimi.", - + // "admin.access-control.groups.form.subgroups-list.notification.failure.subgroupToAddIsActiveGroup": "This is the current group, can't be added.", "admin.access-control.groups.form.subgroups-list.notification.failure.subgroupToAddIsActiveGroup": "Tämä on nykyinen ryhmä, lisäys ei ole mahdollista.", - + // "admin.access-control.groups.form.subgroups-list.no-items": "No groups found with this in their name or this as UUID", "admin.access-control.groups.form.subgroups-list.no-items": "Ei ryhmiä, joiden nimi tai UUID täsmää", - + // "admin.access-control.groups.form.subgroups-list.no-subgroups-yet": "No subgroups in group yet.", "admin.access-control.groups.form.subgroups-list.no-subgroups-yet": "Ryhmällä ei ole alaryhmiä.", - + // "admin.access-control.groups.form.return": "Return to groups", "admin.access-control.groups.form.return": "Palaa ryhmiin", - - - + + + // "admin.search.breadcrumbs": "Administrative Search", "admin.search.breadcrumbs": "Hallinnollinen haku", - + // "admin.search.collection.edit": "Edit", "admin.search.collection.edit": "Muokkaa", - + // "admin.search.community.edit": "Edit", "admin.search.community.edit": "Muokkaa", - + // "admin.search.item.delete": "Delete", "admin.search.item.delete": "Poista", - + // "admin.search.item.edit": "Edit", "admin.search.item.edit": "Muokkaa", - + // "admin.search.item.make-private": "Make Private", "admin.search.item.make-private": "Tee yksityiseksi", - + // "admin.search.item.make-public": "Make Public", "admin.search.item.make-public": "Tee julkiseksi", - + // "admin.search.item.move": "Move", "admin.search.item.move": "Siirrä", - + // "admin.search.item.reinstate": "Reinstate", "admin.search.item.reinstate": "Palauta käyttöön", - + // "admin.search.item.withdraw": "Withdraw", "admin.search.item.withdraw": "Poista käytöstä", - + // "admin.search.title": "Administrative Search", "admin.search.title": "Hallinnollinen haku", - + // "administrativeView.search.results.head": "Administrative Search", "administrativeView.search.results.head": "Hallinnollinen haku", - - - - + + + + // "admin.workflow.breadcrumbs": "Administer Workflow", - // TODO New key - Add a translation - "admin.workflow.breadcrumbs": "Administer Workflow", - + "admin.workflow.breadcrumbs": "Hallinnointityönkulku", + // "admin.workflow.title": "Administer Workflow", - // TODO New key - Add a translation - "admin.workflow.title": "Administer Workflow", - + "admin.workflow.title": "Hallinnointityönkulku", + // "admin.workflow.item.workflow": "Workflow", - // TODO New key - Add a translation - "admin.workflow.item.workflow": "Workflow", - + "admin.workflow.item.workflow": "Työnkulku", + // "admin.workflow.item.delete": "Delete", - // TODO New key - Add a translation - "admin.workflow.item.delete": "Delete", - + "admin.workflow.item.delete": "Poista", + // "admin.workflow.item.send-back": "Send back", - // TODO New key - Add a translation - "admin.workflow.item.send-back": "Send back", - - - + "admin.workflow.item.send-back": "Lähetä takaisin", + + + // "admin.metadata-import.breadcrumbs": "Import Metadata", - // TODO New key - Add a translation - "admin.metadata-import.breadcrumbs": "Import Metadata", - + "admin.metadata-import.breadcrumbs": "Importoi metadata", + // "admin.metadata-import.title": "Import Metadata", - // TODO New key - Add a translation - "admin.metadata-import.title": "Import Metadata", - + "admin.metadata-import.title": "Importoi metadata", + // "admin.metadata-import.page.header": "Import Metadata", - // TODO New key - Add a translation - "admin.metadata-import.page.header": "Import Metadata", - + "admin.metadata-import.page.header": "Importoi metadata", + // "admin.metadata-import.page.help": "You can drop or browse CSV files that contain batch metadata operations on files here", - // TODO New key - Add a translation - "admin.metadata-import.page.help": "You can drop or browse CSV files that contain batch metadata operations on files here", - + "admin.metadata-import.page.help": "Voit pudottaa tähän tai selata CSV-tiedostoja, joilla voit tehdä metadatatoimintoja usealle tietueelle", + // "admin.metadata-import.page.dropMsg": "Drop a metadata CSV to import", - // TODO New key - Add a translation - "admin.metadata-import.page.dropMsg": "Drop a metadata CSV to import", - + "admin.metadata-import.page.dropMsg": "Pudota CSV-metadata importoimista varten", + // "admin.metadata-import.page.dropMsgReplace": "Drop to replace the metadata CSV to import", - // TODO New key - Add a translation - "admin.metadata-import.page.dropMsgReplace": "Drop to replace the metadata CSV to import", - + "admin.metadata-import.page.dropMsgReplace": "Pudota korvataksesi importoitava CSV-metadata", + // "admin.metadata-import.page.button.return": "Return", - // TODO New key - Add a translation - "admin.metadata-import.page.button.return": "Return", - + "admin.metadata-import.page.button.return": "Paluu", + // "admin.metadata-import.page.button.proceed": "Proceed", - // TODO New key - Add a translation - "admin.metadata-import.page.button.proceed": "Proceed", - + "admin.metadata-import.page.button.proceed": "Jatka", + // "admin.metadata-import.page.error.addFile": "Select file first!", - // TODO New key - Add a translation - "admin.metadata-import.page.error.addFile": "Select file first!", - - - - + "admin.metadata-import.page.error.addFile": "Valitse ensin tiedosto!", + + + + // "auth.errors.invalid-user": "Invalid email address or password.", "auth.errors.invalid-user": "Virheellinen sähköpostiosoite tai salasana.", - + // "auth.messages.expired": "Your session has expired. Please log in again.", "auth.messages.expired": "Istuntosi on vanhentunut. Kirjaudu uudelleen.", - - - + + + // "bitstream.edit.bitstream": "Bitstream: ", "bitstream.edit.bitstream": "Tiedosto: ", - + // "bitstream.edit.form.description.hint": "Optionally, provide a brief description of the file, for example \"Main article\" or \"Experiment data readings\".", "bitstream.edit.form.description.hint": "Voit lisätä lyhyen kuvauksen tiedostosta, esimerkiksi \"Pääartikkeli\" tai \"Testidatan tulkinta\".", - + // "bitstream.edit.form.description.label": "Description", "bitstream.edit.form.description.label": "Kuvaus", - + // "bitstream.edit.form.embargo.hint": "The first day from which access is allowed. This date cannot be modified on this form. To set an embargo date for a bitstream, go to the Item Status tab, click Authorizations..., create or edit the bitstream's READ policy, and set the Start Date as desired.", - "bitstream.edit.form.embargo.hint": "Ensimmäinen päivä, jona tietue on saatavilla. Päivämäärää ei voi muokata tällä lomakkeella. Embargon päivämäärä asetetaan seuraavasti: Valitse Tietueen tila-välilehti ja sieltä Käyttöoikeudet..., luo tai muokkaa olemassa olevaa READ-toimintatapaa ja aseta Alkupäivämäärä.", - + "bitstream.edit.form.embargo.hint": "Ensimmäinen päivä, jona tietue on saatavilla. Päivämäärää ei voi muokata tällä lomakkeella. Embargon päivämäärä asetetaan seuraavasti: Valitse Tietueen tila-välilehti ja sieltä Käyttöoikeudet..., luo tai muokkaa olemassa olevaa READ-käytäntöä ja aseta Alkupäivämäärä.", + // "bitstream.edit.form.embargo.label": "Embargo until specific date", "bitstream.edit.form.embargo.label": "Embargo tiettyyn päivämäärään asti", - + // "bitstream.edit.form.fileName.hint": "Change the filename for the bitstream. Note that this will change the display bitstream URL, but old links will still resolve as long as the sequence ID does not change.", "bitstream.edit.form.fileName.hint": "Vaihda tiedoston tiedostonimi. Huomaa, että tämä muuttaa näkyvää tiedoston URL-osoitetta, mutta vanhat linkit toimivat, kunhan järjestys-ID ei muutu.", - + // "bitstream.edit.form.fileName.label": "Filename", "bitstream.edit.form.fileName.label": "Tiedostonimi", - + // "bitstream.edit.form.newFormat.label": "Describe new format", "bitstream.edit.form.newFormat.label": "Kuvaile uusi formaatti", - + // "bitstream.edit.form.newFormat.hint": "The application you used to create the file, and the version number (for example, \"ACMESoft SuperApp version 1.5\").", "bitstream.edit.form.newFormat.hint": "Tiedoston luonnissa käytetty sovellus ja versionumero (esim. \"ACMESoft SuperApp version 1.5\").", - + // "bitstream.edit.form.primaryBitstream.label": "Primary bitstream", "bitstream.edit.form.primaryBitstream.label": "Ensisijainen tiedosto", - + // "bitstream.edit.form.selectedFormat.hint": "If the format is not in the above list, select \"format not in list\" above and describe it under \"Describe new format\".", "bitstream.edit.form.selectedFormat.hint": "Jos formaatti ei ole yllä olevassa luettelossa, valitse yltä \"Formaatti ei ole luettelossa\" ja kuvaile se kohdassa \"Kuvaile uusi formaatti\".", - + // "bitstream.edit.form.selectedFormat.label": "Selected Format", "bitstream.edit.form.selectedFormat.label": "Valittu formaatti", - + // "bitstream.edit.form.selectedFormat.unknown": "Format not in list", "bitstream.edit.form.selectedFormat.unknown": "Formaatti ei ole luettelossa", - + // "bitstream.edit.notifications.error.format.title": "An error occurred saving the bitstream's format", "bitstream.edit.notifications.error.format.title": "Virhe tallennettaessa tiedoston formaattia", - + // "bitstream.edit.notifications.saved.content": "Your changes to this bitstream were saved.", "bitstream.edit.notifications.saved.content": "Muutokset tiedostoon on tallennettu.", - + // "bitstream.edit.notifications.saved.title": "Bitstream saved", "bitstream.edit.notifications.saved.title": "Tiedosto tallennettu", - + // "bitstream.edit.title": "Edit bitstream", "bitstream.edit.title": "Muokkaa tiedostoa", - - - + + + // "browse.comcol.by.author": "By Author", "browse.comcol.by.author": "Tekijän mukaan", - + // "browse.comcol.by.dateissued": "By Issue Date", "browse.comcol.by.dateissued": "Julkaisuajan mukaan", - + // "browse.comcol.by.subject": "By Subject", "browse.comcol.by.subject": "Asiasanan mukaan", - + // "browse.comcol.by.title": "By Title", "browse.comcol.by.title": "Nimekkeen mukaan", - + // "browse.comcol.head": "Browse", "browse.comcol.head": "Selaa", - + // "browse.empty": "No items to show.", "browse.empty": "Ei tietueita.", - + // "browse.metadata.author": "Author", "browse.metadata.author": "Tekijä", - + // "browse.metadata.dateissued": "Issue Date", "browse.metadata.dateissued": "Julkaisuaika", - + // "browse.metadata.subject": "Subject", "browse.metadata.subject": "Asiasana", - + // "browse.metadata.title": "Title", "browse.metadata.title": "Nimeke", - + // "browse.metadata.author.breadcrumbs": "Browse by Author", "browse.metadata.author.breadcrumbs": "Selaa tekijän mukaan", - + // "browse.metadata.dateissued.breadcrumbs": "Browse by Date", "browse.metadata.dateissued.breadcrumbs": "Selaa päivämäärän mukaan", - + // "browse.metadata.subject.breadcrumbs": "Browse by Subject", "browse.metadata.subject.breadcrumbs": "Selaa asiasanan mukaan", - + // "browse.metadata.title.breadcrumbs": "Browse by Title", "browse.metadata.title.breadcrumbs": "Selaa nimekkeen mukaan", - + // "browse.startsWith.choose_start": "(Choose start)", "browse.startsWith.choose_start": "(Valitse alku)", - + // "browse.startsWith.choose_year": "(Choose year)", "browse.startsWith.choose_year": "(Valitse vuosi)", - + // "browse.startsWith.jump": "Jump to a point in the index:", "browse.startsWith.jump": "Hyppää indeksin kohtaan:", - + // "browse.startsWith.months.april": "April", "browse.startsWith.months.april": "Huhtikuu", - + // "browse.startsWith.months.august": "August", "browse.startsWith.months.august": "Elokuu", - + // "browse.startsWith.months.december": "December", "browse.startsWith.months.december": "Joulukuu", - + // "browse.startsWith.months.february": "February", "browse.startsWith.months.february": "Helmikuu", - + // "browse.startsWith.months.january": "January", "browse.startsWith.months.january": "Tammikuu", - + // "browse.startsWith.months.july": "July", "browse.startsWith.months.july": "Heinäkuu", - + // "browse.startsWith.months.june": "June", "browse.startsWith.months.june": "Kesäkuu", - + // "browse.startsWith.months.march": "March", "browse.startsWith.months.march": "Maaliskuu", - + // "browse.startsWith.months.may": "May", "browse.startsWith.months.may": "Toukokuu", - + // "browse.startsWith.months.none": "(Choose month)", "browse.startsWith.months.none": "(Valitse kuukausi)", - + // "browse.startsWith.months.november": "November", "browse.startsWith.months.november": "Marraskuu", - + // "browse.startsWith.months.october": "October", "browse.startsWith.months.october": "Lokakuu", - + // "browse.startsWith.months.september": "September", "browse.startsWith.months.september": "Syyskuu", - + // "browse.startsWith.submit": "Go", "browse.startsWith.submit": "Ok", - + // "browse.startsWith.type_date": "Or type in a date (year-month):", "browse.startsWith.type_date": "Tai anna päiväys (vuosi-kuukausi):", - + // "browse.startsWith.type_text": "Or enter first few letters:", "browse.startsWith.type_text": "Tai anna muutama alkukirjain:", - + // "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", "browse.title": "Selataan {{ collection }}-kokoelmaa {{ field }}-kentän arvolla {{ value }}", - - + + // "chips.remove": "Remove chip", "chips.remove": "Poista chip", - - - + + + // "collection.create.head": "Create a Collection", "collection.create.head": "Luo kokoelma", - + // "collection.create.notifications.success": "Successfully created the Collection", "collection.create.notifications.success": "Kokoelma luotu", - + // "collection.create.sub-head": "Create a Collection for Community {{ parent }}", "collection.create.sub-head": "Luo kokoelma {{ parent }}-yhteisöön", - + // "collection.curate.header": "Curate Collection: {{collection}}", - // TODO New key - Add a translation - "collection.curate.header": "Curate Collection: {{collection}}", - + "collection.curate.header": "Kuratoi kokoelmaa: {{collection}}", + // "collection.delete.cancel": "Cancel", "collection.delete.cancel": "Peruuta", - + // "collection.delete.confirm": "Confirm", "collection.delete.confirm": "Vahvista", - + // "collection.delete.head": "Delete Collection", "collection.delete.head": "Poista kokoelma", - + // "collection.delete.notification.fail": "Collection could not be deleted", "collection.delete.notification.fail": "Kokoelman poisto epäonnistui", - + // "collection.delete.notification.success": "Successfully deleted collection", "collection.delete.notification.success": "Kokoelma poistettu", - + // "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", "collection.delete.text": "Haluatko varmasti poistaa kokoelman \"{{ dso }}\"", - - - + + + // "collection.edit.delete": "Delete this collection", "collection.edit.delete": "Poista kokoelma", - + // "collection.edit.head": "Edit Collection", "collection.edit.head": "Muokkaa kokoelmaa", - + // "collection.edit.breadcrumbs": "Edit Collection", "collection.edit.breadcrumbs": "Muokkaa kokoelmaa", - - - + + + // "collection.edit.tabs.mapper.head": "Item Mapper", - // TODO New key - Add a translation - "collection.edit.tabs.mapper.head": "Item Mapper", - + "collection.edit.tabs.mapper.head": "Tietueliitosväline", + // "collection.edit.tabs.item-mapper.title": "Collection Edit - Item Mapper", - // TODO New key - Add a translation - "collection.edit.tabs.item-mapper.title": "Collection Edit - Item Mapper", - + "collection.edit.tabs.item-mapper.title": "Kokoelman muokkaus - Tietueliitosväline", + // "collection.edit.item-mapper.cancel": "Cancel", "collection.edit.item-mapper.cancel": "Peruuta", - + // "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", "collection.edit.item-mapper.collection": "Kokoelma: \"{{name}}\"", - + // "collection.edit.item-mapper.confirm": "Map selected items", "collection.edit.item-mapper.confirm": "Liitä valitut tietueet", - + // "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", - "collection.edit.item-mapper.description": "Tällä työkalulla kokoelmien ylläpitäjät voivat liittää tietueita muista kokoelmista tähän kokoelmaan. Voit etsiä tietueita muista kokoelmista ja liittää ne tähän kokoelmaan tai selata luetteloa tähän kokoelmaan liitetyistä tietueista.", - + "collection.edit.item-mapper.description": "Tällä työkalulla kokoelmien ylläpitäjät voivat liittää tietueita muista kokoelmista tähän kokoelmaan. Voit hakea tietueita muista kokoelmista ja liittää ne tähän kokoelmaan tai selata luetteloa tähän kokoelmaan liitetyistä tietueista.", + // "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", "collection.edit.item-mapper.head": "Tietueliitosväline - Liitä tietueita muista kokoelmista", - + // "collection.edit.item-mapper.no-search": "Please enter a query to search", "collection.edit.item-mapper.no-search": "Anna hakulauseke", - + // "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", "collection.edit.item-mapper.notifications.map.error.content": "Virheitä liitettäessä {{amount}} tietuetta.", - + // "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", "collection.edit.item-mapper.notifications.map.error.head": "Virheitä liitoksissa", - + // "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", "collection.edit.item-mapper.notifications.map.success.content": "Liitetty {{amount}} tietuetta.", - + // "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", "collection.edit.item-mapper.notifications.map.success.head": "Liitos valmis", - + // "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", "collection.edit.item-mapper.notifications.unmap.error.content": "Virheitä {{amount}} tietueen liitoksia poistettaessa.", - + // "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", "collection.edit.item-mapper.notifications.unmap.error.head": "Poista virheelliset liitokset", - + // "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", "collection.edit.item-mapper.notifications.unmap.success.content": "Poistettu {{amount}} tietueen liitokset.", - + // "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", "collection.edit.item-mapper.notifications.unmap.success.head": "Liitosten poisto valmis", - + // "collection.edit.item-mapper.remove": "Remove selected item mappings", "collection.edit.item-mapper.remove": "Poista valitut tietueliitokset", - + // "collection.edit.item-mapper.tabs.browse": "Browse mapped items", "collection.edit.item-mapper.tabs.browse": "Selaa liitettyjä tietueita", - + // "collection.edit.item-mapper.tabs.map": "Map new items", "collection.edit.item-mapper.tabs.map": "Liitä uusia tietueita", - - - + + + // "collection.edit.logo.label": "Collection logo", "collection.edit.logo.label": "Kokoelman logo", - + // "collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.", "collection.edit.logo.notifications.add.error": "Kokoelman logon lataus epäonnistui. Tarkista sisältö ennen kuin yrität uudelleen.", - + // "collection.edit.logo.notifications.add.success": "Upload Collection logo successful.", "collection.edit.logo.notifications.add.success": "Kokoelman logo ladattu.", - + // "collection.edit.logo.notifications.delete.success.title": "Logo deleted", "collection.edit.logo.notifications.delete.success.title": "Logo poistettu", - + // "collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo", "collection.edit.logo.notifications.delete.success.content": "Kokoelman logo poistettu", - + // "collection.edit.logo.notifications.delete.error.title": "Error deleting logo", "collection.edit.logo.notifications.delete.error.title": "Virhe logoa poistettaessa", - + // "collection.edit.logo.upload": "Drop a Collection Logo to upload", "collection.edit.logo.upload": "Pudota kokoelman logo ladattavaksi", - - - + + + // "collection.edit.notifications.success": "Successfully edited the Collection", "collection.edit.notifications.success": "Kokoelman muokkaus onnistui", - + // "collection.edit.return": "Return", "collection.edit.return": "Palaa", - - - + + + // "collection.edit.tabs.curate.head": "Curate", "collection.edit.tabs.curate.head": "Kuratoi", - + // "collection.edit.tabs.curate.title": "Collection Edit - Curate", "collection.edit.tabs.curate.title": "Kokoelman muokkaus - Kuratoi", - + // "collection.edit.tabs.authorizations.head": "Authorizations", - // TODO New key - Add a translation - "collection.edit.tabs.authorizations.head": "Authorizations", - + "collection.edit.tabs.authorizations.head": "Käyttöoikeudet", + // "collection.edit.tabs.authorizations.title": "Collection Edit - Authorizations", - // TODO New key - Add a translation - "collection.edit.tabs.authorizations.title": "Collection Edit - Authorizations", - + "collection.edit.tabs.authorizations.title": "Kokoelman muokkaus - Käyttöoikeudet", + // "collection.edit.tabs.metadata.head": "Edit Metadata", "collection.edit.tabs.metadata.head": "Muokkaa metadataa", - + // "collection.edit.tabs.metadata.title": "Collection Edit - Metadata", "collection.edit.tabs.metadata.title": "Kokoelman muokkaus - Metadata", - + // "collection.edit.tabs.roles.head": "Assign Roles", - "collection.edit.tabs.roles.head": "Määritä roolit", - + "collection.edit.tabs.roles.head": "Määritä rooleja", + // "collection.edit.tabs.roles.title": "Collection Edit - Roles", "collection.edit.tabs.roles.title": "Kokoelman muokkaus - Roolit", - + // "collection.edit.tabs.source.external": "This collection harvests its content from an external source", "collection.edit.tabs.source.external": "Tämän kokoeleman sisältö haravoidaan ulkoisesta lähteestä", - + // "collection.edit.tabs.source.form.errors.oaiSource.required": "You must provide a set id of the target collection.", "collection.edit.tabs.source.form.errors.oaiSource.required": "Kohdekokoelman joukko-ID on annettava.", - + // "collection.edit.tabs.source.form.harvestType": "Content being harvested", "collection.edit.tabs.source.form.harvestType": "Haravoitava sisältö", - + // "collection.edit.tabs.source.form.head": "Configure an external source", "collection.edit.tabs.source.form.head": "Aseta ulkoinen lähde", - + // "collection.edit.tabs.source.form.metadataConfigId": "Metadata Format", "collection.edit.tabs.source.form.metadataConfigId": "Metadataformaatti", - + // "collection.edit.tabs.source.form.oaiSetId": "OAI specific set id", "collection.edit.tabs.source.form.oaiSetId": "OAI-kohtainen joukko-ID", - + // "collection.edit.tabs.source.form.oaiSource": "OAI Provider", "collection.edit.tabs.source.form.oaiSource": "OAI-tarjoaja", - + // "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_BITSTREAMS": "Harvest metadata and bitstreams (requires ORE support)", "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_BITSTREAMS": "Haravoi metadata ja tiedostot (vaatii ORE-tuen)", - + // "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_REF": "Harvest metadata and references to bitstreams (requires ORE support)", "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_REF": "Haravoi metadata ja tiedostojen osoitteet (vaatii ORE-tuen)", - + // "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Harvest metadata only", "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Haravoi vain metadata", - + // "collection.edit.tabs.source.head": "Content Source", "collection.edit.tabs.source.head": "Sisältölähde", - + // "collection.edit.tabs.source.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "collection.edit.tabs.source.notifications.discarded.content": "Muutokset hylätty. Valitse 'Kumoa' palauttaaksesi muutokset", - + // "collection.edit.tabs.source.notifications.discarded.title": "Changed discarded", "collection.edit.tabs.source.notifications.discarded.title": "Muutokset hylätty", - + // "collection.edit.tabs.source.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", "collection.edit.tabs.source.notifications.invalid.content": "Muutoksia ei tallennettu. Tarkista kaikkien kenttien oikeellisuus ennen tallennusta.", - + // "collection.edit.tabs.source.notifications.invalid.title": "Metadata invalid", "collection.edit.tabs.source.notifications.invalid.title": "Virheellinen metadata", - + // "collection.edit.tabs.source.notifications.saved.content": "Your changes to this collection's content source were saved.", "collection.edit.tabs.source.notifications.saved.content": "Muutokset kokoelman sisältölähteeseen on tallennettu.", - + // "collection.edit.tabs.source.notifications.saved.title": "Content Source saved", "collection.edit.tabs.source.notifications.saved.title": "Sisältölähde tallennettu", - + // "collection.edit.tabs.source.title": "Collection Edit - Content Source", "collection.edit.tabs.source.title": "Kokoelman muokkaus - Sisältölähde", - - - + + + // "collection.edit.template.add-button": "Add", - // TODO New key - Add a translation - "collection.edit.template.add-button": "Add", - + "collection.edit.template.add-button": "Lisää", + // "collection.edit.template.breadcrumbs": "Item template", - // TODO New key - Add a translation - "collection.edit.template.breadcrumbs": "Item template", - + "collection.edit.template.breadcrumbs": "Tietueen mallipohja", + // "collection.edit.template.cancel": "Cancel", - // TODO New key - Add a translation - "collection.edit.template.cancel": "Cancel", - + "collection.edit.template.cancel": "Peruuta", + // "collection.edit.template.delete-button": "Delete", - // TODO New key - Add a translation - "collection.edit.template.delete-button": "Delete", - + "collection.edit.template.delete-button": "Poista", + // "collection.edit.template.edit-button": "Edit", - // TODO New key - Add a translation - "collection.edit.template.edit-button": "Edit", - + "collection.edit.template.edit-button": "Muokkaa", + // "collection.edit.template.head": "Edit Template Item for Collection \"{{ collection }}\"", - // TODO New key - Add a translation - "collection.edit.template.head": "Edit Template Item for Collection \"{{ collection }}\"", - + "collection.edit.template.head": "Muokkaa kokoelman \"{{ collection }}\" mallitietuetta", + // "collection.edit.template.label": "Template item", - // TODO New key - Add a translation - "collection.edit.template.label": "Template item", - + "collection.edit.template.label": "Mallitietue", + // "collection.edit.template.notifications.delete.error": "Failed to delete the item template", - // TODO New key - Add a translation - "collection.edit.template.notifications.delete.error": "Failed to delete the item template", - + "collection.edit.template.notifications.delete.error": "Tietueen mallipohjan poisto epäonnistui", + // "collection.edit.template.notifications.delete.success": "Successfully deleted the item template", - // TODO New key - Add a translation - "collection.edit.template.notifications.delete.success": "Successfully deleted the item template", - + "collection.edit.template.notifications.delete.success": "Tietueen mallipohja poistettu", + // "collection.edit.template.title": "Edit Template Item", - // TODO New key - Add a translation - "collection.edit.template.title": "Edit Template Item", - - - + "collection.edit.template.title": "Muokkaa mallitietuetta", + + + // "collection.form.abstract": "Short Description", "collection.form.abstract": "Lyhyt kuvaus", - + // "collection.form.description": "Introductory text (HTML)", "collection.form.description": "Johdantoteksti (HTML)", - + // "collection.form.errors.title.required": "Please enter a collection name", "collection.form.errors.title.required": "Anna kokoelman nimi", - + // "collection.form.license": "License", "collection.form.license": "Lisenssi", - + // "collection.form.provenance": "Provenance", "collection.form.provenance": "Provenanssi", - + // "collection.form.rights": "Copyright text (HTML)", "collection.form.rights": "Tekijänoikeusteksti (HTML)", - + // "collection.form.tableofcontents": "News (HTML)", "collection.form.tableofcontents": "Uutiset (HTML)", - + // "collection.form.title": "Name", "collection.form.title": "Nimi", - - - + + + // "collection.listelement.badge": "Collection", - // TODO New key - Add a translation - "collection.listelement.badge": "Collection", - - - + "collection.listelement.badge": "Kokoelma", + + + // "collection.page.browse.recent.head": "Recent Submissions", "collection.page.browse.recent.head": "Viimeksi lisätyt", - + // "collection.page.browse.recent.empty": "No items to show", - "collection.page.browse.recent.empty": "No items to show", - + "collection.page.browse.recent.empty": "Ei tietueita", + // "collection.page.edit": "Edit this collection", - // TODO New key - Add a translation - "collection.page.edit": "Edit this collection", - + "collection.page.edit": "Muokkaa tätä kokoelmaa", + // "collection.page.handle": "Permanent URI for this collection", "collection.page.handle": "Kokoelman pysyvä URL-osoite", - + // "collection.page.license": "License", "collection.page.license": "Lisenssi", - + // "collection.page.news": "News", "collection.page.news": "Uutiset", - - - + + + // "collection.select.confirm": "Confirm selected", "collection.select.confirm": "Vahvista valinta", - + // "collection.select.empty": "No collections to show", "collection.select.empty": "Ei kokoelmia", - + // "collection.select.table.title": "Title", "collection.select.table.title": "Nimeke", - - - + + + // "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.", "collection.source.update.notifications.error.content": "Annetut asetukset on testattu. Ne eivät toimi.", - + // "collection.source.update.notifications.error.title": "Server Error", "collection.source.update.notifications.error.title": "Palvelinvirhe", - - - + + + // "communityList.tabTitle": "DSpace - Community List", "communityList.tabTitle": "DSpace - Yhteisöluettelo", - + // "communityList.title": "List of Communities", "communityList.title": "Luettelo yhteisöistä", - + // "communityList.showMore": "Show More", "communityList.showMore": "Näytä lisää", - - - + + + // "community.create.head": "Create a Community", "community.create.head": "Luo yhteisö", - + // "community.create.notifications.success": "Successfully created the Community", "community.create.notifications.success": "Yhteisö luotu", - + // "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", "community.create.sub-head": "Luo alayhteisö {{ parent }}-yhteisölle", - + // "community.curate.header": "Curate Community: {{community}}", - // TODO New key - Add a translation - "community.curate.header": "Curate Community: {{community}}", - + "community.curate.header": "Kuratoi yhteisöä: {{community}}", + // "community.delete.cancel": "Cancel", "community.delete.cancel": "Peruuta", - + // "community.delete.confirm": "Confirm", "community.delete.confirm": "Vahvista", - + // "community.delete.head": "Delete Community", "community.delete.head": "Poista yhteisö", - + // "community.delete.notification.fail": "Community could not be deleted", "community.delete.notification.fail": "Yhteisön poisto epäonnistui", - + // "community.delete.notification.success": "Successfully deleted community", "community.delete.notification.success": "Yhteisö poistettu", - + // "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", "community.delete.text": "Haluatko varmasti poistaa yhteisön \"{{ dso }}\"", - + // "community.edit.delete": "Delete this community", "community.edit.delete": "Poista tämä yhteisö", - + // "community.edit.head": "Edit Community", "community.edit.head": "Muokkaa yhteisöä", - + // "community.edit.breadcrumbs": "Edit Community", "community.edit.breadcrumbs": "Muokkaa yhteisöä", - - + + // "community.edit.logo.label": "Community logo", "community.edit.logo.label": "Yhteisön logo", - + // "community.edit.logo.notifications.add.error": "Uploading Community logo failed. Please verify the content before retrying.", "community.edit.logo.notifications.add.error": "Yhteisön logon lataus epäonnistui. Tarkista sisältö ennen kuin yrität uudelleen.", - + // "community.edit.logo.notifications.add.success": "Upload Community logo successful.", "community.edit.logo.notifications.add.success": "Yhteisön logo ladattu.", - + // "community.edit.logo.notifications.delete.success.title": "Logo deleted", "community.edit.logo.notifications.delete.success.title": "Logo poistettu", - + // "community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo", "community.edit.logo.notifications.delete.success.content": "Yhteisön logo poistettu", - + // "community.edit.logo.notifications.delete.error.title": "Error deleting logo", "community.edit.logo.notifications.delete.error.title": "Virhe logoa poistettaessa", - + // "community.edit.logo.upload": "Drop a Community Logo to upload", "community.edit.logo.upload": "Pudota yhteisön logo ladattavaksi", - - - + + + // "community.edit.notifications.success": "Successfully edited the Community", "community.edit.notifications.success": "Yhteisön muokkaus onnistui", - + // "community.edit.notifications.unauthorized": "You do not have privileges to make this change", - // TODO New key - Add a translation - "community.edit.notifications.unauthorized": "You do not have privileges to make this change", - + "community.edit.notifications.unauthorized": "Sinulla ei ole oikeuksia tehdä tätä muutosta", + // "community.edit.notifications.error": "An error occured while editing the Community", - // TODO New key - Add a translation - "community.edit.notifications.error": "An error occured while editing the Community", - + "community.edit.notifications.error": "Virhe muokattaessa yhteisöä", + // "community.edit.return": "Return", "community.edit.return": "Palaa", - - - + + + // "community.edit.tabs.curate.head": "Curate", "community.edit.tabs.curate.head": "Kuratoi", - + // "community.edit.tabs.curate.title": "Community Edit - Curate", "community.edit.tabs.curate.title": "Yhteisön muokkaus - Kuratoi", - + // "community.edit.tabs.metadata.head": "Edit Metadata", "community.edit.tabs.metadata.head": "Muokkaa metadataa", - + // "community.edit.tabs.metadata.title": "Community Edit - Metadata", "community.edit.tabs.metadata.title": "Yhteisön muokkaus - Metadata", - + // "community.edit.tabs.roles.head": "Assign Roles", "community.edit.tabs.roles.head": "Määritä rooleja", - + // "community.edit.tabs.roles.title": "Community Edit - Roles", "community.edit.tabs.roles.title": "Yhteisön muokkaus - Roolit", - + // "community.edit.tabs.authorizations.head": "Authorizations", - // TODO New key - Add a translation - "community.edit.tabs.authorizations.head": "Authorizations", - + "community.edit.tabs.authorizations.head": "Käyttöoikeudet", + // "community.edit.tabs.authorizations.title": "Community Edit - Authorizations", - // TODO New key - Add a translation - "community.edit.tabs.authorizations.title": "Community Edit - Authorizations", - - - + "community.edit.tabs.authorizations.title": "Yhteisön muokkaus - Käyttöoikeudet", + + + // "community.listelement.badge": "Community", - // TODO New key - Add a translation - "community.listelement.badge": "Community", - - - + "community.listelement.badge": "Yhteisö", + + + // "comcol-role.edit.no-group": "None", - // TODO New key - Add a translation - "comcol-role.edit.no-group": "None", - + "comcol-role.edit.no-group": "Ei mitään", + // "comcol-role.edit.create": "Create", - // TODO New key - Add a translation - "comcol-role.edit.create": "Create", - + "comcol-role.edit.create": "Luo", + // "comcol-role.edit.restrict": "Restrict", - // TODO New key - Add a translation - "comcol-role.edit.restrict": "Restrict", - + "comcol-role.edit.restrict": "Rajoita", + // "comcol-role.edit.delete": "Delete", - // TODO New key - Add a translation - "comcol-role.edit.delete": "Delete", - - + "comcol-role.edit.delete": "Poista", + + // "comcol-role.edit.community-admin.name": "Administrators", - // TODO New key - Add a translation - "comcol-role.edit.community-admin.name": "Administrators", - + "comcol-role.edit.community-admin.name": "Ylläpitäjät", + // "comcol-role.edit.collection-admin.name": "Administrators", - // TODO New key - Add a translation - "comcol-role.edit.collection-admin.name": "Administrators", - - + "comcol-role.edit.collection-admin.name": "Ylläpitäjät", + + // "comcol-role.edit.community-admin.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", - // TODO New key - Add a translation - "comcol-role.edit.community-admin.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", - + "comcol-role.edit.community-admin.description": "Yhteisön ylläpitäjät voivat luoda alayhteisöjä tai kokoelmia ja hallita niitä sekä myöntää niihin hallintaoikeuksia. He päättävät, kuka saa tallentaa tietueita alakokoelmiin, muokata metadataa (tallennuksen jälkeen) ja lisätä (liittää) olemassa olevia tietueita muista kokoelmista (käyttöoikeuksien puitteissa).", + // "comcol-role.edit.collection-admin.description": "Collection administrators decide who can submit items to the collection, edit item metadata (after submission), and add (map) existing items from other collections to this collection (subject to authorization for that collection).", - // TODO New key - Add a translation - "comcol-role.edit.collection-admin.description": "Collection administrators decide who can submit items to the collection, edit item metadata (after submission), and add (map) existing items from other collections to this collection (subject to authorization for that collection).", - - + "comcol-role.edit.collection-admin.description": "Kokoelman ylläpitäjät päättävät, kuka saa tallentaa tietueita alakokoelmiin, muokata metadataa (tallennuksen jälkeen) ja lisätä (liittää) olemassa olevia tietueita muista kokoelmista (k.o. kokoelman käyttöoikeuksien puitteissa).", + + // "comcol-role.edit.submitters.name": "Submitters", - // TODO New key - Add a translation - "comcol-role.edit.submitters.name": "Submitters", - + "comcol-role.edit.submitters.name": "Tallentajat", + // "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", - // TODO New key - Add a translation - "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", - - + "comcol-role.edit.submitters.description": "Käyttäjät ja ryhmät, joilla on oikeus tallentaa uusia tietueita tähän kokoelmaan", + + // "comcol-role.edit.item_read.name": "Default item read access", - // TODO New key - Add a translation - "comcol-role.edit.item_read.name": "Default item read access", - + "comcol-role.edit.item_read.name": "Tietueen oletuslukuoikeus", + // "comcol-role.edit.item_read.description": "E-People and Groups that can read new items submitted to this collection. Changes to this role are not retroactive. Existing items in the system will still be viewable by those who had read access at the time of their addition.", - // TODO New key - Add a translation - "comcol-role.edit.item_read.description": "E-People and Groups that can read new items submitted to this collection. Changes to this role are not retroactive. Existing items in the system will still be viewable by those who had read access at the time of their addition.", - + "comcol-role.edit.item_read.description": "Käyttäjät ja ryhmät, jotka voivat lukea tähän kokoelmaan tallennettuja uusia tietueita. Muutokset eivät tule käyttöön taannehtivasti. Olemassa olevat tietueet näkyvät niille, joilla oli lukuoikeus tietueiden lisäyshetkellä.", + // "comcol-role.edit.item_read.anonymous-group": "Default read for incoming items is currently set to Anonymous.", - // TODO New key - Add a translation - "comcol-role.edit.item_read.anonymous-group": "Default read for incoming items is currently set to Anonymous.", - - + "comcol-role.edit.item_read.anonymous-group": "Uusien tietuiden oletuslukuoikeus on tällä hetkellä Anonymous.", + + // "comcol-role.edit.bitstream_read.name": "Default bitstream read access", - // TODO New key - Add a translation - "comcol-role.edit.bitstream_read.name": "Default bitstream read access", - + "comcol-role.edit.bitstream_read.name": "Tiedostojen oletuslukuoikeus", + // "comcol-role.edit.bitstream_read.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", - // TODO New key - Add a translation - "comcol-role.edit.bitstream_read.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", - + "comcol-role.edit.bitstream_read.description": "Yhteisön ylläpitäjät voivat luoda alayhteisöjä tai kokoelmia ja hallita niitä sekä myöntää niihin hallintaoikeuksia. He päättävät, kuka saa tallentaa tietueita alakokoelmiin, muokata metadataa (tallennuksen jälkeen) ja lisätä (liittää) olemassa olevia tietueita muista kokoelmista (käyttöoikeuksien puitteissa).", + // "comcol-role.edit.bitstream_read.anonymous-group": "Default read for incoming bitstreams is currently set to Anonymous.", - // TODO New key - Add a translation - "comcol-role.edit.bitstream_read.anonymous-group": "Default read for incoming bitstreams is currently set to Anonymous.", - - + "comcol-role.edit.bitstream_read.anonymous-group": "Uusien tiedostojen oletuslukuoikeus on tällä hetkellä Anonymous.", + + // "comcol-role.edit.editor.name": "Editors", - // TODO New key - Add a translation - "comcol-role.edit.editor.name": "Editors", - + "comcol-role.edit.editor.name": "Muokkaajat", + // "comcol-role.edit.editor.description": "Editors are able to edit the metadata of incoming submissions, and then accept or reject them.", - // TODO New key - Add a translation - "comcol-role.edit.editor.description": "Editors are able to edit the metadata of incoming submissions, and then accept or reject them.", - - + "comcol-role.edit.editor.description": "Muokkaajat voivat muokata uusien tallennusten metadataa sekä hyväksyä tai hylätä tallennukset.", + + // "comcol-role.edit.finaleditor.name": "Final editors", - // TODO New key - Add a translation - "comcol-role.edit.finaleditor.name": "Final editors", - + "comcol-role.edit.finaleditor.name": "Loppumuokkaajat", + // "comcol-role.edit.finaleditor.description": "Final editors are able to edit the metadata of incoming submissions, but will not be able to reject them.", - // TODO New key - Add a translation - "comcol-role.edit.finaleditor.description": "Final editors are able to edit the metadata of incoming submissions, but will not be able to reject them.", - - + "comcol-role.edit.finaleditor.description": "Loppumuokkaajat voivat muokata uusien tallennusten metadataa. He eivät voi hylätä tallennuksia.", + + // "comcol-role.edit.reviewer.name": "Reviewers", - // TODO New key - Add a translation - "comcol-role.edit.reviewer.name": "Reviewers", - + "comcol-role.edit.reviewer.name": "Tarkastajat", + // "comcol-role.edit.reviewer.description": "Reviewers are able to accept or reject incoming submissions. However, they are not able to edit the submission's metadata.", - // TODO New key - Add a translation - "comcol-role.edit.reviewer.description": "Reviewers are able to accept or reject incoming submissions. However, they are not able to edit the submission's metadata.", - - - + "comcol-role.edit.reviewer.description": "Tarkastajat voivat hyväksyä tai hylätä uudet tallennukset. He eivät voi muokata tallennusten metadataa.", + + + // "community.form.abstract": "Short Description", "community.form.abstract": "Lyhyt kuvaus", - + // "community.form.description": "Introductory text (HTML)", "community.form.description": "Johdantoteksti (HTML)", - + // "community.form.errors.title.required": "Please enter a community name", "community.form.errors.title.required": "Anna kokoelman nimi", - + // "community.form.rights": "Copyright text (HTML)", "community.form.rights": "Tekijänoikeusteksti (HTML)", - + // "community.form.tableofcontents": "News (HTML)", "community.form.tableofcontents": "Uutiset (HTML)", - + // "community.form.title": "Name", "community.form.title": "Nimi", - + // "community.page.edit": "Edit this community", - // TODO New key - Add a translation - "community.page.edit": "Edit this community", - + "community.page.edit": "Muokkaa yhteisöä", + // "community.page.handle": "Permanent URI for this community", "community.page.handle": "Yhteisön pysyvä URL-osoite", - + // "community.page.license": "License", "community.page.license": "Lisenssi", - + // "community.page.news": "News", "community.page.news": "Uutiset", - + // "community.all-lists.head": "Subcommunities and Collections", "community.all-lists.head": "Alayhteisöt ja kokoelmat", - + // "community.sub-collection-list.head": "Collections of this Community", "community.sub-collection-list.head": "Yhteisön kokoelmat", - + // "community.sub-community-list.head": "Communities of this Community", "community.sub-community-list.head": "Yhteisön alayhteisöt", - - - + + + // "cookies.consent.accept-all": "Accept all", - // TODO New key - Add a translation - "cookies.consent.accept-all": "Accept all", - + "cookies.consent.accept-all": "Hyväksy kaikki", + // "cookies.consent.accept-selected": "Accept selected", - // TODO New key - Add a translation - "cookies.consent.accept-selected": "Accept selected", - + "cookies.consent.accept-selected": "Hyväksy valitut", + // "cookies.consent.app.opt-out.description": "This app is loaded by default (but you can opt out)", - // TODO New key - Add a translation - "cookies.consent.app.opt-out.description": "This app is loaded by default (but you can opt out)", - + "cookies.consent.app.opt-out.description": "Tämä sovellus ladataan oletuksena (mutta voit kieltää sen)", + // "cookies.consent.app.opt-out.title": "(opt-out)", - // TODO New key - Add a translation - "cookies.consent.app.opt-out.title": "(opt-out)", - + "cookies.consent.app.opt-out.title": "(kiellä)", + // "cookies.consent.app.purpose": "purpose", - // TODO New key - Add a translation - "cookies.consent.app.purpose": "purpose", - + "cookies.consent.app.purpose": "tarkoitus", + // "cookies.consent.app.required.description": "This application is always required", - // TODO New key - Add a translation - "cookies.consent.app.required.description": "This application is always required", - + "cookies.consent.app.required.description": "Tämä sovellus on aina pakollinen", + // "cookies.consent.app.required.title": "(always required)", - // TODO New key - Add a translation - "cookies.consent.app.required.title": "(always required)", - + "cookies.consent.app.required.title": "(aina pakollinen)", + // "cookies.consent.update": "There were changes since your last visit, please update your consent.", - // TODO New key - Add a translation - "cookies.consent.update": "There were changes since your last visit, please update your consent.", - + "cookies.consent.update": "Viime käyntisi jälkeen on tehty muutoksia. Ole hyvä ja päivitä suostumuksesi.", + // "cookies.consent.close": "Close", - // TODO New key - Add a translation - "cookies.consent.close": "Close", - + "cookies.consent.close": "Sulje", + // "cookies.consent.decline": "Decline", - // TODO New key - Add a translation - "cookies.consent.decline": "Decline", - + "cookies.consent.decline": "Kieltäydy", + // "cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: Authentication, Preferences, Acknowledgement and Statistics.
    To learn more, please read our {privacyPolicy}.", - // TODO New key - Add a translation - "cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: Authentication, Preferences, Acknowledgement and Statistics.
    To learn more, please read our {privacyPolicy}.", - + "cookies.consent.content-notice.description": "Keräämme ja käsittelemme tietojasi seuraaviin tarkoituksiin: todentaminen, asetukset, kuittaus ja tilastot.
    Lisätietoa saat lukemalla tämän: {privacyPolicy}.", + // "cookies.consent.content-notice.learnMore": "Customize", - // TODO New key - Add a translation - "cookies.consent.content-notice.learnMore": "Customize", - + "cookies.consent.content-notice.learnMore": "Räätälöi", + // "cookies.consent.content-modal.description": "Here you can see and customize the information that we collect about you.", - // TODO New key - Add a translation - "cookies.consent.content-modal.description": "Here you can see and customize the information that we collect about you.", - + "cookies.consent.content-modal.description": "Tässä voit tarkastella ja räätälöidä meidän sinusta keräämäämme tietoa.", + // "cookies.consent.content-modal.privacy-policy.name": "privacy policy", - // TODO New key - Add a translation - "cookies.consent.content-modal.privacy-policy.name": "privacy policy", - + "cookies.consent.content-modal.privacy-policy.name": "yksilönsuoja", + // "cookies.consent.content-modal.privacy-policy.text": "To learn more, please read our {privacyPolicy}.", - // TODO New key - Add a translation - "cookies.consent.content-modal.privacy-policy.text": "To learn more, please read our {privacyPolicy}.", - + "cookies.consent.content-modal.privacy-policy.text": "Lisätietoja saadaksesi lue: {privacyPolicy}.", + // "cookies.consent.content-modal.title": "Information that we collect", - // TODO New key - Add a translation - "cookies.consent.content-modal.title": "Information that we collect", - - - + "cookies.consent.content-modal.title": "Keräämämme tieto", + + + // "cookies.consent.app.title.authentication": "Authentication", - // TODO New key - Add a translation - "cookies.consent.app.title.authentication": "Authentication", - + "cookies.consent.app.title.authentication": "Todentaminen", + // "cookies.consent.app.description.authentication": "Required for signing you in", - // TODO New key - Add a translation - "cookies.consent.app.description.authentication": "Required for signing you in", - - + "cookies.consent.app.description.authentication": "Vaadittu kirjautumista varten", + + // "cookies.consent.app.title.preferences": "Preferences", - // TODO New key - Add a translation - "cookies.consent.app.title.preferences": "Preferences", - + "cookies.consent.app.title.preferences": "Asetukset", + // "cookies.consent.app.description.preferences": "Required for saving your preferences", - // TODO New key - Add a translation - "cookies.consent.app.description.preferences": "Required for saving your preferences", - - - + "cookies.consent.app.description.preferences": "Vaadittu asetustesi tallentamista varten", + + + // "cookies.consent.app.title.acknowledgement": "Acknowledgement", - // TODO New key - Add a translation - "cookies.consent.app.title.acknowledgement": "Acknowledgement", - + "cookies.consent.app.title.acknowledgement": "Kuittaus", + // "cookies.consent.app.description.acknowledgement": "Required for saving your acknowledgements and consents", - // TODO New key - Add a translation - "cookies.consent.app.description.acknowledgement": "Required for saving your acknowledgements and consents", - - - + "cookies.consent.app.description.acknowledgement": "Vaadittu kuittaustesi ja hyväksyntäsi tallentamista varten", + + + // "cookies.consent.app.title.google-analytics": "Google Analytics", - // TODO New key - Add a translation "cookies.consent.app.title.google-analytics": "Google Analytics", - + // "cookies.consent.app.description.google-analytics": "Allows us to track statistical data", - // TODO New key - Add a translation - "cookies.consent.app.description.google-analytics": "Allows us to track statistical data", - - - + "cookies.consent.app.description.google-analytics": "Sallii tilastollisen datan seurannan", + + + // "cookies.consent.purpose.functional": "Functional", - // TODO New key - Add a translation - "cookies.consent.purpose.functional": "Functional", - + "cookies.consent.purpose.functional": "Toiminnallinen", + // "cookies.consent.purpose.statistical": "Statistical", - // TODO New key - Add a translation - "cookies.consent.purpose.statistical": "Statistical", - - + "cookies.consent.purpose.statistical": "Tilastollinen", + + // "curation-task.task.checklinks.label": "Check Links in Metadata", - // TODO New key - Add a translation - "curation-task.task.checklinks.label": "Check Links in Metadata", - + "curation-task.task.checklinks.label": "Tarkista metadatan linkit", + // "curation-task.task.noop.label": "NOOP", - // TODO New key - Add a translation - "curation-task.task.noop.label": "NOOP", - + "curation-task.task.noop.label": "Ei toimintaa", + // "curation-task.task.profileformats.label": "Profile Bitstream Formats", - // TODO New key - Add a translation - "curation-task.task.profileformats.label": "Profile Bitstream Formats", - + "curation-task.task.profileformats.label": "Tiedostoformaattiprofiilit", + // "curation-task.task.requiredmetadata.label": "Check for Required Metadata", - // TODO New key - Add a translation - "curation-task.task.requiredmetadata.label": "Check for Required Metadata", - + "curation-task.task.requiredmetadata.label": "Tarkista pakollinen metadata", + // "curation-task.task.translate.label": "Microsoft Translator", - // TODO New key - Add a translation - "curation-task.task.translate.label": "Microsoft Translator", - + "curation-task.task.translate.label": "Microsoft-kääntäjä", + // "curation-task.task.vscan.label": "Virus Scan", - // TODO New key - Add a translation - "curation-task.task.vscan.label": "Virus Scan", - - - + "curation-task.task.vscan.label": "Virusskannaus", + + + // "curation.form.task-select.label": "Task:", - // TODO New key - Add a translation - "curation.form.task-select.label": "Task:", - + "curation.form.task-select.label": "Tehtävä:", + // "curation.form.submit": "Start", - // TODO New key - Add a translation - "curation.form.submit": "Start", - + "curation.form.submit": "Aloita", + // "curation.form.submit.success.head": "The curation task has been started successfully", - // TODO New key - Add a translation - "curation.form.submit.success.head": "The curation task has been started successfully", - + "curation.form.submit.success.head": "Kuratointitehtävä aloitettu", + // "curation.form.submit.success.content": "You will be redirected to the corresponding process page.", - // TODO New key - Add a translation - "curation.form.submit.success.content": "You will be redirected to the corresponding process page.", - + "curation.form.submit.success.content": "Sinut siirretään vastaavalle prosessin sivulle.", + // "curation.form.submit.error.head": "Running the curation task failed", - // TODO New key - Add a translation - "curation.form.submit.error.head": "Running the curation task failed", - + "curation.form.submit.error.head": "Kuratointitehtävän suoritus epäonnistui", + // "curation.form.submit.error.content": "An error occured when trying to start the curation task.", - // TODO New key - Add a translation - "curation.form.submit.error.content": "An error occured when trying to start the curation task.", - + "curation.form.submit.error.content": "Virhe kuratointitehtävää aloitettaessa.", + // "curation.form.handle.label": "Handle:", - // TODO New key - Add a translation "curation.form.handle.label": "Handle:", - + // "curation.form.handle.hint": "Hint: Enter [your-handle-prefix]/0 to run a task across entire site (not all tasks may support this capability)", - // TODO New key - Add a translation - "curation.form.handle.hint": "Hint: Enter [your-handle-prefix]/0 to run a task across entire site (not all tasks may support this capability)", - - - + "curation.form.handle.hint": "Vinkki: Syötä [oma-handle-prefix]/0 suorittaaksesi tehtävän koko sivustolla (kaikki tehtävät eivät tue tätä toimintoa)", + + + // "dso-selector.create.collection.head": "New collection", "dso-selector.create.collection.head": "Uusi kokoelma", - + // "dso-selector.create.collection.sub-level": "Create a new collection in", - // TODO New key - Add a translation - "dso-selector.create.collection.sub-level": "Create a new collection in", - + "dso-selector.create.collection.sub-level": "Luo uusi kokoelma tänne: ", + // "dso-selector.create.community.head": "New community", - "dso-selector.create.community.head": "Uusi yhteisö", - + "zxzcommunity.head": "Uusi yhteisö", + // "dso-selector.create.community.sub-level": "Create a new community in", - "dso-selector.create.community.sub-level": "Luo uusi yhteisö julkaisuarkistoon", - + "dso-selector.create.community.sub-level": "Luo uusi yhteisö tänne: ", + // "dso-selector.create.community.top-level": "Create a new top-level community", "dso-selector.create.community.top-level": "Luo uusi ylätason yhteisö", - + // "dso-selector.create.item.head": "New item", "dso-selector.create.item.head": "Uusi tietue", - + // "dso-selector.create.item.sub-level": "Create a new item in", - // TODO New key - Add a translation - "dso-selector.create.item.sub-level": "Create a new item in", - + "dso-selector.create.item.sub-level": "Luo uusi tietue tänne: ", + // "dso-selector.create.submission.head": "New submission", - // TODO New key - Add a translation - "dso-selector.create.submission.head": "New submission", - + "dso-selector.create.submission.head": "Uusi tallennus", + // "dso-selector.edit.collection.head": "Edit collection", "dso-selector.edit.collection.head": "Muokkaa kokoelmaa", - + // "dso-selector.edit.community.head": "Edit community", "dso-selector.edit.community.head": "Muokkaa yhteisöä", - + // "dso-selector.edit.item.head": "Edit item", "dso-selector.edit.item.head": "Muokkaa tietuetta", - + // "dso-selector.export-metadata.dspaceobject.head": "Export metadata from", - // TODO New key - Add a translation - "dso-selector.export-metadata.dspaceobject.head": "Export metadata from", - + "dso-selector.export-metadata.dspaceobject.head": "Eksportoi metadata täältä: ", + // "dso-selector.no-results": "No {{ type }} found", "dso-selector.no-results": "Ei {{ type }}-tyyppiä.", - + // "dso-selector.placeholder": "Search for a {{ type }}", - "dso-selector.placeholder": "Hae {{ type }}", - - - + "dso-selector.placeholder": "Hae {{ type }}-tyyppiä", + + + // "confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}", - // TODO New key - Add a translation - "confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}", - + "confirmation-modal.export-metadata.header": "Eksportoi {{ dsoName }}:n metadata", + // "confirmation-modal.export-metadata.info": "Are you sure you want to export metadata for {{ dsoName }}", - // TODO New key - Add a translation - "confirmation-modal.export-metadata.info": "Are you sure you want to export metadata for {{ dsoName }}", - + "confirmation-modal.export-metadata.info": "Haluatko varmasti eksportoida {{ dsoName }}:n metadatan", + // "confirmation-modal.export-metadata.cancel": "Cancel", - // TODO New key - Add a translation - "confirmation-modal.export-metadata.cancel": "Cancel", - + "confirmation-modal.export-metadata.cancel": "Peruuta", + // "confirmation-modal.export-metadata.confirm": "Export", - // TODO New key - Add a translation - "confirmation-modal.export-metadata.confirm": "Export", - + "confirmation-modal.export-metadata.confirm": "Eksportoi", + // "confirmation-modal.delete-eperson.header": "Delete EPerson \"{{ dsoName }}\"", - // TODO New key - Add a translation - "confirmation-modal.delete-eperson.header": "Delete EPerson \"{{ dsoName }}\"", - + "confirmation-modal.delete-eperson.header": "Poista käyttäjä \"{{ dsoName }}\"", + // "confirmation-modal.delete-eperson.info": "Are you sure you want to delete EPerson \"{{ dsoName }}\"", - // TODO New key - Add a translation - "confirmation-modal.delete-eperson.info": "Are you sure you want to delete EPerson \"{{ dsoName }}\"", - + "confirmation-modal.delete-eperson.info": "Haluatko varmasti poistaa käyttäjän \"{{ dsoName }}\"", + // "confirmation-modal.delete-eperson.cancel": "Cancel", - // TODO New key - Add a translation - "confirmation-modal.delete-eperson.cancel": "Cancel", - + "confirmation-modal.delete-eperson.cancel": "Peruuta", + // "confirmation-modal.delete-eperson.confirm": "Delete", - // TODO New key - Add a translation - "confirmation-modal.delete-eperson.confirm": "Delete", - - + "confirmation-modal.delete-eperson.confirm": "Poista", + + // "error.bitstream": "Error fetching bitstream", "error.bitstream": "Virhe tiedostoa noudettaessa", - + // "error.browse-by": "Error fetching items", "error.browse-by": "Virhe tietueita noudettaessa", - + // "error.collection": "Error fetching collection", "error.collection": "Virhe kokoelmaa noudettaessa", - + // "error.collections": "Error fetching collections", "error.collections": "Virhe kokoelmia noudettaessa", - + // "error.community": "Error fetching community", "error.community": "Virhe yhteisöä noudettaessa", - + // "error.identifier": "No item found for the identifier", "error.identifier": "Ei tunnistetta vastaavaa tietuetta", - + // "error.default": "Error", "error.default": "Virhe", - + // "error.item": "Error fetching item", "error.item": "Virhe tietuetta noudettaessa", - + // "error.items": "Error fetching items", "error.items": "Virhe tietueita noudettaessa", - + // "error.objects": "Error fetching objects", "error.objects": "Virhe kohteita noudettaessa", - + // "error.recent-submissions": "Error fetching recent submissions", "error.recent-submissions": "Virhe viimeksi lisättyjä julkaisuja noudettaessa", - + // "error.search-results": "Error fetching search results", "error.search-results": "Virhe hakutuloksia noudettaessa", - + // "error.sub-collections": "Error fetching sub-collections", "error.sub-collections": "Virhe alakokoelmia noudettaessa", - + // "error.sub-communities": "Error fetching sub-communities", "error.sub-communities": "Virhe alayhteisöjä noudettaessa", - + // "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

    ", "error.submission.sections.init-form-error": "Virhe osiota alustettaessa, tarkista syöttölomakkeesi asetukset. Lisätietoja alla:

    ", - + // "error.top-level-communities": "Error fetching top-level communities", "error.top-level-communities": "Virhe ylätason yhteisöjä noudettaessa", - + // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - "error.validation.license.notgranted": "Julkaisuprosessia ei voi päättää, ellet hyväksy julkaisulisenssiä. Voit myös tallentaa tiedot ja jatkaa tallennusta myöhemmin tai poistaa kaikki syöttämäsi tiedot.", - + "error.validation.license.notgranted": "Tallennusprosessia ei voi päättää, ellet hyväksy julkaisulisenssiä. Voit myös tallentaa tiedot ja jatkaa tallennusta myöhemmin tai poistaa kaikki syöttämäsi tiedot.", + // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", "error.validation.pattern": "Syötteen on noudatettava seuraavaa kaavaa: {{ pattern }}.", - + // "error.validation.filerequired": "The file upload is mandatory", "error.validation.filerequired": "Tiedoston lataus on pakollinen", - - - + + + // "file-section.error.header": "Error obtaining files for this item", - // TODO New key - Add a translation - "file-section.error.header": "Error obtaining files for this item", - - - + "file-section.error.header": "Virhe tietueen tiedostoja noudettaessa", + + + // "footer.copyright": "copyright © 2002-{{ year }}", "footer.copyright": "tekijänoikeus © 2002-{{ year }}", - + // "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 - "footer.link.cookies": "Cookie settings", - + "footer.link.cookies": "Evästeasetukset", + // "footer.link.privacy-policy": "Privacy policy", - // TODO New key - Add a translation - "footer.link.privacy-policy": "Privacy policy", - + "footer.link.privacy-policy": "Yksilönsuoja", + // "footer.link.end-user-agreement":"End User Agreement", - // TODO New key - Add a translation - "footer.link.end-user-agreement":"End User Agreement", - - - + "footer.link.end-user-agreement":"Käyttöehdot", + + + // "forgot-email.form.header": "Forgot Password", - // TODO New key - Add a translation - "forgot-email.form.header": "Forgot Password", - + "forgot-email.form.header": "Unohtunut salasana", + // "forgot-email.form.info": "Enter Register an account to subscribe to collections for email updates, and submit new items to DSpace.", - // TODO New key - Add a translation - "forgot-email.form.info": "Enter Register an account to subscribe to collections for email updates, and submit new items to DSpace.", - + "forgot-email.form.info": "Rekisteröi käyttäjätili voidaksesi tilata sähköposti-ilmoituksia kokoelmien päivityksistä ja lisätä uusia tietueita julkaisuarkistoon.", + // "forgot-email.form.email": "Email Address *", - // TODO New key - Add a translation - "forgot-email.form.email": "Email Address *", - + "forgot-email.form.email": "Sähköpostiosoite *", + // "forgot-email.form.email.error.required": "Please fill in an email address", - // TODO New key - Add a translation - "forgot-email.form.email.error.required": "Please fill in an email address", - + "forgot-email.form.email.error.required": "Anna sähköpostiosoite", + // "forgot-email.form.email.error.pattern": "Please fill in a valid email address", - // TODO New key - Add a translation - "forgot-email.form.email.error.pattern": "Please fill in a valid email address", - + "forgot-email.form.email.error.pattern": "Anna oikea sähköpostiosoite", + // "forgot-email.form.email.hint": "This address will be verified and used as your login name.", - // TODO New key - Add a translation - "forgot-email.form.email.hint": "This address will be verified and used as your login name.", - + "forgot-email.form.email.hint": "Osoite varmistetaan, ja se toimii käyttäjätunnuksenasi kirjautumisessa.", + // "forgot-email.form.submit": "Submit", - // TODO New key - Add a translation - "forgot-email.form.submit": "Submit", - + "forgot-email.form.submit": "Lähetä", + // "forgot-email.form.success.head": "Verification email sent", - // TODO New key - Add a translation - "forgot-email.form.success.head": "Verification email sent", - + "forgot-email.form.success.head": "Varmistusviesti lähetetty", + // "forgot-email.form.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", - // TODO New key - Add a translation - "forgot-email.form.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", - + "forgot-email.form.success.content": "Viesti on lähetetty osoitteeseen {{ email }}. Viestissä on URL-osoite ja lisäohjeita.", + // "forgot-email.form.error.head": "Error when trying to register email", - // TODO New key - Add a translation - "forgot-email.form.error.head": "Error when trying to register email", - + "forgot-email.form.error.head": "Virhe sähköpostiosoitetta rekisteröitäessä", + // "forgot-email.form.error.content": "An error occured when registering the following email address: {{ email }}", - // TODO New key - Add a translation - "forgot-email.form.error.content": "An error occured when registering the following email address: {{ email }}", - - - + "forgot-email.form.error.content": "Virhe rekisteröitäessä tätä sähköpostiosoitetta: {{ email }}", + + + // "forgot-password.title": "Forgot Password", - // TODO New key - Add a translation - "forgot-password.title": "Forgot Password", - + "forgot-password.title": "Unohtunut salasana", + // "forgot-password.form.head": "Forgot Password", - // TODO New key - Add a translation - "forgot-password.form.head": "Forgot Password", - + "forgot-password.form.head": "Unohtunut salasana", + // "forgot-password.form.info": "Enter a new password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", - // TODO New key - Add a translation - "forgot-password.form.info": "Enter a new password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", - + "forgot-password.form.info": "Syötä uusi salasana alla olevaan kenttään ja vahvista se kirjoittamalla salasana uudelleen seuraavaan kenttään. Salasanan on oltava vähintään kuusi merkkiä pitkä.", + // "forgot-password.form.card.security": "Security", - // TODO New key - Add a translation - "forgot-password.form.card.security": "Security", - + "forgot-password.form.card.security": "Turvallisuus", + // "forgot-password.form.identification.header": "Identify", - // TODO New key - Add a translation - "forgot-password.form.identification.header": "Identify", - + "forgot-password.form.identification.header": "Tunnistautuminen", + // "forgot-password.form.identification.email": "Email address: ", - // TODO New key - Add a translation - "forgot-password.form.identification.email": "Email address: ", - + "forgot-password.form.identification.email": "Sähköpostiosoite: ", + // "forgot-password.form.label.password": "Password", - // TODO New key - Add a translation - "forgot-password.form.label.password": "Password", - + "forgot-password.form.label.password": "Salasana", + // "forgot-password.form.label.passwordrepeat": "Retype to confirm", - // TODO New key - Add a translation - "forgot-password.form.label.passwordrepeat": "Retype to confirm", - + "forgot-password.form.label.passwordrepeat": "Kirjoita uudelleen vahvistaaksesi", + // "forgot-password.form.error.empty-password": "Please enter a password in the box below.", - // TODO New key - Add a translation - "forgot-password.form.error.empty-password": "Please enter a password in the box below.", - + "forgot-password.form.error.empty-password": "Syötä salasana alla olevaan kenttään", + // "forgot-password.form.error.matching-passwords": "The passwords do not match.", - // TODO New key - Add a translation - "forgot-password.form.error.matching-passwords": "The passwords do not match.", - + "forgot-password.form.error.matching-passwords": "Salasanat eivät täsmää.", + // "forgot-password.form.error.password-length": "The password should be at least 6 characters long.", - // TODO New key - Add a translation - "forgot-password.form.error.password-length": "The password should be at least 6 characters long.", - + "forgot-password.form.error.password-length": "Salasanan on oltava vähintään 6 merkkiä pitkä.", + // "forgot-password.form.notification.error.title": "Error when trying to submit new password", - // TODO New key - Add a translation - "forgot-password.form.notification.error.title": "Error when trying to submit new password", - + "forgot-password.form.notification.error.title": "Virhe uutta salasanaa lähetettäessä", + // "forgot-password.form.notification.success.content": "The password reset was successful. You have been logged in as the created user.", - // TODO New key - Add a translation - "forgot-password.form.notification.success.content": "The password reset was successful. You have been logged in as the created user.", - + "forgot-password.form.notification.success.content": "Salasanan nollaus onnistui. Sinut on kirjattu sisään.", + // "forgot-password.form.notification.success.title": "Password reset completed", - // TODO New key - Add a translation - "forgot-password.form.notification.success.title": "Password reset completed", - + "forgot-password.form.notification.success.title": "Salasana nollattu", + // "forgot-password.form.submit": "Submit password", - // TODO New key - Add a translation - "forgot-password.form.submit": "Submit password", - - - + "forgot-password.form.submit": "Lähetä salasana", + + + // "form.add": "Add", "form.add": "Lisää", - + // "form.add-help": "Click here to add the current entry and to add another one", "form.add-help": "Valitse lisätäksesi nykyisen kohteen ja toisen kohteen", - + // "form.cancel": "Cancel", "form.cancel": "Peruuta", - + // "form.clear": "Clear", "form.clear": "Tyhjennä", - + // "form.clear-help": "Click here to remove the selected value", "form.clear-help": "Napauta tästä poistaaksesi valitun arvon", - + // "form.edit": "Edit", "form.edit": "Muokkaa", - + // "form.edit-help": "Click here to edit the selected value", "form.edit-help": "Napauta tästä muokataksesi valittua arvoa", - + // "form.first-name": "First name", "form.first-name": "Etunimi", - + // "form.group-collapse": "Collapse", "form.group-collapse": "Sulje", - + // "form.group-collapse-help": "Click here to collapse", "form.group-collapse-help": "Sulje napauttamalla", - + // "form.group-expand": "Expand", "form.group-expand": "Laajenna", - + // "form.group-expand-help": "Click here to expand and add more elements", "form.group-expand-help": "Avaa napauttamalla lisätäksesi uusia elementtejä", - + // "form.last-name": "Last name", "form.last-name": "Sukunimi", - + // "form.loading": "Loading...", "form.loading": "Ladataan...", - + // "form.lookup": "Lookup", "form.lookup": "Hakeminen", - + // "form.lookup-help": "Click here to look up an existing relation", "form.lookup-help": "Valitse hakeaksesi olemassa olevaa relaatiota", - + // "form.no-results": "No results found", "form.no-results": "Ei tuloksia", - + // "form.no-value": "No value entered", "form.no-value": "Ei syötettyä arvoa", - + // "form.other-information": {}, "form.other-information": {}, - + // "form.remove": "Remove", "form.remove": "Poista", - + // "form.save": "Save", "form.save": "Tallenna", - + // "form.save-help": "Save changes", "form.save-help": "Tallenna muutokset", - + // "form.search": "Search", "form.search": "Hae", - + // "form.search-help": "Click here to look for an existing correspondence", - // TODO Source message changed - Revise the translation "form.search-help": "Valitse etsiäksesi olemassa olevaa vastaavuutta", - + // "form.submit": "Submit", "form.submit": "Lähetä", - - - + + + // "home.description": "", "home.description": "", - + // "home.breadcrumbs": "Home", - // TODO New key - Add a translation - "home.breadcrumbs": "Home", - + "home.breadcrumbs": "Etusivu", + // "home.title": "DSpace Angular :: Home", "home.title": "DSpace Angular :: Etusivu", - + // "home.top-level-communities.head": "Communities in DSpace", "home.top-level-communities.head": "Julkaisuarkiston yhteisöt", - + // "home.top-level-communities.help": "Select a community to browse its collections.", "home.top-level-communities.help": "Valitse yhteisö, jonka kokoelmia haluat selata.", - - - + + + // "info.end-user-agreement.accept": "I have read and I agree to the End User Agreement", - // TODO New key - Add a translation - "info.end-user-agreement.accept": "I have read and I agree to the End User Agreement", - + "info.end-user-agreement.accept": "Olen lukenut ja hyväksyn käyttöehdot", + // "info.end-user-agreement.accept.error": "An error occurred accepting the End User Agreement", - // TODO New key - Add a translation - "info.end-user-agreement.accept.error": "An error occurred accepting the End User Agreement", - + "info.end-user-agreement.accept.error": "Virhe hyväksyttäessä käyttöehtoja", + // "info.end-user-agreement.accept.success": "Successfully updated the End User Agreement", - // TODO New key - Add a translation - "info.end-user-agreement.accept.success": "Successfully updated the End User Agreement", - + "info.end-user-agreement.accept.success": "Käyttöehdot päivitetty", + // "info.end-user-agreement.breadcrumbs": "End User Agreement", - // TODO New key - Add a translation - "info.end-user-agreement.breadcrumbs": "End User Agreement", - + "info.end-user-agreement.breadcrumbs": "Käyttöehdot", + // "info.end-user-agreement.buttons.cancel": "Cancel", - // TODO New key - Add a translation - "info.end-user-agreement.buttons.cancel": "Cancel", - + "info.end-user-agreement.buttons.cancel": "Peruuta", + // "info.end-user-agreement.buttons.save": "Save", - // TODO New key - Add a translation - "info.end-user-agreement.buttons.save": "Save", - + "info.end-user-agreement.buttons.save": "Tallenna", + // "info.end-user-agreement.head": "End User Agreement", - // TODO New key - Add a translation - "info.end-user-agreement.head": "End User Agreement", - + "info.end-user-agreement.head": "Käyttöehdot", + // "info.end-user-agreement.title": "End User Agreement", - // TODO New key - Add a translation - "info.end-user-agreement.title": "End User Agreement", - + "info.end-user-agreement.title": "Käyttöehdot", + // "info.privacy.breadcrumbs": "Privacy Statement", - // TODO New key - Add a translation - "info.privacy.breadcrumbs": "Privacy Statement", - + "info.privacy.breadcrumbs": "Tietosuojalauseke", + // "info.privacy.head": "Privacy Statement", - // TODO New key - Add a translation - "info.privacy.head": "Privacy Statement", - + "info.privacy.head": "Tietosuojalauseke", + // "info.privacy.title": "Privacy Statement", - // TODO New key - Add a translation - "info.privacy.title": "Privacy Statement", - - - + "info.privacy.title": "Tietosuojalauseke", + + + // "item.alerts.private": "This item is private", - // TODO New key - Add a translation - "item.alerts.private": "This item is private", - + "item.alerts.private": "Tietue on yksityinen", + // "item.alerts.withdrawn": "This item has been withdrawn", - // TODO New key - Add a translation - "item.alerts.withdrawn": "This item has been withdrawn", - - - + "item.alerts.withdrawn": "Tietue on poistettu käytöstä", + + + // "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", - // TODO New key - Add a translation - "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", - + "item.edit.authorizations.heading": "Tässä voit tarkastella ja muokata tietuekäytäntöjä sekä tietueen yksittäisten komponenttien eli nippujen ja tiedostojen käytäntöjä. Tietue on nippujen säiliö ja niput ovat tiedostojen säiliöitä. Säiliöillä on yleensä LISÄYS/POISTO/LUKU/KIRJOITUS-käytännöt, mutta tiedostoihin sovelletaan vain LUKU/KIRJOITUS-käytäntöjä.", + + + // "item.edit.authorizations.title": "Edit item's Policies", - // TODO New key - Add a translation - "item.edit.authorizations.title": "Edit item's Policies", - - - + "item.edit.authorizations.title": "Muokkaa tietuekäytäntöjä", + + + // "item.badge.private": "Private", - // TODO New key - Add a translation - "item.badge.private": "Private", - + "item.badge.private": "Yksityinen", + // "item.badge.withdrawn": "Withdrawn", - // TODO New key - Add a translation - "item.badge.withdrawn": "Withdrawn", - - - + "item.badge.withdrawn": "Poistettu käytöstä", + + + // "item.bitstreams.upload.bundle": "Bundle", "item.bitstreams.upload.bundle": "Nippu", - + // "item.bitstreams.upload.bundle.placeholder": "Select a bundle", "item.bitstreams.upload.bundle.placeholder": "Valitse nippu", - + // "item.bitstreams.upload.bundle.new": "Create bundle", "item.bitstreams.upload.bundle.new": "Luo nippu", - + // "item.bitstreams.upload.bundles.empty": "This item doesn\'t contain any bundles to upload a bitstream to.", "item.bitstreams.upload.bundles.empty": "Tällä tietueella ei ole nippua, johon tiedoston voisi ladata.", - + // "item.bitstreams.upload.cancel": "Cancel", "item.bitstreams.upload.cancel": "Peruuta", - + // "item.bitstreams.upload.drop-message": "Drop a file to upload", "item.bitstreams.upload.drop-message": "Pudota ladattava tiedosto", - + // "item.bitstreams.upload.item": "Item: ", "item.bitstreams.upload.item": "Tietue: ", - + // "item.bitstreams.upload.notifications.bundle.created.content": "Successfully created new bundle.", "item.bitstreams.upload.notifications.bundle.created.content": "Uusi nippu on luotu.", - + // "item.bitstreams.upload.notifications.bundle.created.title": "Created bundle", "item.bitstreams.upload.notifications.bundle.created.title": "Luotu nippu", - + // "item.bitstreams.upload.notifications.upload.failed": "Upload failed. Please verify the content before retrying.", "item.bitstreams.upload.notifications.upload.failed": "Lataus epäonnistui. Tarkista sisältö ennen kuin yrität uudelleen.", - + // "item.bitstreams.upload.title": "Upload bitstream", "item.bitstreams.upload.title": "Lataa tiedosto", - - - + + + // "item.edit.bitstreams.bundle.edit.buttons.upload": "Upload", "item.edit.bitstreams.bundle.edit.buttons.upload": "Lataus", - + // "item.edit.bitstreams.bundle.displaying": "Currently displaying {{ amount }} bitstreams of {{ total }}.", "item.edit.bitstreams.bundle.displaying": "Näytetään {{ amount }} / {{ total }} tiedostoa.", - + // "item.edit.bitstreams.bundle.load.all": "Load all ({{ total }})", "item.edit.bitstreams.bundle.load.all": "Lataa kaikki ({{ total }})", - + // "item.edit.bitstreams.bundle.load.more": "Load more", "item.edit.bitstreams.bundle.load.more": "Lataa lisää", - + // "item.edit.bitstreams.bundle.name": "BUNDLE: {{ name }}", "item.edit.bitstreams.bundle.name": "NIPPU: {{ name }}", - + // "item.edit.bitstreams.discard-button": "Discard", "item.edit.bitstreams.discard-button": "Hylkää", - + // "item.edit.bitstreams.edit.buttons.download": "Download", "item.edit.bitstreams.edit.buttons.download": "Lataa", - + // "item.edit.bitstreams.edit.buttons.drag": "Drag", "item.edit.bitstreams.edit.buttons.drag": "Raahaa", - + // "item.edit.bitstreams.edit.buttons.edit": "Edit", "item.edit.bitstreams.edit.buttons.edit": "Muokkaa", - + // "item.edit.bitstreams.edit.buttons.remove": "Remove", "item.edit.bitstreams.edit.buttons.remove": "Poista", - + // "item.edit.bitstreams.edit.buttons.undo": "Undo changes", "item.edit.bitstreams.edit.buttons.undo": "Peruuta muutokset", - + // "item.edit.bitstreams.empty": "This item doesn't contain any bitstreams. Click the upload button to create one.", "item.edit.bitstreams.empty": "Tietueeseen ei liity tiedostoja. Valitse latauspainike luodaksesi tiedoston.", - + // "item.edit.bitstreams.headers.actions": "Actions", "item.edit.bitstreams.headers.actions": "Toiminnot", - + // "item.edit.bitstreams.headers.bundle": "Bundle", "item.edit.bitstreams.headers.bundle": "Nippu", - + // "item.edit.bitstreams.headers.description": "Description", "item.edit.bitstreams.headers.description": "Kuvaus", - + // "item.edit.bitstreams.headers.format": "Format", "item.edit.bitstreams.headers.format": "Formaatti", - + // "item.edit.bitstreams.headers.name": "Name", "item.edit.bitstreams.headers.name": "Nimi", - + // "item.edit.bitstreams.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.bitstreams.notifications.discarded.content": "Muutokset hylätty. Valitse 'Kumoa' palauttaaksesi muutokset", - + // "item.edit.bitstreams.notifications.discarded.title": "Changes discarded", "item.edit.bitstreams.notifications.discarded.title": "Muutokset hylätty", - + // "item.edit.bitstreams.notifications.move.failed.title": "Error moving bitstreams", "item.edit.bitstreams.notifications.move.failed.title": "Virhe siirrettäessä tiedostoja", - + // "item.edit.bitstreams.notifications.move.saved.content": "Your move changes to this item's bitstreams and bundles have been saved.", "item.edit.bitstreams.notifications.move.saved.content": "Tietueen tiedostojen ja nippujen siirtomuutokset on tallennettu.", - + // "item.edit.bitstreams.notifications.move.saved.title": "Move changes saved", "item.edit.bitstreams.notifications.move.saved.title": "Siirtomuutokset tallennettu", - + // "item.edit.bitstreams.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", "item.edit.bitstreams.notifications.outdated.content": "Toinen käyttäjä on muuttanut parhaillaan muokkaamaasi tietuetta. Tekemäsi muutokset on hylätty ristiriitojen estämiseksi", - + // "item.edit.bitstreams.notifications.outdated.title": "Changes outdated", "item.edit.bitstreams.notifications.outdated.title": "Muutokset vanhentuneet", - + // "item.edit.bitstreams.notifications.remove.failed.title": "Error deleting bitstream", "item.edit.bitstreams.notifications.remove.failed.title": "Virhe tiedostoa poistettaessa", - + // "item.edit.bitstreams.notifications.remove.saved.content": "Your removal changes to this item's bitstreams have been saved.", "item.edit.bitstreams.notifications.remove.saved.content": "Tietueen tiedostojen poistomuutokset on tallennettu.", - + // "item.edit.bitstreams.notifications.remove.saved.title": "Removal changes saved", "item.edit.bitstreams.notifications.remove.saved.title": "Poistomuutokset tallennettu", - + // "item.edit.bitstreams.reinstate-button": "Undo", "item.edit.bitstreams.reinstate-button": "Kumoa", - + // "item.edit.bitstreams.save-button": "Save", "item.edit.bitstreams.save-button": "Tallenna", - + // "item.edit.bitstreams.upload-button": "Upload", "item.edit.bitstreams.upload-button": "Lataa", - - - + + + // "item.edit.delete.cancel": "Cancel", "item.edit.delete.cancel": "Peruuta", - + // "item.edit.delete.confirm": "Delete", "item.edit.delete.confirm": "Poista", - + // "item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.", "item.edit.delete.description": "Haluatko varmasti poistaa tiedoston pysyvästi?", - + // "item.edit.delete.error": "An error occurred while deleting the item", "item.edit.delete.error": "Virhe tietuetta poistettaessa", - + // "item.edit.delete.header": "Delete item: {{ id }}", "item.edit.delete.header": "Poista tietue: {{ id }}", - + // "item.edit.delete.success": "The item has been deleted", "item.edit.delete.success": "Tietue poistettu", - + // "item.edit.head": "Edit Item", "item.edit.head": "Muokkaa tietuetta", - + // "item.edit.breadcrumbs": "Edit Item", "item.edit.breadcrumbs": "Muokkaa tietuetta", - - + + // "item.edit.tabs.mapper.head": "Collection Mapper", - // TODO New key - Add a translation - "item.edit.tabs.mapper.head": "Collection Mapper", - + "item.edit.tabs.mapper.head": "Kokoelmaliitosväline", + // "item.edit.tabs.item-mapper.title": "Item Edit - Collection Mapper", - // TODO New key - Add a translation - "item.edit.tabs.item-mapper.title": "Item Edit - Collection Mapper", - + "item.edit.tabs.item-mapper.title": "Tietueen muokkaus - Kokoelmaliitosväline", + // "item.edit.item-mapper.buttons.add": "Map item to selected collections", "item.edit.item-mapper.buttons.add": "Liitä tietue valittuihin kokoelmiin", - + // "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", "item.edit.item-mapper.buttons.remove": "Poista tietueen liitos valituista kokoelmista", - + // "item.edit.item-mapper.cancel": "Cancel", "item.edit.item-mapper.cancel": "Peruuta", - + // "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", "item.edit.item-mapper.description": "Tällä työkalulla kokoelmien ylläpitäjät voivat liittää tietueen muihin kokoelmiin. Voit hakea kokoelmia ja liittää aineiston niihin tai selata luetteloa kokoelmista, joihin aineisto on liitetty.", - + // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", "item.edit.item-mapper.head": "Tietueliitosväline - Liitä tietue kokoelmiin", - + // "item.edit.item-mapper.item": "Item: \"{{name}}\"", "item.edit.item-mapper.item": "Tietue: \"{{name}}\"", - + // "item.edit.item-mapper.no-search": "Please enter a query to search", "item.edit.item-mapper.no-search": "Anna hakulauseke", - + // "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", "item.edit.item-mapper.notifications.add.error.content": "Virhe liitettäessä tietuetta {{amount}} kokoelmaan.", - + // "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", "item.edit.item-mapper.notifications.add.error.head": "Virhe liitettäessä", - + // "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", "item.edit.item-mapper.notifications.add.success.content": "Tietue liitetty {{amount}} kokoelmaan.", - + // "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", "item.edit.item-mapper.notifications.add.success.head": "Liitos valmis", - + // "item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.", "item.edit.item-mapper.notifications.remove.error.content": "Virheitä poistettaessa liitosta {{amount}} kokoelmaan.", - + // "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", "item.edit.item-mapper.notifications.remove.error.head": "Virheellisten liitosten poisto", - + // "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", "item.edit.item-mapper.notifications.remove.success.content": "Poistettu tietueen liitos {{amount}} kokoelmasta.", - + // "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", "item.edit.item-mapper.notifications.remove.success.head": "Liitos poistettu", - + // "item.edit.item-mapper.tabs.browse": "Browse mapped collections", "item.edit.item-mapper.tabs.browse": "Selaa liitettyjä kokoelmia", - + // "item.edit.item-mapper.tabs.map": "Map new collections", "item.edit.item-mapper.tabs.map": "Liitä uusia kokoelmia", - - - + + + // "item.edit.metadata.add-button": "Add", "item.edit.metadata.add-button": "Lisää", - + // "item.edit.metadata.discard-button": "Discard", "item.edit.metadata.discard-button": "Hylkää", - + // "item.edit.metadata.edit.buttons.edit": "Edit", "item.edit.metadata.edit.buttons.edit": "Muokkaa", - + // "item.edit.metadata.edit.buttons.remove": "Remove", "item.edit.metadata.edit.buttons.remove": "Poista", - + // "item.edit.metadata.edit.buttons.undo": "Undo changes", "item.edit.metadata.edit.buttons.undo": "Kumoa muutokset", - + // "item.edit.metadata.edit.buttons.unedit": "Stop editing", "item.edit.metadata.edit.buttons.unedit": "Lopeta muokkaus", - + // "item.edit.metadata.empty": "The item currently doesn't contain any metadata. Click Add to start adding a metadata value.", - // TODO New key - Add a translation - "item.edit.metadata.empty": "The item currently doesn't contain any metadata. Click Add to start adding a metadata value.", - + "item.edit.metadata.empty": "Tietueessa ei ole metadataa. Valitse Lisää lisätäksesi metadataa.", + // "item.edit.metadata.headers.edit": "Edit", "item.edit.metadata.headers.edit": "Muokkaa", - + // "item.edit.metadata.headers.field": "Field", "item.edit.metadata.headers.field": "Kenttä", - + // "item.edit.metadata.headers.language": "Lang", "item.edit.metadata.headers.language": "Kieli", - + // "item.edit.metadata.headers.value": "Value", "item.edit.metadata.headers.value": "Arvo", - + // "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", "item.edit.metadata.metadatafield.invalid": "Valitse oikea metadatakenttä", - + // "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.metadata.notifications.discarded.content": "Muutokset hylätty. Valitse 'Kumoa' palauttaaksesi muutokset", - + // "item.edit.metadata.notifications.discarded.title": "Changed discarded", "item.edit.metadata.notifications.discarded.title": "Muutokset hylätty", - + // "item.edit.metadata.notifications.error.title": "An error occurred", - // TODO New key - Add a translation - "item.edit.metadata.notifications.error.title": "An error occurred", - + "item.edit.metadata.notifications.error.title": "Tapahtui virhe", + // "item.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", "item.edit.metadata.notifications.invalid.content": "Muutoksia ei tallennettu. Tarkista kaikkien kenttien oikeellisuus ennen tallennusta.", - + // "item.edit.metadata.notifications.invalid.title": "Metadata invalid", "item.edit.metadata.notifications.invalid.title": "Virheellinen metadata", - + // "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", "item.edit.metadata.notifications.outdated.content": "Toinen käyttäjä on muuttanut parhaillaan muokkaamaasi tietuetta. Tekemäsi muutokset on hylätty ristiriitojen estämiseksi", - + // "item.edit.metadata.notifications.outdated.title": "Changed outdated", "item.edit.metadata.notifications.outdated.title": "Muutokset vanhentuneet", - + // "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", "item.edit.metadata.notifications.saved.content": "Muutokset tietueen metadataan tallennettu.", - + // "item.edit.metadata.notifications.saved.title": "Metadata saved", "item.edit.metadata.notifications.saved.title": "Metadata tallennettu", - + // "item.edit.metadata.reinstate-button": "Undo", "item.edit.metadata.reinstate-button": "Peruuta", - + // "item.edit.metadata.save-button": "Save", "item.edit.metadata.save-button": "Tallenna", - - - + + + // "item.edit.modify.overview.field": "Field", "item.edit.modify.overview.field": "Kenttä", - + // "item.edit.modify.overview.language": "Language", "item.edit.modify.overview.language": "Kieli", - + // "item.edit.modify.overview.value": "Value", "item.edit.modify.overview.value": "Arvo", - - - + + + // "item.edit.move.cancel": "Cancel", "item.edit.move.cancel": "Peruuta", - + // "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", "item.edit.move.description": "Valitse kokoelma, johon haluat siirtää tietueen. Voit antaa hakulausekkeen kokoelmien määrän pienentämiseksi.", - + // "item.edit.move.error": "An error occurred when attempting to move the item", "item.edit.move.error": "Virhe tietuetta siirrettäessä", - + // "item.edit.move.head": "Move item: {{id}}", "item.edit.move.head": "Siirrä tietue: {{id}}", - + // "item.edit.move.inheritpolicies.checkbox": "Inherit policies", - "item.edit.move.inheritpolicies.checkbox": "Peri toimintatavat", - + "item.edit.move.inheritpolicies.checkbox": "Peri käytännöt", + // "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", - "item.edit.move.inheritpolicies.description": "Peri kohdekokoelman oletustoimintatavat", - + "item.edit.move.inheritpolicies.description": "Peri kohdekokoelman oletuskäytännöt", + // "item.edit.move.move": "Move", "item.edit.move.move": "Siirrä", - + // "item.edit.move.processing": "Moving...", "item.edit.move.processing": "Siirretään...", - + // "item.edit.move.search.placeholder": "Enter a search query to look for collections", "item.edit.move.search.placeholder": "Anna hakulauseke kokoelmien etsimiseksi", - + // "item.edit.move.success": "The item has been moved successfully", "item.edit.move.success": "Tietue siirretty", - + // "item.edit.move.title": "Move item", "item.edit.move.title": "Siirrä tietue", - - - + + + // "item.edit.private.cancel": "Cancel", "item.edit.private.cancel": "Peruuta", - + // "item.edit.private.confirm": "Make it Private", "item.edit.private.confirm": "Muuta yksityiseksi", - + // "item.edit.private.description": "Are you sure this item should be made private in the archive?", - "item.edit.private.description": "Oletko varma, että haluat muuttaa tietueen yksityiseksi?", - + "item.edit.private.description": "Haluatko varmasti muuttaa tietueen yksityiseksi?", + // "item.edit.private.error": "An error occurred while making the item private", "item.edit.private.error": "Virhe muutettaessa tietuetta yksityiseksi", - + // "item.edit.private.header": "Make item private: {{ id }}", "item.edit.private.header": "Muuta yksityiseksi tietue: {{ id }}", - + // "item.edit.private.success": "The item is now private", "item.edit.private.success": "Tietue on yksityinen", - - - + + + // "item.edit.public.cancel": "Cancel", "item.edit.public.cancel": "Peruuta", - + // "item.edit.public.confirm": "Make it Public", "item.edit.public.confirm": "Muuta julkiseksi", - + // "item.edit.public.description": "Are you sure this item should be made public in the archive?", - "item.edit.public.description": "Oletko varma, että haluat muuttaa tietueen julkiseksi?", - + "item.edit.public.description": "Haluatko varmasti muuttaa tietueen julkiseksi?", + // "item.edit.public.error": "An error occurred while making the item public", "item.edit.public.error": "Virhe muutettaessa tietuetta julkiseksi", - + // "item.edit.public.header": "Make item public: {{ id }}", "item.edit.public.header": "Muuta julkiseksi tietue: {{ id }}", - + // "item.edit.public.success": "The item is now public", "item.edit.public.success": "Tietue on julkinen", - - - + + + // "item.edit.reinstate.cancel": "Cancel", "item.edit.reinstate.cancel": "Peruuta", - + // "item.edit.reinstate.confirm": "Reinstate", "item.edit.reinstate.confirm": "Palauta", - + // "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", - "item.edit.reinstate.description": "Oletko varma, että haluat palauttaa tietueen käyttöön?", - + "item.edit.reinstate.description": "Haluatko varmasti palauttaa tietueen käyttöön?", + // "item.edit.reinstate.error": "An error occurred while reinstating the item", "item.edit.reinstate.error": "Virhe palautettaessa tietuetta käyttöön", - + // "item.edit.reinstate.header": "Reinstate item: {{ id }}", "item.edit.reinstate.header": "Palauta käyttöön tietue: {{ id }}", - + // "item.edit.reinstate.success": "The item was reinstated successfully", "item.edit.reinstate.success": "Tietue palautettu käyttöön", - - - + + + // "item.edit.relationships.discard-button": "Discard", "item.edit.relationships.discard-button": "Hylkää", - + // "item.edit.relationships.edit.buttons.add": "Add", - // TODO New key - Add a translation - "item.edit.relationships.edit.buttons.add": "Add", - + "item.edit.relationships.edit.buttons.add": "Lisää", + // "item.edit.relationships.edit.buttons.remove": "Remove", "item.edit.relationships.edit.buttons.remove": "Poista", - + // "item.edit.relationships.edit.buttons.undo": "Undo changes", "item.edit.relationships.edit.buttons.undo": "Peruuta muutokset", - + // "item.edit.relationships.no-relationships": "No relationships", - // TODO New key - Add a translation - "item.edit.relationships.no-relationships": "No relationships", - + "item.edit.relationships.no-relationships": "Ei yhteyksiä", + // "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.relationships.notifications.discarded.content": "Muutoksesi hylättiin. Valitse 'Peruuta' palauttaaksesi ne.", - + // "item.edit.relationships.notifications.discarded.title": "Changes discarded", "item.edit.relationships.notifications.discarded.title": "Muutokset hylätty", - + // "item.edit.relationships.notifications.failed.title": "Error editing relationships", - // TODO Source message changed - Revise the translation - "item.edit.relationships.notifications.failed.title": "Virhe yhteyksiä poistettaessa", - + "item.edit.relationships.notifications.failed.title": "Virhe yhteyksiä muokattaessa", + // "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", "item.edit.relationships.notifications.outdated.content": "Toinen käyttäjä on muuttanut parhaillaan muokkaamaasi tietuetta. Tekemäsi muutokset on hylätty ristiriitojen estämiseksi", - + // "item.edit.relationships.notifications.outdated.title": "Changes outdated", "item.edit.relationships.notifications.outdated.title": "Muutokset vanhentuneet", - + // "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", "item.edit.relationships.notifications.saved.content": "Muutokset tietueen yhteyksiin tallennettu.", - + // "item.edit.relationships.notifications.saved.title": "Relationships saved", "item.edit.relationships.notifications.saved.title": "Yhteydet tallennettu", - + // "item.edit.relationships.reinstate-button": "Undo", "item.edit.relationships.reinstate-button": "Peruuta", - + // "item.edit.relationships.save-button": "Save", "item.edit.relationships.save-button": "Tallenna", - + // "item.edit.relationships.no-entity-type": "Add 'dspace.entity.type' metadata to enable relationships for this item", - // TODO New key - Add a translation - "item.edit.relationships.no-entity-type": "Add 'dspace.entity.type' metadata to enable relationships for this item", - - - + "item.edit.relationships.no-entity-type": "Lisää 'dspace.entity.type' -metadataa aktivoidaksesi yhteydet tietueessa", + // "item.edit.tabs.bitstreams.head": "Bitstreams", "item.edit.tabs.bitstreams.head": "Tietueen tiedostot", - + // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", "item.edit.tabs.bitstreams.title": "Tietueen muokkaus - Tiedostot", - + // "item.edit.tabs.curate.head": "Curate", "item.edit.tabs.curate.head": "Kuratoi", - + // "item.edit.tabs.curate.title": "Item Edit - Curate", "item.edit.tabs.curate.title": "Tietueen muokkaus - Kuratointi", - + // "item.edit.tabs.metadata.head": "Metadata", "item.edit.tabs.metadata.head": "Tietueen metadata", - + // "item.edit.tabs.metadata.title": "Item Edit - Metadata", "item.edit.tabs.metadata.title": "Tietueen muokkaus - Metadata", - + // "item.edit.tabs.relationships.head": "Relationships", "item.edit.tabs.relationships.head": "Tietueen yhteydet", - + // "item.edit.tabs.relationships.title": "Item Edit - Relationships", "item.edit.tabs.relationships.title": "Tietueen muokkaus - Yhteydet", - + // "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", "item.edit.tabs.status.buttons.authorizations.button": "Käyttöoikeudet...", - + // "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", "item.edit.tabs.status.buttons.authorizations.label": "Muokkaa tietueen käyttöoikeussääntöjä", - + // "item.edit.tabs.status.buttons.delete.button": "Permanently delete", "item.edit.tabs.status.buttons.delete.button": "Poista pysyvästi", - + // "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", "item.edit.tabs.status.buttons.delete.label": "Poista tietue kokonaan", - + // "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", "item.edit.tabs.status.buttons.mappedCollections.button": "Liitetyt kokoelmat", - + // "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", "item.edit.tabs.status.buttons.mappedCollections.label": "Hallinnoi liitettyjä kokoelmia", - + // "item.edit.tabs.status.buttons.move.button": "Move...", "item.edit.tabs.status.buttons.move.button": "Siirrä...", - + // "item.edit.tabs.status.buttons.move.label": "Move item to another collection", "item.edit.tabs.status.buttons.move.label": "Siirrä tietue toiseen kokoelmaan", - + // "item.edit.tabs.status.buttons.private.button": "Make it private...", "item.edit.tabs.status.buttons.private.button": "Muuta yksityiseksi...", - + // "item.edit.tabs.status.buttons.private.label": "Make item private", "item.edit.tabs.status.buttons.private.label": "Muuta tietue yksityiseksi", - + // "item.edit.tabs.status.buttons.public.button": "Make it public...", "item.edit.tabs.status.buttons.public.button": "Muuta julkiseksi...", - + // "item.edit.tabs.status.buttons.public.label": "Make item public", "item.edit.tabs.status.buttons.public.label": "Muuta tietue julkiseksi", - + // "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", "item.edit.tabs.status.buttons.reinstate.button": "Palauta käyttöön...", - + // "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", "item.edit.tabs.status.buttons.reinstate.label": "Palauta tietue arkistoon", - + // "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", "item.edit.tabs.status.buttons.withdraw.button": "Poista käytöstä...", - + // "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", "item.edit.tabs.status.buttons.withdraw.label": "Poista tietue käytöstä", - + // "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", "item.edit.tabs.status.description": "Tervetuloa tietueen hallintasivulle. Täällä voit poistaa käytöstä, palauttaa käyttöön, siirtää tai poistaa tietueen. Voit myös päivittää tai lisätä uutta metadataa / tiedostoja muilla välilehdillä.", - + // "item.edit.tabs.status.head": "Status", "item.edit.tabs.status.head": "Tietueen tila", - + // "item.edit.tabs.status.labels.handle": "Handle", "item.edit.tabs.status.labels.handle": "Handle-tunnus", - + // "item.edit.tabs.status.labels.id": "Item Internal ID", "item.edit.tabs.status.labels.id": "Tietueen sisäinen ID-tunnus", - + // "item.edit.tabs.status.labels.itemPage": "Item Page", "item.edit.tabs.status.labels.itemPage": "Tietueen tiedot", - + // "item.edit.tabs.status.labels.lastModified": "Last Modified", "item.edit.tabs.status.labels.lastModified": "Viimeksi muokattu", - + // "item.edit.tabs.status.title": "Item Edit - Status", "item.edit.tabs.status.title": "Tietueen muokkaus - Tila", - + // "item.edit.tabs.versionhistory.head": "Version History", "item.edit.tabs.versionhistory.head": "Versiohistoria", - + // "item.edit.tabs.versionhistory.title": "Item Edit - Version History", "item.edit.tabs.versionhistory.title": "Tietueen muokkaus - Versiohistoria", - + // "item.edit.tabs.versionhistory.under-construction": "Editing or adding new versions is not yet possible in this user interface.", "item.edit.tabs.versionhistory.under-construction": "Uusien versioiden muokkaus tai lisäys ei ole vielä mahdollista käyttöliittymässä.", - + // "item.edit.tabs.view.head": "View Item", "item.edit.tabs.view.head": "Näytä tietue", - + // "item.edit.tabs.view.title": "Item Edit - View", "item.edit.tabs.view.title": "Tietueen muokkaus - Näytä", - - - + + + // "item.edit.withdraw.cancel": "Cancel", "item.edit.withdraw.cancel": "Peruuta", - + // "item.edit.withdraw.confirm": "Withdraw", "item.edit.withdraw.confirm": "Poista käytöstä", - + // "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", - "item.edit.withdraw.description": "Oletko varma tietueen käytöstä poistamisesta?", - + "item.edit.withdraw.description": "Haluatko varmasti poistaa tietueen käytöstä?", + // "item.edit.withdraw.error": "An error occurred while withdrawing the item", "item.edit.withdraw.error": "Virhe tietuetta käytöstä poistettaessa", - + // "item.edit.withdraw.header": "Withdraw item: {{ id }}", "item.edit.withdraw.header": "Poistettu käytöstä tietue: {{ id }}", - + // "item.edit.withdraw.success": "The item was withdrawn successfully", "item.edit.withdraw.success": "Tietue poistettu käytöstä", - - - + + + // "item.listelement.badge": "Item", - // TODO New key - Add a translation - "item.listelement.badge": "Item", - + "item.listelement.badge": "Tietue", + // "item.page.description": "Description", - // TODO New key - Add a translation - "item.page.description": "Description", - + "item.page.description": "Kuvaus", + // "item.page.edit": "Edit this item", - // TODO New key - Add a translation - "item.page.edit": "Edit this item", - + "item.page.edit": "Muokkaa tietuetta", + // "item.page.journal-issn": "Journal ISSN", - // TODO New key - Add a translation - "item.page.journal-issn": "Journal ISSN", - + "item.page.journal-issn": "Kausijulkaisun ISSN", + // "item.page.journal-title": "Journal Title", - // TODO New key - Add a translation - "item.page.journal-title": "Journal Title", - + "item.page.journal-title": "Kausijulkaisun nimi", + // "item.page.publisher": "Publisher", - // TODO New key - Add a translation - "item.page.publisher": "Publisher", - + "item.page.publisher": "Julkaisija", + // "item.page.titleprefix": "Item: ", - // TODO New key - Add a translation - "item.page.titleprefix": "Item: ", - + "item.page.titleprefix": "Tietue: ", + // "item.page.volume-title": "Volume Title", - // TODO New key - Add a translation - "item.page.volume-title": "Volume Title", - + "item.page.volume-title": "Vuosikerran nimi", + // "item.search.results.head": "Item Search Results", - // TODO New key - Add a translation - "item.search.results.head": "Item Search Results", - + "item.search.results.head": "Tietuehaun tulokset", + // "item.search.title": "DSpace Angular :: Item Search", - // TODO New key - Add a translation - "item.search.title": "DSpace Angular :: Item Search", - - - + "item.search.title": "DSpace Angular :: Tietuehaku", + + + // "item.page.abstract": "Abstract", "item.page.abstract": "Tiivistelmä", - + // "item.page.author": "Authors", "item.page.author": "Tekijät", - + // "item.page.citation": "Citation", "item.page.citation": "Viittaus", - + // "item.page.collections": "Collections", "item.page.collections": "Kokoelmat", - + // "item.page.date": "Date", "item.page.date": "Päivämäärä", - + // "item.page.edit": "Edit this item", - // TODO New key - Add a translation - "item.page.edit": "Edit this item", - + "item.page.edit": "Muokkaa tietuetta", + // "item.page.files": "Files", "item.page.files": "Tiedostot", - + // "item.page.filesection.description": "Description:", "item.page.filesection.description": "Kuvaus:", - + // "item.page.filesection.download": "Download", "item.page.filesection.download": "Lataa", - + // "item.page.filesection.format": "Format:", "item.page.filesection.format": "Formaatti:", - + // "item.page.filesection.name": "Name:", "item.page.filesection.name": "Nimi:", - + // "item.page.filesection.size": "Size:", "item.page.filesection.size": "Koko:", - + // "item.page.journal.search.title": "Articles in this journal", "item.page.journal.search.title": "Artikkelit tässä julkaisussa", - + // "item.page.link.full": "Full item page", "item.page.link.full": "Tietueen kaikki tiedot", - + // "item.page.link.simple": "Simple item page", "item.page.link.simple": "Tietueen suppeat tiedot", - + // "item.page.person.search.title": "Articles by this author", "item.page.person.search.title": "Tekijän artikkelit", - + // "item.page.related-items.view-more": "Show {{ amount }} more", "item.page.related-items.view-more": "Näytä lisää", - + // "item.page.related-items.view-less": "Hide last {{ amount }}", "item.page.related-items.view-less": "Näytä vähemmän", - + // "item.page.relationships.isAuthorOfPublication": "Publications", "item.page.relationships.isAuthorOfPublication": "Julkaisut", - + // "item.page.relationships.isJournalOfPublication": "Publications", "item.page.relationships.isJournalOfPublication": "Julkaisut", - + // "item.page.relationships.isOrgUnitOfPerson": "Authors", "item.page.relationships.isOrgUnitOfPerson": "Tekijät", - + // "item.page.relationships.isOrgUnitOfProject": "Research Projects", "item.page.relationships.isOrgUnitOfProject": "Tutkimusprojektit", - + // "item.page.subject": "Keywords", "item.page.subject": "Avainsanat", - + // "item.page.uri": "URI", "item.page.uri": "URL-osoite", - + // "item.page.bitstreams.view-more": "Show more", - // TODO New key - Add a translation - "item.page.bitstreams.view-more": "Show more", - + "item.page.bitstreams.view-more": "Näytä lisää", + // "item.page.bitstreams.collapse": "Collapse", - // TODO New key - Add a translation - "item.page.bitstreams.collapse": "Collapse", - + "item.page.bitstreams.collapse": "Sulje", + // "item.page.filesection.original.bundle" : "Original bundle", - // TODO New key - Add a translation - "item.page.filesection.original.bundle" : "Original bundle", - + "item.page.filesection.original.bundle" : "Alkuperäinen nippu", + // "item.page.filesection.license.bundle" : "License bundle", - // TODO New key - Add a translation - "item.page.filesection.license.bundle" : "License bundle", - + "item.page.filesection.license.bundle" : "Lisenssinippu", + // "item.preview.dc.identifier.uri": "Identifier:", - // TODO New key - Add a translation - "item.preview.dc.identifier.uri": "Identifier:", - + "item.preview.dc.identifier.uri": "Tunnus:", + // "item.preview.dc.contributor.author": "Authors:", - // TODO New key - Add a translation - "item.preview.dc.contributor.author": "Authors:", - + "item.preview.dc.contributor.author": "Tekijät:", + // "item.preview.dc.date.issued": "Published date:", - // TODO New key - Add a translation - "item.preview.dc.date.issued": "Published date:", - + "item.preview.dc.date.issued": "Julkaisuajankohta:", + // "item.preview.dc.description.abstract": "Abstract:", - // TODO New key - Add a translation - "item.preview.dc.description.abstract": "Abstract:", - + "item.preview.dc.description.abstract": "Tiivistelmä:", + // "item.preview.dc.identifier.other": "Other identifier:", - // TODO New key - Add a translation - "item.preview.dc.identifier.other": "Other identifier:", - + "item.preview.dc.identifier.other": "Muu tunnus:", + // "item.preview.dc.language.iso": "Language:", - // TODO New key - Add a translation - "item.preview.dc.language.iso": "Language:", - + "item.preview.dc.language.iso": "Kieli:", + // "item.preview.dc.subject": "Subjects:", - // TODO New key - Add a translation - "item.preview.dc.subject": "Subjects:", - + "item.preview.dc.subject": "Asiasanat:", + // "item.preview.dc.title": "Title:", - // TODO New key - Add a translation - "item.preview.dc.title": "Title:", - + "item.preview.dc.title": "Nimeke:", + // "item.preview.person.familyName": "Surname:", - // TODO New key - Add a translation - "item.preview.person.familyName": "Surname:", - + "item.preview.person.familyName": "Sukunimi:", + // "item.preview.person.givenName": "Name:", - // TODO New key - Add a translation - "item.preview.person.givenName": "Name:", - + "item.preview.person.givenName": "Nimi:", + // "item.preview.person.identifier.orcid": "ORCID:", - // TODO New key - Add a translation - "item.preview.person.identifier.orcid": "ORCID:", - - + "item.preview.person.identifier.orcid": "ORCID-tunniste:", + + // "item.select.confirm": "Confirm selected", "item.select.confirm": "Vahvista valinta", - + // "item.select.empty": "No items to show", "item.select.empty": "Ei tietueita", - + // "item.select.table.author": "Author", "item.select.table.author": "Tekijä", - + // "item.select.table.collection": "Collection", "item.select.table.collection": "Kokoelma", - + // "item.select.table.title": "Title", "item.select.table.title": "Nimeke", - - + + // "item.version.history.empty": "There are no other versions for this item yet.", "item.version.history.empty": "Tietueesta ei ole muita versioita.", - + // "item.version.history.head": "Version History", "item.version.history.head": "Versiohistoria", - + // "item.version.history.return": "Return", "item.version.history.return": "Palaa", - + // "item.version.history.selected": "Selected version", "item.version.history.selected": "Valittu versio", - + // "item.version.history.table.version": "Version", "item.version.history.table.version": "Versio", - + // "item.version.history.table.item": "Item", "item.version.history.table.item": "Tietue", - + // "item.version.history.table.editor": "Editor", "item.version.history.table.editor": "Toimittaja", - + // "item.version.history.table.date": "Date", "item.version.history.table.date": "Päivämäärä", - + // "item.version.history.table.summary": "Summary", "item.version.history.table.summary": "Yhteenveto", - - - + + + // "item.version.notice": "This is not the latest version of this item. The latest version can be found here.", "item.version.notice": "Tämä ei ole tietueen uusin versio. Uusin versio löytyy täältä.", - - - + + + // "journal.listelement.badge": "Journal", "journal.listelement.badge": "Kausijulkaisu", - + // "journal.page.description": "Description", "journal.page.description": "Kuvaus", - + // "journal.page.edit": "Edit this item", - // TODO New key - Add a translation - "journal.page.edit": "Edit this item", - + "journal.page.edit": "Muokkaa tietuetta", + // "journal.page.editor": "Editor-in-Chief", "journal.page.editor": "Päätoimittaja", - + // "journal.page.issn": "ISSN", "journal.page.issn": "ISSN-tunnus", - + // "journal.page.publisher": "Publisher", "journal.page.publisher": "Julkaisija", - + // "journal.page.titleprefix": "Journal: ", "journal.page.titleprefix": "Kausijulkaisu: ", - + // "journal.search.results.head": "Journal Search Results", "journal.search.results.head": "Kausijulkaisuhaun tulokset", - + // "journal.search.title": "DSpace Angular :: Journal Search", "journal.search.title": "DSpace Angular :: Kausijulkaisuhaku", - - - + + + // "journalissue.listelement.badge": "Journal Issue", "journalissue.listelement.badge": "Kausijulkaisun numero", - + // "journalissue.page.description": "Description", "journalissue.page.description": "Kuvaus", - + // "journalissue.page.edit": "Edit this item", - // TODO New key - Add a translation - "journalissue.page.edit": "Edit this item", - + "journalissue.page.edit": "Muokkaa tietuetta", + // "journalissue.page.issuedate": "Issue Date", "journalissue.page.issuedate": "Julkaisuaika", - + // "journalissue.page.journal-issn": "Journal ISSN", "journalissue.page.journal-issn": "Kausijulkaisun ISSN-tunnus", - + // "journalissue.page.journal-title": "Journal Title", "journalissue.page.journal-title": "Kausijulkaisun nimi", - + // "journalissue.page.keyword": "Keywords", "journalissue.page.keyword": "Asiasanat", - + // "journalissue.page.number": "Number", "journalissue.page.number": "Numero", - + // "journalissue.page.titleprefix": "Journal Issue: ", "journalissue.page.titleprefix": "Kausijulkaisun numero: ", - - - + + + // "journalvolume.listelement.badge": "Journal Volume", "journalvolume.listelement.badge": "Kausijulkaisun vuosikerta", - + // "journalvolume.page.description": "Description", "journalvolume.page.description": "Kuvaus", - + // "journalvolume.page.edit": "Edit this item", - // TODO New key - Add a translation - "journalvolume.page.edit": "Edit this item", - + "journalvolume.page.edit": "Muokkaa tietuetta", + // "journalvolume.page.issuedate": "Issue Date", "journalvolume.page.issuedate": "Julkaisuaika", - + // "journalvolume.page.titleprefix": "Journal Volume: ", "journalvolume.page.titleprefix": "Kausijulkaisun vuosikerta: ", - + // "journalvolume.page.volume": "Volume", "journalvolume.page.volume": "Vuosikerta", - - - + + + // "loading.bitstream": "Loading bitstream...", "loading.bitstream": "Ladataan tiedostoa...", - + // "loading.bitstreams": "Loading bitstreams...", "loading.bitstreams": "Ladataan tiedostoja...", - + // "loading.browse-by": "Loading items...", "loading.browse-by": "Ladataan tietueita...", - + // "loading.browse-by-page": "Loading page...", "loading.browse-by-page": "Ladataan sivua...", - + // "loading.collection": "Loading collection...", "loading.collection": "Ladataan kokoelmaa...", - + // "loading.collections": "Loading collections...", "loading.collections": "Ladataan kokoelmia...", - + // "loading.content-source": "Loading content source...", "loading.content-source": "Ladataan sisältölähdettä...", - + // "loading.community": "Loading community...", "loading.community": "Ladataan yhteisöä...", - + // "loading.default": "Loading...", "loading.default": "Ladataan...", - + // "loading.item": "Loading item...", "loading.item": "Ladataan tietuetta...", - + // "loading.items": "Loading items...", "loading.items": "Ladataan tietueita...", - + // "loading.mydspace-results": "Loading items...", "loading.mydspace-results": "Ladataan tietueita...", - + // "loading.objects": "Loading...", "loading.objects": "Ladataan...", - + // "loading.recent-submissions": "Loading recent submissions...", "loading.recent-submissions": "Ladataan viimeksi lisättyjä...", - + // "loading.search-results": "Loading search results...", "loading.search-results": "Ladataan hakutuloksia...", - + // "loading.sub-collections": "Loading sub-collections...", "loading.sub-collections": "Ladataan alakokoelmia...", - + // "loading.sub-communities": "Loading sub-communities...", "loading.sub-communities": "Ladataan alayhteisöjä...", - + // "loading.top-level-communities": "Loading top-level communities...", "loading.top-level-communities": "Ladataan ylätason yhteisöjä...", - - - + + + // "login.form.email": "Email address", "login.form.email": "Sähköpostiosoite", - + // "login.form.forgot-password": "Have you forgotten your password?", "login.form.forgot-password": "Unohditko salasanasi?", - + // "login.form.header": "Please log in to DSpace", "login.form.header": "Kirjaudu sisään", - + // "login.form.new-user": "New user? Click here to register.", "login.form.new-user": "Uusi käyttäjä? Rekisteröidy tästä.", - + // "login.form.or-divider": "or", "login.form.or-divider": "tai", - + // "login.form.password": "Password", "login.form.password": "Salasana", - + // "login.form.shibboleth": "Log in with Shibboleth", "login.form.shibboleth": "Shibboleth-kirjautuminen", - + // "login.form.submit": "Log in", "login.form.submit": "Kirjaudu sisään", - + // "login.title": "Login", "login.title": "Sisäänkirjautuminen", - + // "login.breadcrumbs": "Login", "login.breadcrumbs": "Sisäänkirjautuminen", - - - + + + // "logout.form.header": "Log out from DSpace", "logout.form.header": "Kirjaudu ulos", - + // "logout.form.submit": "Log out", "logout.form.submit": "Kirjaudu ulos", - + // "logout.title": "Logout", "logout.title": "Uloskirjautuminen", - - - + + + // "menu.header.admin": "Admin", "menu.header.admin": "Ylläpitäjä", - + // "menu.header.image.logo": "Repository logo", "menu.header.image.logo": "Arkiston logo", - - - + + + // "menu.section.access_control": "Access Control", "menu.section.access_control": "Pääsyoikeudet", - + // "menu.section.access_control_authorizations": "Authorizations", "menu.section.access_control_authorizations": "Käyttöoikeudet", - + // "menu.section.access_control_groups": "Groups", "menu.section.access_control_groups": "Ryhmät", - + // "menu.section.access_control_people": "People", "menu.section.access_control_people": "Käyttäjät", - - - + + + // "menu.section.admin_search": "Admin Search", "menu.section.admin_search": "Admin-haku", - - - + + + // "menu.section.browse_community": "This Community", "menu.section.browse_community": "Tämä yhteisö", - + // "menu.section.browse_community_by_author": "By Author", "menu.section.browse_community_by_author": "Tekijän mukaan", - + // "menu.section.browse_community_by_issue_date": "By Issue Date", "menu.section.browse_community_by_issue_date": "Julkaisuajankohdan mukaan", - + // "menu.section.browse_community_by_title": "By Title", "menu.section.browse_community_by_title": "Nimekkeen mukaan", - + // "menu.section.browse_global": "All of DSpace", "menu.section.browse_global": "Koko julkaisuarkisto", - + // "menu.section.browse_global_by_author": "By Author", "menu.section.browse_global_by_author": "Tekijän mukaan", - + // "menu.section.browse_global_by_dateissued": "By Issue Date", "menu.section.browse_global_by_dateissued": "Julkaisuajankohdan mukaan", - + // "menu.section.browse_global_by_subject": "By Subject", "menu.section.browse_global_by_subject": "Asiasanan mukaan", - + // "menu.section.browse_global_by_title": "By Title", "menu.section.browse_global_by_title": "Nimekkeen mukaan", - + // "menu.section.browse_global_communities_and_collections": "Communities & Collections", "menu.section.browse_global_communities_and_collections": "Yhteisöt & kokoelmat", - - - + + + // "menu.section.control_panel": "Control Panel", "menu.section.control_panel": "Hallintapaneeli", - + // "menu.section.curation_task": "Curation Task", "menu.section.curation_task": "Kuratointitehtävä", - - - + + + // "menu.section.edit": "Edit", "menu.section.edit": "Muokkaa", - + // "menu.section.edit_collection": "Collection", "menu.section.edit_collection": "Kokoelma", - + // "menu.section.edit_community": "Community", "menu.section.edit_community": "Yhteisö", - + // "menu.section.edit_item": "Item", "menu.section.edit_item": "Tietue", - - - + + + // "menu.section.export": "Export", "menu.section.export": "Eksportoi", - + // "menu.section.export_collection": "Collection", "menu.section.export_collection": "Kokoelma", - + // "menu.section.export_community": "Community", "menu.section.export_community": "Yhteisö", - + // "menu.section.export_item": "Item", "menu.section.export_item": "Tietue", - + // "menu.section.export_metadata": "Metadata", "menu.section.export_metadata": "Metadata", - - - + + + // "menu.section.icon.access_control": "Access Control menu section", "menu.section.icon.access_control": "Pääsyoikeudet", - + // "menu.section.icon.admin_search": "Admin search menu section", "menu.section.icon.admin_search": "Admin-haku", - + // "menu.section.icon.control_panel": "Control Panel menu section", "menu.section.icon.control_panel": "Hallintapaneeli", - + // "menu.section.icon.curation_task": "Curation Task menu section", "menu.section.icon.curation_task": "Kuratointi", - + // "menu.section.icon.edit": "Edit menu section", "menu.section.icon.edit": "Muokkaus", - + // "menu.section.icon.export": "Export menu section", "menu.section.icon.export": "Eksportointi", - + // "menu.section.icon.find": "Find menu section", "menu.section.icon.find": "Haku", - + // "menu.section.icon.import": "Import menu section", "menu.section.icon.import": "Importointi", - + // "menu.section.icon.new": "New menu section", "menu.section.icon.new": "Uusi", - + // "menu.section.icon.pin": "Pin sidebar", "menu.section.icon.pin": "Kiinnitä sivupalkki", - + // "menu.section.icon.processes": "Processes menu section", - // TODO New key - Add a translation - "menu.section.icon.processes": "Processes menu section", - + "menu.section.icon.processes": "Prosessit", + // "menu.section.icon.registries": "Registries menu section", "menu.section.icon.registries": "Rekisterit", - + // "menu.section.icon.statistics_task": "Statistics Task menu section", "menu.section.icon.statistics_task": "Tilastot", - + // "menu.section.icon.unpin": "Unpin sidebar", "menu.section.icon.unpin": "Vapauta sivupalkki", - - - + + + // "menu.section.import": "Import", "menu.section.import": "Importoi", - + // "menu.section.import_batch": "Batch Import (ZIP)", "menu.section.import_batch": "Importoi useamman tiedoston erä (ZIP)", - + // "menu.section.import_metadata": "Metadata", "menu.section.import_metadata": "Metadata", - - - + + + // "menu.section.new": "New", "menu.section.new": "Uusi", - + // "menu.section.new_collection": "Collection", "menu.section.new_collection": "Kokoelma", - + // "menu.section.new_community": "Community", "menu.section.new_community": "Yhteisö", - + // "menu.section.new_item": "Item", "menu.section.new_item": "Tietue", - + // "menu.section.new_item_version": "Item Version", "menu.section.new_item_version": "Tietueen versio", - + // "menu.section.new_process": "Process", - // TODO New key - Add a translation - "menu.section.new_process": "Process", - - - + "menu.section.new_process": "Prosessi", + + + // "menu.section.pin": "Pin sidebar", "menu.section.pin": "Kiinnitä sivupalkki", - + // "menu.section.unpin": "Unpin sidebar", "menu.section.unpin": "Vapauta sivupalkki", - - - + + + // "menu.section.processes": "Processes", - // TODO New key - Add a translation - "menu.section.processes": "Processes", - - - + "menu.section.processes": "Prosessit", + + + // "menu.section.registries": "Registries", "menu.section.registries": "Rekisterit", - + // "menu.section.registries_format": "Format", "menu.section.registries_format": "Formaatti", - + // "menu.section.registries_metadata": "Metadata", "menu.section.registries_metadata": "Metadata", - - - + + + // "menu.section.statistics": "Statistics", "menu.section.statistics": "Tilastot", - + // "menu.section.statistics_task": "Statistics Task", "menu.section.statistics_task": "Tilastointitehtävä", - - - + + + // "menu.section.toggle.access_control": "Toggle Access Control section", "menu.section.toggle.access_control": "Vaihda Pääsyoikeudet-osion tilaa", - + // "menu.section.toggle.control_panel": "Toggle Control Panel section", "menu.section.toggle.control_panel": "Vaihda Hallintapaneeli-osion tilaa", - + // "menu.section.toggle.curation_task": "Toggle Curation Task section", "menu.section.toggle.curation_task": "Vaihda Kuratointitehtävä-osion tilaa", - + // "menu.section.toggle.edit": "Toggle Edit section", "menu.section.toggle.edit": "Vaihda Muokkaus-osion tilaa", - + // "menu.section.toggle.export": "Toggle Export section", "menu.section.toggle.export": "Vaihda Eksportointi-osion tilaa", - + // "menu.section.toggle.find": "Toggle Find section", "menu.section.toggle.find": "Vaihda Haku-osion tilaa", - + // "menu.section.toggle.import": "Toggle Import section", "menu.section.toggle.import": "Vaihda Importointi-osion tilaa", - + // "menu.section.toggle.new": "Toggle New section", "menu.section.toggle.new": "Vaihda Uusi-osion tilaa", - + // "menu.section.toggle.registries": "Toggle Registries section", "menu.section.toggle.registries": "Vaihda Rekisterit-osion tilaa", - + // "menu.section.toggle.statistics_task": "Toggle Statistics Task section", "menu.section.toggle.statistics_task": "Vaihda Tilastointitehtävä-osion tilaa", - - + + // "menu.section.workflow": "Administer Workflow", - // TODO New key - Add a translation - "menu.section.workflow": "Administer Workflow", - - + "menu.section.workflow": "Hallinnointityönkulku", + + // "mydspace.description": "", "mydspace.description": "", - + // "mydspace.general.text-here": "here", - // TODO Source message changed - Revise the translation - "mydspace.general.text-here": "TÄSSÄ", - + "mydspace.general.text-here": "tässä", + // "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", - "mydspace.messages.controller-help": "Valitse tämä, jos haluat lähettää viestin tietueen julkaisijalle.", - + "mydspace.messages.controller-help": "Valitse tämä, jos haluat lähettää viestin tietueen tallentajalle.", + // "mydspace.messages.description-placeholder": "Insert your message here...", "mydspace.messages.description-placeholder": "Kirjoita viestisi tähän...", - + // "mydspace.messages.hide-msg": "Hide message", "mydspace.messages.hide-msg": "Piilota viesti", - + // "mydspace.messages.mark-as-read": "Mark as read", "mydspace.messages.mark-as-read": "Merkitse luetuksi", - + // "mydspace.messages.mark-as-unread": "Mark as unread", "mydspace.messages.mark-as-unread": "Merkitse lukemattomaksi", - + // "mydspace.messages.no-content": "No content.", "mydspace.messages.no-content": "Ei sisältöä.", - + // "mydspace.messages.no-messages": "No messages yet.", "mydspace.messages.no-messages": "Ei viestejä.", - + // "mydspace.messages.send-btn": "Send", "mydspace.messages.send-btn": "Lähetä", - + // "mydspace.messages.show-msg": "Show message", "mydspace.messages.show-msg": "Näytä viesti", - + // "mydspace.messages.subject-placeholder": "Subject...", "mydspace.messages.subject-placeholder": "Asiasana...", - + // "mydspace.messages.submitter-help": "Select this option to send a message to controller.", "mydspace.messages.submitter-help": "Valitse tämä, jos haluat lähettää viestin tarkastajalle.", - + // "mydspace.messages.title": "Messages", "mydspace.messages.title": "Viestit", - + // "mydspace.messages.to": "To", "mydspace.messages.to": "Vastaanottaja", - + // "mydspace.new-submission": "New submission", - "mydspace.new-submission": "Uusi julkaisu", - + "mydspace.new-submission": "Uusi tallennus", + // "mydspace.new-submission-external": "Import metadata from external source", - // TODO New key - Add a translation - "mydspace.new-submission-external": "Import metadata from external source", - + "mydspace.new-submission-external": "Importoi metadataa ulkoisesta lähteestä", + // "mydspace.new-submission-external-short": "Import metadata", - // TODO New key - Add a translation - "mydspace.new-submission-external-short": "Import metadata", - + "mydspace.new-submission-external-short": "Importoi metadataa", + // "mydspace.results.head": "Your submissions", - "mydspace.results.head": "Julkaisusi", - + "mydspace.results.head": "Tallennuksesi", + // "mydspace.results.no-abstract": "No Abstract", "mydspace.results.no-abstract": "Ei tiivistelmää", - + // "mydspace.results.no-authors": "No Authors", "mydspace.results.no-authors": "Ei tekijöitä", - + // "mydspace.results.no-collections": "No Collections", "mydspace.results.no-collections": "Ei kokoelmia", - + // "mydspace.results.no-date": "No Date", "mydspace.results.no-date": "Ei päivämäärää", - + // "mydspace.results.no-files": "No Files", "mydspace.results.no-files": "Ei tiedostoja", - + // "mydspace.results.no-results": "There were no items to show", "mydspace.results.no-results": "Ei tietueita", - + // "mydspace.results.no-title": "No title", "mydspace.results.no-title": "Ei nimikettä", - + // "mydspace.results.no-uri": "No Uri", "mydspace.results.no-uri": "Ei URL-osoitetta", - + // "mydspace.show.workflow": "All tasks", "mydspace.show.workflow": "Kaikki tehtävät", - + // "mydspace.show.workspace": "Your Submissions", - "mydspace.show.workspace": "Julkaisusi", - + "mydspace.show.workspace": "Tallennuksesi", + // "mydspace.status.archived": "Archived", "mydspace.status.archived": "Arkistoitu", - + // "mydspace.status.validation": "Validation", "mydspace.status.validation": "Tarkastaminen", - + // "mydspace.status.waiting-for-controller": "Waiting for controller", "mydspace.status.waiting-for-controller": "Odotetaan tarkastajaa", - + // "mydspace.status.workflow": "Workflow", "mydspace.status.workflow": "Työnkulku", - + // "mydspace.status.workspace": "Workspace", "mydspace.status.workspace": "Työtila", - + // "mydspace.title": "MyDSpace", "mydspace.title": "Omat tiedot", - + // "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", "mydspace.upload.upload-failed": "Virhe uutta työtilaa luotaessa. Tarkista ladattava sisältö ennen kuin yrität uudelleen.", - + // "mydspace.upload.upload-failed-manyentries": "Unprocessable file. Detected too many entries but allowed only one for file.", - // TODO New key - Add a translation - "mydspace.upload.upload-failed-manyentries": "Unprocessable file. Detected too many entries but allowed only one for file.", - + "mydspace.upload.upload-failed-manyentries": "Tiedostoa ei voida käsitellä. Vain yksi kohde on sallittu, mutta niitä on useita.", + // "mydspace.upload.upload-failed-moreonefile": "Unprocessable request. Only one file is allowed.", - // TODO New key - Add a translation - "mydspace.upload.upload-failed-moreonefile": "Unprocessable request. Only one file is allowed.", - + "mydspace.upload.upload-failed-moreonefile": "Pyyntöä ei voida käsitellä. Vain yksi tiedosto sallittu.", + // "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", "mydspace.upload.upload-multiple-successful": "{{qty}} uutta työtilaa luotu.", - + // "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", "mydspace.upload.upload-successful": "Uusi työtila luotu. Napauta tästä muokataksesi sitä.", - + // "mydspace.view-btn": "View", "mydspace.view-btn": "Näytä", - - - + + + // "nav.browse.header": "All of DSpace", "nav.browse.header": "Koko julkaisuarkisto", - + // "nav.community-browse.header": "By Community", "nav.community-browse.header": "Yhteisön mukaan", - + // "nav.language": "Language switch", "nav.language": "Kielivalinta", - + // "nav.login": "Log In", "nav.login": "Kirjaudu sisään", - + // "nav.logout": "Log Out", "nav.logout": "Kirjaudu ulos", - + // "nav.mydspace": "MyDSpace", "nav.mydspace": "Omat tiedot", - + // "nav.profile": "Profile", "nav.profile": "Profiili", - + // "nav.search": "Search", "nav.search": "Hae", - + // "nav.statistics.header": "Statistics", "nav.statistics.header": "Tilastot", - + // "nav.stop-impersonating": "Stop impersonating EPerson", - // TODO New key - Add a translation - "nav.stop-impersonating": "Stop impersonating EPerson", - - - + "nav.stop-impersonating": "Lopeta käyttäjänä esiintyminen", + + + // "orgunit.listelement.badge": "Organizational Unit", "orgunit.listelement.badge": "Organisaatioyksikkö", - + // "orgunit.page.city": "City", "orgunit.page.city": "Kaupunki", - + // "orgunit.page.country": "Country", "orgunit.page.country": "Maa", - + // "orgunit.page.dateestablished": "Date established", "orgunit.page.dateestablished": "Perustamispäivämäärä", - + // "orgunit.page.description": "Description", "orgunit.page.description": "Kuvaus", - + // "orgunit.page.edit": "Edit this item", - // TODO New key - Add a translation - "orgunit.page.edit": "Edit this item", - + "orgunit.page.edit": "Muokkaa tietuetta", + // "orgunit.page.id": "ID", "orgunit.page.id": "ID-tunnus", - + // "orgunit.page.titleprefix": "Organizational Unit: ", "orgunit.page.titleprefix": "Organisaatioyksikkö: ", - - - + + + // "pagination.results-per-page": "Results Per Page", "pagination.results-per-page": "Tuloksia sivulla", - + // "pagination.showing.detail": "{{ range }} of {{ total }}", "pagination.showing.detail": "{{ range }} / {{ total }}", - + // "pagination.showing.label": "Now showing ", "pagination.showing.label": "Näytetään ", - + // "pagination.sort-direction": "Sort Options", "pagination.sort-direction": "Lajitteluvalinnat", - - - + + + // "person.listelement.badge": "Person", "person.listelement.badge": "Käyttäjä", - + // "person.listelement.no-title": "No name found", - // TODO New key - Add a translation - "person.listelement.no-title": "No name found", - + "person.listelement.no-title": "Nimeä ei löytynyt", + // "person.page.birthdate": "Birth Date", "person.page.birthdate": "Syntymäaika", - + // "person.page.edit": "Edit this item", - // TODO New key - Add a translation - "person.page.edit": "Edit this item", - + "person.page.edit": "Muokkaa tietuetta", + // "person.page.email": "Email Address", "person.page.email": "Sähköpostiosoite", - + // "person.page.firstname": "First Name", "person.page.firstname": "Etunimi", - + // "person.page.jobtitle": "Job Title", "person.page.jobtitle": "Tehtävänimike", - + // "person.page.lastname": "Last Name", "person.page.lastname": "Sukunimi", - + // "person.page.link.full": "Show all metadata", "person.page.link.full": "Näytä kaikki metadata", - + // "person.page.orcid": "ORCID", "person.page.orcid": "ORCID-tunniste", - + // "person.page.staffid": "Staff ID", "person.page.staffid": "Henkilökunnan ID-tunnus", - + // "person.page.titleprefix": "Person: ", "person.page.titleprefix": "Käyttäjä: ", - + // "person.search.results.head": "Person Search Results", "person.search.results.head": "Käyttäjähaun tulokset", - + // "person.search.title": "DSpace Angular :: Person Search", "person.search.title": "DSpace Angular :: Käyttäjähaku", - - - + + + // "process.new.select-parameters": "Parameters", - // TODO New key - Add a translation - "process.new.select-parameters": "Parameters", - + "process.new.select-parameters": "Parametrit", + // "process.new.cancel": "Cancel", - // TODO New key - Add a translation - "process.new.cancel": "Cancel", - + "process.new.cancel": "Peruuta", + // "process.new.submit": "Submit", - // TODO New key - Add a translation - "process.new.submit": "Submit", - + "process.new.submit": "Lähetä", + // "process.new.select-script": "Script", - // TODO New key - Add a translation - "process.new.select-script": "Script", - + "process.new.select-script": "Skripti", + // "process.new.select-script.placeholder": "Choose a script...", - // TODO New key - Add a translation - "process.new.select-script.placeholder": "Choose a script...", - + "process.new.select-script.placeholder": "Valitse skripti...", + // "process.new.select-script.required": "Script is required", - // TODO New key - Add a translation - "process.new.select-script.required": "Script is required", - + "process.new.select-script.required": "Skripti on pakollinen", + // "process.new.parameter.file.upload-button": "Select file...", - // TODO New key - Add a translation - "process.new.parameter.file.upload-button": "Select file...", - + "process.new.parameter.file.upload-button": "Valitse tiedosto...", + // "process.new.parameter.file.required": "Please select a file", - // TODO New key - Add a translation - "process.new.parameter.file.required": "Please select a file", - + "process.new.parameter.file.required": "Valitse tiedosto, ole hyvä", + // "process.new.parameter.string.required": "Parameter value is required", - // TODO New key - Add a translation - "process.new.parameter.string.required": "Parameter value is required", - + "process.new.parameter.string.required": "Parametrin arvo on pakollinen tieto", + // "process.new.parameter.type.value": "value", - // TODO New key - Add a translation - "process.new.parameter.type.value": "value", - + "process.new.parameter.type.value": "arvo", + // "process.new.parameter.type.file": "file", - // TODO New key - Add a translation - "process.new.parameter.type.file": "file", - + "process.new.parameter.type.file": "tiedosto", + // "process.new.parameter.required.missing": "The following parameters are required but still missing:", - // TODO New key - Add a translation - "process.new.parameter.required.missing": "The following parameters are required but still missing:", - + "process.new.parameter.required.missing": "Seuraavat parametrit ovat pakollisia, mutta niitä ei ole annettu:", + // "process.new.notification.success.title": "Success", - // TODO New key - Add a translation - "process.new.notification.success.title": "Success", - + "process.new.notification.success.title": "Valmis", + // "process.new.notification.success.content": "The process was successfully created", - // TODO New key - Add a translation - "process.new.notification.success.content": "The process was successfully created", - + "process.new.notification.success.content": "Prosessi luotu", + // "process.new.notification.error.title": "Error", - // TODO New key - Add a translation - "process.new.notification.error.title": "Error", - + "process.new.notification.error.title": "Virhe", + // "process.new.notification.error.content": "An error occurred while creating this process", - // TODO New key - Add a translation - "process.new.notification.error.content": "An error occurred while creating this process", - + "process.new.notification.error.content": "Virhe prosessia luotaessa", + // "process.new.header": "Create a new process", - // TODO New key - Add a translation - "process.new.header": "Create a new process", - + "process.new.header": "Luo uusi prosessi", + // "process.new.title": "Create a new process", - // TODO New key - Add a translation - "process.new.title": "Create a new process", - + "process.new.title": "Luo uusi prosessi", + // "process.new.breadcrumbs": "Create a new process", - // TODO New key - Add a translation - "process.new.breadcrumbs": "Create a new process", - - - + "process.new.breadcrumbs": "Luo uusi prosessi", + + + // "process.detail.arguments" : "Arguments", - // TODO New key - Add a translation - "process.detail.arguments" : "Arguments", - + "process.detail.arguments" : "Muuttujat", + // "process.detail.arguments.empty" : "This process doesn't contain any arguments", - // TODO New key - Add a translation - "process.detail.arguments.empty" : "This process doesn't contain any arguments", - + "process.detail.arguments.empty" : "Prosessiin ei liity muuttujia", + // "process.detail.back" : "Back", - // TODO New key - Add a translation - "process.detail.back" : "Back", - + "process.detail.back" : "Paluu", + // "process.detail.output" : "Process Output", - // TODO New key - Add a translation - "process.detail.output" : "Process Output", - + "process.detail.output" : "Prosessin tulos", + // "process.detail.logs.button": "Retrieve process output", - // TODO New key - Add a translation - "process.detail.logs.button": "Retrieve process output", - + "process.detail.logs.button": "Nouda prosessin tulos", + // "process.detail.logs.loading": "Retrieving", - // TODO New key - Add a translation - "process.detail.logs.loading": "Retrieving", - + "process.detail.logs.loading": "Noudetaan", + // "process.detail.logs.none": "This process has no output", - // TODO New key - Add a translation - "process.detail.logs.none": "This process has no output", - + "process.detail.logs.none": "Prosessilla ei tulosta", + // "process.detail.output-files" : "Output Files", - // TODO New key - Add a translation - "process.detail.output-files" : "Output Files", - + "process.detail.output-files" : "Tulostiedostot", + // "process.detail.output-files.empty" : "This process doesn't contain any output files", - // TODO New key - Add a translation - "process.detail.output-files.empty" : "This process doesn't contain any output files", - + "process.detail.output-files.empty" : "Prosessilla ei ole tulostiedostoja", + // "process.detail.script" : "Script", - // TODO New key - Add a translation - "process.detail.script" : "Script", - + "process.detail.script" : "Skripti", + // "process.detail.title" : "Process: {{ id }} - {{ name }}", - // TODO New key - Add a translation - "process.detail.title" : "Process: {{ id }} - {{ name }}", - + "process.detail.title" : "Prosessi: {{ id }} - {{ name }}", + // "process.detail.start-time" : "Start time", - // TODO New key - Add a translation - "process.detail.start-time" : "Start time", - + "process.detail.start-time" : "Aloitusaika", + // "process.detail.end-time" : "Finish time", - // TODO New key - Add a translation - "process.detail.end-time" : "Finish time", - + "process.detail.end-time" : "Lopetusaika", + // "process.detail.status" : "Status", - // TODO New key - Add a translation - "process.detail.status" : "Status", - + "process.detail.status" : "Tila", + // "process.detail.create" : "Create similar process", - // TODO New key - Add a translation - "process.detail.create" : "Create similar process", - - - + "process.detail.create" : "Luo vastaava prosessi", + + + // "process.overview.table.finish" : "Finish time", - // TODO New key - Add a translation - "process.overview.table.finish" : "Finish time", - + "process.overview.table.finish" : "Lopetusaika", + // "process.overview.table.id" : "Process ID", - // TODO New key - Add a translation - "process.overview.table.id" : "Process ID", - + "process.overview.table.id" : "Prosessin ID", + // "process.overview.table.name" : "Name", - // TODO New key - Add a translation - "process.overview.table.name" : "Name", - + "process.overview.table.name" : "Nimi", + // "process.overview.table.start" : "Start time", - // TODO New key - Add a translation - "process.overview.table.start" : "Start time", - + "process.overview.table.start" : "Aloitusaika", + // "process.overview.table.status" : "Status", - // TODO New key - Add a translation - "process.overview.table.status" : "Status", - + "process.overview.table.status" : "Tila", + // "process.overview.table.user" : "User", - // TODO New key - Add a translation - "process.overview.table.user" : "User", - + "process.overview.table.user" : "Käyttäjä", + // "process.overview.title": "Processes Overview", - // TODO New key - Add a translation - "process.overview.title": "Processes Overview", - + "process.overview.title": "Prosessien yleiskatsaukset", + // "process.overview.breadcrumbs": "Processes Overview", - // TODO New key - Add a translation - "process.overview.breadcrumbs": "Processes Overview", - + "process.overview.breadcrumbs": "Prosessien yleiskatsaukset", + // "process.overview.new": "New", - // TODO New key - Add a translation - "process.overview.new": "New", - - + "process.overview.new": "Uusi", + + // "profile.breadcrumbs": "Update Profile", "profile.breadcrumbs": "Päivitä profiili", - + // "profile.card.identify": "Identify", "profile.card.identify": "Tunnistaudu", - + // "profile.card.security": "Security", "profile.card.security": "Suojaus", - + // "profile.form.submit": "Update Profile", "profile.form.submit": "Päivitä profiili", - + // "profile.groups.head": "Authorization groups you belong to", "profile.groups.head": "Ryhmät, joihin kuulut", - + // "profile.head": "Update Profile", "profile.head": "Päivitä profiili", - + // "profile.metadata.form.error.firstname.required": "First Name is required", "profile.metadata.form.error.firstname.required": "Etunimi on pakollinen", - + // "profile.metadata.form.error.lastname.required": "Last Name is required", "profile.metadata.form.error.lastname.required": "Sukunimi on pakollinen", - + // "profile.metadata.form.label.email": "Email Address", "profile.metadata.form.label.email": "Sähköpostiosoite", - + // "profile.metadata.form.label.firstname": "First Name", "profile.metadata.form.label.firstname": "Etunimi", - + // "profile.metadata.form.label.language": "Language", "profile.metadata.form.label.language": "Kieli", - + // "profile.metadata.form.label.lastname": "Last Name", "profile.metadata.form.label.lastname": "Sukunimi", - + // "profile.metadata.form.label.phone": "Contact Telephone", "profile.metadata.form.label.phone": "Puhelinnumero", - + // "profile.metadata.form.notifications.success.content": "Your changes to the profile were saved.", "profile.metadata.form.notifications.success.content": "Muutokset profiiliin on tallennettu.", - + // "profile.metadata.form.notifications.success.title": "Profile saved", "profile.metadata.form.notifications.success.title": "Profiili tallennettu", - + // "profile.notifications.warning.no-changes.content": "No changes were made to the Profile.", "profile.notifications.warning.no-changes.content": "Profiilia ei muutettu.", - + // "profile.notifications.warning.no-changes.title": "No changes", "profile.notifications.warning.no-changes.title": "Ei muutoksia", - + // "profile.security.form.error.matching-passwords": "The passwords do not match.", "profile.security.form.error.matching-passwords": "Salasanat eivät täsmää.", - + // "profile.security.form.error.password-length": "The password should be at least 6 characters long.", "profile.security.form.error.password-length": "Salasanan on oltava vähintään 6 merkkiä pitkä.", - + // "profile.security.form.info": "Optionally, you can enter a new password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", "profile.security.form.info": "Voit kirjoittaa uuden salasanan alla olevaan kenttään ja varmentaa sen kirjoittamalla sen uudelleen seuraavaan kenttään. Salasanan pituus on vähintään kuusi merkkiä.", - + // "profile.security.form.label.password": "Password", "profile.security.form.label.password": "Salasana", - + // "profile.security.form.label.passwordrepeat": "Retype to confirm", "profile.security.form.label.passwordrepeat": "Kirjoita uudelleen", - + // "profile.security.form.notifications.success.content": "Your changes to the password were saved.", "profile.security.form.notifications.success.content": "Muuttunut salasana on tallennettu.", - + // "profile.security.form.notifications.success.title": "Password saved", "profile.security.form.notifications.success.title": "Salasana tallennettu", - + // "profile.security.form.notifications.error.title": "Error changing passwords", "profile.security.form.notifications.error.title": "Virhe salasanaa muutettaessa", - + // "profile.security.form.notifications.error.not-long-enough": "The password has to be at least 6 characters long.", "profile.security.form.notifications.error.not-long-enough": "Salasanan on oltava vähintään 6 merkkiä pitkä.", - + // "profile.security.form.notifications.error.not-same": "The provided passwords are not the same.", "profile.security.form.notifications.error.not-same": "Annetut salasanat eivät täsmää.", - + // "profile.title": "Update Profile", "profile.title": "Päivitä profiili", - - - + + + // "project.listelement.badge": "Research Project", "project.listelement.badge": "Tutkimusprojekti", - + // "project.page.contributor": "Contributors", "project.page.contributor": "Muut tekijät", - + // "project.page.description": "Description", "project.page.description": "Kuvaus", - + // "project.page.edit": "Edit this item", - // TODO New key - Add a translation - "project.page.edit": "Edit this item", - + "project.page.edit": "Muokkaa tietuetta", + // "project.page.expectedcompletion": "Expected Completion", - "project.page.expectedcompletion": "Todennäköinen päättyminen", - + "project.page.expectedcompletion": "Odotettu päätös", + // "project.page.funder": "Funders", "project.page.funder": "Rahoittajat", - + // "project.page.id": "ID", "project.page.id": "ID-tunnus", - + // "project.page.keyword": "Keywords", "project.page.keyword": "Asiasanat", - + // "project.page.status": "Status", "project.page.status": "Tila", - + // "project.page.titleprefix": "Research Project: ", "project.page.titleprefix": "Tutkimusprojekti: ", - + // "project.search.results.head": "Project Search Results", "project.search.results.head": "Projektihaun tulokset", - - - + + + // "publication.listelement.badge": "Publication", "publication.listelement.badge": "Julkaisu", - + // "publication.page.description": "Description", "publication.page.description": "Kuvaus", - + // "publication.page.edit": "Edit this item", - // TODO New key - Add a translation - "publication.page.edit": "Edit this item", - + "publication.page.edit": "Muokkaa tietuetta", + // "publication.page.journal-issn": "Journal ISSN", "publication.page.journal-issn": "Kausijulkaisun ISSN-tunnus", - + // "publication.page.journal-title": "Journal Title", "publication.page.journal-title": "Kausijulkaisun nimi", - + // "publication.page.publisher": "Publisher", "publication.page.publisher": "Julkaisija", - + // "publication.page.titleprefix": "Publication: ", "publication.page.titleprefix": "Julkaisu: ", - + // "publication.page.volume-title": "Volume Title", "publication.page.volume-title": "Vuosikerran nimi", - + // "publication.search.results.head": "Publication Search Results", "publication.search.results.head": "Aineistohaun tulokset", - + // "publication.search.title": "DSpace Angular :: Publication Search", "publication.search.title": "DSpace Angular :: Aineistohaku", - - + + // "register-email.title": "New user registration", - // TODO New key - Add a translation - "register-email.title": "New user registration", - + "register-email.title": "Uuden käyttäjän rekisteröinti", + // "register-page.create-profile.header": "Create Profile", - // TODO New key - Add a translation - "register-page.create-profile.header": "Create Profile", - + "register-page.create-profile.header": "Luo profiili", + // "register-page.create-profile.identification.header": "Identify", - // TODO New key - Add a translation - "register-page.create-profile.identification.header": "Identify", - + "register-page.create-profile.identification.header": "Tunnistaudu", + // "register-page.create-profile.identification.email": "Email Address", - // TODO New key - Add a translation - "register-page.create-profile.identification.email": "Email Address", - + "register-page.create-profile.identification.email": "Sähköpostiosoite", + // "register-page.create-profile.identification.first-name": "First Name *", - // TODO New key - Add a translation - "register-page.create-profile.identification.first-name": "First Name *", - + "register-page.create-profile.identification.first-name": "Etunimi *", + // "register-page.create-profile.identification.first-name.error": "Please fill in a First Name", - // TODO New key - Add a translation - "register-page.create-profile.identification.first-name.error": "Please fill in a First Name", - + "register-page.create-profile.identification.first-name.error": "Anna etunimi, ole hyvä", + // "register-page.create-profile.identification.last-name": "Last Name *", - // TODO New key - Add a translation - "register-page.create-profile.identification.last-name": "Last Name *", - + "register-page.create-profile.identification.last-name": "Sukunimi *", + // "register-page.create-profile.identification.last-name.error": "Please fill in a Last Name", - // TODO New key - Add a translation - "register-page.create-profile.identification.last-name.error": "Please fill in a Last Name", - + "register-page.create-profile.identification.last-name.error": "Anna sukunimi, ole hyvä", + // "register-page.create-profile.identification.contact": "Contact Telephone", - // TODO New key - Add a translation - "register-page.create-profile.identification.contact": "Contact Telephone", - + "register-page.create-profile.identification.contact": "Puhelinnumero", + // "register-page.create-profile.identification.language": "Language", - // TODO New key - Add a translation - "register-page.create-profile.identification.language": "Language", - + "register-page.create-profile.identification.language": "Kieli", + // "register-page.create-profile.security.header": "Security", - // TODO New key - Add a translation - "register-page.create-profile.security.header": "Security", - + "register-page.create-profile.security.header": "Suojaus", + // "register-page.create-profile.security.info": "Please enter a password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", - // TODO New key - Add a translation - "register-page.create-profile.security.info": "Please enter a password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", - + "register-page.create-profile.security.info": "Syötä salasana alla olevaan kenttään ja vahvista se kirjoittamalla salasana uudelleen seuraavaan kenttään. Salasanan on oltava vähintään kuusi merkkiä pitkä.", + // "register-page.create-profile.security.label.password": "Password *", - // TODO New key - Add a translation - "register-page.create-profile.security.label.password": "Password *", - + "register-page.create-profile.security.label.password": "Salasana *", + // "register-page.create-profile.security.label.passwordrepeat": "Retype to confirm *", - // TODO New key - Add a translation - "register-page.create-profile.security.label.passwordrepeat": "Retype to confirm *", - + "register-page.create-profile.security.label.passwordrepeat": "Salasana uudelleen *", + // "register-page.create-profile.security.error.empty-password": "Please enter a password in the box below.", - // TODO New key - Add a translation - "register-page.create-profile.security.error.empty-password": "Please enter a password in the box below.", - + "register-page.create-profile.security.error.empty-password": "Anna salasana alla olevaan kenttään, ole hyvä.", + // "register-page.create-profile.security.error.matching-passwords": "The passwords do not match.", - // TODO New key - Add a translation - "register-page.create-profile.security.error.matching-passwords": "The passwords do not match.", - + "register-page.create-profile.security.error.matching-passwords": "Salasanat eivät täsmää.", + // "register-page.create-profile.security.error.password-length": "The password should be at least 6 characters long.", - // TODO New key - Add a translation - "register-page.create-profile.security.error.password-length": "The password should be at least 6 characters long.", - + "register-page.create-profile.security.error.password-length": "Salasanan on oltava vähintään 6 merkkiä pitkä.", + // "register-page.create-profile.submit": "Complete Registration", - // TODO New key - Add a translation - "register-page.create-profile.submit": "Complete Registration", - + "register-page.create-profile.submit": "Viimeistele rekisteröinti", + // "register-page.create-profile.submit.error.content": "Something went wrong while registering a new user.", - // TODO New key - Add a translation - "register-page.create-profile.submit.error.content": "Something went wrong while registering a new user.", - + "register-page.create-profile.submit.error.content": "Tapahtui virhe uuden käyttäjän rekisteröinnissä.", + // "register-page.create-profile.submit.error.head": "Registration failed", - // TODO New key - Add a translation - "register-page.create-profile.submit.error.head": "Registration failed", - + "register-page.create-profile.submit.error.head": "Rekisteröinti epäonnistui", + // "register-page.create-profile.submit.success.content": "The registration was successful. You have been logged in as the created user.", - // TODO New key - Add a translation - "register-page.create-profile.submit.success.content": "The registration was successful. You have been logged in as the created user.", - + "register-page.create-profile.submit.success.content": "Rekisteröinti onnistui. Olet kirjautunut sisään.", + // "register-page.create-profile.submit.success.head": "Registration completed", - // TODO New key - Add a translation - "register-page.create-profile.submit.success.head": "Registration completed", - - + "register-page.create-profile.submit.success.head": "Rekisteröinti valmis", + + // "register-page.registration.header": "New user registration", - // TODO New key - Add a translation - "register-page.registration.header": "New user registration", - + "register-page.registration.header": "Uuden käyttäjän rekisteröinti", + // "register-page.registration.info": "Register an account to subscribe to collections for email updates, and submit new items to DSpace.", - // TODO New key - Add a translation - "register-page.registration.info": "Register an account to subscribe to collections for email updates, and submit new items to DSpace.", - + "register-page.registration.info": "Rekisteröi käyttäjätili voidaksesi tilata sähköposti-ilmoituksia kokoelmien päivityksistä ja lisätä uusia tietueita julkaisuarkistoon.", + // "register-page.registration.email": "Email Address *", - // TODO New key - Add a translation - "register-page.registration.email": "Email Address *", - + "register-page.registration.email": "Sähköpostiosoite *", + // "register-page.registration.email.error.required": "Please fill in an email address", - // TODO New key - Add a translation - "register-page.registration.email.error.required": "Please fill in an email address", - + "register-page.registration.email.error.required": "Anna sähköpostiosoite", + // "register-page.registration.email.error.pattern": "Please fill in a valid email address", - // TODO New key - Add a translation - "register-page.registration.email.error.pattern": "Please fill in a valid email address", - + "register-page.registration.email.error.pattern": "Anna toimiva sähköpostiosoite, ole hyvä", + // "register-page.registration.email.hint": "This address will be verified and used as your login name.", - // TODO New key - Add a translation - "register-page.registration.email.hint": "This address will be verified and used as your login name.", - + "register-page.registration.email.hint": "Osoite varmistetaan, ja se toimii käyttäjätunnuksenasi kirjautumisessa.", + // "register-page.registration.submit": "Register", - // TODO New key - Add a translation - "register-page.registration.submit": "Register", - + "register-page.registration.submit": "Rekisteröi", + // "register-page.registration.success.head": "Verification email sent", - // TODO New key - Add a translation - "register-page.registration.success.head": "Verification email sent", - + "register-page.registration.success.head": "Varmistusviesti lähetetty", + // "register-page.registration.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", - // TODO New key - Add a translation - "register-page.registration.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", - + "register-page.registration.success.content": "Viesti on lähetetty osoitteeseen {{ email }}. Viestissä on URL-osoite ja lisäohjeita.", + // "register-page.registration.error.head": "Error when trying to register email", - // TODO New key - Add a translation - "register-page.registration.error.head": "Error when trying to register email", - + "register-page.registration.error.head": "Virhe sähköpostiosoitetta rekisteröitäessä", + // "register-page.registration.error.content": "An error occured when registering the following email address: {{ email }}", - // TODO New key - Add a translation - "register-page.registration.error.content": "An error occured when registering the following email address: {{ email }}", - - - + "register-page.registration.error.content": "Virhe rekisteröitäessä tätä sähköpostiosoitetta: {{ email }}", + + + // "relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items", - // TODO New key - Add a translation - "relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items", - + "relationships.add.error.relationship-type.content": "Ei {{ type }}-tyyppistä yhteysvastaavuutta kahden tietueen välillä", + // "relationships.add.error.server.content": "The server returned an error", - // TODO New key - Add a translation - "relationships.add.error.server.content": "The server returned an error", - + "relationships.add.error.server.content": "Palvelin palautti virheen", + // "relationships.add.error.title": "Unable to add relationship", - // TODO New key - Add a translation - "relationships.add.error.title": "Unable to add relationship", - + "relationships.add.error.title": "Yhteyttä ei voi lisätä", + // "relationships.isAuthorOf": "Authors", "relationships.isAuthorOf": "Tekijät", - + // "relationships.isAuthorOf.Person": "Authors (persons)", - // TODO New key - Add a translation - "relationships.isAuthorOf.Person": "Authors (persons)", - + "relationships.isAuthorOf.Person": "Tekijät (henkilöt)", + // "relationships.isAuthorOf.OrgUnit": "Authors (organizational units)", - // TODO New key - Add a translation - "relationships.isAuthorOf.OrgUnit": "Authors (organizational units)", - + "relationships.isAuthorOf.OrgUnit": "Tekijät (organisaatioyksiköt)", + // "relationships.isIssueOf": "Journal Issues", "relationships.isIssueOf": "Kausijulkaisun numerot", - + // "relationships.isJournalIssueOf": "Journal Issue", "relationships.isJournalIssueOf": "Kausijulkaisun numero", - + // "relationships.isJournalOf": "Journals", "relationships.isJournalOf": "Kausijulkaisut", - + // "relationships.isOrgUnitOf": "Organizational Units", "relationships.isOrgUnitOf": "Organisaatioyksiköt", - + // "relationships.isPersonOf": "Authors", "relationships.isPersonOf": "Tekijät", - + // "relationships.isProjectOf": "Research Projects", "relationships.isProjectOf": "Tutkimusprojektit", - + // "relationships.isPublicationOf": "Publications", "relationships.isPublicationOf": "Julkaisut", - + // "relationships.isPublicationOfJournalIssue": "Articles", "relationships.isPublicationOfJournalIssue": "Artikkelit", - + // "relationships.isSingleJournalOf": "Journal", "relationships.isSingleJournalOf": "Kausijulkaisu", - + // "relationships.isSingleVolumeOf": "Journal Volume", "relationships.isSingleVolumeOf": "Kausijulkaisun vuosikerta", - + // "relationships.isVolumeOf": "Journal Volumes", "relationships.isVolumeOf": "Kausijulkaisun vuosikerrat", - + // "relationships.isContributorOf": "Contributors", "relationships.isContributorOf": "Muut tekijät", - - - + + + // "resource-policies.add.button": "Add", - // TODO New key - Add a translation - "resource-policies.add.button": "Add", - + "resource-policies.add.button": "Lisää", + // "resource-policies.add.for.": "Add a new policy", - // TODO New key - Add a translation - "resource-policies.add.for.": "Add a new policy", - + "resource-policies.add.for.": "Lisää uusi käytäntö", + // "resource-policies.add.for.bitstream": "Add a new Bitstream policy", - // TODO New key - Add a translation - "resource-policies.add.for.bitstream": "Add a new Bitstream policy", - + "resource-policies.add.for.bitstream": "Lisää uusi tiedostoa koskeva käytäntö", + // "resource-policies.add.for.bundle": "Add a new Bundle policy", - // TODO New key - Add a translation - "resource-policies.add.for.bundle": "Add a new Bundle policy", - + "resource-policies.add.for.bundle": "Lisää uusi nippua koskeva käytäntö", + // "resource-policies.add.for.item": "Add a new Item policy", - // TODO New key - Add a translation - "resource-policies.add.for.item": "Add a new Item policy", - + "resource-policies.add.for.item": "Lisää uusi tietuetta koskeva käytäntö", + // "resource-policies.add.for.community": "Add a new Community policy", - // TODO New key - Add a translation - "resource-policies.add.for.community": "Add a new Community policy", - + "resource-policies.add.for.community": "Lisää uusi yhteisöä koskeva käytäntö", + // "resource-policies.add.for.collection": "Add a new Collection policy", - // TODO New key - Add a translation - "resource-policies.add.for.collection": "Add a new Collection policy", - + "resource-policies.add.for.collection": "Lisää uusi kokoelmaa koskeva käytäntö", + // "resource-policies.create.page.heading": "Create new resource policy for ", - // TODO New key - Add a translation - "resource-policies.create.page.heading": "Create new resource policy for ", - + "resource-policies.create.page.heading": "Luo uusi resurssikäytäntö tälle: ", + // "resource-policies.create.page.failure.content": "An error occurred while creating the resource policy.", - // TODO New key - Add a translation - "resource-policies.create.page.failure.content": "An error occurred while creating the resource policy.", - + "resource-policies.create.page.failure.content": "Virhe resurssikäytäntöä luotaessa", + // "resource-policies.create.page.success.content": "Operation successful", - // TODO New key - Add a translation - "resource-policies.create.page.success.content": "Operation successful", - + "resource-policies.create.page.success.content": "Toiminto onnistui", + // "resource-policies.create.page.title": "Create new resource policy", - // TODO New key - Add a translation - "resource-policies.create.page.title": "Create new resource policy", - + "resource-policies.create.page.title": "Luo uusi resurssikäytäntö", + // "resource-policies.delete.btn": "Delete selected", - // TODO New key - Add a translation - "resource-policies.delete.btn": "Delete selected", - + "resource-policies.delete.btn": "Poista valitut", + // "resource-policies.delete.btn.title": "Delete selected resource policies", - // TODO New key - Add a translation - "resource-policies.delete.btn.title": "Delete selected resource policies", - + "resource-policies.delete.btn.title": "Poista valitut resurssikäytännöt", + // "resource-policies.delete.failure.content": "An error occurred while deleting selected resource policies.", - // TODO New key - Add a translation - "resource-policies.delete.failure.content": "An error occurred while deleting selected resource policies.", - + "resource-policies.delete.failure.content": "Virhe poistettaessa valittuja resurssikäytäntöjä.", + // "resource-policies.delete.success.content": "Operation successful", - // TODO New key - Add a translation - "resource-policies.delete.success.content": "Operation successful", - + "resource-policies.delete.success.content": "Toiminto onnistui", + // "resource-policies.edit.page.heading": "Edit resource policy ", - // TODO New key - Add a translation - "resource-policies.edit.page.heading": "Edit resource policy ", - + "resource-policies.edit.page.heading": "Muokkaa resurssikäytäntöä ", + // "resource-policies.edit.page.failure.content": "An error occurred while editing the resource policy.", - // TODO New key - Add a translation - "resource-policies.edit.page.failure.content": "An error occurred while editing the resource policy.", - + "resource-policies.edit.page.failure.content": "Virhe muokattaessa resurssikäytäntöä.", + // "resource-policies.edit.page.success.content": "Operation successful", - // TODO New key - Add a translation - "resource-policies.edit.page.success.content": "Operation successful", - + "resource-policies.edit.page.success.content": "Toiminto onnistui", + // "resource-policies.edit.page.title": "Edit resource policy", - // TODO New key - Add a translation - "resource-policies.edit.page.title": "Edit resource policy", - + "resource-policies.edit.page.title": "Muokkaa resurssikäytäntöä", + // "resource-policies.form.action-type.label": "Select the action type", - // TODO New key - Add a translation - "resource-policies.form.action-type.label": "Select the action type", - + "resource-policies.form.action-type.label": "Valitse toimenpiteen tyyppi", + // "resource-policies.form.action-type.required": "You must select the resource policy action.", - // TODO New key - Add a translation - "resource-policies.form.action-type.required": "You must select the resource policy action.", - + "resource-policies.form.action-type.required": "Sinun on valittava resurssikäytännön toimenpide.", + // "resource-policies.form.eperson-group-list.label": "The eperson or group that will be granted the permission", - // TODO New key - Add a translation - "resource-policies.form.eperson-group-list.label": "The eperson or group that will be granted the permission", - + "resource-policies.form.eperson-group-list.label": "Käyttäjä tai ryhmä, jolle oikeus myönnetään", + // "resource-policies.form.eperson-group-list.select.btn": "Select", - // TODO New key - Add a translation - "resource-policies.form.eperson-group-list.select.btn": "Select", - + "resource-policies.form.eperson-group-list.select.btn": "Valitse", + // "resource-policies.form.eperson-group-list.tab.eperson": "Search for a ePerson", - // TODO New key - Add a translation - "resource-policies.form.eperson-group-list.tab.eperson": "Search for a ePerson", - + "resource-policies.form.eperson-group-list.tab.eperson": "Hae käyttäjää", + // "resource-policies.form.eperson-group-list.tab.group": "Search for a group", - // TODO New key - Add a translation - "resource-policies.form.eperson-group-list.tab.group": "Search for a group", - + "resource-policies.form.eperson-group-list.tab.group": "Hae ryhmää", + // "resource-policies.form.eperson-group-list.table.headers.action": "Action", - // TODO New key - Add a translation - "resource-policies.form.eperson-group-list.table.headers.action": "Action", - + "resource-policies.form.eperson-group-list.table.headers.action": "Toimenpide", + // "resource-policies.form.eperson-group-list.table.headers.id": "ID", - // TODO New key - Add a translation - "resource-policies.form.eperson-group-list.table.headers.id": "ID", - + "resource-policies.form.eperson-group-list.table.headers.id": "ID-tunnus", + // "resource-policies.form.eperson-group-list.table.headers.name": "Name", - // TODO New key - Add a translation - "resource-policies.form.eperson-group-list.table.headers.name": "Name", - + "resource-policies.form.eperson-group-list.table.headers.name": "Nimi", + // "resource-policies.form.date.end.label": "End Date", - // TODO New key - Add a translation - "resource-policies.form.date.end.label": "End Date", - - // "resource-policies.form.date.start.label": "Start Date", - // TODO New key - Add a translation - "resource-policies.form.date.start.label": "Start Date", - + "resource-policies.form.date.end.label": "Loppupäivämäärä", + + // "resource-policies.form.date.start.label": "Start Date + "resource-policies.form.date.start.label": "Alkupäivämäärä", + // "resource-policies.form.description.label": "Description", - // TODO New key - Add a translation - "resource-policies.form.description.label": "Description", - + "resource-policies.form.description.label": "Kuvaus", + // "resource-policies.form.name.label": "Name", - // TODO New key - Add a translation - "resource-policies.form.name.label": "Name", - + "resource-policies.form.name.label": "Nimi", + // "resource-policies.form.policy-type.label": "Select the policy type", - // TODO New key - Add a translation - "resource-policies.form.policy-type.label": "Select the policy type", - + "resource-policies.form.policy-type.label": "Valitse käytäntötyyppi", + // "resource-policies.form.policy-type.required": "You must select the resource policy type.", - // TODO New key - Add a translation - "resource-policies.form.policy-type.required": "You must select the resource policy type.", - + "resource-policies.form.policy-type.required": "Sinun on valittava käytäntötyyppi.", + // "resource-policies.table.headers.action": "Action", - // TODO New key - Add a translation - "resource-policies.table.headers.action": "Action", - + "resource-policies.table.headers.action": "Toimenpide", + // "resource-policies.table.headers.date.end": "End Date", - // TODO New key - Add a translation - "resource-policies.table.headers.date.end": "End Date", - + "resource-policies.table.headers.date.end": "Loppupäivämäärä", + // "resource-policies.table.headers.date.start": "Start Date", - // TODO New key - Add a translation - "resource-policies.table.headers.date.start": "Start Date", - + "resource-policies.table.headers.date.start": "Alkupäivämäärä", + // "resource-policies.table.headers.edit": "Edit", - // TODO New key - Add a translation - "resource-policies.table.headers.edit": "Edit", - + "resource-policies.table.headers.edit": "Muokkaa", + // "resource-policies.table.headers.edit.group": "Edit group", - // TODO New key - Add a translation - "resource-policies.table.headers.edit.group": "Edit group", - + "resource-policies.table.headers.edit.group": "Muokkaa ryhmää", + // "resource-policies.table.headers.edit.policy": "Edit policy", - // TODO New key - Add a translation - "resource-policies.table.headers.edit.policy": "Edit policy", - + "resource-policies.table.headers.edit.policy": "Muokkaa käytäntöä", + // "resource-policies.table.headers.eperson": "EPerson", - // TODO New key - Add a translation - "resource-policies.table.headers.eperson": "EPerson", - + "resource-policies.table.headers.eperson": "Käyttäjä", + // "resource-policies.table.headers.group": "Group", - // TODO New key - Add a translation - "resource-policies.table.headers.group": "Group", - + "resource-policies.table.headers.group": "Ryhmä", + // "resource-policies.table.headers.id": "ID", - // TODO New key - Add a translation - "resource-policies.table.headers.id": "ID", - + "resource-policies.table.headers.id": "ID-tunnus", + // "resource-policies.table.headers.name": "Name", - // TODO New key - Add a translation - "resource-policies.table.headers.name": "Name", - + "resource-policies.table.headers.name": "Nimi", + // "resource-policies.table.headers.policyType": "type", - // TODO New key - Add a translation - "resource-policies.table.headers.policyType": "type", - + "resource-policies.table.headers.policyType": "tyyppi", + // "resource-policies.table.headers.title.for.bitstream": "Policies for Bitstream", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.bitstream": "Policies for Bitstream", - + "resource-policies.table.headers.title.for.bitstream": "Tiedostokäytännöt", + // "resource-policies.table.headers.title.for.bundle": "Policies for Bundle", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.bundle": "Policies for Bundle", - + "resource-policies.table.headers.title.for.bundle": "Nippukäytännöt", + // "resource-policies.table.headers.title.for.item": "Policies for Item", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.item": "Policies for Item", - + "resource-policies.table.headers.title.for.item": "Tietuekäytännöt", + // "resource-policies.table.headers.title.for.community": "Policies for Community", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.community": "Policies for Community", - + "resource-policies.table.headers.title.for.community": "Yhteisökäytännöt", + // "resource-policies.table.headers.title.for.collection": "Policies for Collection", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.collection": "Policies for Collection", - - - + "resource-policies.table.headers.title.for.collection": "Kokoelmakäytännöt", + + + // "search.description": "", "search.description": "", - + // "search.switch-configuration.title": "Show", "search.switch-configuration.title": "Näytä", - + // "search.title": "DSpace Angular :: Search", "search.title": "DSpace Angular :: Hae", - + // "search.breadcrumbs": "Search", "search.breadcrumbs": "Hae", - - + + // "search.filters.applied.f.author": "Author", "search.filters.applied.f.author": "Tekijä", - + // "search.filters.applied.f.dateIssued.max": "End date", "search.filters.applied.f.dateIssued.max": "Loppupäivämäärä", - + // "search.filters.applied.f.dateIssued.min": "Start date", "search.filters.applied.f.dateIssued.min": "Alkupäivämäärä", - + // "search.filters.applied.f.dateSubmitted": "Date submitted", "search.filters.applied.f.dateSubmitted": "Tallennusajankohta", - + // "search.filters.applied.f.discoverable": "Private", "search.filters.applied.f.discoverable": "Yksityinen", - + // "search.filters.applied.f.entityType": "Item Type", "search.filters.applied.f.entityType": "Tietueen tyyppi", - + // "search.filters.applied.f.has_content_in_original_bundle": "Has files", "search.filters.applied.f.has_content_in_original_bundle": "On tiedostoja", - + // "search.filters.applied.f.itemtype": "Type", "search.filters.applied.f.itemtype": "Tyyppi", - + // "search.filters.applied.f.namedresourcetype": "Status", "search.filters.applied.f.namedresourcetype": "Tila", - + // "search.filters.applied.f.subject": "Subject", "search.filters.applied.f.subject": "Asiasana", - + // "search.filters.applied.f.submitter": "Submitter", - "search.filters.applied.f.submitter": "Julkaisija", - + "search.filters.applied.f.submitter": "Tallentaja", + // "search.filters.applied.f.jobTitle": "Job Title", "search.filters.applied.f.jobTitle": "Tehtävänimike", - + // "search.filters.applied.f.birthDate.max": "End birth date", "search.filters.applied.f.birthDate.max": "Viimeisin syntymäaika", - + // "search.filters.applied.f.birthDate.min": "Start birth date", "search.filters.applied.f.birthDate.min": "Varhaisin syntymäaika", - + // "search.filters.applied.f.withdrawn": "Withdrawn", "search.filters.applied.f.withdrawn": "Poistettu käytöstä", - - - + + + // "search.filters.filter.author.head": "Author", "search.filters.filter.author.head": "Tekijä", - + // "search.filters.filter.author.placeholder": "Author name", "search.filters.filter.author.placeholder": "Tekijän nimi", - + // "search.filters.filter.birthDate.head": "Birth Date", "search.filters.filter.birthDate.head": "Syntymäaika", - + // "search.filters.filter.birthDate.placeholder": "Birth Date", "search.filters.filter.birthDate.placeholder": "Syntymäaika", - + // "search.filters.filter.creativeDatePublished.head": "Date Published", "search.filters.filter.creativeDatePublished.head": "Julkaisuajankohta", - + // "search.filters.filter.creativeDatePublished.placeholder": "Date Published", "search.filters.filter.creativeDatePublished.placeholder": "Julkaisuajankohta", - + // "search.filters.filter.creativeWorkEditor.head": "Editor", "search.filters.filter.creativeWorkEditor.head": "Toimittaja", - + // "search.filters.filter.creativeWorkEditor.placeholder": "Editor", "search.filters.filter.creativeWorkEditor.placeholder": "Toimittaja", - + // "search.filters.filter.creativeWorkKeywords.head": "Subject", "search.filters.filter.creativeWorkKeywords.head": "Asiasana", - + // "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", "search.filters.filter.creativeWorkKeywords.placeholder": "Asiasana", - + // "search.filters.filter.creativeWorkPublisher.head": "Publisher", "search.filters.filter.creativeWorkPublisher.head": "Julkaisija", - + // "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", "search.filters.filter.creativeWorkPublisher.placeholder": "Julkaisija", - + // "search.filters.filter.dateIssued.head": "Date", "search.filters.filter.dateIssued.head": "Päivämäärä", - + // "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", "search.filters.filter.dateIssued.max.placeholder": "Alkupäivämäärä", - + // "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", "search.filters.filter.dateIssued.min.placeholder": "Loppupäivämäärä", - + // "search.filters.filter.dateSubmitted.head": "Date submitted", "search.filters.filter.dateSubmitted.head": "Tallennusajankohta", - + // "search.filters.filter.dateSubmitted.placeholder": "Date submitted", "search.filters.filter.dateSubmitted.placeholder": "Tallennnusajankohta", - + // "search.filters.filter.discoverable.head": "Private", "search.filters.filter.discoverable.head": "Ykstyinen", - + // "search.filters.filter.withdrawn.head": "Withdrawn", "search.filters.filter.withdrawn.head": "Poistettu käytöstä", - + // "search.filters.filter.entityType.head": "Item Type", "search.filters.filter.entityType.head": "Tietueen tyyppi", - + // "search.filters.filter.entityType.placeholder": "Item Type", "search.filters.filter.entityType.placeholder": "Tietueen tyyppi", - + // "search.filters.filter.has_content_in_original_bundle.head": "Has files", "search.filters.filter.has_content_in_original_bundle.head": "On tiedostoja", - + // "search.filters.filter.itemtype.head": "Type", "search.filters.filter.itemtype.head": "Tyyppi", - + // "search.filters.filter.itemtype.placeholder": "Type", "search.filters.filter.itemtype.placeholder": "Tyyppi", - + // "search.filters.filter.jobTitle.head": "Job Title", "search.filters.filter.jobTitle.head": "Tehtävänimike", - + // "search.filters.filter.jobTitle.placeholder": "Job Title", "search.filters.filter.jobTitle.placeholder": "Tehtävänimike", - + // "search.filters.filter.knowsLanguage.head": "Known language", "search.filters.filter.knowsLanguage.head": "Tunnettu kieli", - + // "search.filters.filter.knowsLanguage.placeholder": "Known language", "search.filters.filter.knowsLanguage.placeholder": "Tunnettu kieli", - + // "search.filters.filter.namedresourcetype.head": "Status", "search.filters.filter.namedresourcetype.head": "Tila", - + // "search.filters.filter.namedresourcetype.placeholder": "Status", "search.filters.filter.namedresourcetype.placeholder": "Tila", - + // "search.filters.filter.objectpeople.head": "People", "search.filters.filter.objectpeople.head": "Käyttäjät", - + // "search.filters.filter.objectpeople.placeholder": "People", "search.filters.filter.objectpeople.placeholder": "Käyttäjät", - + // "search.filters.filter.organizationAddressCountry.head": "Country", "search.filters.filter.organizationAddressCountry.head": "Maa", - + // "search.filters.filter.organizationAddressCountry.placeholder": "Country", "search.filters.filter.organizationAddressCountry.placeholder": "Maa", - + // "search.filters.filter.organizationAddressLocality.head": "City", "search.filters.filter.organizationAddressLocality.head": "Kaupunki", - + // "search.filters.filter.organizationAddressLocality.placeholder": "City", "search.filters.filter.organizationAddressLocality.placeholder": "Kaupunki", - + // "search.filters.filter.organizationFoundingDate.head": "Date Founded", "search.filters.filter.organizationFoundingDate.head": "Perustamispäivämäärä", - + // "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", "search.filters.filter.organizationFoundingDate.placeholder": "Perustamispäivämäärä", - + // "search.filters.filter.scope.head": "Scope", "search.filters.filter.scope.head": "Rajaus", - + // "search.filters.filter.scope.placeholder": "Scope filter", "search.filters.filter.scope.placeholder": "Haun tarkennus", - + // "search.filters.filter.show-less": "Collapse", "search.filters.filter.show-less": "Sulje", - + // "search.filters.filter.show-more": "Show more", "search.filters.filter.show-more": "Näytä lisää", - + // "search.filters.filter.subject.head": "Subject", "search.filters.filter.subject.head": "Asiasana", - + // "search.filters.filter.subject.placeholder": "Subject", "search.filters.filter.subject.placeholder": "Asiasana", - + // "search.filters.filter.submitter.head": "Submitter", - "search.filters.filter.submitter.head": "Julkaisija", - + "search.filters.filter.submitter.head": "Tallentaja", + // "search.filters.filter.submitter.placeholder": "Submitter", - "search.filters.filter.submitter.placeholder": "Julkaisija", - - - + "search.filters.filter.submitter.placeholder": "Tallentaja", + + + // "search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalIssue": "Kausijulkaisun numero", - + // "search.filters.entityType.JournalVolume": "Journal Volume", "search.filters.entityType.JournalVolume": "Kausijulkaisun vuosikerta", - + // "search.filters.entityType.OrgUnit": "Organizational Unit", "search.filters.entityType.OrgUnit": "Organisaatioyksikkö", - + // "search.filters.has_content_in_original_bundle.true": "Yes", "search.filters.has_content_in_original_bundle.true": "Kyllä", - + // "search.filters.has_content_in_original_bundle.false": "No", "search.filters.has_content_in_original_bundle.false": "Ei", - + // "search.filters.discoverable.true": "No", "search.filters.discoverable.true": "Ei", - + // "search.filters.discoverable.false": "Yes", "search.filters.discoverable.false": "Kyllä", - + // "search.filters.withdrawn.true": "Yes", "search.filters.withdrawn.true": "Kyllä", - + // "search.filters.withdrawn.false": "No", "search.filters.withdrawn.false": "Ei", - - + + // "search.filters.head": "Filters", "search.filters.head": "Suodattimet", - + // "search.filters.reset": "Reset filters", "search.filters.reset": "Tyhjennä suodattimet", - - - + + + // "search.form.search": "Search", "search.form.search": "Hae", - + // "search.form.search_dspace": "Search DSpace", "search.form.search_dspace": "Hae arkistosta", - + // "search.form.search_mydspace": "Search MyDSpace", "search.form.search_mydspace": "Hae omista tiedoista", - - - + + + // "search.results.head": "Search Results", "search.results.head": "Hakutulokset", - + // "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", "search.results.no-results": "Ei hakutuloksia. Jos sinulla on hakuongelmia, voit", - + // "search.results.no-results-link": "quotes around it", "search.results.no-results-link": "käyttää lainausmerkkejä", - + // "search.results.empty": "Your search returned no results.", "search.results.empty": "Ei hakutuloksia.", - - - + + + // "search.sidebar.close": "Back to results", "search.sidebar.close": "Paluu tuloksiin", - + // "search.sidebar.filters.title": "Filters", "search.sidebar.filters.title": "Suodattimet", - + // "search.sidebar.open": "Search Tools", "search.sidebar.open": "Hakutyökalut", - + // "search.sidebar.results": "results", "search.sidebar.results": "tulokset", - + // "search.sidebar.settings.rpp": "Results per page", "search.sidebar.settings.rpp": "Tulosta sivulla", - + // "search.sidebar.settings.sort-by": "Sort By", "search.sidebar.settings.sort-by": "Järjestä", - + // "search.sidebar.settings.title": "Settings", "search.sidebar.settings.title": "Asetukset", - - - + + + // "search.view-switch.show-detail": "Show detail", "search.view-switch.show-detail": "Näytä lisätiedot", - + // "search.view-switch.show-grid": "Show as grid", "search.view-switch.show-grid": "Näydä ruudukkona", - + // "search.view-switch.show-list": "Show as list", "search.view-switch.show-list": "Näytä luettelona", - - - + + + // "sorting.ASC": "Ascending", - // TODO New key - Add a translation - "sorting.ASC": "Ascending", - + "sorting.ASC": "Laskeva", + // "sorting.DESC": "Descending", - // TODO New key - Add a translation - "sorting.DESC": "Descending", - + "sorting.DESC": "Nouseva", + // "sorting.dc.title.ASC": "Title Ascending", "sorting.dc.title.ASC": "Nimeke (A-Ö)", - + // "sorting.dc.title.DESC": "Title Descending", "sorting.dc.title.DESC": "Nimeke (Ö-A)", - + // "sorting.score.DESC": "Relevance", "sorting.score.DESC": "Relevanssi", - - - + + + // "statistics.title": "Statistics", - // TODO New key - Add a translation - "statistics.title": "Statistics", - + "statistics.title": "Tilastot", + // "statistics.header": "Statistics for {{ scope }}", - // TODO New key - Add a translation - "statistics.header": "Statistics for {{ scope }}", - + "statistics.header": "{{ scope }}:n tilastot", + // "statistics.breadcrumbs": "Statistics", - // TODO New key - Add a translation - "statistics.breadcrumbs": "Statistics", - + "statistics.breadcrumbs": "Tilastot", + // "statistics.page.no-data": "No data available", - // TODO New key - Add a translation - "statistics.page.no-data": "No data available", - + "statistics.page.no-data": "Ei dataa", + // "statistics.table.no-data": "No data available", - // TODO New key - Add a translation - "statistics.table.no-data": "No data available", - + "statistics.table.no-data": "Ei dataa", + // "statistics.table.title.TotalVisits": "Total visits", - // TODO New key - Add a translation - "statistics.table.title.TotalVisits": "Total visits", - + "statistics.table.title.TotalVisits": "Käyntejä yhteensä", + // "statistics.table.title.TotalVisitsPerMonth": "Total visits per month", - // TODO New key - Add a translation - "statistics.table.title.TotalVisitsPerMonth": "Total visits per month", - + "statistics.table.title.TotalVisitsPerMonth": "Käyntejä kuukaudessa", + // "statistics.table.title.TotalDownloads": "File Visits", - // TODO New key - Add a translation - "statistics.table.title.TotalDownloads": "File Visits", - + "statistics.table.title.TotalDownloads": "Tiedostokäyntejä", + // "statistics.table.title.TopCountries": "Top country views", - // TODO New key - Add a translation - "statistics.table.title.TopCountries": "Top country views", - + "statistics.table.title.TopCountries": "Eniten latauksia maittain", + // "statistics.table.title.TopCities": "Top city views", - // TODO New key - Add a translation - "statistics.table.title.TopCities": "Top city views", - + "statistics.table.title.TopCities": "Eniten latauksia kaupungeittain", + // "statistics.table.header.views": "Views", - // TODO New key - Add a translation - "statistics.table.header.views": "Views", - - - + "statistics.table.header.views": "Latauksia", + + + // "submission.edit.title": "Edit Submission", - "submission.edit.title": "Muokkaa julkaisua", - + "submission.edit.title": "Muokkaa tallennusta", + // "submission.general.cannot_submit": "You have not the privilege to make a new submission.", - "submission.general.cannot_submit": "Sinulla ei ole oikeuksia aineiston julkaisemiseen.", - + "submission.general.cannot_submit": "Sinulla ei ole oikeuksia aineiston tallentamiseen.", + // "submission.general.deposit": "Deposit", "submission.general.deposit": "Tallenna", - + // "submission.general.discard.confirm.cancel": "Cancel", "submission.general.discard.confirm.cancel": "Peruuta", - + // "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", "submission.general.discard.confirm.info": "Tätä operaatiota ei voi perua. Oletko varma?", - + // "submission.general.discard.confirm.submit": "Yes, I'm sure", "submission.general.discard.confirm.submit": "Kyllä, olen varma", - + // "submission.general.discard.confirm.title": "Discard submission", - "submission.general.discard.confirm.title": "Hylkää julkaisu", - + "submission.general.discard.confirm.title": "Hylkää tallennus", + // "submission.general.discard.submit": "Discard", "submission.general.discard.submit": "Hylkää", - + // "submission.general.save": "Save", "submission.general.save": "Tallenna", - + // "submission.general.save-later": "Save for later", "submission.general.save-later": "Tallenna myöhemmäksi", - - + + // "submission.import-external.page.title": "Import metadata from an external source", - // TODO New key - Add a translation - "submission.import-external.page.title": "Import metadata from an external source", - + "submission.import-external.page.title": "Importoi metadata ulkoisesta lähteestä", + // "submission.import-external.title": "Import metadata from an external source", - // TODO New key - Add a translation - "submission.import-external.title": "Import metadata from an external source", - + "submission.import-external.title": "Importoi metadata ulkoisesta lähteestä", + // "submission.import-external.page.hint": "Enter a query above to find items from the web to import in to DSpace.", - // TODO New key - Add a translation - "submission.import-external.page.hint": "Enter a query above to find items from the web to import in to DSpace.", - + "submission.import-external.page.hint": "Anna hakulauseke etsiäksesi verkosta arkistoon importoitavia tietueita.", + // "submission.import-external.back-to-my-dspace": "Back to MyDSpace", - // TODO New key - Add a translation - "submission.import-external.back-to-my-dspace": "Back to MyDSpace", - + "submission.import-external.back-to-my-dspace": "Paluu omiin tietoihin", + // "submission.import-external.search.placeholder": "Search the external source", - // TODO New key - Add a translation - "submission.import-external.search.placeholder": "Search the external source", - + "submission.import-external.search.placeholder": "Hae ulkoisesta lähteestä", + // "submission.import-external.search.button": "Search", - // TODO New key - Add a translation - "submission.import-external.search.button": "Search", - + "submission.import-external.search.button": "Hae", + // "submission.import-external.search.button.hint": "Write some words to search", - // TODO New key - Add a translation - "submission.import-external.search.button.hint": "Write some words to search", - + "submission.import-external.search.button.hint": "Anna muutama hakusana", + // "submission.import-external.search.source.hint": "Pick an external source", - // TODO New key - Add a translation - "submission.import-external.search.source.hint": "Pick an external source", - + "submission.import-external.search.source.hint": "Valitse ulkoinen lähde", + // "submission.import-external.source.arxiv": "arXiv", - // TODO New key - Add a translation "submission.import-external.source.arxiv": "arXiv", - + // "submission.import-external.source.loading": "Loading ...", - // TODO New key - Add a translation - "submission.import-external.source.loading": "Loading ...", - + "submission.import-external.source.loading": "Ladataan ...", + // "submission.import-external.source.sherpaJournal": "SHERPA Journals", - // TODO New key - Add a translation - "submission.import-external.source.sherpaJournal": "SHERPA Journals", - + "submission.import-external.source.sherpaJournal": "SHERPA-kausijulkaisut", + // "submission.import-external.source.sherpaPublisher": "SHERPA Publishers", - // TODO New key - Add a translation - "submission.import-external.source.sherpaPublisher": "SHERPA Publishers", - + "submission.import-external.source.sherpaPublisher": "SHERPA-kustantajat", + // "submission.import-external.source.orcid": "ORCID", - // TODO New key - Add a translation - "submission.import-external.source.orcid": "ORCID", - + "submission.import-external.source.orcid": "ORCID-tunniste", + // "submission.import-external.source.pubmed": "Pubmed", - // TODO New key - Add a translation "submission.import-external.source.pubmed": "Pubmed", - + // "submission.import-external.source.lcname": "Library of Congress Names", - // TODO New key - Add a translation - "submission.import-external.source.lcname": "Library of Congress Names", - + "submission.import-external.source.lcname": "Library of Congress -nimet", + // "submission.import-external.preview.title": "Item Preview", - // TODO New key - Add a translation - "submission.import-external.preview.title": "Item Preview", - + "submission.import-external.preview.title": "Tietueen esikatselu", + // "submission.import-external.preview.subtitle": "The metadata below was imported from an external source. It will be pre-filled when you start the submission.", - // TODO New key - Add a translation - "submission.import-external.preview.subtitle": "The metadata below was imported from an external source. It will be pre-filled when you start the submission.", - + "submission.import-external.preview.subtitle": "Alla oleva metadata importoitiin ulkoisesta lähteestä. Sillä esitäytetään metadata, kun aloitat tallennuksen.", + // "submission.import-external.preview.button.import": "Start submission", - // TODO New key - Add a translation - "submission.import-external.preview.button.import": "Start submission", - + "submission.import-external.preview.button.import": "Aloita tallentaminen", + // "submission.import-external.preview.error.import.title": "Submission error", - // TODO New key - Add a translation - "submission.import-external.preview.error.import.title": "Submission error", - + "submission.import-external.preview.error.import.title": "Tallennusvirhe", + // "submission.import-external.preview.error.import.body": "An error occurs during the external source entry import process.", - // TODO New key - Add a translation - "submission.import-external.preview.error.import.body": "An error occurs during the external source entry import process.", - + "submission.import-external.preview.error.import.body": "Virhe importoitaessa tietuetta ulkoisesta lähteestä.", + // "submission.sections.describe.relationship-lookup.close": "Close", "submission.sections.describe.relationship-lookup.close": "Sulje", - + // "submission.sections.describe.relationship-lookup.external-source.added": "Successfully added local entry to the selection", "submission.sections.describe.relationship-lookup.external-source.added": "Paikallinen kohde lisätty valintaan", - + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.isAuthorOfPublication": "Import remote author", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.external-source.import-button-title.isAuthorOfPublication": "Import remote author", - + "submission.sections.describe.relationship-lookup.external-source.import-button-title.isAuthorOfPublication": "Importoi tekijä ulkoisesta lähteestä", + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal": "Import remote journal", "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal": "Importoi kausijulkaisu ulkoisesta lähteestä", - + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Issue": "Import remote journal issue", "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Issue": "Importoi kausijulkaisun numero ulkoisesta lähteestä", - + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Volume": "Import remote journal volume", "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Volume": "Importoi kausijulkaisun vuosikerta ulkoisesta lähteestä", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.title": "Import Remote Author", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.title": "Import Remote Author", - + "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.title": "Importoi tekijä ulkoisesta lähteestä", + // "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Successfully added local author to the selection", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Successfully added local author to the selection", - + "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Paikallinen tekijä lisätty valintaan", + // "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.new-entity": "Successfully imported and added external author to the selection", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.new-entity": "Successfully imported and added external author to the selection", - + "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.new-entity": "Ulkoinen tekijä importoitu ja lisätty valintaan", + // "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Authority", "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Auktoriteetti", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.authority.new": "Import as a new local authority entry", "submission.sections.describe.relationship-lookup.external-source.import-modal.authority.new": "Importoi uudeksi paikalliseksi auktoriteettikohteeksi", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Cancel", "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Peruuta", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.collection": "Select a collection to import new entries to", "submission.sections.describe.relationship-lookup.external-source.import-modal.collection": "Valitse kokoelma, johon uudet kohteet importoidaan", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Entities", "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Kokonaisuudet", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.entities.new": "Import as a new local entity", "submission.sections.describe.relationship-lookup.external-source.import-modal.entities.new": "Importoi uudeksi paikalliseksi kokonaisuudeksi", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name", "submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importointi Library of Congress -nimestä", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID", - + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importointi ORCIDista", + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal", "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importointi Sherpa-julkaisusta", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaPublisher": "Importing from Sherpa Publisher", "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaPublisher": "Importointi Sherpa-julkaisijalta", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.pubmed": "Importing from PubMed", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.external-source.import-modal.head.pubmed": "Importing from PubMed", - + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.pubmed": "Importointi PubMedista", + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.arxiv": "Importing from arXiv", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.external-source.import-modal.head.arxiv": "Importing from arXiv", - + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.arxiv": "Importointi arXivista", + // "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Import", "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Importoi", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.title": "Import Remote Journal", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.title": "Importoi kausijulkaisu ulkoisesta lähteestä", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.local-entity": "Successfully added local journal to the selection", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.local-entity": "Paikallinen kausijulkaisu lisätty valintaan", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.new-entity": "Successfully imported and added external journal to the selection", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.new-entity": "Ulkoinen kausijulkaisu importoitu ja lisätty valintaan", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.title": "Import Remote Journal Issue", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.title": "Importoi kausijulkaisun numero ulkoisesta lähteestä", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.local-entity": "Successfully added local journal issue to the selection", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.local-entity": "Paikallinen kausijulkaisun numero lisätty valintaan", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.new-entity": "Successfully imported and added external journal issue to the selection", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.new-entity": "Ulkoinen kausijulkaisun numero importoitu ja lisätty valintaan", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.title": "Import Remote Journal Volume", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.title": "Importoi kausijulkaisun vuosikerta ulkoisesta lähteestä", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.local-entity": "Successfully added local journal volume to the selection", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.local-entity": "Paikallinen kausijulkaisun vuosikerta lisätty valintaan", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.new-entity": "Successfully imported and added external journal volume to the selection", "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.new-entity": "Ulkoinen kausijulkaisun vuosikerta importoitu ja lisätty valintaan", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Select a local match:", "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Valitse paikallinen vastaavuus:", - + // "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all", "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Poista kaikkien valinta", - + // "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page", "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Poista sivun valinta", - + // "submission.sections.describe.relationship-lookup.search-tab.loading": "Loading...", "submission.sections.describe.relationship-lookup.search-tab.loading": "Ladataan...", - + // "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Search query", "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Hakulauseke", - + // "submission.sections.describe.relationship-lookup.search-tab.search": "Go", "submission.sections.describe.relationship-lookup.search-tab.search": "Ok", - + // "submission.sections.describe.relationship-lookup.search-tab.select-all": "Select all", "submission.sections.describe.relationship-lookup.search-tab.select-all": "Valitse kaikki", - + // "submission.sections.describe.relationship-lookup.search-tab.select-page": "Select page", "submission.sections.describe.relationship-lookup.search-tab.select-page": "Valitse sivu", - + // "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", "submission.sections.describe.relationship-lookup.selected": "Valittu {{ size }} tietuetta", - + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Local Authors ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Local Authors ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Paikalliset tekijät ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Local Journals ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Local Journals ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Paikalliset kausijulkaisut ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Project": "Local Projects ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Project": "Local Projects ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Project": "Paikalliset projektit ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Publication": "Local Publications ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Publication": "Local Publications ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Publication": "Paikalliset julkaisut ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Person": "Local Authors ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Person": "Local Authors ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Person": "Paikalliset tekijät ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.OrgUnit": "Local Organizational Units ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.OrgUnit": "Local Organizational Units ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.OrgUnit": "Paikalliset organisaatioyksiköt ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataPackage": "Local Data Packages ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataPackage": "Local Data Packages ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataPackage": "Paikalliset datapaketit ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataFile": "Local Data Files ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataFile": "Local Data Files ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataFile": "Paikalliset datatiedostot ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Paikallisia kausijulkaisuja ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Paikalliset kausijulkaisut ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalIssueOfPublication": "Local Journal Issues ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalIssueOfPublication": "Local Journal Issues ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalIssueOfPublication": "Paikalliset kausijulkaisun numerot ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalIssue": "Local Journal Issues ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalIssue": "Local Journal Issues ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalIssue": "Paikalliset kausijulkaisun numerot ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Local Journal Volumes ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Local Journal Volumes ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Paikalliset kausijulkaisun vuosikerrat ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalVolume": "Local Journal Volumes ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalVolume": "Local Journal Volumes ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalVolume": "Paikalliset kausijulkaisun vuosikerrat ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa-kausijulkaisuja ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa-kausijulkaisut ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa-julkaisijoita ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa-julkaisijat ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID-tunnisteet ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "Library of Congress -nimiä ({{ count }})", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "Library of Congress -nimet ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.pubmed": "PubMed ({{ count }})", - // TODO New key - Add a translation "submission.sections.describe.relationship-lookup.search-tab.tab-title.pubmed": "PubMed ({{ count }})", - + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.arxiv": "arXiv ({{ count }})", - // TODO New key - Add a translation "submission.sections.describe.relationship-lookup.search-tab.tab-title.arxiv": "arXiv ({{ count }})", - + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingAgencyOfPublication": "Search for Funding Agencies", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingAgencyOfPublication": "Search for Funding Agencies", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingAgencyOfPublication": "Hae rahoittajatahoja", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingOfPublication": "Search for Funding", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingOfPublication": "Search for Funding", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingOfPublication": "Hae rahoitusta", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isChildOrgUnitOf": "Search for Organizational Units", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isChildOrgUnitOf": "Search for Organizational Units", - + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isChildOrgUnitOf": "Hae organisaatioyksiköitä", + // "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Nykyinen valinta ({{ count }})", - + // "submission.sections.describe.relationship-lookup.title.isJournalIssueOfPublication": "Journal Issues", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.isJournalIssueOfPublication": "Journal Issues", + "submission.sections.describe.relationship-lookup.title.isJournalIssueOfPublication": "Kausijulkaisun numerot", + + // "submission.sections.describe.relationship-lookup.title.JournalIssue": "Journal Issues", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.JournalIssue": "Journal Issues", - + "submission.sections.describe.relationship-lookup.title.JournalIssue": "Kausijulkaisun numerot", + // "submission.sections.describe.relationship-lookup.title.isJournalVolumeOfPublication": "Journal Volumes", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.isJournalVolumeOfPublication": "Journal Volumes", + "submission.sections.describe.relationship-lookup.title.isJournalVolumeOfPublication": "Kausijulkaisun vuosikerrat", + // "submission.sections.describe.relationship-lookup.title.JournalVolume": "Journal Volumes", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.JournalVolume": "Journal Volumes", - + "submission.sections.describe.relationship-lookup.title.JournalVolume": "Kausijulkaisun vuosikerrat", + // "submission.sections.describe.relationship-lookup.title.isJournalOfPublication": "Journals", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.isJournalOfPublication": "Journals", - + "submission.sections.describe.relationship-lookup.title.isJournalOfPublication": "Kausijulkaisut", + // "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "Authors", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "Authors", - + "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "Tekijät", + // "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfPublication": "Funding Agency", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfPublication": "Funding Agency", + "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfPublication": "Rahoittajataho", + // "submission.sections.describe.relationship-lookup.title.Project": "Projects", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.Project": "Projects", - + "submission.sections.describe.relationship-lookup.title.Project": "Projektit", + // "submission.sections.describe.relationship-lookup.title.Publication": "Publications", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.Publication": "Publications", - + "submission.sections.describe.relationship-lookup.title.Publication": "Julkaisut", + // "submission.sections.describe.relationship-lookup.title.Person": "Authors", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.Person": "Authors", - + "submission.sections.describe.relationship-lookup.title.Person": "Tekijät", + // "submission.sections.describe.relationship-lookup.title.OrgUnit": "Organizational Units", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.OrgUnit": "Organizational Units", - + "submission.sections.describe.relationship-lookup.title.OrgUnit": "Organisaatioyksiköt", + // "submission.sections.describe.relationship-lookup.title.DataPackage": "Data Packages", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.DataPackage": "Data Packages", - + "submission.sections.describe.relationship-lookup.title.DataPackage": "Datapaketit", + // "submission.sections.describe.relationship-lookup.title.DataFile": "Data Files", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.DataFile": "Data Files", - + "submission.sections.describe.relationship-lookup.title.DataFile": "Datatiedostot", + // "submission.sections.describe.relationship-lookup.title.Funding Agency": "Funding Agency", "submission.sections.describe.relationship-lookup.title.Funding Agency": "Rahoittajataho", - + // "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Funding", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Funding", - + "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Rahoitus", + // "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Parent Organizational Unit", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Parent Organizational Unit", - + "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Ylempi organisaatioyksikkö", + // "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown", "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Vaihda valikon tilaa", - + // "submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings", "submission.sections.describe.relationship-lookup.selection-tab.settings": "Asetukset", - + // "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Your selection is currently empty.", "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Valinta on tyhjä.", - + // "submission.sections.describe.relationship-lookup.selection-tab.title.isAuthorOfPublication": "Selected Authors", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.isAuthorOfPublication": "Selected Authors", - + "submission.sections.describe.relationship-lookup.selection-tab.title.isAuthorOfPublication": "Valitut tekijät", + // "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalOfPublication": "Selected Journals", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalOfPublication": "Selected Journals", - + "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalOfPublication": "Valitut kausijulkaisut", + // "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfPublication": "Selected Journal Volume", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfPublication": "Selected Journal Volume", + "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfPublication": "Valittu kausijulkaisun vuosikerta", + // "submission.sections.describe.relationship-lookup.selection-tab.title.Project": "Selected Projects", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.Project": "Selected Projects", - + "submission.sections.describe.relationship-lookup.selection-tab.title.Project": "Valitut projektit", + // "submission.sections.describe.relationship-lookup.selection-tab.title.Publication": "Selected Publications", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.Publication": "Selected Publications", - + "submission.sections.describe.relationship-lookup.selection-tab.title.Publication": "Valitut julkaisut", + // "submission.sections.describe.relationship-lookup.selection-tab.title.Person": "Selected Authors", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.Person": "Selected Authors", - + "submission.sections.describe.relationship-lookup.selection-tab.title.Person": "Valitut tekijät", + // "submission.sections.describe.relationship-lookup.selection-tab.title.OrgUnit": "Selected Organizational Units", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.OrgUnit": "Selected Organizational Units", - + "submission.sections.describe.relationship-lookup.selection-tab.title.OrgUnit": "Valitut organisaatioyksiköt", + // "submission.sections.describe.relationship-lookup.selection-tab.title.DataPackage": "Selected Data Packages", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.DataPackage": "Selected Data Packages", - + "submission.sections.describe.relationship-lookup.selection-tab.title.DataPackage": "Valitut datapaketit", + // "submission.sections.describe.relationship-lookup.selection-tab.title.DataFile": "Selected Data Files", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.DataFile": "Selected Data Files", - + "submission.sections.describe.relationship-lookup.selection-tab.title.DataFile": "Valitut datatiedostot", + // "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals", "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Valitut kausijulkaisut", - + // "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalIssueOfPublication": "Selected Issue", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalIssueOfPublication": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalIssueOfPublication": "Valittu numero", + // "submission.sections.describe.relationship-lookup.selection-tab.title.JournalVolume": "Selected Journal Volume", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.JournalVolume": "Selected Journal Volume", - + "submission.sections.describe.relationship-lookup.selection-tab.title.JournalVolume": "Valittu kausijulkaisun vuosikerta", + // "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingAgencyOfPublication": "Selected Funding Agency", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingAgencyOfPublication": "Selected Funding Agency", - + "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingAgencyOfPublication": "Valittu rahoittajataho", + // "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingOfPublication": "Selected Funding", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingOfPublication": "Selected Funding", + "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingOfPublication": "Valittu rahoitus", + // "submission.sections.describe.relationship-lookup.selection-tab.title.JournalIssue": "Selected Issue", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.JournalIssue": "Selected Issue", - + "submission.sections.describe.relationship-lookup.selection-tab.title.JournalIssue": "Valittu numero", + // "submission.sections.describe.relationship-lookup.selection-tab.title.isChildOrgUnitOf": "Selected Organizational Unit", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.isChildOrgUnitOf": "Selected Organizational Unit", - + "submission.sections.describe.relationship-lookup.selection-tab.title.isChildOrgUnitOf": "Valittu organisaatioyksikkö", + // "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results", "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Hakutulokset", - + // "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results", "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Hakutulokset", - + // "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results", - + "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Hakutulokset", + // "submission.sections.describe.relationship-lookup.selection-tab.title.orcidv2": "Search Results", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.orcidv2": "Search Results", - + "submission.sections.describe.relationship-lookup.selection-tab.title.orcidv2": "Hakutulokset", + // "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results", "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Hakutulokset", - + // "submission.sections.describe.relationship-lookup.selection-tab.title.pubmed": "Search Results", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.pubmed": "Search Results", - + "submission.sections.describe.relationship-lookup.selection-tab.title.pubmed": "Hakutulokset", + // "submission.sections.describe.relationship-lookup.selection-tab.title.arxiv": "Search Results", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.arxiv": "Search Results", - + "submission.sections.describe.relationship-lookup.selection-tab.title.arxiv": "Hakutulokset", + // "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.", - "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Haluatko tallentaa nimen \"{{ value }}\" käyttäjän vaihtoehtoiseksi nimeksi, jota muutkin voivat käyttää uudelleen myös tulevissa julkaisuissa? Ellet tallenna nimeä, voit silti käyttää sitä tässä julkaisussa.", - + "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Haluatko tallentaa nimen \"{{ value }}\" käyttäjän vaihtoehtoiseksi nimeksi, jota muutkin voivat käyttää uudelleen myös tulevissa tallennuksissa? Ellet tallenna nimeä, voit silti käyttää sitä tässä tallennuksessa.", + // "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant", "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Tallenna uusi vaihtoehtoinen nimi", - + // "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Use only for this submission", - "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Käytä vain tässä julkaisussa", - + "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Käytä vain tässä tallennuksessa", + // "submission.sections.ccLicense.type": "License Type", - // TODO New key - Add a translation - "submission.sections.ccLicense.type": "License Type", - + "submission.sections.ccLicense.type": "Lisenssityyppi", + // "submission.sections.ccLicense.select": "Select a license type…", - // TODO New key - Add a translation - "submission.sections.ccLicense.select": "Select a license type…", - + "submission.sections.ccLicense.select": "Valitse lisenssityyppi…", + // "submission.sections.ccLicense.change": "Change your license type…", - // TODO New key - Add a translation - "submission.sections.ccLicense.change": "Change your license type…", - + "submission.sections.ccLicense.change": "Vaihda lisenssityyppi…", + // "submission.sections.ccLicense.none": "No licenses available", - // TODO New key - Add a translation - "submission.sections.ccLicense.none": "No licenses available", - + "submission.sections.ccLicense.none": "Ei lisenssejä", + // "submission.sections.ccLicense.option.select": "Select an option…", - // TODO New key - Add a translation - "submission.sections.ccLicense.option.select": "Select an option…", - + "submission.sections.ccLicense.option.select": "Valitse vaihtoehto…", + // "submission.sections.ccLicense.link": "You’ve selected the following license:", - // TODO New key - Add a translation - "submission.sections.ccLicense.link": "You’ve selected the following license:", - + "submission.sections.ccLicense.link": "Olet valinunt seuraavan lisenssin:", + // "submission.sections.ccLicense.confirmation": "I grant the license above", - // TODO New key - Add a translation - "submission.sections.ccLicense.confirmation": "I grant the license above", - + "submission.sections.ccLicense.confirmation": "Myönnän yllä olevan lisenssin", + // "submission.sections.general.add-more": "Add more", "submission.sections.general.add-more": "Lisää enemmän", - + // "submission.sections.general.collection": "Collection", "submission.sections.general.collection": "Kokoelma", - + // "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", - "submission.sections.general.deposit_error_notice": "Ongelma tietueen julkaisemisessa, yritä myöhemmin uudelleen.", - + "submission.sections.general.deposit_error_notice": "Ongelma tietueen tallentamisessa, yritä myöhemmin uudelleen.", + // "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", - "submission.sections.general.deposit_success_notice": "Julkaisu tallennettu.", - + "submission.sections.general.deposit_success_notice": "Tallennus onnistui.", + // "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", "submission.sections.general.discard_error_notice": "Ongelma tietueen hylkäämisessä, yritä myöhemmin uudelleen.", - + // "submission.sections.general.discard_success_notice": "Submission discarded successfully.", - "submission.sections.general.discard_success_notice": "Julkaisu hylätty.", - + "submission.sections.general.discard_success_notice": "Tallennus hylätty.", + // "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", "submission.sections.general.metadata-extracted": "Uusi metadata poimittu ja lisätty {{sectionId}}-osioon.", - + // "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", - "submission.sections.general.metadata-extracted-new-section": "Uusi {{sectionId}}-osio lisätty julkaisuun.", - + "submission.sections.general.metadata-extracted-new-section": "Uusi {{sectionId}}-osio lisätty tallennukseen.", + // "submission.sections.general.no-collection": "No collection found", "submission.sections.general.no-collection": "Ei kokoelmia", - + // "submission.sections.general.no-sections": "No options available", "submission.sections.general.no-sections": "Ei vaihtoehtoja", - + // "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", "submission.sections.general.save_error_notice": "Ongelma tietueen tallentamisessa, yritä myöhemmin uudelleen.", - + // "submission.sections.general.save_success_notice": "Submission saved successfully.", - "submission.sections.general.save_success_notice": "Julkaisun tallennus onnistui.", - + "submission.sections.general.save_success_notice": "Tallennus onnistui.", + // "submission.sections.general.search-collection": "Search for a collection", "submission.sections.general.search-collection": "Hae kokoelmaa", - + // "submission.sections.general.sections_not_valid": "There are incomplete sections.", "submission.sections.general.sections_not_valid": "Tallennuksessa keskeneräisiä osioita.", - - - + + + // "submission.sections.submit.progressbar.CClicense": "Creative commons license", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.CClicense": "Creative commons license", - + "submission.sections.submit.progressbar.CClicense": "Creative commons -lisenssi", + // "submission.sections.submit.progressbar.describe.recycle": "Recycle", "submission.sections.submit.progressbar.describe.recycle": "Kierrätä", - + // "submission.sections.submit.progressbar.describe.stepcustom": "Describe", "submission.sections.submit.progressbar.describe.stepcustom": "Kuvaile", - + // "submission.sections.submit.progressbar.describe.stepone": "Describe", "submission.sections.submit.progressbar.describe.stepone": "Kuvaile", - + // "submission.sections.submit.progressbar.describe.steptwo": "Describe", "submission.sections.submit.progressbar.describe.steptwo": "Kuvaile", - + // "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", "submission.sections.submit.progressbar.detect-duplicate": "Mahdollisia kaksoiskappaleita", - + // "submission.sections.submit.progressbar.license": "Deposit license", "submission.sections.submit.progressbar.license": "Tallennuslisenssi", - + // "submission.sections.submit.progressbar.upload": "Upload files", "submission.sections.submit.progressbar.upload": "Lataa tiedostoja", - - - + + + // "submission.sections.upload.delete.confirm.cancel": "Cancel", "submission.sections.upload.delete.confirm.cancel": "Peruuta", - + // "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", "submission.sections.upload.delete.confirm.info": "Tätä toimintoa ei voi peruuttaa. Oletko varma?", - + // "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", "submission.sections.upload.delete.confirm.submit": "Kyllä, olen varma", - + // "submission.sections.upload.delete.confirm.title": "Delete bitstream", "submission.sections.upload.delete.confirm.title": "Poista tiedosto", - + // "submission.sections.upload.delete.submit": "Delete", "submission.sections.upload.delete.submit": "Poista", - + // "submission.sections.upload.drop-message": "Drop files to attach them to the item", "submission.sections.upload.drop-message": "Pudota tiedostot liittääksesi ne tietueeseen", - + // "submission.sections.upload.form.access-condition-label": "Access condition type", "submission.sections.upload.form.access-condition-label": "Pääsyoikeustyyppi", - + // "submission.sections.upload.form.date-required": "Date is required.", "submission.sections.upload.form.date-required": "Päivämäärä on pakollinen tieto.", - + // "submission.sections.upload.form.from-label": "Grant access from", - // TODO Source message changed - Revise the translation "submission.sections.upload.form.from-label": "Pääsyoikeus alkaa", - + // "submission.sections.upload.form.from-placeholder": "From", - "submission.sections.upload.form.from-placeholder": "Lähettäjä", - + "submission.sections.upload.form.from-placeholder": "Alkaen", + // "submission.sections.upload.form.group-label": "Group", "submission.sections.upload.form.group-label": "Ryhmä", - + // "submission.sections.upload.form.group-required": "Group is required.", "submission.sections.upload.form.group-required": "Ryhmä on pakollinen tieto.", - + // "submission.sections.upload.form.until-label": "Grant access until", - // TODO Source message changed - Revise the translation "submission.sections.upload.form.until-label": "Pääsyoikeus päättyy", - + // "submission.sections.upload.form.until-placeholder": "Until", "submission.sections.upload.form.until-placeholder": "Asti", - + // "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", "submission.sections.upload.header.policy.default.nolist": "{{collectionName}}-kokoelmaan ladatut tiedostot ovat seuraavien ryhmien saatavilla:", - + // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", "submission.sections.upload.header.policy.default.withlist": "Yksittäisten tiedostojen pääsyrajoitusten lisäksi {{collectionName}}-kokoelmaan ladatut tiedostot ovat seuraavien ryhmien saatavilla:", - + // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", "submission.sections.upload.info": "Tietueen kaikki tiedostot on lueteltu tässä. Voit päivittää tiedoston metadataa ja pääsyehtoja tai ladata lisää tiedostoja raahaamalla ne mihin hyvänsä sivun kohtaan", - + // "submission.sections.upload.no-entry": "No", "submission.sections.upload.no-entry": "Ei", - + // "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", "submission.sections.upload.no-file-uploaded": "Tiedostoa ei vielä ladattu.", - + // "submission.sections.upload.save-metadata": "Save metadata", "submission.sections.upload.save-metadata": "Tallenna metadata", - + // "submission.sections.upload.undo": "Cancel", "submission.sections.upload.undo": "Peruuta", - + // "submission.sections.upload.upload-failed": "Upload failed", "submission.sections.upload.upload-failed": "Lataus epäonnistui", - + // "submission.sections.upload.upload-successful": "Upload successful", "submission.sections.upload.upload-successful": "Lataus valmis", - - - - // "submission.submit.title": "Submission", + + + + // "submission.submit.title": "Tallennus", "submission.submit.title": "Julkaisu", - - - + + + // "submission.workflow.generic.delete": "Delete", "submission.workflow.generic.delete": "Poista", - + // "submission.workflow.generic.delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.", "submission.workflow.generic.delete-help": "Valitse \"Poista\" hylätäksesi tietueen. Poisto pyydetään vielä vahvistamaan.", - + // "submission.workflow.generic.edit": "Edit", "submission.workflow.generic.edit": "Muokkaa", - + // "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", "submission.workflow.generic.edit-help": "Valitse tämä muuttaaksesi tietueen metadataa.", - + // "submission.workflow.generic.view": "View", "submission.workflow.generic.view": "Näytä", - + // "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", "submission.workflow.generic.view-help": "Valitse tämä katsoaksesi tietueen metadataa.", - - - + + + // "submission.workflow.tasks.claimed.approve": "Approve", "submission.workflow.tasks.claimed.approve": "Hyväksy", - + // "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", "submission.workflow.tasks.claimed.approve_help": "Jos olet tarkistanut tietueen, ja se on kelvollinen kokoelmaan lisättäväksi, valitse \"Hyväksy\".", - + // "submission.workflow.tasks.claimed.edit": "Edit", "submission.workflow.tasks.claimed.edit": "Muokkaa", - + // "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", "submission.workflow.tasks.claimed.edit_help": "Valitse tämä muuttaaksesi tietueen metadataa.", - + // "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", - "submission.workflow.tasks.claimed.reject.reason.info": "Syötä kenttään syy julkaisun hylkäämiselle. Kerro myös, voiko julkaisija korjata ongelman ja lähettää aineiston uudelleen.", - + "submission.workflow.tasks.claimed.reject.reason.info": "Syötä kenttään syy tallennuksen hylkäämiselle. Kerro myös, voiko tallentaja korjata ongelman ja lähettää aineiston uudelleen.", + // "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", "submission.workflow.tasks.claimed.reject.reason.placeholder": "Kuvaa hylkäyksen syy", - + // "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", "submission.workflow.tasks.claimed.reject.reason.submit": "Hylkää tietue", - + // "submission.workflow.tasks.claimed.reject.reason.title": "Reason", "submission.workflow.tasks.claimed.reject.reason.title": "Syy", - + // "submission.workflow.tasks.claimed.reject.submit": "Reject", "submission.workflow.tasks.claimed.reject.submit": "Hylkää", - + // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", - "submission.workflow.tasks.claimed.reject_help": "Jos olet tarkistanut tietueen, ja se ei on kelvollinen kokoelmaan lisättäväksi, valitse \"Hylkää\". Tämän jälkeen sinua pyydetään kertomaan, miksi tietue ei ole kelvollinen sekä voiko julkaisija tehdä muutoksia ja lähettää aineiston uudelleen.", - + "submission.workflow.tasks.claimed.reject_help": "Jos olet tarkistanut tietueen, ja se ei on kelvollinen kokoelmaan lisättäväksi, valitse \"Hylkää\". Tämän jälkeen sinua pyydetään kertomaan, miksi tietue ei ole kelvollinen sekä voiko tallentaja tehdä muutoksia ja lähettää aineiston uudelleen.", + // "submission.workflow.tasks.claimed.return": "Return to pool", "submission.workflow.tasks.claimed.return": "Palauta tehtäväjonoon", - + // "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", "submission.workflow.tasks.claimed.return_help": "Palauta tehtävä tehtäväjonoon, jotta toinen käyttäjä voi suorittaa tehtävän.", - - - + + + // "submission.workflow.tasks.generic.error": "Error occurred during operation...", "submission.workflow.tasks.generic.error": "Virhe toimintoa suoritettaessa...", - + // "submission.workflow.tasks.generic.processing": "Processing...", "submission.workflow.tasks.generic.processing": "Käsitellään...", - + // "submission.workflow.tasks.generic.submitter": "Submitter", - "submission.workflow.tasks.generic.submitter": "Julkaisija", - + "submission.workflow.tasks.generic.submitter": "Tallentaja", + // "submission.workflow.tasks.generic.success": "Operation successful", "submission.workflow.tasks.generic.success": "Toiminto onnistui", - - - + + + // "submission.workflow.tasks.pool.claim": "Claim", "submission.workflow.tasks.pool.claim": "Ota itsellesi", - + // "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", "submission.workflow.tasks.pool.claim_help": "Ota tehtävä itsellesi.", - + // "submission.workflow.tasks.pool.hide-detail": "Hide detail", "submission.workflow.tasks.pool.hide-detail": "Piilota lisätiedot", - + // "submission.workflow.tasks.pool.show-detail": "Show detail", "submission.workflow.tasks.pool.show-detail": "Näytä lisätiedot", - - - + + + // "title": "DSpace", "title": "Julkaisuarkisto", - - - + + + // "vocabulary-treeview.header": "Hierarchical tree view", - // TODO New key - Add a translation - "vocabulary-treeview.header": "Hierarchical tree view", - + "vocabulary-treeview.header": "Hierarkkinen puunäkymä", + // "vocabulary-treeview.load-more": "Load more", - // TODO New key - Add a translation - "vocabulary-treeview.load-more": "Load more", - + "vocabulary-treeview.load-more": "Lataa lisää", + // "vocabulary-treeview.search.form.reset": "Reset", - // TODO New key - Add a translation - "vocabulary-treeview.search.form.reset": "Reset", - + "vocabulary-treeview.search.form.reset": "Nollaa", + // "vocabulary-treeview.search.form.search": "Search", - // TODO New key - Add a translation - "vocabulary-treeview.search.form.search": "Search", - + "vocabulary-treeview.search.form.search": "Hae", + // "vocabulary-treeview.search.no-result": "There were no items to show", - // TODO New key - Add a translation - "vocabulary-treeview.search.no-result": "There were no items to show", - + "vocabulary-treeview.search.no-result": "Ei tietueita", + // "vocabulary-treeview.tree.description.nsi": "The Norwegian Science Index", - // TODO New key - Add a translation "vocabulary-treeview.tree.description.nsi": "The Norwegian Science Index", - + // "vocabulary-treeview.tree.description.srsc": "Research Subject Categories", - // TODO New key - Add a translation - "vocabulary-treeview.tree.description.srsc": "Research Subject Categories", - - - + "vocabulary-treeview.tree.description.srsc": "Tutkimusaiheiden kategoriat", + + + // "uploader.browse": "browse", "uploader.browse": "selaa", - + // "uploader.drag-message": "Drag & Drop your files here", "uploader.drag-message": "Raahaa tiedostot tähän", - + // "uploader.or": ", or ", - // TODO Source message changed - Revise the translation "uploader.or": " tai", - + // "uploader.processing": "Processing", "uploader.processing": "Käsitellään", - + // "uploader.queue-length": "Queue length", "uploader.queue-length": "Jonon pituus", - + // "virtual-metadata.delete-item.info": "Select the types for which you want to save the virtual metadata as real metadata", "virtual-metadata.delete-item.info": "Valitse tyypit, joiden virtuaalisen metadatan haluat tallentaa varsinaiseksi metadataksi", - + // "virtual-metadata.delete-item.modal-head": "The virtual metadata of this relation", "virtual-metadata.delete-item.modal-head": "Relaation virtuaalinen metadata", - + // "virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata", "virtual-metadata.delete-relationship.modal-head": "Valitse tietueet, joiden virtuaalisen metadatan haluat tallentaa varsinaiseksi metadataksi", - - - - // "workflowAdmin.search.results.head": "Administer Workflow", - // TODO New key - Add a translation - "workflowAdmin.search.results.head": "Administer Workflow", - - - - // "workflow-item.delete.notification.success.title": "Deleted", - // TODO New key - Add a translation - "workflow-item.delete.notification.success.title": "Deleted", - - // "workflow-item.delete.notification.success.content": "This workflow item was successfully deleted", - // TODO New key - Add a translation - "workflow-item.delete.notification.success.content": "This workflow item was successfully deleted", - - // "workflow-item.delete.notification.error.title": "Something went wrong", - // TODO New key - Add a translation - "workflow-item.delete.notification.error.title": "Something went wrong", - - // "workflow-item.delete.notification.error.content": "The workflow item could not be deleted", - // TODO New key - Add a translation - "workflow-item.delete.notification.error.content": "The workflow item could not be deleted", - - // "workflow-item.delete.title": "Delete workflow item", - // TODO New key - Add a translation - "workflow-item.delete.title": "Delete workflow item", - - // "workflow-item.delete.header": "Delete workflow item", - // TODO New key - Add a translation - "workflow-item.delete.header": "Delete workflow item", - - // "workflow-item.delete.button.cancel": "Cancel", - // TODO New key - Add a translation - "workflow-item.delete.button.cancel": "Cancel", - - // "workflow-item.delete.button.confirm": "Delete", - // TODO New key - Add a translation - "workflow-item.delete.button.confirm": "Delete", - - - // "workflow-item.send-back.notification.success.title": "Sent back to submitter", - // TODO New key - Add a translation - "workflow-item.send-back.notification.success.title": "Sent back to submitter", - - // "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", - // TODO New key - Add a translation - "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", - - // "workflow-item.send-back.notification.error.title": "Something went wrong", - // TODO New key - Add a translation - "workflow-item.send-back.notification.error.title": "Something went wrong", - - // "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", - // TODO New key - Add a translation - "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", - - // "workflow-item.send-back.title": "Send workflow item back to submitter", - // TODO New key - Add a translation - "workflow-item.send-back.title": "Send workflow item back to submitter", - - // "workflow-item.send-back.header": "Send workflow item back to submitter", - // TODO New key - Add a translation - "workflow-item.send-back.header": "Send workflow item back to submitter", - - // "workflow-item.send-back.button.cancel": "Cancel", - // TODO New key - Add a translation - "workflow-item.send-back.button.cancel": "Cancel", - - // "workflow-item.send-back.button.confirm": "Send back" - // TODO New key - Add a translation - "workflow-item.send-back.button.confirm": "Send back" - -} \ No newline at end of file + + + // "workflowAdmin.search.results.head": "Administer Workflow", + "workflowAdmin.search.results.head": "Hallinnointityönkulku", + + + + // "workflow-item.delete.notification.success.title": "Deleted", + "workflow-item.delete.notification.success.title": "Poistettu", + + // "workflow-item.delete.notification.success.content": "This workflow item was successfully deleted", + "workflow-item.delete.notification.success.content": "Tarkastamaton tietue poistettu", + + // "workflow-item.delete.notification.error.title": "Something went wrong", + "workflow-item.delete.notification.error.title": "Tapahtui virhe", + + // "workflow-item.delete.notification.error.content": "The workflow item could not be deleted", + "workflow-item.delete.notification.error.content": "Tarkastamatonta tietuetta ei voitu poistaa", + + // "workflow-item.delete.title": "Delete workflow item", + "workflow-item.delete.title": "Poista tarkastamaton tietue", + + // "workflow-item.delete.header": "Delete workflow item", + "workflow-item.delete.header": "Poista tarkastamaton tietue", + + // "workflow-item.delete.button.cancel": "Cancel", + "workflow-item.delete.button.cancel": "Peruuta", + + // "workflow-item.delete.button.confirm": "Delete", + "workflow-item.delete.button.confirm": "Poista", + + + // "workflow-item.send-back.notification.success.title": "Sent back to submitter", + "workflow-item.send-back.notification.success.title": "Lähetetty takaisin tallentajalle", + + // "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", + "workflow-item.send-back.notification.success.content": "Tarkastamaton tietue lähetetty takaisin tallentajalle", + + // "workflow-item.send-back.notification.error.title": "Something went wrong", + "workflow-item.send-back.notification.error.title": "Tapahtui virhe", + + // "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", + "workflow-item.send-back.notification.error.content": "Tarkastamatonta tietuetta ei voitu lähettää takaisin tallentajalle", + + // "workflow-item.send-back.title": "Send workflow item back to submitter", + "workflow-item.send-back.title": "Lähetä tarkastamaton tietue takaisin tallentajalle", + + // "workflow-item.send-back.header": "Send workflow item back to submitter", + "workflow-item.send-back.header": "Lähetä tarkastamaton tietue takaisin tallentajalle", + + // "workflow-item.send-back.button.cancel": "Cancel", + "workflow-item.send-back.button.cancel": "Peruuta", + + // "workflow-item.send-back.button.confirm": "Send back" + "workflow-item.send-back.button.confirm": "Lähetä takaisin" + + +} diff --git a/src/assets/i18n/fr.json5 b/src/assets/i18n/fr.json5 index 7b43330e10..982a360c7d 100644 --- a/src/assets/i18n/fr.json5 +++ b/src/assets/i18n/fr.json5 @@ -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 diff --git a/src/assets/i18n/hu.json5 b/src/assets/i18n/hu.json5 index 15b05c93e8..a42c36f263 100644 --- a/src/assets/i18n/hu.json5 +++ b/src/assets/i18n/hu.json5 @@ -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", diff --git a/src/assets/i18n/ja.json5 b/src/assets/i18n/ja.json5 index 8383c440cd..0d036ff7d3 100644 --- a/src/assets/i18n/ja.json5 +++ b/src/assets/i18n/ja.json5 @@ -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 diff --git a/src/assets/i18n/lv.json5 b/src/assets/i18n/lv.json5 index ff998ab774..738baca186 100644 --- a/src/assets/i18n/lv.json5 +++ b/src/assets/i18n/lv.json5 @@ -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 diff --git a/src/assets/i18n/nl.json5 b/src/assets/i18n/nl.json5 index 8acbe901f0..bb0b3b121c 100644 --- a/src/assets/i18n/nl.json5 +++ b/src/assets/i18n/nl.json5 @@ -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 diff --git a/src/assets/i18n/pl.json5 b/src/assets/i18n/pl.json5 index 8383c440cd..0d036ff7d3 100644 --- a/src/assets/i18n/pl.json5 +++ b/src/assets/i18n/pl.json5 @@ -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 diff --git a/src/assets/i18n/pt-BR.json5 b/src/assets/i18n/pt-BR.json5 index 4c7d75e11a..fbe06877fb 100644 --- a/src/assets/i18n/pt-BR.json5 +++ b/src/assets/i18n/pt-BR.json5 @@ -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 diff --git a/src/assets/i18n/pt-PT.json5 b/src/assets/i18n/pt-PT.json5 index 4c7d75e11a..fbe06877fb 100644 --- a/src/assets/i18n/pt-PT.json5 +++ b/src/assets/i18n/pt-PT.json5 @@ -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 diff --git a/src/assets/i18n/sw.json5 b/src/assets/i18n/sw.json5 index 8383c440cd..0d036ff7d3 100644 --- a/src/assets/i18n/sw.json5 +++ b/src/assets/i18n/sw.json5 @@ -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 diff --git a/src/assets/i18n/tr.json5 b/src/assets/i18n/tr.json5 index 8383c440cd..0d036ff7d3 100644 --- a/src/assets/i18n/tr.json5 +++ b/src/assets/i18n/tr.json5 @@ -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 diff --git a/src/assets/images/dspace-logo-monochrome.svg b/src/assets/images/dspace-logo-monochrome.svg deleted file mode 100644 index 5a2204ad83..0000000000 --- a/src/assets/images/dspace-logo-monochrome.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index b8248890fc..4e246f7243 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -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 diff --git a/src/index.html b/src/index.html index 072938b5ef..cb5d35e4f5 100644 --- a/src/index.html +++ b/src/index.html @@ -7,7 +7,7 @@ DSpace - + diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index 4b6c5c813e..29d138be1a 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -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('req'); @@ -55,6 +57,7 @@ export function getRequest(transferState: TransferState): any { // enableTracing: true, useHash: false, scrollPositionRestoration: 'enabled', + anchorScrolling: 'enabled', preloadingStrategy: NoPreloading }), StatisticsModule.forRoot(), @@ -104,6 +107,10 @@ export function getRequest(transferState: TransferState): any { provide: GoogleAnalyticsService, useClass: GoogleAnalyticsService, }, + { + provide: AuthRequestService, + useClass: BrowserAuthRequestService, + }, { provide: LocationToken, useFactory: locationProvider, diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 906d4c5f35..dad3a60d5c 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -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 diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 04347b3131..ae9e6082f5 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -1,4 +1,3 @@ -@import '../../node_modules/bootstrap/scss/functions.scss'; @import '../../node_modules/bootstrap/scss/mixins.scss'; @mixin word-wrap() { diff --git a/src/themes/dspace/app/+home-page/home-news/home-news.component.html b/src/themes/dspace/app/+home-page/home-news/home-news.component.html new file mode 100644 index 0000000000..94a5975771 --- /dev/null +++ b/src/themes/dspace/app/+home-page/home-news/home-news.component.html @@ -0,0 +1,37 @@ +
    +
    +
    +
    +
    +

    DSpace 7 - Beta 5

    +

    DSpace is the world leading open source repository platform that enables + organisations to:

    +
    +
    +
      +
    • easily ingest documents, audio, video, datasets and their corresponding Dublin Core + metadata +
    • +
    • open up this content to local and global audiences, thanks to the OAI-PMH interface and + Google Scholar optimizations +
    • +
    • issue permanent urls and trustworthy identifiers, including optional integrations with + handle.net and DataCite DOI +
    • +
    +

    Join an international community of leading institutions using DSpace.

    +

    Participate in the official community Testathon + from April 19th through May 7th. The test user accounts below have their password set to the name of + this + software in lowercase.

    +
      +
    • Demo Site Administrator = dspacedemo+admin@gmail.com
    • +
    • Demo Community Administrator = dspacedemo+commadmin@gmail.com
    • +
    • Demo Collection Administrator = dspacedemo+colladmin@gmail.com
    • +
    • Demo Submitter = dspacedemo+submit@gmail.com
    • +
    +
    +
    + Photo by @inspiredimages +
    diff --git a/src/themes/dspace/app/+home-page/home-news/home-news.component.scss b/src/themes/dspace/app/+home-page/home-news/home-news.component.scss new file mode 100644 index 0000000000..b5a070e51e --- /dev/null +++ b/src/themes/dspace/app/+home-page/home-news/home-news.component.scss @@ -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); + } + } +} + + diff --git a/src/themes/dspace/app/+home-page/home-news/home-news.component.ts b/src/themes/dspace/app/+home-page/home-news/home-news.component.ts new file mode 100644 index 0000000000..7f06295407 --- /dev/null +++ b/src/themes/dspace/app/+home-page/home-news/home-news.component.ts @@ -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 {} + diff --git a/src/themes/mantis/app/navbar/navbar.component.scss b/src/themes/dspace/app/navbar/navbar.component.scss similarity index 59% rename from src/themes/mantis/app/navbar/navbar.component.scss rename to src/themes/dspace/app/navbar/navbar.component.scss index 1417acff59..463a4269ee 100644 --- a/src/themes/mantis/app/navbar/navbar.component.scss +++ b/src/themes/dspace/app/navbar/navbar.component.scss @@ -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; } - - diff --git a/src/themes/dspace/app/navbar/navbar.component.ts b/src/themes/dspace/app/navbar/navbar.component.ts new file mode 100644 index 0000000000..e375011683 --- /dev/null +++ b/src/themes/dspace/app/navbar/navbar.component.ts @@ -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 { +} diff --git a/src/themes/dspace/assets/fonts/.gitkeep b/src/themes/dspace/assets/fonts/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/dspace/assets/images/.gitkeep b/src/themes/dspace/assets/images/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/assets/images/banner.jpg b/src/themes/dspace/assets/images/banner.jpg similarity index 100% rename from src/assets/images/banner.jpg rename to src/themes/dspace/assets/images/banner.jpg diff --git a/src/themes/dspace/entry-components.ts b/src/themes/dspace/entry-components.ts new file mode 100644 index 0000000000..2386ecb130 --- /dev/null +++ b/src/themes/dspace/entry-components.ts @@ -0,0 +1,2 @@ +export const ENTRY_COMPONENTS = [ +]; diff --git a/src/themes/dspace/styles/_global-styles.scss b/src/themes/dspace/styles/_global-styles.scss new file mode 100644 index 0000000000..1fb60b64a2 --- /dev/null +++ b/src/themes/dspace/styles/_global-styles.scss @@ -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 + } +} diff --git a/src/themes/dspace/styles/_theme_css_variable_overrides.scss b/src/themes/dspace/styles/_theme_css_variable_overrides.scss new file mode 100644 index 0000000000..2a61babdb7 --- /dev/null +++ b/src/themes/dspace/styles/_theme_css_variable_overrides.scss @@ -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%)}; +} + diff --git a/src/themes/dspace/styles/_theme_sass_variable_overrides.scss b/src/themes/dspace/styles/_theme_sass_variable_overrides.scss new file mode 100644 index 0000000000..70aa0b1850 --- /dev/null +++ b/src/themes/dspace/styles/_theme_sass_variable_overrides.scss @@ -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; + diff --git a/src/themes/dspace/styles/theme.scss b/src/themes/dspace/styles/theme.scss new file mode 100644 index 0000000000..e4cc9e45ed --- /dev/null +++ b/src/themes/dspace/styles/theme.scss @@ -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'; diff --git a/src/themes/dspace/theme.module.ts b/src/themes/dspace/theme.module.ts new file mode 100644 index 0000000000..ed840c2e25 --- /dev/null +++ b/src/themes/dspace/theme.module.ts @@ -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 { +} diff --git a/src/themes/mantis/app/+home-page/home-news/home-news.component.html b/src/themes/mantis/app/+home-page/home-news/home-news.component.html deleted file mode 100644 index 4da3ae12f7..0000000000 --- a/src/themes/mantis/app/+home-page/home-news/home-news.component.html +++ /dev/null @@ -1,21 +0,0 @@ -
    -
    -
    -

    DSpace 7

    -

    DSpace is the world leading open source repository platform that enables - organisations to:

    -
    -
    -
      -
    • easily ingest documents, audio, video, datasets and their corresponding Dublin Core - metadata -
    • -
    • open up this content to local and global audiences, thanks to the OAI-PMH interface and - Google Scholar optimizations -
    • -
    • issue permanent urls and trustworthy identifiers, including optional integrations with - handle.net and DataCite DOI -
    • -
    -

    Join an international community of leading institutions using DSpace.

    -
    diff --git a/src/themes/mantis/app/+home-page/home-news/home-news.component.scss b/src/themes/mantis/app/+home-page/home-news/home-news.component.scss deleted file mode 100644 index b82d84a71e..0000000000 --- a/src/themes/mantis/app/+home-page/home-news/home-news.component.scss +++ /dev/null @@ -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); - } - } -} diff --git a/src/themes/mantis/app/+home-page/home-page.component.html b/src/themes/mantis/app/+home-page/home-page.component.html deleted file mode 100644 index 43edbee9ca..0000000000 --- a/src/themes/mantis/app/+home-page/home-page.component.html +++ /dev/null @@ -1,10 +0,0 @@ -
    -
    - - -
    - Photo by @inspiredimages -
    -
    - -
    diff --git a/src/themes/mantis/app/+home-page/home-page.component.scss b/src/themes/mantis/app/+home-page/home-page.component.scss deleted file mode 100644 index d4350c1e13..0000000000 --- a/src/themes/mantis/app/+home-page/home-page.component.scss +++ /dev/null @@ -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; - } -} diff --git a/src/themes/mantis/app/+item-page/simple/item-page.component.html b/src/themes/mantis/app/+item-page/simple/item-page.component.html deleted file mode 100644 index 83f910e0cd..0000000000 --- a/src/themes/mantis/app/+item-page/simple/item-page.component.html +++ /dev/null @@ -1,9 +0,0 @@ -
    -
    -
    - -
    -
    - - -
    diff --git a/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html b/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html deleted file mode 100644 index 705d40a7c7..0000000000 --- a/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html +++ /dev/null @@ -1,87 +0,0 @@ -a
    -
    -
    - - -
    - -
    -
    - - - - - - - - - - - -
    -
    - - - - - - < - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - -
    -
    -
    diff --git a/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss b/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss deleted file mode 100644 index 6d12fcec71..0000000000 --- a/src/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.scss +++ /dev/null @@ -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); - } - } -} diff --git a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html deleted file mode 100644 index eb872ca175..0000000000 --- a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ /dev/null @@ -1,77 +0,0 @@ -
    -
    -
    - - -
    -

    - {{'journalissue.page.titleprefix' | translate}} - -

    -
    -
    - - - - - - - - -
    -
    - - - - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - -
    -
    -
    diff --git a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss deleted file mode 100644 index c0a314d715..0000000000 --- a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss +++ /dev/null @@ -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); - } - } -} diff --git a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html deleted file mode 100644 index bc11d4ba74..0000000000 --- a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html +++ /dev/null @@ -1,62 +0,0 @@ -
    -
    -
    - - -
    -

    - {{'journalvolume.page.titleprefix' | translate}} - -

    -
    -
    - - - - - -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - -
    -
    -
    diff --git a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss deleted file mode 100644 index 6e418258d3..0000000000 --- a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss +++ /dev/null @@ -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); - } - } -} diff --git a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html deleted file mode 100644 index cf4cef27de..0000000000 --- a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html +++ /dev/null @@ -1,70 +0,0 @@ -
    -
    -
    - -
    -

    - {{'journal.page.titleprefix' | translate}} - -

    -
    -
    - - - - - - - -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -

    {{"item.page.journal.search.title" | translate}}

    -
    - - -
    diff --git a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss b/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss deleted file mode 100644 index d2a7c8ca46..0000000000 --- a/src/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss +++ /dev/null @@ -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; - } - } -} diff --git a/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html deleted file mode 100644 index 114443468a..0000000000 --- a/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ /dev/null @@ -1,81 +0,0 @@ -
    -
    -
    - - -
    -

    - {{'orgunit.page.titleprefix' | translate}} - -

    -
    -
    - - - - - - -
    -
    - - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - - -
    -
    diff --git a/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss b/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss deleted file mode 100644 index aff2622323..0000000000 --- a/src/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss +++ /dev/null @@ -1,30 +0,0 @@ -@import 'src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.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); - } - } -} diff --git a/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html deleted file mode 100644 index 077ac1b9f1..0000000000 --- a/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html +++ /dev/null @@ -1,88 +0,0 @@ -
    -
    -
    - - -
    -

    - {{'person.page.titleprefix' | translate}} -

    -
    -
    - - - - - - - - - - - - -
    -
    - - - - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - -
    -
    -
    -
    -
    -

    {{"item.page.person.search.title" | translate}}

    -
    - - -
    diff --git a/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss b/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss deleted file mode 100644 index 51c15716f9..0000000000 --- a/src/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss +++ /dev/null @@ -1,38 +0,0 @@ -@import 'src/app/entity-groups/research-entities/item-pages/person/person.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; - } - } -} diff --git a/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html b/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html deleted file mode 100644 index e5b0884f14..0000000000 --- a/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html +++ /dev/null @@ -1,84 +0,0 @@ -
    -
    -
    - - -
    -

    - {{'project.page.titleprefix' | translate}} -

    -
    -
    - - - - - - - - - - - - -
    -
    - - - - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - -
    -
    -
    - diff --git a/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss b/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss deleted file mode 100644 index 076baad1a0..0000000000 --- a/src/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss +++ /dev/null @@ -1,30 +0,0 @@ -@import 'src/app/entity-groups/research-entities/item-pages/project/project.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); - } - } -} diff --git a/src/themes/mantis/app/navbar/navbar.component.html b/src/themes/mantis/app/navbar/navbar.component.html deleted file mode 100644 index d06eceb222..0000000000 --- a/src/themes/mantis/app/navbar/navbar.component.html +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/src/themes/mantis/app/shared/search-form/search-form.component.html b/src/themes/mantis/app/shared/search-form/search-form.component.html deleted file mode 100644 index ea2f54813e..0000000000 --- a/src/themes/mantis/app/shared/search-form/search-form.component.html +++ /dev/null @@ -1,21 +0,0 @@ -
    -
    - -
    -
    -
    - - - - -
    -
    - diff --git a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html deleted file mode 100644 index 86076dfd10..0000000000 --- a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {{filterValue.value}} - - {{filterValue.count}} - - \ No newline at end of file diff --git a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html deleted file mode 100644 index bdb37cb52d..0000000000 --- a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html +++ /dev/null @@ -1,8 +0,0 @@ - - {{filterValue.value}} - - {{filterValue.count}} - - \ No newline at end of file diff --git a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html deleted file mode 100644 index 850447a39e..0000000000 --- a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html +++ /dev/null @@ -1,7 +0,0 @@ -
    -
    {{'search.filters.filter.' + filter.name + '.head'| translate}}
    -
    - -
    -
    diff --git a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss deleted file mode 100644 index 0e78c64629..0000000000 --- a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'src/app/shared/search/search-filters/search-filter/search-filter.component.scss'; - -.facet-filter { - background-color: var(--bs-light); - border-radius: var(--bs-border-radius); - - h5 { - font-size: 1.1rem - } -} diff --git a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss b/src/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss deleted file mode 100644 index 158a0d3b4e..0000000000 --- a/src/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import 'src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss'; - -::ng-deep .noUi-connect { - background: var(--bs-info); -} diff --git a/src/themes/mantis/app/shared/search/search-filters/search-filters.component.html b/src/themes/mantis/app/shared/search/search-filters/search-filters.component.html deleted file mode 100644 index b7bb1bf50f..0000000000 --- a/src/themes/mantis/app/shared/search/search-filters/search-filters.component.html +++ /dev/null @@ -1,7 +0,0 @@ -

    {{"search.filters.head" | translate}}

    -
    -
    - -
    -
    -{{"search.filters.reset" | translate}} diff --git a/src/themes/mantis/app/shared/search/search-settings/search-settings.component.html b/src/themes/mantis/app/shared/search/search-settings/search-settings.component.html deleted file mode 100644 index 1321ced928..0000000000 --- a/src/themes/mantis/app/shared/search/search-settings/search-settings.component.html +++ /dev/null @@ -1,24 +0,0 @@ - -

    {{ 'search.sidebar.settings.title' | translate}}

    -
    -
    {{ 'search.sidebar.settings.sort-by' | translate}}
    - -
    - -
    -
    {{ 'search.sidebar.settings.rpp' | translate}}
    - -
    -
    \ No newline at end of file diff --git a/src/themes/mantis/app/shared/search/search-settings/search-settings.component.scss b/src/themes/mantis/app/shared/search/search-settings/search-settings.component.scss deleted file mode 100644 index b3ee0ba60e..0000000000 --- a/src/themes/mantis/app/shared/search/search-settings/search-settings.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'src/app/shared/search/search-settings/search-settings.component.scss'; - -.setting-option { - background-color: var(--bs-light); - border-radius: var(--bs-border-radius); - h5 { - font-size: 1.1rem - } -} - diff --git a/src/themes/mantis/readme.md b/src/themes/mantis/readme.md deleted file mode 100644 index 93bdf36f47..0000000000 --- a/src/themes/mantis/readme.md +++ /dev/null @@ -1,2 +0,0 @@ -#Note -For now the existing mantis theme has only been moved to the new themes folder, it has not yet been adapted to work as a dynamic theme. \ No newline at end of file diff --git a/src/themes/mantis/styles/_themed_bootstrap_variables.scss b/src/themes/mantis/styles/_themed_bootstrap_variables.scss deleted file mode 100644 index e606502e09..0000000000 --- a/src/themes/mantis/styles/_themed_bootstrap_variables.scss +++ /dev/null @@ -1,19 +0,0 @@ -@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,200i,300,300i,400,400i,600,600i,700,700i,900,900i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext,vietnamese'); - -$font-family-sans-serif: 'Source Sans Pro', -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; // #eee -$gray-800: #444444 !default; // #444 - -$navbar-dark-color: #FFFFFF; - -/* Reassign color vars to semantic color scheme */ -$blue: #43515f !default; -$green: #92C642 !default; -$cyan: #2e80a3 !default; -$yellow: #ec9433 !default; -$red: #CF4444 !default; -$dark: #43515f !default; - -$body-color: $gray-800 !default; - -$yiq-contrasted-threshold: 170 !default; diff --git a/src/themes/mantis/styles/_themed_custom_variables.scss b/src/themes/mantis/styles/_themed_custom_variables.scss deleted file mode 100644 index 60d248c4ae..0000000000 --- a/src/themes/mantis/styles/_themed_custom_variables.scss +++ /dev/null @@ -1,4 +0,0 @@ -:root { - --ds-banner-text-background: rgba(0, 0, 0, 0.35); - --ds-banner-background-gradient-width: 300px; -} diff --git a/webpack/helpers.ts b/webpack/helpers.ts index 532b170bf3..43855f6c72 100644 --- a/webpack/helpers.ts +++ b/webpack/helpers.ts @@ -6,8 +6,8 @@ export const projectRoot = (relativePath) => { export const globalCSSImports = () => { return [ - projectRoot('src/styles/_variables.scss'), - projectRoot('src/styles/_mixins.scss'), + projectRoot(path.join('src', 'styles', '_variables.scss')), + projectRoot(path.join('src', 'styles', '_mixins.scss')), ]; }; diff --git a/webpack/webpack.common.ts b/webpack/webpack.common.ts index 926241e0bb..07e55d89d4 100644 --- a/webpack/webpack.common.ts +++ b/webpack/webpack.common.ts @@ -17,7 +17,9 @@ export const copyWebpackOptions = { to: 'assets', }, { - from: path.join(__dirname, '..', 'src', 'themes', '*', 'assets', '**', '*'), + // replace(/\\/g, '/') because glob patterns need forward slashes, even on windows: + // https://github.com/mrmlnc/fast-glob#how-to-write-patterns-on-windows + from: path.join(__dirname, '..', 'src', 'themes', '*', 'assets', '**', '*').replace(/\\/g, '/'), to: 'assets', noErrorOnMissing: true, transformPath(targetPath, absolutePath) { @@ -77,7 +79,7 @@ export const commonExports = { test: /\.scss$/, exclude: [ /node_modules/, - /(_exposed)?_variables.scss$|\/src\/themes\/[^/]+\/styles\/.+.scss$/ + /(_exposed)?_variables.scss$|[\/|\\]src[\/|\\]themes[\/|\\].+?[\/|\\]styles[\/|\\].+\.scss$/ ], use: [ ...SCSS_LOADERS, @@ -90,7 +92,7 @@ export const commonExports = { ] }, { - test: /(_exposed)?_variables.scss$|\/src\/themes\/[^/]+\/styles\/.+.scss$/, + test: /(_exposed)?_variables.scss$|[\/|\\]src[\/|\\]themes[\/|\\].+?[\/|\\]styles[\/|\\].+\.scss$/, exclude: [/node_modules/], use: [ ...SCSS_LOADERS,