diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 00ec2fa8f7..64303ca8bb 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -31,6 +31,10 @@ jobs: # We turn off 'latest' tag by default. TAGS_FLAVOR: | latest=false + # Architectures / Platforms for which we will build Docker images + # If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work. + # If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64. + PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }} steps: # https://github.com/actions/checkout @@ -41,6 +45,10 @@ jobs: - name: Setup Docker Buildx uses: docker/setup-buildx-action@v1 + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU emulation to build for multiple architectures + uses: docker/setup-qemu-action@v2 + # https://github.com/docker/login-action - name: Login to DockerHub # Only login if not a PR, as PRs only trigger a Docker build and not a push @@ -70,6 +78,7 @@ jobs: with: context: . file: ./Dockerfile + platforms: ${{ env.PLATFORMS }} # For pull requests, we run the Docker build (to ensure no PR changes break the build), # but we ONLY do an image push to DockerHub if it's NOT a PR push: ${{ github.event_name != 'pull_request' }} diff --git a/.gitignore b/.gitignore index b27e636620..bdd0d4e589 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ package-lock.json .env /nbproject/ + +junit.xml diff --git a/README.md b/README.md index ca27ce9ebe..0e26d9e492 100644 --- a/README.md +++ b/README.md @@ -330,8 +330,11 @@ All E2E tests must be created under the `./cypress/integration/` folder, and mus * In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_. * From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page. * Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector + * It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test. * Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc. - * Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions. + * When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail. + * To work around this issue, define the attributes you use for Cypress selectors as `[attr.data-test]="'button' | ngBrowserOnly"`. This will only show the attribute in CSR HTML, forcing Cypress to wait until CSR is complete before interacting with the element. + * Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions. * Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly. * Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests. diff --git a/angular.json b/angular.json index 7c880b7c36..56e06bd86c 100644 --- a/angular.json +++ b/angular.json @@ -47,7 +47,6 @@ ], "styles": [ "src/styles/startup.scss", - "./node_modules/ngx-ui-switch/ui-switch.component.css", { "input": "src/styles/base-theme.scss", "inject": false, diff --git a/cypress/integration/my-dspace.spec.ts b/cypress/integration/my-dspace.spec.ts index eb931adda7..fa923dbcbc 100644 --- a/cypress/integration/my-dspace.spec.ts +++ b/cypress/integration/my-dspace.spec.ts @@ -65,7 +65,7 @@ describe('My DSpace page', () => { cy.visit('/mydspace'); // Open the New Submission dropdown - cy.get('#dropdownSubmission').click(); + cy.get('button[data-test="submission-dropdown"]').click(); // Click on the "Item" type in that dropdown cy.get('#entityControlsDropdownMenu button[title="none"]').click(); @@ -98,7 +98,7 @@ describe('My DSpace page', () => { const id = subpaths[2]; // Click the "Save for Later" button to save this submission - cy.get('button#saveForLater').click(); + cy.get('ds-submission-form-footer [data-test="save-for-later"]').click(); // "Save for Later" should send us to MyDSpace cy.url().should('include', '/mydspace'); @@ -122,7 +122,7 @@ describe('My DSpace page', () => { cy.url().should('include', '/workspaceitems/' + id + '/edit'); // Discard our new submission by clicking Discard in Submission form & confirming - cy.get('button#discard').click(); + cy.get('ds-submission-form-footer [data-test="discard"]').click(); cy.get('button#discard_submit').click(); // Discarding should send us back to MyDSpace @@ -135,7 +135,7 @@ describe('My DSpace page', () => { cy.visit('/mydspace'); // Open the New Import dropdown - cy.get('#dropdownImport').click(); + cy.get('button[data-test="import-dropdown"]').click(); // Click on the "Item" type in that dropdown cy.get('#importControlsDropdownMenu button[title="none"]').click(); diff --git a/cypress/integration/search-page.spec.ts b/cypress/integration/search-page.spec.ts index de279c7f2e..623c370c56 100644 --- a/cypress/integration/search-page.spec.ts +++ b/cypress/integration/search-page.spec.ts @@ -24,7 +24,7 @@ describe('Search Page', () => { // Click each filter toggle to open *every* filter // (As we want to scan filter section for accessibility issues as well) - cy.get('.filter-toggle').click({ multiple: true }); + cy.get('[data-test="filter-toggle"]').click({ multiple: true }); // Analyze for accessibility issues testA11y( diff --git a/cypress/support/index.ts b/cypress/support/index.ts index d9b6409a0d..024b46cdde 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -21,7 +21,7 @@ import './commands'; import 'cypress-axe'; // Runs once before the first test in each "block" -before(() => { +beforeEach(() => { // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie // This just ensures it doesn't get in the way of matching other objects in the page. cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}'); diff --git a/package.json b/package.json index dd63ca0a00..dbb4cca8a5 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,11 @@ "start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"", "start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr", "start:mirador:prod": "yarn run build:mirador && yarn run start:prod", - "serve": "ng serve -c development", + "preserve": "yarn base-href", + "serve": "ng serve --configuration development", "serve:ssr": "node dist/server/main", "analyze": "webpack-bundle-analyzer dist/browser/stats.json", - "build": "ng build -c development", + "build": "ng build --configuration development", "build:stats": "ng build --stats-json", "build:prod": "yarn run build:ssr", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", @@ -37,6 +38,7 @@ "cypress:open": "cypress open", "cypress:run": "cypress run", "env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts", + "base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts", "check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./" }, "browser": { @@ -78,6 +80,7 @@ "@nicky-lenaers/ngx-scroll-to": "^9.0.0", "angular-idle-preload": "3.0.0", "angulartics2": "^12.0.0", + "axios": "^0.27.2", "bootstrap": "4.3.1", "caniuse-lite": "^1.0.30001165", "cerialize": "0.1.18", @@ -104,7 +107,7 @@ "mirador": "^3.3.0", "mirador-dl-plugin": "^0.13.0", "mirador-share-plugin": "^0.11.0", - "moment": "^2.29.1", + "moment": "^2.29.2", "morgan": "^1.10.0", "ng-mocks": "^13.1.1", "ng2-file-upload": "1.4.0", @@ -155,7 +158,7 @@ "@typescript-eslint/eslint-plugin": "5.11.0", "@typescript-eslint/parser": "5.11.0", "axe-core": "^4.3.3", - "compression-webpack-plugin": "^3.0.1", + "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", "cross-env": "^7.0.3", "css-loader": "^6.2.0", @@ -171,6 +174,7 @@ "eslint-plugin-import": "^2.25.4", "eslint-plugin-jsdoc": "^38.0.6", "eslint-plugin-unused-imports": "^2.0.0", + "express-static-gzip": "^2.1.5", "fork-ts-checker-webpack-plugin": "^6.0.3", "html-loader": "^1.3.2", "jasmine-core": "^3.8.0", @@ -210,4 +214,4 @@ "webpack-cli": "^4.2.0", "webpack-dev-server": "^4.5.0" } -} \ No newline at end of file +} diff --git a/scripts/base-href.ts b/scripts/base-href.ts new file mode 100644 index 0000000000..aee547b46d --- /dev/null +++ b/scripts/base-href.ts @@ -0,0 +1,36 @@ +import * as fs from 'fs'; +import { join } from 'path'; + +import { AppConfig } from '../src/config/app-config.interface'; +import { buildAppConfig } from '../src/config/config.server'; + +/** + * Script to set baseHref as `ui.nameSpace` for development mode. Adds `baseHref` to angular.json build options. + * + * Usage (see package.json): + * + * yarn base-href + */ + +const appConfig: AppConfig = buildAppConfig(); + +const angularJsonPath = join(process.cwd(), 'angular.json'); + +if (!fs.existsSync(angularJsonPath)) { + console.error(`Error:\n${angularJsonPath} does not exist\n`); + process.exit(1); +} + +try { + const angularJson = require(angularJsonPath); + + const baseHref = `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`; + + console.log(`Setting baseHref to ${baseHref} in angular.json`); + + angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref; + + fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n'); +} catch (e) { + console.error(e); +} diff --git a/scripts/sync-i18n-files.ts b/scripts/sync-i18n-files.ts index ad8a712f21..96ba0d4010 100755 --- a/scripts/sync-i18n-files.ts +++ b/scripts/sync-i18n-files.ts @@ -1,4 +1,5 @@ -import { projectRoot} from '../webpack/helpers'; +import { projectRoot } from '../webpack/helpers'; + const commander = require('commander'); const fs = require('fs'); const JSON5 = require('json5'); @@ -119,7 +120,7 @@ function syncFileWithSource(pathToTargetFile, pathToOutputFile) { outputChunks.forEach(function (chunk) { progressBar.increment(); chunk.split("\n").forEach(function (line) { - file.write(" " + line + "\n"); + file.write((line === '' ? '' : ` ${line}`) + "\n"); }); }); file.write("\n}"); @@ -192,7 +193,10 @@ function createNewChunkComparingSourceAndTarget(correspondingTargetChunk, source const targetList = correspondingTargetChunk.split("\n"); const oldKeyValueInTargetComments = getSubStringWithRegex(correspondingTargetChunk, "\\s*\\/\\/\\s*\".*"); - const keyValueTarget = targetList[targetList.length - 1]; + let keyValueTarget = targetList[targetList.length - 1]; + if (!keyValueTarget.endsWith(",")) { + keyValueTarget = keyValueTarget + ","; + } if (oldKeyValueInTargetComments != null) { const oldKeyValueUncommented = getSubStringWithRegex(oldKeyValueInTargetComments[0], "\".*")[0]; diff --git a/server.ts b/server.ts index bc840fb73c..9fe03fe5b5 100644 --- a/server.ts +++ b/server.ts @@ -19,12 +19,14 @@ import 'zone.js/node'; import 'reflect-metadata'; import 'rxjs'; +import axios from 'axios'; import * as pem from 'pem'; import * as https from 'https'; import * as morgan from 'morgan'; import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as compression from 'compression'; +import * as expressStaticGzip from 'express-static-gzip'; import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; @@ -37,14 +39,14 @@ import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import { hasValue, hasNoValue } from './src/app/shared/empty.util'; +import { hasNoValue, hasValue } from './src/app/shared/empty.util'; import { UIServerConfig } from './src/config/ui-server-config.interface'; import { ServerAppModule } from './src/main.server'; import { buildAppConfig } from './src/config/config.server'; -import { AppConfig, APP_CONFIG } from './src/config/app-config.interface'; +import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './src/config/config.util'; /* @@ -66,6 +68,8 @@ extendEnvironmentWithAppConfig(environment, appConfig); // The Express app is exported so that it can be used by serverless Functions. export function app() { + const router = express.Router(); + /* * Create a new express application */ @@ -74,11 +78,15 @@ export function app() { /* * If production mode is enabled in the environment file: * - Enable Angular's production mode - * - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression) + * - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression) */ if (environment.production) { enableProdMode(); - server.use(compression()); + server.use(compression({ + // only compress responses we've marked as SSR + // otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin + filter: (_, res) => res.locals.ssr, + })); } /* @@ -133,7 +141,11 @@ export function app() { /** * Proxy the sitemaps */ - server.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true })); + router.use('/sitemap**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}/sitemaps`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); /** * Checks if the rateLimiter property is present @@ -150,15 +162,28 @@ export function app() { /* * Serve static resources (images, i18n messages, …) + * Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip) */ - server.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false })); + router.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, { + index: false, + enableBrotli: true, + orderPreference: ['br', 'gzip'], + })); + /* * Fallthrough to the IIIF viewer (must be included in the build). */ - server.use('/iiif', express.static(IIIF_VIEWER, {index:false})); + router.use('/iiif', express.static(IIIF_VIEWER, { index: false })); + + /** + * Checking server status + */ + server.get('/app/health', healthCheck); // Register the ngApp callback function to handle incoming requests - server.get('*', ngApp); + router.get('*', ngApp); + + server.use(environment.ui.nameSpace, router); return server; } @@ -180,6 +205,7 @@ function ngApp(req, res) { providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }, (err, data) => { if (hasNoValue(err) && hasValue(data)) { + res.locals.ssr = true; // mark response as SSR 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 @@ -191,13 +217,25 @@ function ngApp(req, res) { if (hasValue(err)) { console.warn('Error details : ', err); } - res.sendFile(DIST_FOLDER + '/index.html'); + res.render(indexHtml, { + req, + providers: [{ + provide: APP_BASE_HREF, + useValue: req.baseUrl + }] + }); } }); } else { // If preboot is disabled, just serve the client console.log('Universal off, serving for direct CSR'); - res.sendFile(DIST_FOLDER + '/index.html'); + res.render(indexHtml, { + req, + providers: [{ + provide: APP_BASE_HREF, + useValue: req.baseUrl + }] + }); } } @@ -287,6 +325,21 @@ function start() { } } +/* + * The callback function to serve health check requests + */ +function healthCheck(req, res) { + const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`; + axios.get(baseUrl) + .then((response) => { + res.status(response.status).send(response.data); + }) + .catch((error) => { + res.status(error.response.status).send({ + error: error.message + }); + }); +} // Webpack will replace 'require' with '__webpack_require__' // '__non_webpack_require__' is a proxy to Node 'require' // The below code is to ensure that the server is run only when not requiring the bundle. diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index 2307f3c6fa..09487a7eaa 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule, FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms'; -import { BrowserModule } from '@angular/platform-browser'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { BrowserModule, By } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; @@ -35,6 +35,7 @@ import { RouterMock } from '../../../shared/mocks/router.mock'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { Operation } from 'fast-json-patch'; import { ValidateGroupExists } from './validators/group-exists.validator'; +import { NoContent } from '../../../core/shared/NoContent.model'; describe('GroupFormComponent', () => { let component: GroupFormComponent; @@ -87,6 +88,9 @@ describe('GroupFormComponent', () => { patch(group: Group, operations: Operation[]) { return null; }, + delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return createSuccessfulRemoteDataObject$({}); + }, cancelEditGroup(): void { this.activeGroup = null; }, @@ -348,4 +352,46 @@ describe('GroupFormComponent', () => { }); }); + describe('delete', () => { + let deleteButton; + + beforeEach(() => { + component.initialisePage(); + + component.canEdit$ = observableOf(true); + component.groupBeingEdited = { + permanent: false + } as Group; + + fixture.detectChanges(); + deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement; + + spyOn(groupsDataServiceStub, 'delete').and.callThrough(); + spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' })); + }); + + describe('if confirmed via modal', () => { + beforeEach(waitForAsync(() => { + deleteButton.click(); + fixture.detectChanges(); + (document as any).querySelector('.modal-footer .confirm').click(); + })); + + it('should call GroupDataService.delete', () => { + expect(groupsDataServiceStub.delete).toHaveBeenCalledWith('active-group'); + }); + }); + + describe('if canceled via modal', () => { + beforeEach(waitForAsync(() => { + deleteButton.click(); + fixture.detectChanges(); + (document as any).querySelector('.modal-footer .cancel').click(); + })); + + it('should not call GroupDataService.delete', () => { + expect(groupsDataServiceStub.delete).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 826b7dbe69..b0178f1294 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -426,7 +426,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { .subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name })); - this.reset(); + this.onCancel(); } else { this.notificationsService.error( this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }), @@ -439,16 +439,6 @@ export class GroupFormComponent implements OnInit, OnDestroy { }); } - /** - * This method will ensure that the page gets reset and that the cache is cleared - */ - reset() { - this.groupDataService.getBrowseEndpoint().pipe(take(1)).subscribe((href: string) => { - this.requestService.removeByHrefSubstring(href); - }); - this.onCancel(); - } - /** * Cancel the current edit when component is destroyed & unsub all subscriptions */ 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 e791b7f2a0..237dedde09 100644 --- a/src/app/access-control/group-registry/groups-registry.component.html +++ b/src/app/access-control/group-registry/groups-registry.component.html @@ -79,7 +79,7 @@ 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 0b30a551fd..99586ee5f0 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 @@ -31,6 +31,7 @@ import { RouterMock } from '../../shared/mocks/router.mock'; import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { NoContent } from '../../core/shared/NoContent.model'; describe('GroupRegistryComponent', () => { let component: GroupsRegistryComponent; @@ -145,7 +146,10 @@ describe('GroupRegistryComponent', () => { totalPages: 1, currentPage: 1 }), [result])); - } + }, + delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return createSuccessfulRemoteDataObject$({}); + }, }; dsoDataServiceStub = { findByHref(href: string): Observable> { @@ -301,4 +305,29 @@ describe('GroupRegistryComponent', () => { }); }); }); + + describe('delete', () => { + let deleteButton; + + beforeEach(fakeAsync(() => { + spyOn(groupsDataServiceStub, 'delete').and.callThrough(); + + setIsAuthorized(true, true); + + // force rerender after setup changes + component.search({ query: '' }); + tick(); + fixture.detectChanges(); + + // only mockGroup[0] is deletable, so we should only get one button + deleteButton = fixture.debugElement.query(By.css('.btn-delete')).nativeElement; + })); + + it('should call GroupDataService.delete', () => { + deleteButton.click(); + fixture.detectChanges(); + + expect(groupsDataServiceStub.delete).toHaveBeenCalledWith(mockGroups[0].id); + }); + }); }); 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 da861518da..1770762a34 100644 --- a/src/app/access-control/group-registry/groups-registry.component.ts +++ b/src/app/access-control/group-registry/groups-registry.component.ts @@ -9,7 +9,7 @@ import { of as observableOf, Subscription } from 'rxjs'; -import { catchError, map, switchMap, take, tap } from 'rxjs/operators'; +import { catchError, map, switchMap, tap } 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'; @@ -199,7 +199,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { if (rd.hasSucceeded) { this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id]; this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name })); - this.reset(); } else { this.notificationsService.error( this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }), @@ -209,17 +208,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { } } - /** - * This method will set everything to stale, which will cause the lists on this page to update. - */ - reset() { - this.groupService.getBrowseEndpoint().pipe( - take(1) - ).subscribe((href: string) => { - this.requestService.setStaleByHrefSubstring(href); - }); - } - /** * Get the members (epersons embedded value of a group) * @param group 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 8574c4678b..857034604e 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 @@ -128,7 +128,6 @@ export class MetadataRegistryComponent { * Delete all the selected metadata schemas */ deleteSchemas() { - this.registryService.clearMetadataSchemaRequests().subscribe(); this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe( (schemas) => { const tasks$ = []; @@ -148,7 +147,6 @@ export class MetadataRegistryComponent { } this.registryService.deselectAllMetadataSchema(); this.registryService.cancelEditMetadataSchema(); - this.forceUpdateSchemas(); }); } ); 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 8a2086d5e2..d0827e6e4d 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 @@ -174,15 +174,12 @@ export class MetadataSchemaComponent implements OnInit { const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); if (successResponses.length > 0) { this.showNotification(true, successResponses.length); - this.registryService.clearMetadataFieldRequests(); - } if (failedResponses.length > 0) { this.showNotification(false, failedResponses.length); } this.registryService.deselectAllMetadataField(); this.registryService.cancelEditMetadataField(); - this.forceUpdateFields(); }); } ); diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts index 2a5a544a40..3aca092b52 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts @@ -182,176 +182,4 @@ describe('AdminSidebarComponent', () => { expect(menuService.collapseMenuPreview).toHaveBeenCalled(); })); }); - - describe('menu', () => { - beforeEach(() => { - spyOn(menuService, 'addSection'); - }); - - describe('for regular user', () => { - beforeEach(() => { - authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => { - return observableOf(false); - }); - }); - - beforeEach(() => { - comp.createMenu(); - }); - - it('should not show site admin section', () => { - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'admin_search', visible: false, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'registries', visible: false, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - parentID: 'registries', visible: false, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'curation_tasks', visible: false, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'workflow', visible: false, - })); - }); - - it('should not show edit_community', () => { - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'edit_community', visible: false, - })); - - }); - - it('should not show edit_collection', () => { - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'edit_collection', visible: false, - })); - }); - - it('should not show access control section', () => { - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'access_control', visible: false, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - parentID: 'access_control', visible: false, - })); - }); - - // We check that the menu section has not been called with visible set to true - // The reason why we don't check if it has been called with visible set to false - // Is because the function does not get called unless a user is authorised - it('should not show the import section', () => { - expect(menuService.addSection).not.toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'import', visible: true, - })); - }); - - // We check that the menu section has not been called with visible set to true - // The reason why we don't check if it has been called with visible set to false - // Is because the function does not get called unless a user is authorised - it('should not show the export section', () => { - expect(menuService.addSection).not.toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'export', visible: true, - })); - }); - }); - - describe('for site admin', () => { - beforeEach(() => { - authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => { - return observableOf(featureID === FeatureID.AdministratorOf); - }); - }); - - beforeEach(() => { - comp.createMenu(); - }); - - it('should contain site admin section', () => { - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'admin_search', visible: true, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'registries', visible: true, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - parentID: 'registries', visible: true, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'curation_tasks', visible: true, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'workflow', visible: true, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'workflow', visible: true, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'import', visible: true, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'export', visible: true, - })); - }); - }); - - describe('for community admin', () => { - beforeEach(() => { - authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => { - return observableOf(featureID === FeatureID.IsCommunityAdmin); - }); - }); - - beforeEach(() => { - comp.createMenu(); - }); - - it('should show edit_community', () => { - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'edit_community', visible: true, - })); - }); - }); - - describe('for collection admin', () => { - beforeEach(() => { - authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => { - return observableOf(featureID === FeatureID.IsCollectionAdmin); - }); - }); - - beforeEach(() => { - comp.createMenu(); - }); - - it('should show edit_collection', () => { - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'edit_collection', visible: true, - })); - }); - }); - - describe('for group admin', () => { - beforeEach(() => { - authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => { - return observableOf(featureID === FeatureID.CanManageGroups); - }); - }); - - beforeEach(() => { - comp.createMenu(); - }); - - it('should show access control section', () => { - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'access_control', visible: true, - })); - expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - parentID: 'access_control', visible: true, - })); - }); - }); - }); }); diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.ts index fef6904177..b244039a25 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.ts @@ -1,45 +1,13 @@ import { Component, HostListener, Injector, OnInit } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { BehaviorSubject, combineLatest as observableCombineLatest, combineLatest, Observable } from 'rxjs'; -import { debounceTime, distinctUntilChanged, filter, first, map, take, withLatestFrom } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators'; import { AuthService } from '../../core/auth/auth.service'; -import { - METADATA_EXPORT_SCRIPT_NAME, - METADATA_IMPORT_SCRIPT_NAME, - ScriptDataService -} from '../../core/data/processes/script-data.service'; import { slideHorizontal, slideSidebar } from '../../shared/animations/slide'; -import { - CreateCollectionParentSelectorComponent -} from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; -import { - CreateCommunityParentSelectorComponent -} from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; -import { - CreateItemParentSelectorComponent -} from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; -import { - EditCollectionSelectorComponent -} from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; -import { - EditCommunitySelectorComponent -} from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; -import { - EditItemSelectorComponent -} from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; -import { - ExportMetadataSelectorComponent -} from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; -import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model'; -import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model'; -import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model'; import { MenuComponent } from '../../shared/menu/menu.component'; import { MenuService } from '../../shared/menu/menu.service'; import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { MenuID } from '../../shared/menu/menu-id.model'; -import { MenuItemType } from '../../shared/menu/menu-item-type.model'; import { ActivatedRoute } from '@angular/router'; /** @@ -85,11 +53,9 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { constructor( protected menuService: MenuService, protected injector: Injector, - protected variableService: CSSVariableService, - protected authService: AuthService, - protected modalService: NgbModal, + private variableService: CSSVariableService, + private authService: AuthService, public authorizationService: AuthorizationDataService, - protected scriptDataService: ScriptDataService, public route: ActivatedRoute ) { super(menuService, injector, authorizationService, route); @@ -105,7 +71,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { this.authService.isAuthenticated() .subscribe((loggedIn: boolean) => { if (loggedIn) { - this.createMenu(); this.menuService.showMenu(this.menuID); } }); @@ -135,503 +100,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { }); } - /** - * Initialize all menu sections and items for this menu - */ - createMenu() { - this.createMainMenuSections(); - this.createSiteAdministratorMenuSections(); - this.createExportMenuSections(); - this.createImportMenuSections(); - this.createAccessControlMenuSections(); - } - - /** - * Initialize the main menu sections. - * edit_community / edit_collection is only included if the current user is a Community or Collection admin - */ - createMainMenuSections() { - combineLatest([ - this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin), - this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin), - this.authorizationService.isAuthorized(FeatureID.AdministratorOf) - ]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => { - const menuList = [ - /* News */ - { - id: 'new', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.new' - } as TextMenuItemModel, - icon: 'plus', - index: 0 - }, - { - id: 'new_community', - parentID: 'new', - active: false, - visible: isCommunityAdmin, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.new_community', - function: () => { - this.modalService.open(CreateCommunityParentSelectorComponent); - } - } as OnClickMenuItemModel, - }, - { - id: 'new_collection', - parentID: 'new', - active: false, - visible: isCommunityAdmin, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.new_collection', - function: () => { - this.modalService.open(CreateCollectionParentSelectorComponent); - } - } as OnClickMenuItemModel, - }, - { - id: 'new_item', - parentID: 'new', - active: false, - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.new_item', - function: () => { - this.modalService.open(CreateItemParentSelectorComponent); - } - } as OnClickMenuItemModel, - }, - { - id: 'new_process', - parentID: 'new', - active: false, - visible: isCollectionAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.new_process', - link: '/processes/new' - } as LinkMenuItemModel, - }, - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'new_item_version', - // parentID: 'new', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.new_item_version', - // link: '' - // } as LinkMenuItemModel, - // }, - - /* Edit */ - { - id: 'edit', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.edit' - } as TextMenuItemModel, - icon: 'pencil-alt', - index: 1 - }, - { - id: 'edit_community', - parentID: 'edit', - active: false, - visible: isCommunityAdmin, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_community', - function: () => { - this.modalService.open(EditCommunitySelectorComponent); - } - } as OnClickMenuItemModel, - }, - { - id: 'edit_collection', - parentID: 'edit', - active: false, - visible: isCollectionAdmin, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_collection', - function: () => { - this.modalService.open(EditCollectionSelectorComponent); - } - } as OnClickMenuItemModel, - }, - { - id: 'edit_item', - parentID: 'edit', - active: false, - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_item', - function: () => { - this.modalService.open(EditItemSelectorComponent); - } - } as OnClickMenuItemModel, - }, - - /* Statistics */ - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'statistics_task', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.statistics_task', - // link: '' - // } as LinkMenuItemModel, - // icon: 'chart-bar', - // index: 8 - // }, - - /* Control Panel */ - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'control_panel', - // active: false, - // visible: isSiteAdmin, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.control_panel', - // link: '' - // } as LinkMenuItemModel, - // icon: 'cogs', - // index: 9 - // }, - - /* Processes */ - { - id: 'processes', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.processes', - link: '/processes' - } as LinkMenuItemModel, - icon: 'terminal', - index: 10 - }, - ]; - menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { - shouldPersistOnRouteChange: true - }))); - }); - } - - /** - * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not - * the export scripts exist and the current user is allowed to execute them - */ - createExportMenuSections() { - const menuList = [ - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'export_community', - // parentID: 'export', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.export_community', - // link: '' - // } as LinkMenuItemModel, - // shouldPersistOnRouteChange: true - // }, - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'export_collection', - // parentID: 'export', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.export_collection', - // link: '' - // } as LinkMenuItemModel, - // shouldPersistOnRouteChange: true - // }, - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'export_item', - // parentID: 'export', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.export_item', - // link: '' - // } as LinkMenuItemModel, - // shouldPersistOnRouteChange: true - // }, - ]; - menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection)); - - observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME) - ]).pipe( - filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists), - take(1) - ).subscribe(() => { - // Hides the export menu for unauthorised people - // If in the future more sub-menus are added, - // it should be reviewed if they need to be in this subscribe - this.menuService.addSection(this.menuID, { - id: 'export', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.export' - } as TextMenuItemModel, - icon: 'file-export', - index: 3, - shouldPersistOnRouteChange: true - }); - this.menuService.addSection(this.menuID, { - id: 'export_metadata', - parentID: 'export', - active: true, - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.export_metadata', - function: () => { - this.modalService.open(ExportMetadataSelectorComponent); - } - } as OnClickMenuItemModel, - shouldPersistOnRouteChange: true - }); - }); - } - - /** - * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not - * the import scripts exist and the current user is allowed to execute them - */ - createImportMenuSections() { - const menuList = [ - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'import_batch', - // parentID: 'import', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.import_batch', - // link: '' - // } as LinkMenuItemModel, - // } - ]; - menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { - shouldPersistOnRouteChange: true - }))); - - observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME) - ]).pipe( - filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists), - take(1) - ).subscribe(() => { - // Hides the import menu for unauthorised people - // If in the future more sub-menus are added, - // it should be reviewed if they need to be in this subscribe - this.menuService.addSection(this.menuID, { - id: 'import', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.import' - } as TextMenuItemModel, - icon: 'file-import', - index: 2 - }); - this.menuService.addSection(this.menuID, { - id: 'import_metadata', - parentID: 'import', - active: true, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.import_metadata', - link: '/admin/metadata-import' - } as LinkMenuItemModel, - shouldPersistOnRouteChange: true - }); - }); - } - - /** - * Create menu sections dependent on whether or not the current user is a site administrator - */ - createSiteAdministratorMenuSections() { - this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => { - const menuList = [ - /* Admin Search */ - { - id: 'admin_search', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.admin_search', - link: '/admin/search' - } as LinkMenuItemModel, - icon: 'search', - index: 5 - }, - /* Registries */ - { - id: 'registries', - active: false, - visible: authorized, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.registries' - } as TextMenuItemModel, - icon: 'list', - index: 6 - }, - { - id: 'registries_metadata', - parentID: 'registries', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.registries_metadata', - link: 'admin/registries/metadata' - } as LinkMenuItemModel, - }, - { - id: 'registries_format', - parentID: 'registries', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.registries_format', - link: 'admin/registries/bitstream-formats' - } as LinkMenuItemModel, - }, - - /* Curation tasks */ - { - id: 'curation_tasks', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.curation_task', - link: 'admin/curation-tasks' - } as LinkMenuItemModel, - icon: 'filter', - index: 7 - }, - - /* Workflow */ - { - id: 'workflow', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.workflow', - link: '/admin/workflow' - } as LinkMenuItemModel, - icon: 'user-check', - index: 11 - }, - ]; - - menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { - shouldPersistOnRouteChange: true - }))); - }); - } - - /** - * Create menu sections dependent on whether or not the current user can manage access control groups - */ - createAccessControlMenuSections() { - observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - this.authorizationService.isAuthorized(FeatureID.CanManageGroups) - ]).subscribe(([isSiteAdmin, canManageGroups]) => { - const menuList = [ - /* Access Control */ - { - id: 'access_control_people', - parentID: 'access_control', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.access_control_people', - link: '/access-control/epeople' - } as LinkMenuItemModel, - }, - { - id: 'access_control_groups', - parentID: 'access_control', - active: false, - visible: canManageGroups, - model: { - type: MenuItemType.LINK, - text: 'menu.section.access_control_groups', - link: '/access-control/groups' - } as LinkMenuItemModel, - }, - // TODO: enable this menu item once the feature has been implemented - // { - // id: 'access_control_authorizations', - // parentID: 'access_control', - // active: false, - // visible: authorized, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.access_control_authorizations', - // link: '' - // } as LinkMenuItemModel, - // }, - { - id: 'access_control', - active: false, - visible: canManageGroups || isSiteAdmin, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.access_control' - } as TextMenuItemModel, - icon: 'key', - index: 4 - }, - ]; - - menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { - shouldPersistOnRouteChange: true, - }))); - }); - } - @HostListener('focusin') public handleFocusIn() { this.inFocus$.next(true); diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 6524edef77..e9a6376884 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -107,6 +107,8 @@ export function getPageInternalServerErrorRoute() { return `/${INTERNAL_SERVER_ERROR}`; } +export const ERROR_PAGE = 'error'; + export const INFO_MODULE_PATH = 'info'; export function getInfoModulePath() { return `/${INFO_MODULE_PATH}`; @@ -122,3 +124,5 @@ export const REQUEST_COPY_MODULE_PATH = 'request-a-copy'; export function getRequestCopyModulePath() { return `/${REQUEST_COPY_MODULE_PATH}`; } + +export const HEALTH_PAGE_PATH = 'health'; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 88f7791b1b..d426b041ce 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,15 +1,19 @@ import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterModule, NoPreloading } from '@angular/router'; import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; import { AuthenticatedGuard } from './core/auth/authenticated.guard'; -import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; +import { + SiteAdministratorGuard +} from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { ACCESS_CONTROL_MODULE_PATH, ADMIN_MODULE_PATH, BITSTREAM_MODULE_PATH, + ERROR_PAGE, FORBIDDEN_PATH, FORGOT_PASSWORD_PATH, + HEALTH_PAGE_PATH, INFO_MODULE_PATH, INTERNAL_SERVER_ERROR, LEGACY_BITSTREAM_MODULE_PATH, @@ -27,18 +31,26 @@ import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end- import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard'; import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; -import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; -import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; +import { + GroupAdministratorGuard +} from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; +import { + ThemedPageInternalServerErrorComponent +} from './page-internal-server-error/themed-page-internal-server-error.component'; import { ServerCheckGuard } from './core/server-check/server-check.guard'; +import { MenuResolver } from './menu.resolver'; +import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; @NgModule({ imports: [ RouterModule.forRoot([ { path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent }, + { path: ERROR_PAGE , component: ThemedPageErrorComponent }, { path: '', canActivate: [AuthBlockingGuard], canActivateChild: [ServerCheckGuard], + resolve: [MenuResolver], children: [ { path: '', redirectTo: '/home', pathMatch: 'full' }, { @@ -208,6 +220,11 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard'; loadChildren: () => import('./statistics-page/statistics-page-routing.module') .then((m) => m.StatisticsPageRoutingModule) }, + { + path: HEALTH_PAGE_PATH, + loadChildren: () => import('./health-page/health-page.module') + .then((m) => m.HealthPageModule) + }, { path: ACCESS_CONTROL_MODULE_PATH, loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule), @@ -217,6 +234,12 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard'; ] } ], { + // enableTracing: true, + useHash: false, + scrollPositionRestoration: 'enabled', + anchorScrolling: 'enabled', + initialNavigation: 'enabledBlocking', + preloadingStrategy: NoPreloading, onSameUrlNavigation: 'reload', }) ], diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 9f215da46d..f2243d435e 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -187,7 +187,7 @@ describe('App component', () => { link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); link.setAttribute('class', 'theme-css'); - link.setAttribute('href', '/custom-theme.css'); + link.setAttribute('href', 'custom-theme.css'); expect(headSpy.appendChild).toHaveBeenCalledWith(link); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 35686ee3ad..2c743e219d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -12,6 +12,7 @@ import { } from '@angular/core'; import { ActivatedRouteSnapshot, + ActivationEnd, NavigationCancel, NavigationEnd, NavigationStart, ResolveEnd, @@ -72,7 +73,7 @@ export class AppComponent implements OnInit, AfterViewInit { /** * Whether or not the app is in the process of rerouting */ - isRouteLoading$: BehaviorSubject = new BehaviorSubject(true); + isRouteLoading$: BehaviorSubject = new BehaviorSubject(false); /** * Whether or not the theme is in the process of being swapped @@ -121,7 +122,7 @@ export class AppComponent implements OnInit, AfterViewInit { this.themeService.getThemeName$().subscribe((themeName: string) => { if (isPlatformBrowser(this.platformId)) { // the theme css will never download server side, so this should only happen on the browser - this.isThemeCSSLoading$.next(true); + this.distinctNext(this.isThemeCSSLoading$, true); } if (hasValue(themeName)) { this.loadGlobalThemeConfig(themeName); @@ -196,37 +197,57 @@ export class AppComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - let resolveEndFound = false; + let updatingTheme = false; + let snapshot: ActivatedRouteSnapshot; + this.router.events.subscribe((event) => { if (event instanceof NavigationStart) { - resolveEndFound = false; - this.isRouteLoading$.next(true); - } else if (event instanceof ResolveEnd) { - resolveEndFound = true; - const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root; - this.themeService.updateThemeOnRouteChange$(event.urlAfterRedirects, activatedRouteSnapShot).pipe( - switchMap((changed) => { - if (changed) { - return this.isThemeCSSLoading$; - } else { - return [false]; - } - }) - ).subscribe((changed) => { - this.isThemeLoading$.next(changed); - }); - } else if ( - event instanceof NavigationEnd || - event instanceof NavigationCancel - ) { - if (!resolveEndFound) { - this.isThemeLoading$.next(false); + updatingTheme = false; + this.distinctNext(this.isRouteLoading$, true); + } else if (event instanceof ResolveEnd) { + // this is the earliest point where we have all the information we need + // to update the theme, but this event is not emitted on first load + this.updateTheme(event.urlAfterRedirects, event.state.root); + updatingTheme = true; + } else if (!updatingTheme && event instanceof ActivationEnd) { + // if there was no ResolveEnd, keep track of the snapshot... + snapshot = event.snapshot; + } else if (event instanceof NavigationEnd) { + if (!updatingTheme) { + // ...and use it to update the theme on NavigationEnd instead + this.updateTheme(event.urlAfterRedirects, snapshot); + updatingTheme = true; } - this.isRouteLoading$.next(false); + this.distinctNext(this.isRouteLoading$, false); + } else if (event instanceof NavigationCancel) { + if (!updatingTheme) { + this.distinctNext(this.isThemeLoading$, false); + } + this.distinctNext(this.isRouteLoading$, false); } }); } + /** + * Update the theme according to the current route, if applicable. + * @param urlAfterRedirects the current URL after redirects + * @param snapshot the current route snapshot + * @private + */ + private updateTheme(urlAfterRedirects: string, snapshot: ActivatedRouteSnapshot): void { + this.themeService.updateThemeOnRouteChange$(urlAfterRedirects, snapshot).pipe( + switchMap((changed) => { + if (changed) { + return this.isThemeCSSLoading$; + } else { + return [false]; + } + }) + ).subscribe((changed) => { + this.distinctNext(this.isThemeLoading$, changed); + }); + } + @HostListener('window:resize', ['$event']) public onResize(event): void { this.dispatchWindowSize(event.target.innerWidth, event.target.innerHeight); @@ -268,7 +289,7 @@ export class AppComponent implements OnInit, AfterViewInit { link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); link.setAttribute('class', 'theme-css'); - link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`); + link.setAttribute('href', `${encodeURIComponent(themeName)}-theme.css`); // wait for the new css to download before removing the old one to prevent a // flash of unstyled content link.onload = () => { @@ -280,7 +301,7 @@ export class AppComponent implements OnInit, AfterViewInit { }); } // the fact that this callback is used, proves we're on the browser. - this.isThemeCSSLoading$.next(false); + this.distinctNext(this.isThemeCSSLoading$, false); }; head.appendChild(link); } @@ -375,4 +396,17 @@ export class AppComponent implements OnInit, AfterViewInit { } }); } + + /** + * Use nextValue to update a given BehaviorSubject, only if it differs from its current value + * + * @param bs a BehaviorSubject + * @param nextValue the next value for that BehaviorSubject + * @protected + */ + protected distinctNext(bs: BehaviorSubject, nextValue: T): void { + if (bs.getValue() !== nextValue) { + bs.next(nextValue); + } + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c133efdd5c..ebf9aa4937 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,4 +1,4 @@ -import { APP_BASE_HREF, CommonModule } from '@angular/common'; +import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { AbstractControl } from '@angular/forms'; @@ -15,10 +15,6 @@ import { } from '@ng-dynamic-forms/core'; import { TranslateModule } from '@ngx-translate/core'; import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; - -import { AdminSidebarSectionComponent } from './admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; -import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component'; -import { ExpandableAdminSidebarSectionComponent } from './admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { appEffects } from './app.effects'; @@ -27,48 +23,30 @@ import { appReducers, AppState, storeModuleConfig } from './app.reducer'; import { CheckAuthenticationTokenAction } from './core/auth/auth.actions'; import { CoreModule } from './core/core.module'; import { ClientCookieService } from './core/services/client-cookie.service'; -import { FooterComponent } from './footer/footer.component'; -import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component'; -import { HeaderComponent } from './header/header.component'; import { NavbarModule } from './navbar/navbar.module'; -import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer'; -import { NotificationComponent } from './shared/notifications/notification/notification.component'; -import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component'; import { SharedModule } from './shared/shared.module'; -import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component'; import { environment } from '../environments/environment'; -import { ForbiddenComponent } from './forbidden/forbidden.component'; import { AuthInterceptor } from './core/auth/auth.interceptor'; import { LocaleInterceptor } from './core/locale/locale.interceptor'; import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor'; import { LogInterceptor } from './core/log/log.interceptor'; -import { RootComponent } from './root/root.component'; -import { ThemedRootComponent } from './root/themed-root.component'; -import { ThemedEntryComponentModule } from '../themes/themed-entry-component.module'; -import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; -import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; -import { ThemedHeaderComponent } from './header/themed-header.component'; -import { ThemedFooterComponent } from './footer/themed-footer.component'; -import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component'; -import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component'; -import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; -import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; -import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component'; -import { ThemedAdminSidebarComponent } from './admin/admin-sidebar/themed-admin-sidebar.component'; +import { EagerThemesModule } from '../themes/eager-themes.module'; import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { NgxMaskModule } from 'ngx-mask'; - import { StoreDevModules } from '../config/store/devtools'; +import { RootModule } from './root.module'; export function getConfig() { return environment; } -export function getBase(appConfig: AppConfig) { - return appConfig.ui.nameSpace; -} +const getBaseHref = (document: Document, appConfig: AppConfig): string => { + const baseTag = document.querySelector('head > base'); + baseTag.setAttribute('href', `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`); + return baseTag.getAttribute('href'); +}; export function getMetaReducers(appConfig: AppConfig): MetaReducer[] { return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers; @@ -96,8 +74,9 @@ const IMPORTS = [ EffectsModule.forRoot(appEffects), StoreModule.forRoot(appReducers, storeModuleConfig), StoreRouterConnectingModule.forRoot(), - ThemedEntryComponentModule.withEntryComponents(), StoreDevModules, + EagerThemesModule, + RootModule, ]; const PROVIDERS = [ @@ -107,8 +86,8 @@ const PROVIDERS = [ }, { provide: APP_BASE_HREF, - useFactory: getBase, - deps: [APP_CONFIG] + useFactory: getBaseHref, + deps: [DOCUMENT, APP_CONFIG] }, { provide: USER_PROVIDED_META_REDUCERS, @@ -162,29 +141,6 @@ const PROVIDERS = [ const DECLARATIONS = [ AppComponent, - RootComponent, - ThemedRootComponent, - HeaderComponent, - ThemedHeaderComponent, - HeaderNavbarWrapperComponent, - ThemedHeaderNavbarWrapperComponent, - AdminSidebarComponent, - ThemedAdminSidebarComponent, - AdminSidebarSectionComponent, - ExpandableAdminSidebarSectionComponent, - FooterComponent, - ThemedFooterComponent, - PageNotFoundComponent, - ThemedPageNotFoundComponent, - NotificationComponent, - NotificationsBoardComponent, - BreadcrumbsComponent, - ThemedBreadcrumbsComponent, - ForbiddenComponent, - ThemedForbiddenComponent, - IdleModalComponent, - ThemedPageInternalServerErrorComponent, - PageInternalServerErrorComponent ]; const EXPORTS = [ diff --git a/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts b/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts index 3e30373070..1876936efb 100644 --- a/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts +++ b/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts @@ -5,7 +5,6 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { CollectionDataService } from '../../core/data/collection-data.service'; import { Collection } from '../../core/shared/collection.model'; import { TranslateService } from '@ngx-translate/core'; -import { RequestService } from '../../core/data/request.service'; /** * Component that represents the page where a user can delete an existing Collection @@ -24,8 +23,7 @@ export class DeleteCollectionPageComponent extends DeleteComColPageComponent { success: {}, error: {} }); - const objectCache = jasmine.createSpyObj('objectCache', { - remove: {} - }); const requestService = jasmine.createSpyObj('requestService', { setStaleByHrefSubstring: {} }); @@ -65,8 +61,7 @@ describe('CollectionMetadataComponent', () => { { provide: ItemTemplateDataService, useValue: itemTemplateServiceStub }, { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, { provide: NotificationsService, useValue: notificationsService }, - { provide: ObjectCacheService, useValue: objectCache }, - { provide: RequestService, useValue: requestService } + { provide: RequestService, useValue: requestService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -95,21 +90,19 @@ describe('CollectionMetadataComponent', () => { }); describe('deleteItemTemplate', () => { - describe('when delete returns a success', () => { - beforeEach(() => { - (itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true)); - comp.deleteItemTemplate(); - }); + beforeEach(() => { + (itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true)); + comp.deleteItemTemplate(); + }); + it('should call ItemTemplateService.deleteByCollectionID', () => { + expect(itemTemplateService.deleteByCollectionID).toHaveBeenCalledWith(template, 'collection-id'); + }); + + describe('when delete returns a success', () => { it('should display a success notification', () => { expect(notificationsService.success).toHaveBeenCalled(); }); - - it('should reset related object and request cache', () => { - expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collectionTemplateHref); - expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(template.self); - expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collection.self); - }); }); describe('when delete returns a failure', () => { diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index cfaad3767e..d4396fce17 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -8,10 +8,9 @@ import { combineLatest as combineLatestObservable, Observable } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; -import { switchMap, tap } from 'rxjs/operators'; +import { switchMap } from 'rxjs/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RequestService } from '../../../core/data/request.service'; import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths'; @@ -38,8 +37,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent this.itemTemplateService.getCollectionEndpoint(collection.id)), - ); - - combineLatestObservable(collection$, template$, templateHref$).pipe( - switchMap(([collection, template, templateHref]) => { - return this.itemTemplateService.deleteByCollectionID(template, collection.uuid).pipe( - tap((success: boolean) => { - if (success) { - this.objectCache.remove(templateHref); - this.objectCache.remove(template.self); - this.requestService.setStaleByHrefSubstring(template.self); - this.requestService.setStaleByHrefSubstring(templateHref); - this.requestService.setStaleByHrefSubstring(collection.self); - } - }) - ); + combineLatestObservable(collection$, template$).pipe( + switchMap(([collection, template]) => { + return this.itemTemplateService.deleteByCollectionID(template, collection.uuid); }) ).subscribe((success: boolean) => { if (success) { diff --git a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts index 985290a592..5a8ca5b7ab 100644 --- a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts @@ -13,6 +13,8 @@ import { RouterTestingModule } from '@angular/router/testing'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ComcolModule } from '../../../shared/comcol/comcol.module'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; describe('CollectionRolesComponent', () => { @@ -79,6 +81,7 @@ describe('CollectionRolesComponent', () => { { provide: ActivatedRoute, useValue: route }, { provide: RequestService, useValue: requestService }, { provide: GroupDataService, useValue: groupDataService }, + { provide: NotificationsService, useClass: NotificationsServiceStub } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/community-page/delete-community-page/delete-community-page.component.ts b/src/app/community-page/delete-community-page/delete-community-page.component.ts index 0cccc503e1..6e640c64be 100644 --- a/src/app/community-page/delete-community-page/delete-community-page.component.ts +++ b/src/app/community-page/delete-community-page/delete-community-page.component.ts @@ -5,7 +5,6 @@ import { ActivatedRoute, Router } from '@angular/router'; import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { RequestService } from '../../core/data/request.service'; /** * Component that represents the page where a user can delete an existing Community @@ -24,9 +23,8 @@ export class DeleteCommunityPageComponent extends DeleteComColPageComponent { @@ -64,6 +66,7 @@ describe('CommunityRolesComponent', () => { { provide: ActivatedRoute, useValue: route }, { provide: RequestService, useValue: requestService }, { provide: GroupDataService, useValue: groupDataService }, + { provide: NotificationsService, useClass: NotificationsServiceStub } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index da38d730a5..4db4cba612 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -12,13 +12,13 @@ import { AuthStatus } from './models/auth-status.model'; import { ShortLivedToken } from './models/short-lived-token.model'; import { URLCombiner } from '../url-combiner/url-combiner'; import { RestRequest } from '../data/rest-request.model'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * Abstract service to send authentication requests */ export abstract class AuthRequestService { protected linkName = 'authn'; - protected browseEndpoint = ''; protected shortlivedtokensEndpoint = 'shortlivedtokens'; constructor(protected halService: HALEndpointService, @@ -27,14 +27,21 @@ export abstract class AuthRequestService { ) { } - protected fetchRequest(request: RestRequest): Observable> { - return this.rdbService.buildFromRequestUUID(request.uuid).pipe( + protected fetchRequest(request: RestRequest, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.rdbService.buildFromRequestUUID(request.uuid, ...linksToFollow).pipe( getFirstCompletedRemoteData(), ); } - protected getEndpointByMethod(endpoint: string, method: string): string { - return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`; + protected getEndpointByMethod(endpoint: string, method: string, ...linksToFollow: FollowLinkConfig[]): string { + let url = isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`; + if (linksToFollow?.length > 0) { + linksToFollow.forEach((link: FollowLinkConfig, index: number) => { + url += ((index === 0) ? '?' : '&') + `embed=${link.name}`; + }); + } + + return url; } public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable> { @@ -48,14 +55,14 @@ export abstract class AuthRequestService { distinctUntilChanged()); } - public getRequest(method: string, options?: HttpOptions): Observable> { + public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig[]): Observable> { return this.halService.getEndpoint(this.linkName).pipe( filter((href: string) => isNotEmpty(href)), - map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), + map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)), distinctUntilChanged(), map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)), tap((request: GetRequest) => this.requestService.send(request)), - mergeMap((request: GetRequest) => this.fetchRequest(request)), + mergeMap((request: GetRequest) => this.fetchRequest(request, ...linksToFollow)), distinctUntilChanged()); } diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index 8cd587b61a..8ebc9f6cb0 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -192,7 +192,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, - blocking: true, + blocking: false, loading: true, idle: false }; @@ -212,7 +212,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, - blocking: true, + blocking: false, loading: true, idle: false }; @@ -558,7 +558,7 @@ describe('authReducer', () => { state = { authenticated: false, loaded: false, - blocking: true, + blocking: false, loading: true, authMethods: [], idle: false diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 2fc79a8861..6f47a3c20c 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -92,11 +92,15 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut }); case AuthActionTypes.AUTHENTICATED: + return Object.assign({}, state, { + loading: true, + blocking: true + }); + case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN: case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE: return Object.assign({}, state, { loading: true, - blocking: true }); case AuthActionTypes.AUTHENTICATED_ERROR: @@ -210,7 +214,6 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut case AuthActionTypes.RETRIEVE_AUTH_METHODS: return Object.assign({}, state, { loading: true, - blocking: true }); case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS: diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index ced8bb94c8..b38d17aecd 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -32,6 +32,8 @@ import { TranslateService } from '@ngx-translate/core'; import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { SetUserAsIdleAction, UnsetUserAsIdleAction } from './auth.actions'; +import { SpecialGroupDataMock, SpecialGroupDataMock$ } from '../../shared/testing/special-group.mock'; +import { cold } from 'jasmine-marbles'; describe('AuthService test', () => { @@ -56,6 +58,13 @@ describe('AuthService test', () => { let linkService; let hardRedirectService; + const AuthStatusWithSpecialGroups = Object.assign(new AuthStatus(), { + uuid: 'test', + authenticated: true, + okay: true, + specialGroups: SpecialGroupDataMock$ + }); + function init() { mockStore = jasmine.createSpyObj('store', { dispatch: {}, @@ -368,25 +377,25 @@ describe('AuthService test', () => { it('should redirect to reload with redirect url', () => { authService.navigateToRedirectUrl('/collection/123'); // Reload with redirect URL set to /collection/123 - expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123')))); + expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123')))); }); it('should redirect to reload with /home', () => { authService.navigateToRedirectUrl('/home'); // Reload with redirect URL set to /home - expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/home')))); + expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*\\?redirect=' + encodeURIComponent('/home')))); }); it('should redirect to regular reload and not to /login', () => { authService.navigateToRedirectUrl('/login'); // Reload without a redirect URL - expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$'))); + expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*(?!\\?)$'))); }); it('should redirect to regular reload when no redirect url is found', () => { authService.navigateToRedirectUrl(undefined); // Reload without a redirect URL - expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$'))); + expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*(?!\\?)$'))); }); describe('impersonate', () => { @@ -511,6 +520,19 @@ describe('AuthService test', () => { expect((authService as any).navigateToRedirectUrl).toHaveBeenCalled(); }); }); + + describe('getSpecialGroupsFromAuthStatus', () => { + beforeEach(() => { + spyOn(authRequest, 'getRequest').and.returnValue(createSuccessfulRemoteDataObject$(AuthStatusWithSpecialGroups)); + }); + + it('should call navigateToRedirectUrl with no url', () => { + const expectRes = cold('(a|)', { + a: SpecialGroupDataMock + }); + expect(authService.getSpecialGroupsFromAuthStatus()).toBeObservable(expectRes); + }); + }); }); describe('when user is not logged in', () => { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index f89fa21681..999ea863df 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -44,13 +44,18 @@ import { import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { RouteService } from '../services/route.service'; import { EPersonDataService } from '../eperson/eperson-data.service'; -import { getAllSucceededRemoteDataPayload } from '../shared/operators'; +import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators'; import { AuthMethod } from './models/auth.method'; import { HardRedirectService } from '../services/hard-redirect.service'; import { RemoteData } from '../data/remote-data'; import { environment } from '../../../environments/environment'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +import { buildPaginatedList, PaginatedList } from '../data/paginated-list.model'; +import { Group } from '../eperson/models/group.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { PageInfo } from '../shared/page-info.model'; +import { followLink } from '../../shared/utils/follow-link-config.model'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; @@ -211,6 +216,22 @@ export class AuthService { this.store.dispatch(new CheckAuthenticationTokenAction()); } + /** + * Return the special groups list embedded in the AuthStatus model + */ + public getSpecialGroupsFromAuthStatus(): Observable>> { + return this.authRequestService.getRequest('status', null, followLink('specialGroups')).pipe( + getFirstCompletedRemoteData(), + switchMap((status: RemoteData) => { + if (status.hasSucceeded) { + return status.payload.specialGroups; + } else { + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(),[])); + } + }) + ); + } + /** * Checks if token is present into storage and is not expired */ @@ -447,8 +468,8 @@ export class AuthService { */ public navigateToRedirectUrl(redirectUrl: string) { // Don't do redirect if already on reload url - if (!hasValue(redirectUrl) || !redirectUrl.includes('/reload/')) { - let url = `/reload/${new Date().getTime()}`; + if (!hasValue(redirectUrl) || !redirectUrl.includes('reload/')) { + let url = `reload/${new Date().getTime()}`; if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) { url += `?redirect=${encodeURIComponent(redirectUrl)}`; } diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index fbe6ed6476..d18b1ccf9a 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -5,6 +5,8 @@ import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; import { RemoteData } from '../../data/remote-data'; import { EPerson } from '../../eperson/models/eperson.model'; import { EPERSON } from '../../eperson/models/eperson.resource-type'; +import { Group } from '../../eperson/models/group.model'; +import { GROUP } from '../../eperson/models/group.resource-type'; import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; import { excludeFromEquals } from '../../utilities/equals.decorators'; @@ -13,6 +15,7 @@ import { AUTH_STATUS } from './auth-status.resource-type'; import { AuthTokenInfo } from './auth-token-info.model'; import { AuthMethod } from './auth.method'; import { CacheableObject } from '../../cache/cacheable-object.model'; +import { PaginatedList } from '../../data/paginated-list.model'; /** * Object that represents the authenticated status of a user @@ -61,6 +64,7 @@ export class AuthStatus implements CacheableObject { _links: { self: HALLink; eperson: HALLink; + specialGroups: HALLink; }; /** @@ -70,6 +74,13 @@ export class AuthStatus implements CacheableObject { @link(EPERSON) eperson?: Observable>; + /** + * The SpecialGroup of this auth status + * Will be undefined unless the SpecialGroup {@link HALLink} has been resolved. + */ + @link(GROUP, true) + specialGroups?: Observable>>; + /** * True if the token is valid, false if there was no token or the token wasn't valid */ diff --git a/src/app/core/breadcrumbs/dso-name.service.spec.ts b/src/app/core/breadcrumbs/dso-name.service.spec.ts index 7a399ce748..9f2f76599a 100644 --- a/src/app/core/breadcrumbs/dso-name.service.spec.ts +++ b/src/app/core/breadcrumbs/dso-name.service.spec.ts @@ -78,15 +78,32 @@ describe(`DSONameService`, () => { }); describe(`factories.Person`, () => { - beforeEach(() => { - spyOn(mockPerson, 'firstMetadataValue').and.returnValues(...mockPersonName.split(', ')); + describe(`with person.familyName and person.givenName`, () => { + beforeEach(() => { + spyOn(mockPerson, 'firstMetadataValue').and.returnValues(...mockPersonName.split(', ')); + }); + + it(`should return 'person.familyName, person.givenName'`, () => { + const result = (service as any).factories.Person(mockPerson); + expect(result).toBe(mockPersonName); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName'); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName'); + expect(mockPerson.firstMetadataValue).not.toHaveBeenCalledWith('dc.title'); + }); }); - it(`should return 'person.familyName, person.givenName'`, () => { - const result = (service as any).factories.Person(mockPerson); - expect(result).toBe(mockPersonName); - expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName'); - expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName'); + describe(`without person.familyName and person.givenName`, () => { + beforeEach(() => { + spyOn(mockPerson, 'firstMetadataValue').and.returnValues(undefined, undefined, mockPersonName); + }); + + it(`should return dc.title`, () => { + const result = (service as any).factories.Person(mockPerson); + expect(result).toBe(mockPersonName); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName'); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName'); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('dc.title'); + }); }); }); diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index 61d587b6de..82e2da58b1 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -41,7 +41,7 @@ describe('objectCacheReducer', () => { alternativeLinks: [altLink1, altLink2], timeCompleted: new Date().getTime(), msToLive: 900000, - requestUUID: requestUUID1, + requestUUIDs: [requestUUID1], patches: [], isDirty: false, }, @@ -55,7 +55,7 @@ describe('objectCacheReducer', () => { alternativeLinks: [altLink3, altLink4], timeCompleted: new Date().getTime(), msToLive: 900000, - requestUUID: selfLink2, + requestUUIDs: [selfLink2], patches: [], isDirty: false } diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 9001d334ce..1a42408f72 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -63,9 +63,11 @@ export class ObjectCacheEntry implements CacheEntry { msToLive: number; /** - * The UUID of the request that caused this entry to be added + * The UUIDs of the requests that caused this entry to be added + * New UUIDs should be added to the front of the array + * to make retrieving the latest UUID easier. */ - requestUUID: string; + requestUUIDs: string[]; /** * An array of patches that were made on the client side to this entry, but haven't been sent to the server yet @@ -156,11 +158,11 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio data: action.payload.objectToCache, timeCompleted: action.payload.timeCompleted, msToLive: action.payload.msToLive, - requestUUID: action.payload.requestUUID, + requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], isDirty: isNotEmpty(existing.patches), patches: existing.patches || [], alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks] - } + } as ObjectCacheEntry }); } diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index bde6831967..f18c262524 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -211,25 +211,69 @@ describe('ObjectCacheService', () => { }); }); - describe('has', () => { + describe('hasByHref', () => { + describe('with requestUUID not specified', () => { + describe('getByHref emits an object', () => { + beforeEach(() => { + spyOn(service, 'getByHref').and.returnValue(observableOf(cacheEntry)); + }); - describe('getByHref emits an object', () => { - beforeEach(() => { - spyOn(service, 'getByHref').and.returnValue(observableOf(cacheEntry)); + it('should return true', () => { + expect(service.hasByHref(selfLink)).toBe(true); + }); }); - it('should return true', () => { - expect(service.hasByHref(selfLink)).toBe(true); + describe('getByHref emits nothing', () => { + beforeEach(() => { + spyOn(service, 'getByHref').and.returnValue(empty()); + }); + + it('should return false', () => { + expect(service.hasByHref(selfLink)).toBe(false); + }); }); }); - describe('getByHref emits nothing', () => { - beforeEach(() => { - spyOn(service, 'getByHref').and.returnValue(empty()); + describe('with requestUUID specified', () => { + describe('getByHref emits an object that includes the specified requestUUID', () => { + beforeEach(() => { + spyOn(service, 'getByHref').and.returnValue(observableOf(Object.assign(cacheEntry, { + requestUUIDs: [ + 'something', + 'something-else', + 'specific-request', + ] + }))); + }); + + it('should return true', () => { + expect(service.hasByHref(selfLink, 'specific-request')).toBe(true); + }); }); - it('should return false', () => { - expect(service.hasByHref(selfLink)).toBe(false); + describe('getByHref emits an object that doesn\'t include the specified requestUUID', () => { + beforeEach(() => { + spyOn(service, 'getByHref').and.returnValue(observableOf(Object.assign(cacheEntry, { + requestUUIDs: [ + 'something', + 'something-else', + ] + }))); + }); + + it('should return true', () => { + expect(service.hasByHref(selfLink, 'specific-request')).toBe(false); + }); + }); + + describe('getByHref emits nothing', () => { + beforeEach(() => { + spyOn(service, 'getByHref').and.returnValue(empty()); + }); + + it('should return false', () => { + expect(service.hasByHref(selfLink, 'specific-request')).toBe(false); + }); }); }); }); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 6d48242178..cdf87e5c1a 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -197,7 +197,7 @@ export class ObjectCacheService { */ getRequestUUIDBySelfLink(selfLink: string): Observable { return this.getByHref(selfLink).pipe( - map((entry: ObjectCacheEntry) => entry.requestUUID), + map((entry: ObjectCacheEntry) => entry.requestUUIDs[0]), distinctUntilChanged()); } @@ -282,7 +282,7 @@ export class ObjectCacheService { let result = false; this.getByHref(href).subscribe((entry: ObjectCacheEntry) => { if (isNotEmpty(requestUUID)) { - result = entry.requestUUID === requestUUID; + result = entry.requestUUIDs.includes(requestUUID); } else { result = true; } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index bc0dbd2e6c..4ec4ca8592 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -38,7 +38,7 @@ import { SubmissionSectionModel } from './config/models/config-submission-sectio import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model'; import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { coreEffects } from './core.effects'; -import { coreReducers} from './core.reducers'; +import { coreReducers } from './core.reducers'; import { BitstreamFormatDataService } from './data/bitstream-format-data.service'; import { CollectionDataService } from './data/collection-data.service'; import { CommunityDataService } from './data/community-data.service'; @@ -75,7 +75,6 @@ import { RegistryService } from './registry/registry.service'; import { RoleService } from './roles/role.service'; import { FeedbackDataService } from './feedback/feedback-data.service'; -import { ApiService } from './services/api.service'; import { ServerResponseService } from './services/server-response.service'; import { NativeWindowFactory, NativeWindowService } from './services/window.service'; import { BitstreamFormat } from './shared/bitstream-format.model'; @@ -133,11 +132,15 @@ import { Feature } from './shared/feature.model'; import { Authorization } from './shared/authorization.model'; import { FeatureDataService } from './data/feature-authorization/feature-data.service'; import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service'; -import { SiteAdministratorGuard } from './data/feature-authorization/feature-authorization-guard/site-administrator.guard'; +import { + SiteAdministratorGuard +} from './data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { Registration } from './shared/registration.model'; import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; import { MetadataFieldDataService } from './data/metadata-field-data.service'; -import { DsDynamicTypeBindRelationService } from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; +import { + DsDynamicTypeBindRelationService +} from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { TokenResponseParsingService } from './auth/token-response-parsing.service'; import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service'; import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; @@ -164,6 +167,9 @@ import { SequenceService } from './shared/sequence.service'; import { CoreState } from './core-state.model'; import { GroupDataService } from './eperson/group-data.service'; import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; +import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model'; +import { AccessStatusDataService } from './data/access-status-data.service'; +import { LinkHeadService } from './services/link-head.service'; import { ResearcherProfileService } from './profile/researcher-profile.service'; import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service'; import { ResearcherProfile } from './profile/model/researcher-profile.model'; @@ -171,9 +177,6 @@ import { OrcidQueueService } from './orcid/orcid-queue.service'; import { OrcidHistoryService } from './orcid/orcid-history.service'; import { OrcidQueue } from './orcid/model/orcid-queue.model'; import { OrcidHistory } from './orcid/model/orcid-history.model'; -import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model'; -import { AccessStatusDataService } from './data/access-status-data.service'; -import { LinkHeadService } from './services/link-head.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -198,7 +201,6 @@ const DECLARATIONS = []; const EXPORTS = []; const PROVIDERS = [ - ApiService, AuthenticatedGuard, CommunityDataService, CollectionDataService, @@ -365,6 +367,7 @@ export const models = Root, SearchConfig, SubmissionAccessesModel, + AccessStatusObject, ResearcherProfile, OrcidQueue, OrcidHistory, diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts index c1ebf90a47..30ef79ee6d 100644 --- a/src/app/core/data/bitstream-format-data.service.spec.ts +++ b/src/app/core/data/bitstream-format-data.service.spec.ts @@ -37,7 +37,12 @@ describe('BitstreamFormatDataService', () => { } } as Store; - const objectCache = {} as ObjectCacheService; + const requestUUIDs = ['some', 'uuid']; + + const objectCache = jasmine.createSpyObj('objectCache', { + getByHref: observableOf({ requestUUIDs }) + }) as ObjectCacheService; + const halEndpointService = { getEndpoint(linkPath: string): Observable { return cold('a', { a: bitstreamFormatsEndpoint }); @@ -76,6 +81,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); @@ -96,6 +102,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); @@ -118,6 +125,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); @@ -139,6 +147,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); @@ -163,6 +172,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); @@ -186,6 +196,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); @@ -209,6 +220,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); @@ -231,6 +243,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); @@ -253,6 +266,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: cold('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); @@ -273,6 +287,7 @@ describe('BitstreamFormatDataService', () => { send: {}, getByHref: observableOf(responseCacheEntry), getByUUID: hot('a', { a: responseCacheEntry }), + setStaleByUUID: observableOf(true), generateRequestId: 'request-id', removeByHrefSubstring: {} }); diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 81d683b37a..dffc97f294 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -22,6 +22,7 @@ import { import { BitstreamDataService } from './bitstream-data.service'; import { CoreState } from '../core-state.model'; import { FindListOptions } from './find-list-options.model'; +import { Bitstream } from '../shared/bitstream.model'; const LINK_NAME = 'test'; @@ -244,4 +245,75 @@ describe('ComColDataService', () => { }); }); }); + + describe('deleteLogo', () => { + let dso; + + beforeEach(() => { + dso = { + _links: { + logo: { + href: 'logo-href' + } + } + }; + }); + + describe('when DSO has no logo', () => { + beforeEach(() => { + dso.logo = undefined; + }); + + it('should return a failed RD', (done) => { + service.deleteLogo(dso).subscribe(rd => { + expect(rd.hasFailed).toBeTrue(); + expect(bitstreamDataService.deleteByHref).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('when DSO has a logo', () => { + let logo; + + beforeEach(() => { + logo = Object.assign(new Bitstream, { + id: 'logo-id', + _links: { + self: { + href: 'logo-href', + } + } + }); + }); + + describe('that can be retrieved', () => { + beforeEach(() => { + dso.logo = createSuccessfulRemoteDataObject$(logo); + }); + + it('should call BitstreamDataService.deleteByHref', (done) => { + service.deleteLogo(dso).subscribe(rd => { + expect(rd.hasSucceeded).toBeTrue(); + expect(bitstreamDataService.deleteByHref).toHaveBeenCalledWith('logo-href'); + done(); + }); + }); + }); + + describe('that cannot be retrieved', () => { + beforeEach(() => { + dso.logo = createFailedRemoteDataObject$(logo); + }); + + it('should not call BitstreamDataService.deleteByHref', (done) => { + service.deleteLogo(dso).subscribe(rd => { + expect(rd.hasFailed).toBeTrue(); + expect(bitstreamDataService.deleteByHref).not.toHaveBeenCalled(); + done(); + }); + }); + }); + }); + }); }); diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index f680fed6a4..64efd58418 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -11,7 +11,11 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { ChangeAnalyzer } from './change-analyzer'; import { DataService } from './data.service'; import { PatchRequest } from './request.models'; @@ -25,9 +29,12 @@ import { RemoteData } from './remote-data'; import { RequestEntryState } from './request-entry-state.model'; import { CoreState } from '../core-state.model'; import { FindListOptions } from './find-list-options.model'; +import { fakeAsync, tick } from '@angular/core/testing'; const endpoint = 'https://rest.api/core'; +const BOOLEAN = { f: false, t: true }; + class TestService extends DataService { constructor( @@ -86,6 +93,9 @@ describe('DataService', () => { }, getObjectBySelfLink: () => { /* empty */ + }, + getByHref: () => { + /* empty */ } } as any; store = {} as Store; @@ -833,4 +843,149 @@ describe('DataService', () => { }); }); + + describe('invalidateByHref', () => { + let getByHrefSpy: jasmine.Spy; + + beforeEach(() => { + getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ + requestUUIDs: ['request1', 'request2', 'request3'] + })); + + }); + + it('should call setStaleByUUID for every request associated with this DSO', (done) => { + service.invalidateByHref('some-href').subscribe((ok) => { + expect(ok).toBeTrue(); + expect(getByHrefSpy).toHaveBeenCalledWith('some-href'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request3'); + done(); + }); + }); + + it('should call setStaleByUUID even if not subscribing to returned Observable', fakeAsync(() => { + service.invalidateByHref('some-href'); + tick(); + + expect(getByHrefSpy).toHaveBeenCalledWith('some-href'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request3'); + })); + + it('should return an Observable that only emits true once all requests are stale', () => { + testScheduler.run(({ cold, expectObservable }) => { + requestService.setStaleByUUID.and.callFake((uuid) => { + switch (uuid) { // fake requests becoming stale at different times + case 'request1': + return cold('--(t|)', BOOLEAN); + case 'request2': + return cold('----(t|)', BOOLEAN); + case 'request3': + return cold('------(t|)', BOOLEAN); + } + }); + + const done$ = service.invalidateByHref('some-href'); + + // emit true as soon as the final request is stale + expectObservable(done$).toBe('------(t|)', BOOLEAN); + }); + }); + }); + + describe('delete', () => { + let MOCK_SUCCEEDED_RD; + let MOCK_FAILED_RD; + + let invalidateByHrefSpy: jasmine.Spy; + let buildFromRequestUUIDSpy: jasmine.Spy; + let getIDHrefObsSpy: jasmine.Spy; + let deleteByHrefSpy: jasmine.Spy; + + beforeEach(() => { + invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true)); + buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough(); + getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough(); + deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough(); + + MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({}); + MOCK_FAILED_RD = createFailedRemoteDataObject('something went wrong'); + }); + + it('should retrieve href by ID and call deleteByHref', () => { + getIDHrefObsSpy.and.returnValue(observableOf('some-href')); + buildFromRequestUUIDSpy.and.returnValue(createSuccessfulRemoteDataObject$({})); + + service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => { + expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id'); + expect(deleteByHrefSpy).toHaveBeenCalledWith('some-href', ['a', 'b', 'c']); + }); + }); + + describe('deleteByHref', () => { + it('should call invalidateByHref if the DELETE request succeeds', (done) => { + buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD)); + + service.deleteByHref('some-href').subscribe(rd => { + expect(rd).toBe(MOCK_SUCCEEDED_RD); + expect(invalidateByHrefSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('should call invalidateByHref even if not subscribing to returned Observable', fakeAsync(() => { + buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD)); + + service.deleteByHref('some-href'); + tick(); + + expect(invalidateByHrefSpy).toHaveBeenCalled(); + })); + + it('should not call invalidateByHref if the DELETE request fails', (done) => { + buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD)); + + service.deleteByHref('some-href').subscribe(rd => { + expect(rd).toBe(MOCK_FAILED_RD); + expect(invalidateByHrefSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should wait for invalidateByHref before emitting', () => { + testScheduler.run(({ cold, expectObservable }) => { + buildFromRequestUUIDSpy.and.returnValue( + cold('(r|)', { r: MOCK_SUCCEEDED_RD}) // RD emits right away + ); + invalidateByHrefSpy.and.returnValue( + cold('----(t|)', BOOLEAN) // but we pretend that setting requests to stale takes longer + ); + + const done$ = service.deleteByHref('some-href'); + expectObservable(done$).toBe( + '----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait until that's done + ); + }); + }); + + it('should wait for the DELETE request to resolve before emitting', () => { + testScheduler.run(({ cold, expectObservable }) => { + buildFromRequestUUIDSpy.and.returnValue( + cold('----(r|)', { r: MOCK_SUCCEEDED_RD}) // the request takes a while + ); + invalidateByHrefSpy.and.returnValue( + cold('(t|)', BOOLEAN) // but we pretend that setting to stale happens sooner + ); // e.g.: maybe already stale before this call? + + const done$ = service.deleteByHref('some-href'); + expectObservable(done$).toBe( + '----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait for the request + ); + }); + }); + }); + }); }); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 310ad704ec..1cd9731a65 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,18 +1,19 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { Operation } from 'fast-json-patch'; -import { Observable, of as observableOf } from 'rxjs'; +import { AsyncSubject, combineLatest, from as observableFrom, Observable, of as observableOf } from 'rxjs'; import { distinctUntilChanged, filter, find, map, mergeMap, + skipWhile, + switchMap, take, takeWhile, - switchMap, tap, - skipWhile, + toArray } from 'rxjs/operators'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; @@ -21,22 +22,17 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { getClassForType } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestParam } from '../cache/models/request-param.model'; +import { ObjectCacheEntry } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getRemoteDataPayload, getFirstSucceededRemoteData, } from '../shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; import { ChangeAnalyzer } from './change-analyzer'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; -import { - CreateRequest, - GetRequest, - PatchRequest, - PutRequest, - DeleteRequest -} from './request.models'; +import { CreateRequest, DeleteRequest, GetRequest, PatchRequest, PutRequest } from './request.models'; import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; import { UpdateDataService } from './update-data.service'; @@ -168,7 +164,7 @@ export abstract class DataService implements UpdateDa * @return {Observable} * Return an observable that emits created HREF */ - protected buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: FollowLinkConfig[]): string { + buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: FollowLinkConfig[]): string { let args = []; if (hasValue(params)) { @@ -579,6 +575,38 @@ export abstract class DataService implements UpdateDa return result$; } + /** + * Invalidate an existing DSpaceObject by marking all requests it is included in as stale + * @param objectId The id of the object to be invalidated + * @return An Observable that will emit `true` once all requests are stale + */ + invalidate(objectId: string): Observable { + return this.getIDHrefObs(objectId).pipe( + switchMap((href: string) => this.invalidateByHref(href)) + ); + } + + /** + * Invalidate an existing DSpaceObject by marking all requests it is included in as stale + * @param href The self link of the object to be invalidated + * @return An Observable that will emit `true` once all requests are stale + */ + invalidateByHref(href: string): Observable { + const done$ = new AsyncSubject(); + + this.objectCache.getByHref(href).pipe( + switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe( + mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)), + toArray(), + )), + ).subscribe(() => { + done$.next(true); + done$.complete(); + }); + + return done$; + } + /** * Delete an existing DSpace Object on the server * @param objectId The id of the object to be removed @@ -600,6 +628,7 @@ export abstract class DataService implements UpdateDa * metadata should be saved as real metadata * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, * errorMessage, timeCompleted, etc + * Only emits once all request related to the DSO has been invalidated. */ deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { const requestId = this.requestService.generateRequestId(); @@ -618,7 +647,27 @@ export abstract class DataService implements UpdateDa } this.requestService.send(request); - return this.rdbService.buildFromRequestUUID(requestId); + const response$ = this.rdbService.buildFromRequestUUID(requestId); + + const invalidated$ = new AsyncSubject(); + response$.pipe( + getFirstCompletedRemoteData(), + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return this.invalidateByHref(href); + } else { + return [true]; + } + }) + ).subscribe(() => { + invalidated$.next(true); + invalidated$.complete(); + }); + + return combineLatest([response$, invalidated$]).pipe( + filter(([_, invalidated]) => invalidated), + map(([response, _]) => response), + ); } /** diff --git a/src/app/core/data/feature-authorization/authorization-data.service.ts b/src/app/core/data/feature-authorization/authorization-data.service.ts index 1f8c8b2284..f27919844d 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.ts @@ -60,14 +60,18 @@ export class AuthorizationDataService extends DataService { /** * Checks if an {@link EPerson} (or anonymous) has access to a specific object within a {@link Feature} - * @param objectUrl URL to the object to search {@link Authorization}s for. - * If not provided, the repository's {@link Site} will be used. - * @param ePersonUuid UUID of the {@link EPerson} to search {@link Authorization}s for. - * If not provided, the UUID of the currently authenticated {@link EPerson} will be used. - * @param featureId ID of the {@link Feature} to check {@link Authorization} for + * @param objectUrl URL to the object to search {@link Authorization}s for. + * If not provided, the repository's {@link Site} will be used. + * @param ePersonUuid UUID of the {@link EPerson} to search {@link Authorization}s for. + * If not provided, the UUID of the currently authenticated {@link EPerson} will be used. + * @param featureId ID of the {@link Feature} to check {@link Authorization} for + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale */ - isAuthorized(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string): Observable { - return this.searchByObject(featureId, objectUrl, ePersonUuid, {}, true, true, followLink('feature')).pipe( + isAuthorized(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable { + return this.searchByObject(featureId, objectUrl, ePersonUuid, {}, useCachedVersionIfAvailable, reRequestOnStale, followLink('feature')).pipe( getFirstCompletedRemoteData(), map((authorizationRD) => { if (authorizationRD.statusCode !== 401 && hasValue(authorizationRD.payload) && isNotEmpty(authorizationRD.payload.page)) { diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index a49761ae5d..fe35d840d7 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -8,7 +8,7 @@ import { defaultUUID, getMockUUIDService } from '../../shared/mocks/uuid.service import { ObjectCacheService } from '../cache/object-cache.service'; import { coreReducers} from '../core.reducers'; import { UUIDService } from '../shared/uuid.service'; -import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; +import { RequestConfigureAction, RequestExecuteAction, RequestStaleAction } from './request.actions'; import { DeleteRequest, GetRequest, @@ -19,7 +19,7 @@ import { PutRequest } from './request.models'; import { RequestService } from './request.service'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; import { storeModuleConfig } from '../../app.reducer'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { RequestEntryState } from './request-entry-state.model'; @@ -426,7 +426,7 @@ describe('RequestService', () => { describe('and it is cached', () => { describe('in the ObjectCache', () => { beforeEach(() => { - (objectCache.getByHref as any).and.returnValue(observableOf({ requestUUID: 'some-uuid' })); + (objectCache.getByHref as any).and.returnValue(observableOf({ requestUUIDs: ['some-uuid'] })); spyOn(serviceAsAny, 'hasByHref').and.returnValue(false); spyOn(serviceAsAny, 'hasByUUID').and.returnValue(true); }); @@ -596,4 +596,33 @@ describe('RequestService', () => { }); }); + describe('setStaleByUUID', () => { + let dispatchSpy: jasmine.Spy; + let getByUUIDSpy: jasmine.Spy; + + beforeEach(() => { + dispatchSpy = spyOn(store, 'dispatch'); + getByUUIDSpy = spyOn(service, 'getByUUID').and.callThrough(); + }); + + it('should dispatch a RequestStaleAction', () => { + service.setStaleByUUID('something'); + const firstAction = dispatchSpy.calls.argsFor(0)[0]; + expect(firstAction).toBeInstanceOf(RequestStaleAction); + expect(firstAction.payload).toEqual({ uuid: 'something' }); + }); + + it('should return an Observable that emits true as soon as the request is stale', fakeAsync(() => { + dispatchSpy.and.callFake(() => { /* empty */ }); // don't actually set as stale + getByUUIDSpy.and.returnValue(cold('a-b--c--d-', { // but fake the state in the cache + a: { state: RequestEntryState.ResponsePending }, + b: { state: RequestEntryState.Success }, + c: { state: RequestEntryState.SuccessStale }, + d: { state: RequestEntryState.Error }, + })); + + const done$ = service.setStaleByUUID('something'); + expect(done$).toBeObservable(cold('-----(t|)', { t: true })); + })); + }); }); diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 3903bcfc99..2d5acb2cb3 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -311,6 +311,21 @@ export class RequestService { ); } + /** + * Mark a request as stale + * @param uuid the UUID of the request + * @return an Observable that will emit true once the Request becomes stale + */ + setStaleByUUID(uuid: string): Observable { + this.store.dispatch(new RequestStaleAction(uuid)); + + return this.getByUUID(uuid).pipe( + map((request: RequestEntry) => isStale(request.state)), + filter((stale: boolean) => stale), + take(1), + ); + } + /** * Check if a GET request is in the cache or if it's still pending * @param {GetRequest} request The request to check @@ -339,7 +354,7 @@ export class RequestService { .subscribe((entry: ObjectCacheEntry) => { // if the object cache has a match, check if the request that the object came with is // still valid - inObjCache = this.hasByUUID(entry.requestUUID); + inObjCache = this.hasByUUID(entry.requestUUIDs[0]); }).unsubscribe(); // we should send the request if it isn't cached diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index e123253e2b..80cb92716a 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -21,7 +21,7 @@ import { EPersonDataService } from './eperson-data.service'; import { EPerson } from './models/eperson.model'; import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; @@ -287,13 +287,12 @@ describe('EPersonDataService', () => { describe('deleteEPerson', () => { beforeEach(() => { - spyOn(service, 'findById').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMock)); + spyOn(service, 'delete').and.returnValue(createNoContentRemoteDataObject$()); service.deleteEPerson(EPersonMock).subscribe(); }); - it('should send DeleteRequest', () => { - const expected = new DeleteRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid); - expect(requestService.send).toHaveBeenCalledWith(expected); + it('should call DataService.delete with the EPerson\'s UUID', () => { + expect(service.delete).toHaveBeenCalledWith(EPersonMock.id); }); }); diff --git a/src/app/core/locale/locale.service.ts b/src/app/core/locale/locale.service.ts index 1052021479..68d2839d42 100644 --- a/src/app/core/locale/locale.service.ts +++ b/src/app/core/locale/locale.service.ts @@ -192,7 +192,7 @@ export class LocaleService { this.routeService.getCurrentUrl().pipe(take(1)).subscribe((currentURL) => { // Hard redirect to the reload page with a unique number behind it // so that all state is definitely lost - this._window.nativeWindow.location.href = `/reload/${new Date().getTime()}?redirect=` + encodeURIComponent(currentURL); + this._window.nativeWindow.location.href = `reload/${new Date().getTime()}?redirect=` + encodeURIComponent(currentURL); }); } diff --git a/src/app/core/profile/model/researcher-profile.model.ts b/src/app/core/profile/model/researcher-profile.model.ts index a07467476e..6c8b19db40 100644 --- a/src/app/core/profile/model/researcher-profile.model.ts +++ b/src/app/core/profile/model/researcher-profile.model.ts @@ -1,10 +1,15 @@ +import { Observable } from 'rxjs'; import { autoserialize, deserialize, deserializeAs } from 'cerialize'; -import { typedObject } from '../../cache/builders/build-decorators'; + +import { link, typedObject } from '../../cache/builders/build-decorators'; import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; import { excludeFromEquals } from '../../utilities/equals.decorators'; import { RESEARCHER_PROFILE } from './researcher-profile.resource-type'; -import {CacheableObject} from '../../cache/cacheable-object.model'; +import { CacheableObject } from '../../cache/cacheable-object.model'; +import { RemoteData } from '../../data/remote-data'; +import { ITEM } from '../../shared/item.resource-type'; +import { Item } from '../../shared/item.model'; /** * Class the represents a Researcher Profile. @@ -46,4 +51,11 @@ export class ResearcherProfile extends CacheableObject { eperson: HALLink }; + /** + * The related person Item + * Will be undefined unless the item {@link HALLink} has been resolved. + */ + @link(ITEM) + item?: Observable>; + } diff --git a/src/app/core/profile/researcher-profile.service.spec.ts b/src/app/core/profile/researcher-profile.service.spec.ts new file mode 100644 index 0000000000..9a6b0477d5 --- /dev/null +++ b/src/app/core/profile/researcher-profile.service.spec.ts @@ -0,0 +1,542 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; + +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RequestService } from '../data/request.service'; +import { PageInfo } from '../shared/page-info.model'; +import { buildPaginatedList } from '../data/paginated-list.model'; +import { + createFailedRemoteDataObject$, + createNoContentRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../shared/remote-data.utils'; +import { RestResponse } from '../cache/response.models'; +import { RequestEntry } from '../data/request-entry.model'; +import { ResearcherProfileService } from './researcher-profile.service'; +import { RouterMock } from '../../shared/mocks/router.mock'; +import { ResearcherProfile } from './model/researcher-profile.model'; +import { Item } from '../shared/item.model'; +import { RemoveOperation, ReplaceOperation } from 'fast-json-patch'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { PostRequest } from '../data/request.models'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; +import { ConfigurationDataService } from '../data/configuration-data.service'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { environment } from '../../../environments/environment'; + +describe('ResearcherProfileService', () => { + let scheduler: TestScheduler; + let service: ResearcherProfileService; + let serviceAsAny: any; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let responseCacheEntry: RequestEntry; + let configurationDataService: ConfigurationDataService; + + const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241'; + const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241'; + const researcherProfileItem: Item = Object.assign(new Item(), { + id: itemId, + _links: { + self: { + href: `https://rest.api/rest/api/items/${itemId}` + }, + } + }); + const researcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: researcherProfileId, + visible: false, + type: 'profile', + _links: { + item: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item` + }, + self: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId}` + }, + } + }); + + const researcherProfilePatched: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: researcherProfileId, + visible: true, + type: 'profile', + _links: { + item: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item` + }, + self: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId}` + }, + } + }); + + const researcherProfileId2 = 'agbf9946-f4ce-479e-8f11-b90cbe9f7241'; + const anotherResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: researcherProfileId2, + visible: false, + type: 'profile', + _links: { + self: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId2}` + }, + } + }); + + const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), { + id: 'mockItemUnlinkedToOrcid', + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }] + } + }); + + const mockItemLinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }], + 'dspace.object.owner': [{ + 'value': 'test person', + 'language': null, + 'authority': 'researcher-profile-id', + 'confidence': 600, + 'place': 0 + }], + 'dspace.orcid.authenticated': [{ + 'value': '2022-06-10T15:15:12.952872', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'dspace.orcid.scope': [{ + 'value': '/authenticate', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }, { + 'value': '/read-limited', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 1 + }, { + 'value': '/activities/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 2 + }, { + 'value': '/person/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 3 + }], + 'person.identifier.orcid': [{ + 'value': 'orcid-id', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }] + } + }); + + const disconnectionAllowAdmin = { + uuid: 'orcid.disconnection.allowed-users', + name: 'orcid.disconnection.allowed-users', + values: ['only_admin'] + } as ConfigurationProperty; + + const disconnectionAllowAdminOwner = { + uuid: 'orcid.disconnection.allowed-users', + name: 'orcid.disconnection.allowed-users', + values: ['admin_and_owner'] + } as ConfigurationProperty; + + const authorizeUrl = { + uuid: 'orcid.authorize-url', + name: 'orcid.authorize-url', + values: ['orcid.authorize-url'] + } as ConfigurationProperty; + const appClientId = { + uuid: 'orcid.application-client-id', + name: 'orcid.application-client-id', + values: ['orcid.application-client-id'] + } as ConfigurationProperty; + const orcidScope = { + uuid: 'orcid.scope', + name: 'orcid.scope', + values: ['/authenticate', '/read-limited'] + } as ConfigurationProperty; + + const endpointURL = `https://rest.api/rest/api/profiles`; + const endpointURLWithEmbed = 'https://rest.api/rest/api/profiles?embed=item'; + const sourceUri = `https://rest.api/rest/api/external-source/profile`; + const requestURL = `https://rest.api/rest/api/profiles/${researcherProfileId}`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + const pageInfo = new PageInfo(); + const array = [researcherProfile, anotherResearcherProfile]; + const paginatedList = buildPaginatedList(pageInfo, array); + const researcherProfileRD = createSuccessfulRemoteDataObject(researcherProfile); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + + beforeEach(() => { + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: endpointURL }) + }); + + responseCacheEntry = new RequestEntry(); + responseCacheEntry.request = { href: 'https://rest.api/' } as any; + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + send: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + setStaleByHrefSubstring: jasmine.createSpy('setStaleByHrefSubstring') + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: hot('a|', { + a: researcherProfileRD + }), + buildList: hot('a|', { + a: paginatedListRD + }), + buildFromRequestUUID: hot('a|', { + a: researcherProfileRD + }) + }); + objectCache = {} as ObjectCacheService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const routerStub: any = new RouterMock(); + const itemService = jasmine.createSpyObj('ItemService', { + findByHref: jasmine.createSpy('findByHref') + }); + configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: jasmine.createSpy('findByPropertyName') + }); + + service = new ResearcherProfileService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + http, + routerStub, + comparator, + itemService, + configurationDataService + ); + serviceAsAny = service; + + spyOn((service as any).dataService, 'create').and.callThrough(); + spyOn((service as any).dataService, 'delete').and.callThrough(); + spyOn((service as any).dataService, 'update').and.callThrough(); + spyOn((service as any).dataService, 'findById').and.callThrough(); + spyOn((service as any).dataService, 'findByHref').and.callThrough(); + spyOn((service as any).dataService, 'searchBy').and.callThrough(); + spyOn((service as any).dataService, 'getLinkPath').and.returnValue(observableOf(endpointURL)); + + }); + + describe('findById', () => { + it('should proxy the call to dataservice.findById with eperson UUID', () => { + scheduler.schedule(() => service.findById(researcherProfileId)); + scheduler.flush(); + + expect((service as any).dataService.findById).toHaveBeenCalledWith(researcherProfileId, true, true); + }); + + it('should return a ResearcherProfile object with the given id', () => { + const result = service.findById(researcherProfileId); + const expected = cold('a|', { + a: researcherProfileRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('create', () => { + it('should proxy the call to dataservice.create with eperson UUID', () => { + scheduler.schedule(() => service.create()); + scheduler.flush(); + + expect((service as any).dataService.create).toHaveBeenCalled(); + }); + + it('should return the RemoteData created', () => { + const result = service.create(); + const expected = cold('a|', { + a: researcherProfileRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('delete', () => { + it('should proxy the call to dataservice.delete', () => { + scheduler.schedule(() => service.delete(researcherProfile)); + scheduler.flush(); + + expect((service as any).dataService.delete).toHaveBeenCalledWith(researcherProfile.id); + }); + }); + + describe('findRelatedItemId', () => { + describe('with a related item', () => { + + beforeEach(() => { + (service as any).itemService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfileItem)); + }); + + it('should proxy the call to dataservice.findById with eperson UUID', () => { + scheduler.schedule(() => service.findRelatedItemId(researcherProfile)); + scheduler.flush(); + + expect((service as any).itemService.findByHref).toHaveBeenCalledWith(researcherProfile._links.item.href, false); + }); + + it('should return a ResearcherProfile object with the given id', () => { + const result = service.findRelatedItemId(researcherProfile); + const expected = cold('(a|)', { + a: itemId + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('without a related item', () => { + + beforeEach(() => { + (service as any).itemService.findByHref.and.returnValue(createNoContentRemoteDataObject$()); + }); + + it('should proxy the call to dataservice.findById with eperson UUID', () => { + scheduler.schedule(() => service.findRelatedItemId(researcherProfile)); + scheduler.flush(); + + expect((service as any).itemService.findByHref).toHaveBeenCalledWith(researcherProfile._links.item.href, false); + }); + + it('should not return a ResearcherProfile object with the given id', () => { + const result = service.findRelatedItemId(researcherProfile); + const expected = cold('(a|)', { + a: null + }); + expect(result).toBeObservable(expected); + }); + }); + }); + + describe('setVisibility', () => { + let patchSpy; + beforeEach(() => { + spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + }); + + it('should proxy the call to dataservice.patch', () => { + const replaceOperation: ReplaceOperation = { + path: '/visible', + op: 'replace', + value: true + }; + + scheduler.schedule(() => service.setVisibility(researcherProfile, true)); + scheduler.flush(); + + expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, [replaceOperation]); + }); + }); + + describe('createFromExternalSource', () => { + + beforeEach(() => { + spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + }); + + it('should proxy the call to dataservice.patch', () => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const request = new PostRequest(requestUUID, endpointURLWithEmbed, sourceUri, options); + + scheduler.schedule(() => service.createFromExternalSource(sourceUri)); + scheduler.flush(); + + expect((service as any).requestService.send).toHaveBeenCalledWith(request); + expect((service as any).rdbService.buildFromRequestUUID).toHaveBeenCalledWith(requestUUID, followLink('item')); + + }); + }); + + describe('isLinkedToOrcid', () => { + it('should return true when item has metadata', () => { + const result = service.isLinkedToOrcid(mockItemLinkedToOrcid); + expect(result).toBeTrue(); + }); + + it('should return true when item has no metadata', () => { + const result = service.isLinkedToOrcid(mockItemUnlinkedToOrcid); + expect(result).toBeFalse(); + }); + }); + + describe('onlyAdminCanDisconnectProfileFromOrcid', () => { + it('should return true when property is only_admin', () => { + spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdmin)); + const result = service.onlyAdminCanDisconnectProfileFromOrcid(); + const expected = cold('(a|)', { + a: true + }); + expect(result).toBeObservable(expected); + }); + + it('should return false on faild', () => { + spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$()); + const result = service.onlyAdminCanDisconnectProfileFromOrcid(); + const expected = cold('(a|)', { + a: false + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('ownerCanDisconnectProfileFromOrcid', () => { + it('should return true when property is admin_and_owner', () => { + spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdminOwner)); + const result = service.ownerCanDisconnectProfileFromOrcid(); + const expected = cold('(a|)', { + a: true + }); + expect(result).toBeObservable(expected); + }); + + it('should return false on faild', () => { + spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$()); + const result = service.ownerCanDisconnectProfileFromOrcid(); + const expected = cold('(a|)', { + a: false + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('unlinkOrcid', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile)); + }); + + it('should call patch method properly', () => { + const operations: RemoveOperation[] = [{ + path: '/orcid', + op: 'remove' + }]; + + scheduler.schedule(() => service.unlinkOrcid(mockItemLinkedToOrcid).subscribe()); + scheduler.flush(); + + expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, operations); + }); + }); + + describe('getOrcidAuthorizeUrl', () => { + beforeEach(() => { + (service as any).configurationService.findByPropertyName.and.returnValues( + createSuccessfulRemoteDataObject$(authorizeUrl), + createSuccessfulRemoteDataObject$(appClientId), + createSuccessfulRemoteDataObject$(orcidScope) + ); + }); + + it('should build the url properly', () => { + const result = service.getOrcidAuthorizeUrl(mockItemUnlinkedToOrcid); + const redirectUri = environment.rest.baseUrl + '/api/eperson/orcid/' + mockItemUnlinkedToOrcid.id + '/?url=undefined'; + const url = 'orcid.authorize-url?client_id=orcid.application-client-id&redirect_uri=' + redirectUri + '&response_type=code&scope=/authenticate /read-limited'; + + const expected = cold('(a|)', { + a: url + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('updateByOrcidOperations', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + }); + + it('should call patch method properly', () => { + scheduler.schedule(() => service.updateByOrcidOperations(researcherProfile, []).subscribe()); + scheduler.flush(); + + expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, []); + }); + }); + + describe('getOrcidAuthorizationScopesByItem', () => { + it('should return list of scopes saved in the item', () => { + const orcidScopes = [ + '/authenticate', + '/read-limited', + '/activities/update', + '/person/update' + ]; + const result = service.getOrcidAuthorizationScopesByItem(mockItemLinkedToOrcid); + expect(result).toEqual(orcidScopes); + }); + }); + + describe('getOrcidAuthorizationScopes', () => { + it('should return list of scopes by configuration', () => { + (service as any).configurationService.findByPropertyName.and.returnValue( + createSuccessfulRemoteDataObject$(orcidScope) + ); + const orcidScopes = [ + '/authenticate', + '/read-limited' + ]; + const expected = cold('(a|)', { + a: orcidScopes + }); + const result = service.getOrcidAuthorizationScopes(); + expect(result).toBeObservable(expected); + }); + }); +}); diff --git a/src/app/core/profile/researcher-profile.service.ts b/src/app/core/profile/researcher-profile.service.ts index b7035a921f..f944d53b46 100644 --- a/src/app/core/profile/researcher-profile.service.ts +++ b/src/app/core/profile/researcher-profile.service.ts @@ -2,10 +2,12 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; + import { Store } from '@ngrx/store'; import { Operation, RemoveOperation, ReplaceOperation } from 'fast-json-patch'; -import { combineLatest, Observable, of as observableOf } from 'rxjs'; -import { catchError, find, map, switchMap, tap } from 'rxjs/operators'; +import { combineLatest, Observable } from 'rxjs'; +import { find, map, switchMap } from 'rxjs/operators'; + import { environment } from '../../../environments/environment'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { dataService } from '../cache/builders/build-decorators'; @@ -19,9 +21,9 @@ import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { ConfigurationProperty } from '../shared/configuration-property.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Item } from '../shared/item.model'; import { NoContent } from '../shared/NoContent.model'; import { + getAllCompletedRemoteData, getFinishedRemoteData, getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload @@ -30,8 +32,11 @@ import { ResearcherProfile } from './model/researcher-profile.model'; import { RESEARCHER_PROFILE } from './model/researcher-profile.resource-type'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { PostRequest } from '../data/request.models'; -import { hasValue } from '../../shared/empty.util'; -import {CoreState} from '../core-state.model'; +import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { CoreState } from '../core-state.model'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { Item } from '../shared/item.model'; +import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils'; /** * A private DataService implementation to delegate specific methods to. @@ -60,14 +65,13 @@ class ResearcherProfileServiceImpl extends DataService { @dataService(RESEARCHER_PROFILE) export class ResearcherProfileService { - dataService: ResearcherProfileServiceImpl; + protected dataService: ResearcherProfileServiceImpl; - responseMsToLive: number = 10 * 1000; + protected responseMsToLive: number = 10 * 1000; constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, @@ -75,9 +79,9 @@ export class ResearcherProfileService { protected router: Router, protected comparator: DefaultChangeAnalyzer, protected itemService: ItemDataService, - protected configurationService: ConfigurationDataService ) { + protected configurationService: ConfigurationDataService) { - this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, store, objectCache, halService, + this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); } @@ -86,18 +90,24 @@ export class ResearcherProfileService { * Find the researcher profile with the given uuid. * * @param uuid the profile uuid + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved */ - findById(uuid: string): Observable { - return this.dataService.findById(uuid, false) - .pipe ( getFinishedRemoteData(), - map((remoteData) => remoteData.payload)); + public findById(uuid: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findById(uuid, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( + getAllCompletedRemoteData(), + ); } /** * Create a new researcher profile for the current user. */ - create(): Observable> { - return this.dataService.create( new ResearcherProfile()); + public create(): Observable> { + return this.dataService.create(new ResearcherProfile()); } /** @@ -105,31 +115,38 @@ export class ResearcherProfileService { * * @param researcherProfile the profile to delete */ - delete(researcherProfile: ResearcherProfile): Observable { + public delete(researcherProfile: ResearcherProfile): Observable { return this.dataService.delete(researcherProfile.id).pipe( getFirstCompletedRemoteData(), - tap((response: RemoteData) => { - if (response.isSuccess) { - this.requestService.setStaleByHrefSubstring(researcherProfile._links.self.href); - } - }), map((response: RemoteData) => response.isSuccess) ); } + /** + * Find a researcher profile by its own related item + * + * @param item + */ + public findByRelatedItem(item: Item): Observable> { + const profileId = item.firstMetadata('dspace.object.owner')?.authority; + if (isEmpty(profileId)) { + return createFailedRemoteDataObject$(); + } else { + return this.findById(profileId); + } + } + /** * Find the item id related to the given researcher profile. * * @param researcherProfile the profile to find for */ - findRelatedItemId( researcherProfile: ResearcherProfile ): Observable { - return this.itemService.findByHref(researcherProfile._links.item.href, false) - .pipe (getFirstSucceededRemoteDataPayload(), - catchError((error) => { - console.debug(error); - return observableOf(null); - }), - map((item) => item != null ? item.id : null )); + public findRelatedItemId(researcherProfile: ResearcherProfile): Observable { + const relatedItem$ = researcherProfile.item ? researcherProfile.item : this.itemService.findByHref(researcherProfile._links.item.href, false); + return relatedItem$.pipe( + getFirstCompletedRemoteData(), + map((itemRD: RemoteData) => (itemRD.hasSucceeded && itemRD.payload) ? itemRD.payload.id : null) + ); } /** @@ -138,21 +155,14 @@ export class ResearcherProfileService { * @param researcherProfile the profile to update * @param visible the visibility value to set */ - setVisibility(researcherProfile: ResearcherProfile, visible: boolean): Observable { - + public setVisibility(researcherProfile: ResearcherProfile, visible: boolean): Observable> { const replaceOperation: ReplaceOperation = { path: '/visible', op: 'replace', value: visible }; - return this.patch(researcherProfile, [replaceOperation]).pipe ( - switchMap( ( ) => this.findById(researcherProfile.id)) - ); - } - - patch(researcherProfile: ResearcherProfile, operations: Operation[]): Observable> { - return this.dataService.patch(researcherProfile, operations); + return this.dataService.patch(researcherProfile, [replaceOperation]); } /** @@ -161,7 +171,7 @@ export class ResearcherProfileService { * @param item the item to check * @returns the check result */ - isLinkedToOrcid(item: Item): boolean { + public isLinkedToOrcid(item: Item): boolean { return item.hasMetadata('dspace.orcid.authenticated'); } @@ -170,9 +180,11 @@ export class ResearcherProfileService { * * @returns the check result */ - onlyAdminCanDisconnectProfileFromOrcid(): Observable { + public onlyAdminCanDisconnectProfileFromOrcid(): Observable { return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe( - map((property) => property.values.map( (value) => value.toLowerCase()).includes('only_admin')) + map((propertyRD: RemoteData) => { + return propertyRD.hasSucceeded && propertyRD.payload.values.map((value) => value.toLowerCase()).includes('only_admin'); + }) ); } @@ -181,25 +193,10 @@ export class ResearcherProfileService { * * @returns the check result */ - ownerCanDisconnectProfileFromOrcid(): Observable { + public ownerCanDisconnectProfileFromOrcid(): Observable { return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe( - map((property) => { - const values = property.values.map( (value) => value.toLowerCase()); - return values.includes('only_owner') || values.includes('admin_and_owner'); - }) - ); - } - - /** - * Returns true if the admin users can disconnect a researcher profile from ORCID. - * - * @returns the check result - */ - adminCanDisconnectProfileFromOrcid(): Observable { - return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe( - map((property) => { - const values = property.values.map( (value) => value.toLowerCase()); - return values.includes('only_admin') || values.includes('admin_and_owner'); + map((propertyRD: RemoteData) => { + return propertyRD.hasSucceeded && propertyRD.payload.values.map( (value) => value.toLowerCase()).includes('admin_and_owner'); }) ); } @@ -207,20 +204,25 @@ export class ResearcherProfileService { /** * If the given item represents a profile unlink it from ORCID. */ - unlinkOrcid(item: Item): Observable> { - + public unlinkOrcid(item: Item): Observable> { const operations: RemoveOperation[] = [{ path:'/orcid', op:'remove' }]; return this.findById(item.firstMetadata('dspace.object.owner').authority).pipe( - switchMap((profile) => this.patch(profile, operations)), + getFirstCompletedRemoteData(), + switchMap((profileRD) => this.dataService.patch(profileRD.payload, operations)), getFinishedRemoteData() ); } - getOrcidAuthorizeUrl(profile: Item): Observable { + /** + * Build and return the url to authenticate with orcid + * + * @param profile + */ + public getOrcidAuthorizeUrl(profile: Item): Observable { return combineLatest([ this.configurationService.findByPropertyName('orcid.authorize-url').pipe(getFirstSucceededRemoteDataPayload()), this.configurationService.findByPropertyName('orcid.application-client-id').pipe(getFirstSucceededRemoteDataPayload()), @@ -248,18 +250,47 @@ export class ResearcherProfileService { href$.pipe( find((href: string) => hasValue(href)), - map((href: string) => { - const request = new PostRequest(requestId, href, sourceUri, options); - this.requestService.send(request); - }) - ).subscribe(); + map((href: string) => this.dataService.buildHrefWithParams(href, [], followLink('item'))) + ).subscribe((endpoint: string) => { + const request = new PostRequest(requestId, endpoint, sourceUri, options); + this.requestService.send(request); + }); - return this.rdbService.buildFromRequestUUID(requestId); + return this.rdbService.buildFromRequestUUID(requestId, followLink('item')); } - private getOrcidDisconnectionAllowedUsersConfiguration(): Observable { + /** + * Update researcher profile by patch orcid operation + * + * @param researcherProfile + * @param operations + */ + public updateByOrcidOperations(researcherProfile: ResearcherProfile, operations: Operation[]): Observable> { + return this.dataService.patch(researcherProfile, operations); + } + + /** + * Return all orcid authorization scopes saved in the given item + * + * @param item + */ + public getOrcidAuthorizationScopesByItem(item: Item): string[] { + return isNotEmpty(item) ? item.allMetadataValues('dspace.orcid.scope') : []; + } + + /** + * Return all orcid authorization scopes available by configuration + */ + public getOrcidAuthorizationScopes(): Observable { + return this.configurationService.findByPropertyName('orcid.scope').pipe( + getFirstCompletedRemoteData(), + map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []) + ); + } + + private getOrcidDisconnectionAllowedUsersConfiguration(): Observable> { return this.configurationService.findByPropertyName('orcid.disconnection.allowed-users').pipe( - getFirstSucceededRemoteDataPayload() + getFirstCompletedRemoteData() ); } diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index db52a0547e..e9dfbe7e2c 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -386,6 +386,10 @@ describe('RegistryService', () => { result = registryService.deleteMetadataSchema(mockSchemasList[0].id); }); + it('should defer to MetadataSchemaDataService.delete', () => { + expect(metadataSchemaService.delete).toHaveBeenCalledWith(`${mockSchemasList[0].id}`); + }); + it('should return a successful response', () => { result.subscribe((response: RemoteData) => { expect(response.hasSucceeded).toBe(true); @@ -400,6 +404,10 @@ describe('RegistryService', () => { result = registryService.deleteMetadataField(mockFieldsList[0].id); }); + it('should defer to MetadataFieldDataService.delete', () => { + expect(metadataFieldService.delete).toHaveBeenCalledWith(`${mockFieldsList[0].id}`); + }); + it('should return a successful response', () => { result.subscribe((response: RemoteData) => { expect(response.hasSucceeded).toBe(true); diff --git a/src/app/core/reload/reload.guard.spec.ts b/src/app/core/reload/reload.guard.spec.ts index 317245bafa..6146f51572 100644 --- a/src/app/core/reload/reload.guard.spec.ts +++ b/src/app/core/reload/reload.guard.spec.ts @@ -1,13 +1,17 @@ -import { ReloadGuard } from './reload.guard'; import { Router } from '@angular/router'; +import { AppConfig } from '../../../config/app-config.interface'; +import { DefaultAppConfig } from '../../../config/default-app-config'; +import { ReloadGuard } from './reload.guard'; describe('ReloadGuard', () => { let guard: ReloadGuard; let router: Router; + let appConfig: AppConfig; beforeEach(() => { router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']); - guard = new ReloadGuard(router); + appConfig = new DefaultAppConfig(); + guard = new ReloadGuard(router, appConfig); }); describe('canActivate', () => { @@ -27,7 +31,7 @@ describe('ReloadGuard', () => { it('should create a UrlTree with the redirect URL', () => { guard.canActivate(route, undefined); - expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl); + expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl.substring(1)); }); }); diff --git a/src/app/core/reload/reload.guard.ts b/src/app/core/reload/reload.guard.ts index 78f9dcf642..1e99a5687a 100644 --- a/src/app/core/reload/reload.guard.ts +++ b/src/app/core/reload/reload.guard.ts @@ -1,5 +1,6 @@ +import { Inject, Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; -import { Injectable } from '@angular/core'; +import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; /** @@ -8,7 +9,10 @@ import { isNotEmpty } from '../../shared/empty.util'; */ @Injectable() export class ReloadGuard implements CanActivate { - constructor(private router: Router) { + constructor( + private router: Router, + @Inject(APP_CONFIG) private appConfig: AppConfig, + ) { } /** @@ -18,7 +22,10 @@ export class ReloadGuard implements CanActivate { */ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree { if (isNotEmpty(route.queryParams.redirect)) { - return this.router.parseUrl(route.queryParams.redirect); + const url = route.queryParams.redirect.startsWith(this.appConfig.ui.nameSpace) + ? route.queryParams.redirect.substring(this.appConfig.ui.nameSpace.length) + : route.queryParams.redirect; + return this.router.parseUrl(url); } else { return this.router.createUrlTree(['home']); } diff --git a/src/app/core/resource-policy/resource-policy.service.spec.ts b/src/app/core/resource-policy/resource-policy.service.spec.ts index 59316c0098..b788a16520 100644 --- a/src/app/core/resource-policy/resource-policy.service.spec.ts +++ b/src/app/core/resource-policy/resource-policy.service.spec.ts @@ -19,6 +19,8 @@ import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils import { RestResponse } from '../cache/response.models'; import { RequestEntry } from '../data/request-entry.model'; import { FindListOptions } from '../data/find-list-options.model'; +import { EPersonDataService } from '../eperson/eperson-data.service'; +import { GroupDataService } from '../eperson/group-data.service'; describe('ResourcePolicyService', () => { let scheduler: TestScheduler; @@ -28,6 +30,8 @@ describe('ResourcePolicyService', () => { let objectCache: ObjectCacheService; let halService: HALEndpointService; let responseCacheEntry: RequestEntry; + let ePersonService: EPersonDataService; + let groupService: GroupDataService; const resourcePolicy: any = { id: '1', @@ -88,6 +92,8 @@ describe('ResourcePolicyService', () => { const resourcePolicyRD = createSuccessfulRemoteDataObject(resourcePolicy); const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + const ePersonEndpoint = 'EPERSON_EP'; + beforeEach(() => { scheduler = getTestScheduler(); @@ -105,6 +111,7 @@ describe('ResourcePolicyService', () => { removeByHrefSubstring: {}, getByHref: observableOf(responseCacheEntry), getByUUID: observableOf(responseCacheEntry), + setStaleByHrefSubstring: {}, }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: hot('a|', { @@ -117,6 +124,11 @@ describe('ResourcePolicyService', () => { a: resourcePolicyRD }) }); + ePersonService = jasmine.createSpyObj('ePersonService', { + getBrowseEndpoint: hot('a', { + a: ePersonEndpoint + }), + }); objectCache = {} as ObjectCacheService; const notificationsService = {} as NotificationsService; const http = {} as HttpClient; @@ -129,7 +141,9 @@ describe('ResourcePolicyService', () => { halService, notificationsService, http, - comparator + comparator, + ePersonService, + groupService ); spyOn((service as any).dataService, 'create').and.callThrough(); @@ -320,4 +334,17 @@ describe('ResourcePolicyService', () => { expect(result).toBeObservable(expected); }); }); + + describe('updateTarget', () => { + it('should create a new PUT request for eperson', () => { + const targetType = 'eperson'; + + const result = service.updateTarget(resourcePolicyId, requestURL, epersonUUID, targetType); + const expected = cold('a|', { + a: resourcePolicyRD + }); + expect(result).toBeObservable(expected); + }); + }); + }); diff --git a/src/app/core/resource-policy/resource-policy.service.ts b/src/app/core/resource-policy/resource-policy.service.ts index 065e58c53d..b0b7a6bd97 100644 --- a/src/app/core/resource-policy/resource-policy.service.ts +++ b/src/app/core/resource-policy/resource-policy.service.ts @@ -1,6 +1,6 @@ /* eslint-disable max-classes-per-file */ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; @@ -23,11 +23,19 @@ import { PaginatedList } from '../data/paginated-list.model'; import { ActionType } from './models/action-type.model'; import { RequestParam } from '../cache/models/request-param.model'; import { isNotEmpty } from '../../shared/empty.util'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { NoContent } from '../shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../shared/operators'; import { CoreState } from '../core-state.model'; import { FindListOptions } from '../data/find-list-options.model'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { PutRequest } from '../data/request.models'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { ResponseParsingService } from '../data/parsing.service'; +import { StatusCodeOnlyResponseParsingService } from '../data/status-code-only-response-parsing.service'; +import { HALLink } from '../shared/hal-link.model'; +import { EPersonDataService } from '../eperson/eperson-data.service'; +import { GroupDataService } from '../eperson/group-data.service'; /** @@ -44,7 +52,8 @@ class DataServiceImpl extends DataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: ChangeAnalyzer) { + protected comparator: ChangeAnalyzer, + ) { super(); } @@ -68,7 +77,10 @@ export class ResourcePolicyService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DefaultChangeAnalyzer) { + protected comparator: DefaultChangeAnalyzer, + protected ePersonService: EPersonDataService, + protected groupService: GroupDataService, + ) { this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); } @@ -221,4 +233,44 @@ export class ResourcePolicyService { return this.dataService.searchBy(this.searchByResourceMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + /** + * Update the target of the resource policy + * @param resourcePolicyId the ID of the resource policy + * @param resourcePolicyHref the link to the resource policy + * @param targetUUID the UUID of the target to which the permission is being granted + * @param targetType the type of the target (eperson or group) to which the permission is being granted + */ + updateTarget(resourcePolicyId: string, resourcePolicyHref: string, targetUUID: string, targetType: string): Observable> { + + const targetService = targetType === 'eperson' ? this.ePersonService : this.groupService; + + const targetEndpoint$ = targetService.getBrowseEndpoint().pipe( + take(1), + map((endpoint: string) =>`${endpoint}/${targetUUID}`), + ); + + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + + const requestId = this.requestService.generateRequestId(); + + this.requestService.setStaleByHrefSubstring(`${this.dataService.getLinkPath()}/${resourcePolicyId}/${targetType}`); + + targetEndpoint$.subscribe((targetEndpoint) => { + const resourceEndpoint = resourcePolicyHref + '/' + targetType; + const request = new PutRequest(requestId, resourceEndpoint, targetEndpoint, options); + Object.assign(request, { + getResponseParser(): GenericConstructor { + return StatusCodeOnlyResponseParsingService; + } + }); + this.requestService.send(request); + }); + + return this.rdbService.buildFromRequestUUID(requestId); + + } + } diff --git a/src/app/core/services/api.service.ts b/src/app/core/services/api.service.ts deleted file mode 100644 index 4f8474e0c1..0000000000 --- a/src/app/core/services/api.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { throwError as observableThrowError } from 'rxjs'; -import { catchError } from 'rxjs/operators'; - -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; - -@Injectable() -export class ApiService { - constructor(public _http: HttpClient) { - - } - - /** - * whatever domain/feature method name - */ - get(url: string, options?: any) { - return this._http.get(url, options).pipe( - catchError((err) => { - console.log('Error: ', err); - return observableThrowError(err); - })); - } - -} diff --git a/src/app/core/submission/models/sherpa-policies-details.model.ts b/src/app/core/submission/models/sherpa-policies-details.model.ts new file mode 100644 index 0000000000..af4d4a5890 --- /dev/null +++ b/src/app/core/submission/models/sherpa-policies-details.model.ts @@ -0,0 +1,89 @@ +/** + * An interface to represent an access condition. + */ +export class SherpaPoliciesDetailsObject { + + /** + * The sherpa policies error + */ + error: boolean; + + /** + * The sherpa policies journal details + */ + journals: Journal[]; + + /** + * The sherpa policies message + */ + message: string; + + /** + * The sherpa policies metadata + */ + metadata: Metadata; + +} + + +export interface Metadata { + id: number; + uri: string; + dateCreated: string; + dateModified: string; + inDOAJ: boolean; + publiclyVisible: boolean; +} + + +export interface Journal { + titles: string[]; + url: string; + issns: string[]; + romeoPub: string; + zetoPub: string; + inDOAJ: boolean; + publisher: Publisher; + publishers: Publisher[]; + policies: Policy[]; +} + +export interface Publisher { + name: string; + relationshipType: string; + country: string; + uri: string; + identifier: string; + paidAccessDescription: string; + paidAccessUrl: string; + publicationCount: number; +} + +export interface Policy { + id: number; + openAccessPermitted: boolean; + uri: string; + internalMoniker: string; + permittedVersions: PermittedVersions[]; + urls: any; + publicationCount: number; + preArchiving: string; + postArchiving: string; + pubArchiving: string; + openAccessProhibited: boolean; +} + +export interface PermittedVersions { + articleVersion: string; + option: number; + conditions: string[]; + prerequisites: string[]; + locations: string[]; + licenses: string[]; + embargo: Embargo; +} + +export interface Embargo { + units: any; + amount: any; +} diff --git a/src/app/core/submission/models/workspaceitem-section-sherpa-policies.model.ts b/src/app/core/submission/models/workspaceitem-section-sherpa-policies.model.ts new file mode 100644 index 0000000000..c57beadbb9 --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-sherpa-policies.model.ts @@ -0,0 +1,22 @@ +import { SherpaPoliciesDetailsObject } from './sherpa-policies-details.model'; + +/** + * An interface to represent the submission's item accesses condition. + */ +export interface WorkspaceitemSectionSherpaPoliciesObject { + + /** + * The access condition id + */ + id: string; + + /** + * The sherpa policies retrievalTime + */ + retrievalTime: string; + + /** + * The sherpa policies details + */ + sherpaResponse: SherpaPoliciesDetailsObject; +} diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts index 084da3f088..1112d740ed 100644 --- a/src/app/core/submission/models/workspaceitem-sections.model.ts +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -3,6 +3,7 @@ import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.mod import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model'; import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model'; import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model'; +import { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model'; /** * An interface to represent submission's section object. @@ -21,4 +22,5 @@ export type WorkspaceitemSectionDataType | WorkspaceitemSectionLicenseObject | WorkspaceitemSectionCcLicenseObject | WorkspaceitemSectionAccessesObject + | WorkspaceitemSectionSherpaPoliciesObject | string; diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html index c8a9ea9e28..6d9cfe10c4 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html @@ -5,7 +5,7 @@ [innerHTML]="name"> + [innerHTML]="name">
- + - +
@@ -21,18 +21,10 @@ [fields]="['person.email']" [label]="'person.page.email'"> - - - - - - - -
+ +
{{"item.page.link.full" | translate}} diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts b/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts index 93b3cf208d..efbc48a209 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts @@ -17,24 +17,12 @@ const mockItem: Item = Object.assign(new Item(), { value: 'fake@email.com' } ], - // 'person.identifier.orcid': [ - // { - // language: 'en_US', - // value: 'ORCID-1' - // } - // ], 'person.birthDate': [ { language: 'en_US', value: '1993' } ], - // 'person.identifier.staffid': [ - // { - // language: 'en_US', - // value: '1' - // } - // ], 'person.jobTitle': [ { language: 'en_US', @@ -62,4 +50,42 @@ const mockItem: Item = Object.assign(new Item(), { } }); -describe('PersonComponent', getItemPageFieldsTest(mockItem, PersonComponent)); +const mockItemWithTitle: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + metadata: { + 'person.email': [ + { + language: 'en_US', + value: 'fake@email.com' + } + ], + 'person.birthDate': [ + { + language: 'en_US', + value: '1993' + } + ], + 'person.jobTitle': [ + { + language: 'en_US', + value: 'Developer' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Doe, John' + } + ] + }, + relationships: createRelationshipsObservable(), + _links: { + self : { + href: 'item-href' + } + } +}); + +describe('PersonComponent with family and given names', getItemPageFieldsTest(mockItem, PersonComponent)); + +describe('PersonComponent with dc.title', getItemPageFieldsTest(mockItemWithTitle, PersonComponent)); diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts index 33db18681f..27fdd2ab15 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts @@ -1,20 +1,10 @@ -import {Component, OnInit} from '@angular/core'; +import { Component } from '@angular/core'; import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component'; import { ViewMode } from '../../../../core/shared/view-mode.model'; -import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; -import {MetadataValue} from '../../../../core/shared/metadata.models'; -import {FeatureID} from '../../../../core/data/feature-authorization/feature-id'; -import {mergeMap, take} from 'rxjs/operators'; -import {getFirstSucceededRemoteData} from '../../../../core/shared/operators'; -import {RemoteData} from '../../../../core/data/remote-data'; -import {ResearcherProfile} from '../../../../core/profile/model/researcher-profile.model'; -import {isNotUndefined} from '../../../../shared/empty.util'; -import {BehaviorSubject, Observable} from 'rxjs'; -import {RouteService} from '../../../../core/services/route.service'; -import {AuthorizationDataService} from '../../../../core/data/feature-authorization/authorization-data.service'; -import {ResearcherProfileService} from '../../../../core/profile/researcher-profile.service'; -import {NotificationsService} from '../../../../shared/notifications/notifications.service'; -import {TranslateService} from '@ngx-translate/core'; +import { + listableObjectComponent +} from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { MetadataValue } from '../../../../core/shared/metadata.models'; @listableObjectComponent('Person', ViewMode.StandalonePage) @Component({ @@ -25,76 +15,25 @@ import {TranslateService} from '@ngx-translate/core'; /** * The component for displaying metadata and relations of an item of the type Person */ -export class PersonComponent extends ItemComponent implements OnInit { - - claimable$: BehaviorSubject = new BehaviorSubject(false); - - constructor(protected routeService: RouteService, - protected authorizationService: AuthorizationDataService, - protected notificationsService: NotificationsService, - protected translate: TranslateService, - protected researcherProfileService: ResearcherProfileService) { - super(routeService); - } - - ngOnInit(): void { - super.ngOnInit(); - - this.authorizationService.isAuthorized(FeatureID.CanClaimItem, this.object._links.self.href).pipe( - take(1) - ).subscribe((isAuthorized: boolean) => { - this.claimable$.next(isAuthorized); - }); - - } - - /** - * Create a new researcher profile claiming the current item. - */ - claim() { - this.researcherProfileService.createFromExternalSource(this.object._links.self.href).pipe( - getFirstSucceededRemoteData(), - mergeMap((rd: RemoteData) => { - return this.researcherProfileService.findRelatedItemId(rd.payload); - })) - .subscribe((id: string) => { - if (isNotUndefined(id)) { - this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'), - this.translate.get('researcherprofile.success.claim.body')); - this.claimable$.next(false); - } else { - this.notificationsService.error( - this.translate.get('researcherprofile.error.claim.title'), - this.translate.get('researcherprofile.error.claim.body')); - } - }); - } - - /** - * Returns true if the item is claimable, false otherwise. - */ - isClaimable(): Observable { - return this.claimable$; - } +export class PersonComponent extends ItemComponent { /** * Returns the metadata values to be used for the page title. */ - getTitleMetadataValues(): MetadataValue[]{ + getTitleMetadataValues(): MetadataValue[] { const metadataValues = []; const familyName = this.object?.firstMetadata('person.familyName'); const givenName = this.object?.firstMetadata('person.givenName'); const title = this.object?.firstMetadata('dc.title'); - if (familyName){ + if (familyName) { metadataValues.push(familyName); } - if (givenName){ + if (givenName) { metadataValues.push(givenName); } - if (metadataValues.length === 0 && title){ + if (metadataValues.length === 0 && title) { metadataValues.push(title); } return metadataValues; } - } diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.html b/src/app/health-page/health-info/health-info-component/health-info-component.component.html new file mode 100644 index 0000000000..dbaaa7a6b6 --- /dev/null +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.html @@ -0,0 +1,27 @@ +
+
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+ +

{{ getPropertyLabel(entry.key) | titlecase }} : {{entry.value}}

+
+
diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.scss b/src/app/health-page/health-info/health-info-component/health-info-component.component.scss new file mode 100644 index 0000000000..a6f0e73413 --- /dev/null +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts b/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts new file mode 100644 index 0000000000..b4532415b8 --- /dev/null +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts @@ -0,0 +1,82 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; + +import { HealthInfoComponentComponent } from './health-info-component.component'; +import { HealthInfoComponentOne, HealthInfoComponentTwo } from '../../../shared/mocks/health-endpoint.mocks'; +import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; + +describe('HealthInfoComponentComponent', () => { + let component: HealthInfoComponentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + NgbCollapseModule, + NoopAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ + HealthInfoComponentComponent, + ObjNgFor + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthInfoComponentComponent); + component = fixture.componentInstance; + }); + + describe('when has nested components', () => { + beforeEach(() => { + component.healthInfoComponentName = 'App'; + component.healthInfoComponent = HealthInfoComponentOne; + component.isCollapsed = false; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display property', () => { + const properties = fixture.debugElement.queryAll(By.css('[data-test="property"]')); + expect(properties.length).toBe(14); + const components = fixture.debugElement.queryAll(By.css('[data-test="info-component"]')); + expect(components.length).toBe(4); + }); + + }); + + describe('when has plain properties', () => { + beforeEach(() => { + component.healthInfoComponentName = 'Java'; + component.healthInfoComponent = HealthInfoComponentTwo; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display property', () => { + const property = fixture.debugElement.queryAll(By.css('[data-test="property"]')); + expect(property.length).toBe(1); + }); + + }); +}); diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.ts b/src/app/health-page/health-info/health-info-component/health-info-component.component.ts new file mode 100644 index 0000000000..d2cb393f09 --- /dev/null +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.ts @@ -0,0 +1,46 @@ +import { Component, Input } from '@angular/core'; + +import { HealthInfoComponent } from '../../models/health-component.model'; +import { HealthComponentComponent } from '../../health-panel/health-component/health-component.component'; + +/** + * Shows a health info object + */ +@Component({ + selector: 'ds-health-info-component', + templateUrl: './health-info-component.component.html', + styleUrls: ['./health-info-component.component.scss'] +}) +export class HealthInfoComponentComponent extends HealthComponentComponent { + + /** + * The HealthInfoComponent object to display + */ + @Input() healthInfoComponent: HealthInfoComponent|string; + + /** + * The HealthInfoComponent object name + */ + @Input() healthInfoComponentName: string; + + /** + * A boolean representing if div should start collapsed + */ + @Input() isNested = false; + + /** + * A boolean representing if div should start collapsed + */ + public isCollapsed = false; + + /** + * Check if the HealthInfoComponent is has only string property or contains object + * + * @param entry The HealthInfoComponent to check + * @return boolean + */ + isPlainProperty(entry: HealthInfoComponent | string): boolean { + return typeof entry === 'string'; + } + +} diff --git a/src/app/health-page/health-info/health-info.component.html b/src/app/health-page/health-info/health-info.component.html new file mode 100644 index 0000000000..4bafcaa2d8 --- /dev/null +++ b/src/app/health-page/health-info/health-info.component.html @@ -0,0 +1,25 @@ + + + + +
+ +
+ +
+ + +
+
+
+
+ + + +
+
+
diff --git a/src/app/health-page/health-info/health-info.component.scss b/src/app/health-page/health-info/health-info.component.scss new file mode 100644 index 0000000000..a6f0e73413 --- /dev/null +++ b/src/app/health-page/health-info/health-info.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-info/health-info.component.spec.ts b/src/app/health-page/health-info/health-info.component.spec.ts new file mode 100644 index 0000000000..5a9b8bf0aa --- /dev/null +++ b/src/app/health-page/health-info/health-info.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HealthInfoComponent } from './health-info.component'; +import { HealthInfoResponseObj } from '../../shared/mocks/health-endpoint.mocks'; +import { ObjNgFor } from '../../shared/utils/object-ngfor.pipe'; +import { By } from '@angular/platform-browser'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; + +describe('HealthInfoComponent', () => { + let component: HealthInfoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + NgbAccordionModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ + HealthInfoComponent, + ObjNgFor + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthInfoComponent); + component = fixture.componentInstance; + component.healthInfoResponse = HealthInfoResponseObj; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create info component properly', () => { + const components = fixture.debugElement.queryAll(By.css('[data-test="info-component"]')); + expect(components.length).toBe(3); + }); +}); diff --git a/src/app/health-page/health-info/health-info.component.ts b/src/app/health-page/health-info/health-info.component.ts new file mode 100644 index 0000000000..186d00299c --- /dev/null +++ b/src/app/health-page/health-info/health-info.component.ts @@ -0,0 +1,46 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { HealthInfoResponse } from '../models/health-component.model'; + +/** + * A component to render a "health-info component" object. + * + * Note that the word "component" in "health-info component" doesn't refer to Angular use of the term + * but rather to the components used in the response of the health endpoint of Spring's Actuator + * API. + */ +@Component({ + selector: 'ds-health-info', + templateUrl: './health-info.component.html', + styleUrls: ['./health-info.component.scss'] +}) +export class HealthInfoComponent implements OnInit { + + @Input() healthInfoResponse: HealthInfoResponse; + + /** + * The first active panel id + */ + activeId: string; + + constructor(private translate: TranslateService) { + } + + ngOnInit(): void { + this.activeId = Object.keys(this.healthInfoResponse)[0]; + } + + /** + * Return translated label if exist for the given property + * + * @param panelKey + */ + public getPanelLabel(panelKey: string): string { + const translationKey = `health-page.section-info.${panelKey}.title`; + const translation = this.translate.instant(translationKey); + + return (translation === translationKey) ? panelKey : translation; + } +} diff --git a/src/app/health-page/health-page.component.html b/src/app/health-page/health-page.component.html new file mode 100644 index 0000000000..8083389e1b --- /dev/null +++ b/src/app/health-page/health-page.component.html @@ -0,0 +1,27 @@ +
+ + diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.scss b/src/app/health-page/health-page.component.scss similarity index 100% rename from src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.scss rename to src/app/health-page/health-page.component.scss diff --git a/src/app/health-page/health-page.component.spec.ts b/src/app/health-page/health-page.component.spec.ts new file mode 100644 index 0000000000..f3847ab092 --- /dev/null +++ b/src/app/health-page/health-page.component.spec.ts @@ -0,0 +1,72 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { By } from '@angular/platform-browser'; + +import { of } from 'rxjs'; +import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { HealthPageComponent } from './health-page.component'; +import { HealthService } from './health.service'; +import { HealthInfoResponseObj, HealthResponseObj } from '../shared/mocks/health-endpoint.mocks'; +import { RawRestResponse } from '../core/dspace-rest/raw-rest-response.model'; +import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; + +describe('HealthPageComponent', () => { + let component: HealthPageComponent; + let fixture: ComponentFixture; + + const healthService = jasmine.createSpyObj('healthDataService', { + getHealth: jasmine.createSpy('getHealth'), + getInfo: jasmine.createSpy('getInfo'), + }); + + const healthRestResponse$ = of({ + payload: HealthResponseObj, + statusCode: 200, + statusText: 'OK' + } as RawRestResponse); + + const healthInfoRestResponse$ = of({ + payload: HealthInfoResponseObj, + statusCode: 200, + statusText: 'OK' + } as RawRestResponse); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + NgbNavModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ HealthPageComponent ], + providers: [ + { provide: HealthService, useValue: healthService } + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthPageComponent); + component = fixture.componentInstance; + healthService.getHealth.and.returnValue(healthRestResponse$); + healthService.getInfo.and.returnValue(healthInfoRestResponse$); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create nav items properly', () => { + const navItems = fixture.debugElement.queryAll(By.css('li.nav-item')); + expect(navItems.length).toBe(2); + }); +}); diff --git a/src/app/health-page/health-page.component.ts b/src/app/health-page/health-page.component.ts new file mode 100644 index 0000000000..aa7bd7cba4 --- /dev/null +++ b/src/app/health-page/health-page.component.ts @@ -0,0 +1,66 @@ +import { Component, OnInit } from '@angular/core'; + +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { HealthService } from './health.service'; +import { HealthInfoResponse, HealthResponse } from './models/health-component.model'; + +@Component({ + selector: 'ds-health-page', + templateUrl: './health-page.component.html', + styleUrls: ['./health-page.component.scss'] +}) +export class HealthPageComponent implements OnInit { + + /** + * Health info endpoint response + */ + healthInfoResponse: BehaviorSubject = new BehaviorSubject(null); + + /** + * Health endpoint response + */ + healthResponse: BehaviorSubject = new BehaviorSubject(null); + + /** + * Represent if the response from health status endpoint is already retrieved or not + */ + healthResponseInitialised: BehaviorSubject = new BehaviorSubject(false); + + /** + * Represent if the response from health info endpoint is already retrieved or not + */ + healthInfoResponseInitialised: BehaviorSubject = new BehaviorSubject(false); + + constructor(private healthDataService: HealthService) { + } + + /** + * Retrieve responses from rest + */ + ngOnInit(): void { + this.healthDataService.getHealth().pipe(take(1)).subscribe({ + next: (data: any) => { + this.healthResponse.next(data.payload); + this.healthResponseInitialised.next(true); + }, + error: () => { + this.healthResponse.next(null); + this.healthResponseInitialised.next(true); + } + }); + + this.healthDataService.getInfo().pipe(take(1)).subscribe({ + next: (data: any) => { + this.healthInfoResponse.next(data.payload); + this.healthInfoResponseInitialised.next(true); + }, + error: () => { + this.healthInfoResponse.next(null); + this.healthInfoResponseInitialised.next(true); + } + }); + + } +} diff --git a/src/app/health-page/health-page.module.ts b/src/app/health-page/health-page.module.ts new file mode 100644 index 0000000000..02a6a91a5f --- /dev/null +++ b/src/app/health-page/health-page.module.ts @@ -0,0 +1,35 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; + +import { HealthPageRoutingModule } from './health-page.routing.module'; +import { HealthPanelComponent } from './health-panel/health-panel.component'; +import { HealthStatusComponent } from './health-panel/health-status/health-status.component'; +import { SharedModule } from '../shared/shared.module'; +import { HealthPageComponent } from './health-page.component'; +import { HealthComponentComponent } from './health-panel/health-component/health-component.component'; +import { HealthInfoComponent } from './health-info/health-info.component'; +import { HealthInfoComponentComponent } from './health-info/health-info-component/health-info-component.component'; + + +@NgModule({ + imports: [ + CommonModule, + HealthPageRoutingModule, + NgbModule, + SharedModule, + TranslateModule + ], + declarations: [ + HealthPageComponent, + HealthPanelComponent, + HealthStatusComponent, + HealthComponentComponent, + HealthInfoComponent, + HealthInfoComponentComponent, + ] +}) +export class HealthPageModule { +} diff --git a/src/app/health-page/health-page.routing.module.ts b/src/app/health-page/health-page.routing.module.ts new file mode 100644 index 0000000000..82d541dc31 --- /dev/null +++ b/src/app/health-page/health-page.routing.module.ts @@ -0,0 +1,28 @@ +import { RouterModule } from '@angular/router'; +import { NgModule } from '@angular/core'; + +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { HealthPageComponent } from './health-page.component'; +import { + SiteAdministratorGuard +} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + resolve: { breadcrumb: I18nBreadcrumbResolver }, + data: { + breadcrumbKey: 'health', + title: 'health-page.title', + }, + canActivate: [SiteAdministratorGuard], + component: HealthPageComponent + } + ]) + ] +}) +export class HealthPageRoutingModule { + +} diff --git a/src/app/health-page/health-panel/health-component/health-component.component.html b/src/app/health-page/health-panel/health-component/health-component.component.html new file mode 100644 index 0000000000..1f29c8c9fc --- /dev/null +++ b/src/app/health-page/health-panel/health-component/health-component.component.html @@ -0,0 +1,30 @@ + +
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+
+ +
+

{{ getPropertyLabel(item.key) | titlecase }} : {{item.value}}

+
+
+ + + diff --git a/src/app/health-page/health-panel/health-component/health-component.component.scss b/src/app/health-page/health-panel/health-component/health-component.component.scss new file mode 100644 index 0000000000..a6f0e73413 --- /dev/null +++ b/src/app/health-page/health-panel/health-component/health-component.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-panel/health-component/health-component.component.spec.ts b/src/app/health-page/health-panel/health-component/health-component.component.spec.ts new file mode 100644 index 0000000000..a8ec2b65e0 --- /dev/null +++ b/src/app/health-page/health-panel/health-component/health-component.component.spec.ts @@ -0,0 +1,85 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; + +import { HealthComponentComponent } from './health-component.component'; +import { HealthComponentOne, HealthComponentTwo } from '../../../shared/mocks/health-endpoint.mocks'; +import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; + +describe('HealthComponentComponent', () => { + let component: HealthComponentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + NgbCollapseModule, + NoopAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ + HealthComponentComponent, + ObjNgFor + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthComponentComponent); + component = fixture.componentInstance; + }); + + describe('when has nested components', () => { + beforeEach(() => { + component.healthComponentName = 'db'; + component.healthComponent = HealthComponentOne; + component.isCollapsed = false; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create collapsible divs properly', () => { + const collapseDivs = fixture.debugElement.queryAll(By.css('[data-test="collapse"]')); + expect(collapseDivs.length).toBe(2); + const detailsDivs = fixture.debugElement.queryAll(By.css('[data-test="details"]')); + expect(detailsDivs.length).toBe(6); + }); + }); + + describe('when has details', () => { + beforeEach(() => { + component.healthComponentName = 'geoIp'; + component.healthComponent = HealthComponentTwo; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create detail divs properly', () => { + const detailsDivs = fixture.debugElement.queryAll(By.css('[data-test="details"]')); + expect(detailsDivs.length).toBe(1); + const collapseDivs = fixture.debugElement.queryAll(By.css('[data-test="collapse"]')); + expect(collapseDivs.length).toBe(0); + }); + }); +}); diff --git a/src/app/health-page/health-panel/health-component/health-component.component.ts b/src/app/health-page/health-panel/health-component/health-component.component.ts new file mode 100644 index 0000000000..e212a07289 --- /dev/null +++ b/src/app/health-page/health-panel/health-component/health-component.component.ts @@ -0,0 +1,53 @@ +import { Component, Input } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { HealthComponent } from '../../models/health-component.model'; +import { AlertType } from '../../../shared/alert/aletr-type'; + +/** + * A component to render a "health component" object. + * + * Note that the word "component" in "health component" doesn't refer to Angular use of the term + * but rather to the components used in the response of the health endpoint of Spring's Actuator + * API. + */ +@Component({ + selector: 'ds-health-component', + templateUrl: './health-component.component.html', + styleUrls: ['./health-component.component.scss'] +}) +export class HealthComponentComponent { + + /** + * The HealthComponent object to display + */ + @Input() healthComponent: HealthComponent; + + /** + * The HealthComponent object name + */ + @Input() healthComponentName: string; + + public AlertTypeEnum = AlertType; + + /** + * A boolean representing if div should start collapsed + */ + public isCollapsed = false; + + constructor(private translate: TranslateService) { + } + + /** + * Return translated label if exist for the given property + * + * @param property + */ + public getPropertyLabel(property: string): string { + const translationKey = `health-page.property.${property}`; + const translation = this.translate.instant(translationKey); + + return (translation === translationKey) ? property : translation; + } +} diff --git a/src/app/health-page/health-panel/health-panel.component.html b/src/app/health-page/health-panel/health-panel.component.html new file mode 100644 index 0000000000..2d67fa537b --- /dev/null +++ b/src/app/health-page/health-panel/health-panel.component.html @@ -0,0 +1,25 @@ +

{{'health-page.status' | translate}} :

+ + + +
+ +
+ +
+ + +
+
+
+
+ + + +
+
+ + diff --git a/src/app/health-page/health-panel/health-panel.component.scss b/src/app/health-page/health-panel/health-panel.component.scss new file mode 100644 index 0000000000..a6f0e73413 --- /dev/null +++ b/src/app/health-page/health-panel/health-panel.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-panel/health-panel.component.spec.ts b/src/app/health-page/health-panel/health-panel.component.spec.ts new file mode 100644 index 0000000000..1d9c856ddb --- /dev/null +++ b/src/app/health-page/health-panel/health-panel.component.spec.ts @@ -0,0 +1,57 @@ +import { CommonModule } from '@angular/common'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; + +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { HealthPanelComponent } from './health-panel.component'; +import { HealthResponseObj } from '../../shared/mocks/health-endpoint.mocks'; +import { ObjNgFor } from '../../shared/utils/object-ngfor.pipe'; + +describe('HealthPanelComponent', () => { + let component: HealthPanelComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + NgbNavModule, + NgbAccordionModule, + CommonModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [ + HealthPanelComponent, + ObjNgFor + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthPanelComponent); + component = fixture.componentInstance; + component.healthResponse = HealthResponseObj; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render a panel for each component', () => { + const components = fixture.debugElement.queryAll(By.css('[data-test="component"]')); + expect(components.length).toBe(5); + }); + +}); diff --git a/src/app/health-page/health-panel/health-panel.component.ts b/src/app/health-page/health-panel/health-panel.component.ts new file mode 100644 index 0000000000..1c056daf20 --- /dev/null +++ b/src/app/health-page/health-panel/health-panel.component.ts @@ -0,0 +1,45 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { HealthResponse } from '../models/health-component.model'; + +/** + * Show the health panel + */ +@Component({ + selector: 'ds-health-panel', + templateUrl: './health-panel.component.html', + styleUrls: ['./health-panel.component.scss'] +}) +export class HealthPanelComponent implements OnInit { + + /** + * Health endpoint response + */ + @Input() healthResponse: HealthResponse; + + /** + * The first active panel id + */ + activeId: string; + + constructor(private translate: TranslateService) { + } + + ngOnInit(): void { + this.activeId = Object.keys(this.healthResponse.components)[0]; + } + + /** + * Return translated label if exist for the given property + * + * @param panelKey + */ + public getPanelLabel(panelKey: string): string { + const translationKey = `health-page.section.${panelKey}.title`; + const translation = this.translate.instant(translationKey); + + return (translation === translationKey) ? panelKey : translation; + } +} diff --git a/src/app/health-page/health-panel/health-status/health-status.component.html b/src/app/health-page/health-panel/health-status/health-status.component.html new file mode 100644 index 0000000000..38a6f72601 --- /dev/null +++ b/src/app/health-page/health-panel/health-status/health-status.component.html @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/app/health-page/health-panel/health-status/health-status.component.scss b/src/app/health-page/health-panel/health-status/health-status.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/health-page/health-panel/health-status/health-status.component.spec.ts b/src/app/health-page/health-panel/health-status/health-status.component.spec.ts new file mode 100644 index 0000000000..f0f61ebdbb --- /dev/null +++ b/src/app/health-page/health-panel/health-status/health-status.component.spec.ts @@ -0,0 +1,60 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { HealthStatusComponent } from './health-status.component'; +import { HealthStatus } from '../../models/health-component.model'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; + +describe('HealthStatusComponent', () => { + let component: HealthStatusComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + NgbTooltipModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ HealthStatusComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HealthStatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create success icon', () => { + component.status = HealthStatus.UP; + fixture.detectChanges(); + const icon = fixture.debugElement.query(By.css('i.text-success')); + expect(icon).toBeTruthy(); + }); + + it('should create warning icon', () => { + component.status = HealthStatus.UP_WITH_ISSUES; + fixture.detectChanges(); + const icon = fixture.debugElement.query(By.css('i.text-warning')); + expect(icon).toBeTruthy(); + }); + + it('should create success icon', () => { + component.status = HealthStatus.DOWN; + fixture.detectChanges(); + const icon = fixture.debugElement.query(By.css('i.text-danger')); + expect(icon).toBeTruthy(); + }); +}); diff --git a/src/app/health-page/health-panel/health-status/health-status.component.ts b/src/app/health-page/health-panel/health-status/health-status.component.ts new file mode 100644 index 0000000000..19f83713fc --- /dev/null +++ b/src/app/health-page/health-panel/health-status/health-status.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; +import { HealthStatus } from '../../models/health-component.model'; + +/** + * Show a health status object + */ +@Component({ + selector: 'ds-health-status', + templateUrl: './health-status.component.html', + styleUrls: ['./health-status.component.scss'] +}) +export class HealthStatusComponent { + /** + * The current status to show + */ + @Input() status: HealthStatus; + + /** + * He + */ + HealthStatus = HealthStatus; + +} diff --git a/src/app/health-page/health.service.ts b/src/app/health-page/health.service.ts new file mode 100644 index 0000000000..7c238769a1 --- /dev/null +++ b/src/app/health-page/health.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { DspaceRestService } from '../core/dspace-rest/dspace-rest.service'; +import { RawRestResponse } from '../core/dspace-rest/raw-rest-response.model'; +import { HALEndpointService } from '../core/shared/hal-endpoint.service'; + +@Injectable({ + providedIn: 'root' +}) +export class HealthService { + constructor(protected halService: HALEndpointService, + protected restService: DspaceRestService) { + } + /** + * @returns health data + */ + getHealth(): Observable { + return this.halService.getEndpoint('/actuator').pipe( + map((restURL: string) => restURL + '/health'), + switchMap((endpoint: string) => this.restService.get(endpoint))); + } + + /** + * @returns information of server + */ + getInfo(): Observable { + return this.halService.getEndpoint('/actuator').pipe( + map((restURL: string) => restURL + '/info'), + switchMap((endpoint: string) => this.restService.get(endpoint))); + } +} diff --git a/src/app/health-page/models/health-component.model.ts b/src/app/health-page/models/health-component.model.ts new file mode 100644 index 0000000000..8461d4d967 --- /dev/null +++ b/src/app/health-page/models/health-component.model.ts @@ -0,0 +1,48 @@ +/** + * Interface for Health Status + */ +export enum HealthStatus { + UP = 'UP', + UP_WITH_ISSUES = 'UP_WITH_ISSUES', + DOWN = 'DOWN' +} + +/** + * Interface describing the Health endpoint response + */ +export interface HealthResponse { + status: HealthStatus; + components: { + [name: string]: HealthComponent; + }; +} + +/** + * Interface describing a single component retrieved from the Health endpoint response + */ +export interface HealthComponent { + status: HealthStatus; + details?: { + [name: string]: number|string; + }; + components?: { + [name: string]: HealthComponent; + }; +} + +/** + * Interface describing the Health info endpoint response + */ +export interface HealthInfoResponse { + [name: string]: HealthInfoComponent|string; +} + +/** + * Interface describing a single component retrieved from the Health info endpoint response + */ +export interface HealthInfoComponent { + [property: string]: HealthInfoComponent|string; +} + + + diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 7599f95ed6..0c7dfb1e34 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -147,7 +147,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme // Perform the setup actions from above in order and display notifications removedResponses$.pipe(take(1)).subscribe((responses: RemoteData[]) => { this.displayNotifications('item.edit.bitstreams.notifications.remove', responses); - this.reset(); this.submitting = false; }); } @@ -242,27 +241,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme ); } - /** - * De-cache the current item (it should automatically reload due to itemUpdateSubscription) - */ - reset() { - this.refreshItemCache(); - } - - /** - * Remove the current item's cache from object- and request-cache - */ - refreshItemCache() { - this.bundles$.pipe(take(1)).subscribe((bundles: Bundle[]) => { - bundles.forEach((bundle: Bundle) => { - this.objectCache.remove(bundle.self); - this.requestService.removeByHrefSubstring(bundle.self); - }); - this.objectCache.remove(this.item.self); - this.requestService.removeByHrefSubstring(this.item.self); - }); - } - /** * Unsubscribe from open subscriptions whenever the component gets destroyed */ diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html index 24db0ec300..5d1edb847f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html @@ -27,7 +27,7 @@ + [attr.data-test]="'download-button' | dsBrowserOnly"> - -
-
- - - -
-
orcid-logo
-
{{ getOrcidNotLinkedMessage() | async }}
-
-
-
- -
-
-
- \ No newline at end of file + + + + {{ 'person.page.orcid.remove-orcid-message' | translate}} + +
+
+ + +
+
+ + + + +
+
+
orcid-logo
+
+ {{ getOrcidNotLinkedMessage() | async }} +
+
+
+
+ +
+
+
+
+ diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.spec.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.spec.ts new file mode 100644 index 0000000000..a2ec1cf9b1 --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.spec.ts @@ -0,0 +1,336 @@ +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; + +import { getTestScheduler } from 'jasmine-marbles'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { Item } from '../../../core/shared/item.model'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { OrcidAuthComponent } from './orcid-auth.component'; +import { NativeWindowService } from '../../../core/services/window.service'; +import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; + +describe('OrcidAuthComponent test suite', () => { + let comp: OrcidAuthComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + let researcherProfileService: jasmine.SpyObj; + let nativeWindowRef; + let notificationsService; + + const orcidScopes = [ + '/authenticate', + '/read-limited', + '/activities/update', + '/person/update' + ]; + + const partialOrcidScopes = [ + '/authenticate', + '/read-limited', + ]; + + const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }] + } + }); + + const mockItemLinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }], + 'dspace.object.owner': [{ + 'value': 'test person', + 'language': null, + 'authority': 'deced3e7-68e2-495d-bf98-7c44fc33b8ff', + 'confidence': 600, + 'place': 0 + }], + 'dspace.orcid.authenticated': [{ + 'value': '2022-06-10T15:15:12.952872', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'dspace.orcid.scope': [{ + 'value': '/authenticate', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }, { + 'value': '/read-limited', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 1 + }, { + 'value': '/activities/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 2 + }, { + 'value': '/person/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 3 + }], + 'person.identifier.orcid': [{ + 'value': 'orcid-id', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }] + } + }); + + beforeEach(waitForAsync(() => { + researcherProfileService = jasmine.createSpyObj('researcherProfileService', { + getOrcidAuthorizationScopes: jasmine.createSpy('getOrcidAuthorizationScopes'), + getOrcidAuthorizationScopesByItem: jasmine.createSpy('getOrcidAuthorizationScopesByItem'), + getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl'), + isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'), + onlyAdminCanDisconnectProfileFromOrcid: jasmine.createSpy('onlyAdminCanDisconnectProfileFromOrcid'), + ownerCanDisconnectProfileFromOrcid: jasmine.createSpy('ownerCanDisconnectProfileFromOrcid'), + unlinkOrcid: jasmine.createSpy('unlinkOrcid') + }); + + void TestBed.configureTestingModule({ + imports: [ + NgbAccordionModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule.withRoutes([]) + ], + declarations: [OrcidAuthComponent], + providers: [ + { provide: NativeWindowService, useFactory: NativeWindowMockFactory }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: ResearcherProfileService, useValue: researcherProfileService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(OrcidAuthComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); + fixture = TestBed.createComponent(OrcidAuthComponent); + comp = fixture.componentInstance; + researcherProfileService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes)); + })); + + describe('when orcid profile is not linked', () => { + beforeEach(waitForAsync(() => { + comp.item = mockItemUnlinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([]); + researcherProfileService.isLinkedToOrcid.and.returnValue(false); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + researcherProfileService.getOrcidAuthorizeUrl.and.returnValue(of('oarcidUrl')); + fixture.detectChanges(); + })); + + it('should create', fakeAsync(() => { + const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]')); + const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]')); + expect(orcidLinked).toBeFalsy(); + expect(orcidNotLinked).toBeTruthy(); + })); + + it('should change location on link', () => { + nativeWindowRef = (comp as any)._window; + scheduler.schedule(() => comp.linkOrcid()); + scheduler.flush(); + + expect(nativeWindowRef.nativeWindow.location.href).toBe('oarcidUrl'); + }); + + }); + + describe('when orcid profile is linked', () => { + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + })); + + describe('', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + notificationsService = (comp as any).notificationsService; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + })); + + describe('and unlink is successfully', () => { + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.unlinkOrcid.and.returnValue(createSuccessfulRemoteDataObject$(new ResearcherProfile())); + spyOn(comp.unlink, 'emit'); + fixture.detectChanges(); + })); + + it('should show success notification', () => { + scheduler.schedule(() => comp.unlinkOrcid()); + scheduler.flush(); + + expect(notificationsService.success).toHaveBeenCalled(); + expect(comp.unlink.emit).toHaveBeenCalled(); + }); + }); + + describe('and unlink is failed', () => { + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.unlinkOrcid.and.returnValue(createFailedRemoteDataObject$()); + fixture.detectChanges(); + })); + + it('should show success notification', () => { + scheduler.schedule(() => comp.unlinkOrcid()); + scheduler.flush(); + + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + }); + + describe('and has orcid authorization scopes', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + fixture.detectChanges(); + })); + + it('should create', fakeAsync(() => { + const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]')); + const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]')); + expect(orcidLinked).toBeTruthy(); + expect(orcidNotLinked).toBeFalsy(); + })); + + it('should display orcid authorizations', fakeAsync(() => { + const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]')); + const noMissingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="noMissingOrcidAuthorizations"]')); + const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]')); + + expect(orcidAuthorizations).toBeTruthy(); + expect(noMissingOrcidAuthorizations).toBeTruthy(); + expect(orcidAuthorizationsList.length).toBe(4); + })); + }); + + describe('and has missing orcid authorization scopes', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + fixture.detectChanges(); + })); + + it('should create', fakeAsync(() => { + const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]')); + const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]')); + expect(orcidLinked).toBeTruthy(); + expect(orcidNotLinked).toBeFalsy(); + })); + + it('should display orcid authorizations', fakeAsync(() => { + const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]')); + const missingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="missingOrcidAuthorizations"]')); + const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]')); + const missingOrcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="missingOrcidAuthorization"]')); + + expect(orcidAuthorizations).toBeTruthy(); + expect(missingOrcidAuthorizations).toBeTruthy(); + expect(orcidAuthorizationsList.length).toBe(2); + expect(missingOrcidAuthorizationsList.length).toBe(2); + })); + }); + + describe('and only admin can unlink scopes', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + fixture.detectChanges(); + })); + + it('should display warning panel', fakeAsync(() => { + const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]')); + const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]')); + expect(unlinkOnlyAdmin).toBeTruthy(); + expect(unlinkOwner).toBeFalsy(); + })); + + }); + + describe('and owner can unlink scopes', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + fixture.detectChanges(); + })); + + it('should display warning panel', fakeAsync(() => { + const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]')); + const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]')); + expect(unlinkOnlyAdmin).toBeFalsy(); + expect(unlinkOwner).toBeTruthy(); + })); + + }); + + }); + + +}); diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts index 8594cba9f5..ed8b1fd3d9 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -1,59 +1,126 @@ -import { Component, Inject, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; + import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; -import { ItemDataService } from '../../../core/data/item-data.service'; -import { RemoteData } from '../../../core/data/remote-data'; + import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; import { Item } from '../../../core/shared/item.model'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; @Component({ selector: 'ds-orcid-auth', templateUrl: './orcid-auth.component.html', styleUrls: ['./orcid-auth.component.scss'] }) -export class OrcidAuthComponent implements OnInit { +export class OrcidAuthComponent implements OnInit, OnChanges { - missingAuthorizations$ = new BehaviorSubject([]); + /** + * The item for which showing the orcid settings + */ + @Input() item: Item; - unlinkProcessing = false; + /** + * The list of exposed orcid authorization scopes for the orcid profile + */ + profileAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); - item: Item + /** + * The list of all orcid authorization scopes missing in the orcid profile + */ + missingAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + + /** + * The list of all orcid authorization scopes available + */ + orcidAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + + /** + * A boolean representing if unlink operation is processing + */ + unlinkProcessing: BehaviorSubject = new BehaviorSubject(false); + + /** + * A boolean representing if orcid profile is linked + */ + private isOrcidLinked$: BehaviorSubject = new BehaviorSubject(false); + + /** + * A boolean representing if only admin can disconnect orcid profile + */ + private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); + + /** + * A boolean representing if owner can disconnect orcid profile + */ + private ownerCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); + + /** + * An event emitted when orcid profile is unliked successfully + */ + @Output() unlink: EventEmitter = new EventEmitter(); constructor( - private configurationService: ConfigurationDataService, private researcherProfileService: ResearcherProfileService, - protected translateService: TranslateService, + private translateService: TranslateService, private notificationsService: NotificationsService, - private itemService: ItemDataService, - private route: ActivatedRoute, @Inject(NativeWindowService) private _window: NativeWindowRef, - ) { - this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { - this.item = data.payload; - }); + ) { } ngOnInit() { - const scopes = this.getOrcidAuthorizations(); - return this.configurationService.findByPropertyName('orcid.scope') - .pipe(getFirstSucceededRemoteDataPayload(), - map((configurationProperty) => configurationProperty.values), - map((allScopes) => allScopes.filter((scope) => !scopes.includes(scope)))) - .subscribe((missingScopes) => this.missingAuthorizations$.next(missingScopes)); + this.researcherProfileService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => { + this.orcidAuthorizationScopes.next(scopes); + this.initOrcidAuthSettings(); + }); } - getOrcidAuthorizations(): string[] { - return this.item.allMetadataValues('dspace.orcid.scope'); + ngOnChanges(changes: SimpleChanges): void { + if (!changes.item.isFirstChange() && changes.item.currentValue !== changes.item.previousValue) { + this.initOrcidAuthSettings(); + } } - isLinkedToOrcid(): boolean { - return this.researcherProfileService.isLinkedToOrcid(this.item); + /** + * Check if the list of exposed orcid authorization scopes for the orcid profile has values + */ + hasOrcidAuthorizations(): Observable { + return this.profileAuthorizationScopes.asObservable().pipe( + map((scopes: string[]) => scopes.length > 0) + ); + } + + /** + * Return the list of exposed orcid authorization scopes for the orcid profile + */ + getOrcidAuthorizations(): Observable { + return this.profileAuthorizationScopes.asObservable(); + } + + /** + * Check if the list of exposed orcid authorization scopes for the orcid profile has values + */ + hasMissingOrcidAuthorizations(): Observable { + return this.missingAuthorizationScopes.asObservable().pipe( + map((scopes: string[]) => scopes.length > 0) + ); + } + + /** + * Return the list of exposed orcid authorization scopes for the orcid profile + */ + getMissingOrcidAuthorizations(): Observable { + return this.profileAuthorizationScopes.asObservable(); + } + + /** + * Return a boolean representing if orcid profile is linked + */ + isLinkedToOrcid(): Observable { + return this.isOrcidLinked$.asObservable(); } getOrcidNotLinkedMessage(): Observable { @@ -65,34 +132,85 @@ export class OrcidAuthComponent implements OnInit { } } + /** + * Get label for a given orcid authorization scope + * + * @param scope + */ getAuthorizationDescription(scope: string) { return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-'); } + /** + * Return a boolean representing if only admin can disconnect orcid profile + */ onlyAdminCanDisconnectProfileFromOrcid(): Observable { - return this.researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid(); + return this.onlyAdminCanDisconnectProfileFromOrcid$.asObservable(); } + /** + * Return a boolean representing if owner can disconnect orcid profile + */ ownerCanDisconnectProfileFromOrcid(): Observable { - return this.researcherProfileService.ownerCanDisconnectProfileFromOrcid(); + return this.ownerCanDisconnectProfileFromOrcid$.asObservable(); } + /** + * Link existing person profile with orcid + */ linkOrcid(): void { this.researcherProfileService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => { this._window.nativeWindow.location.href = authorizeUrl; }); } + /** + * Unlink existing person profile from orcid + */ unlinkOrcid(): void { - this.unlinkProcessing = true; - this.researcherProfileService.unlinkOrcid(this.item).subscribe((remoteData) => { - this.unlinkProcessing = false; + this.unlinkProcessing.next(true); + this.researcherProfileService.unlinkOrcid(this.item).subscribe((remoteData: RemoteData) => { + this.unlinkProcessing.next(false); if (remoteData.isSuccess) { this.notificationsService.success(this.translateService.get('person.page.orcid.unlink.success')); + this.unlink.emit(); } else { this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error')); } }); } + /** + * initialize all Orcid authentication settings + * @private + */ + private initOrcidAuthSettings(): void { + + this.setOrcidAuthorizationsFromItem(); + + this.setMissingOrcidAuthorizations(); + + this.researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => { + this.onlyAdminCanDisconnectProfileFromOrcid$.next(result); + }); + + this.researcherProfileService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => { + this.ownerCanDisconnectProfileFromOrcid$.next(result); + }); + + this.isOrcidLinked$.next(this.researcherProfileService.isLinkedToOrcid(this.item)); + } + + private setMissingOrcidAuthorizations(): void { + const profileScopes = this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item); + const orcidScopes = this.orcidAuthorizationScopes.value; + const missingScopes = orcidScopes.filter((scope) => !profileScopes.includes(scope)); + + this.missingAuthorizationScopes.next(missingScopes); + } + + private setOrcidAuthorizationsFromItem(): void { + this.profileAuthorizationScopes.next(this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item)); + } + } diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html index dd6f455a61..3bb4a0737c 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.html +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -1,3 +1,13 @@ - - + + + + diff --git a/src/app/item-page/orcid-page/orcid-page.component.spec.ts b/src/app/item-page/orcid-page/orcid-page.component.spec.ts new file mode 100644 index 0000000000..9210d56865 --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-page.component.spec.ts @@ -0,0 +1,131 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; + +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; + +import { AuthService } from '../../core/auth/auth.service'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; +import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; +import { OrcidPageComponent } from './orcid-page.component'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Item } from '../../core/shared/item.model'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { ItemDataService } from '../../core/data/item-data.service'; + +describe('OrcidPageComponent test suite', () => { + let comp: OrcidPageComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + let authService: jasmine.SpyObj; + let routeStub: jasmine.SpyObj; + let routeData: any; + let itemDataService: jasmine.SpyObj; + let researcherProfileService: jasmine.SpyObj; + + const mockItem: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'test item' + } + ] + } + }); + const mockItemLinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [ + { + value: 'test item' + } + ], + 'dspace.orcid.authenticated': [ + { + value: 'true' + } + ] + } + }); + + beforeEach(waitForAsync(() => { + authService = jasmine.createSpyObj('authService', { + isAuthenticated: jasmine.createSpy('isAuthenticated'), + navigateByUrl: jasmine.createSpy('navigateByUrl') + }); + + routeData = { + dso: createSuccessfulRemoteDataObject(mockItem), + }; + + routeStub = Object.assign(new ActivatedRouteStub(), { + data: observableOf(routeData) + }); + + researcherProfileService = jasmine.createSpyObj('researcherProfileService', { + isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid') + }); + + itemDataService = jasmine.createSpyObj('ItemDataService', { + findById: jasmine.createSpy('findById') + }); + + void TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule.withRoutes([]) + ], + declarations: [OrcidPageComponent], + providers: [ + { provide: ActivatedRoute, useValue: routeStub }, + { provide: ResearcherProfileService, useValue: researcherProfileService }, + { provide: AuthService, useValue: authService }, + { provide: ItemDataService, useValue: itemDataService }, + ], + + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); + fixture = TestBed.createComponent(OrcidPageComponent); + comp = fixture.componentInstance; + authService.isAuthenticated.and.returnValue(observableOf(true)); + fixture.detectChanges(); + })); + + it('should create', () => { + const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]')); + expect(comp).toBeTruthy(); + expect(btn.length).toBe(1); + }); + + it('should call isLinkedToOrcid', () => { + comp.isLinkedToOrcid(); + + expect(researcherProfileService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item.value); + }); + + it('should update item', fakeAsync(() => { + itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid)); + scheduler.schedule(() => comp.updateItem()); + scheduler.flush(); + + expect(comp.item.value).toEqual(mockItemLinkedToOrcid); + })); + +}); diff --git a/src/app/item-page/orcid-page/orcid-page.component.ts b/src/app/item-page/orcid-page/orcid-page.component.ts index eed5ebc276..be4e0e7945 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.ts +++ b/src/app/item-page/orcid-page/orcid-page.component.ts @@ -1,37 +1,82 @@ -import {Component, Inject} from '@angular/core'; -import {ConfigurationDataService} from '../../core/data/configuration-data.service'; -import {ResearcherProfileService} from '../../core/profile/researcher-profile.service'; -import {TranslateService} from '@ngx-translate/core'; -import {NotificationsService} from '../../shared/notifications/notifications.service'; -import {ItemDataService} from '../../core/data/item-data.service'; -import {ActivatedRoute} from '@angular/router'; -import {NativeWindowRef, NativeWindowService} from '../../core/services/window.service'; -import {getFirstCompletedRemoteData} from '../../core/shared/operators'; -import {RemoteData} from '../../core/data/remote-data'; -import {Item} from '../../core/shared/item.model'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { Item } from '../../core/shared/item.model'; +import { getItemPageRoute } from '../item-page-routing-paths'; +import { AuthService } from '../../core/auth/auth.service'; +import { redirectOn4xx } from '../../core/shared/authorized.operators'; +import { ItemDataService } from '../../core/data/item-data.service'; + +/** + * A component that represents the orcid settings page + */ @Component({ selector: 'ds-orcid-page', templateUrl: './orcid-page.component.html', styleUrls: ['./orcid-page.component.scss'] }) -export class OrcidPageComponent { +export class OrcidPageComponent implements OnInit { - item: Item; + /** + * The item for which showing the orcid settings + */ + item: BehaviorSubject = new BehaviorSubject(null); constructor( + private authService: AuthService, private itemService: ItemDataService, private researcherProfileService: ResearcherProfileService, private route: ActivatedRoute, - @Inject(NativeWindowService) private _window: NativeWindowRef, + private router: Router ) { - this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { - this.item = data.payload; + } + + /** + * Retrieve the item for which showing the orcid settings + */ + ngOnInit(): void { + this.route.data.pipe( + map((data) => data.dso as RemoteData), + redirectOn4xx(this.router, this.authService), + getFirstSucceededRemoteDataPayload() + ).subscribe((item) => { + this.item.next(item); }); } + /** + * Check if the current item is linked to an ORCID profile. + * + * @returns the check result + */ isLinkedToOrcid(): boolean { - return this.researcherProfileService.isLinkedToOrcid(this.item); + return this.researcherProfileService.isLinkedToOrcid(this.item.value); + } + + /** + * Get the route to an item's page + */ + getItemPage(): string { + return getItemPageRoute(this.item.value); + } + + /** + * Retrieve the updated profile item + */ + updateItem(): void { + this.itemService.findById(this.item.value.id, false).pipe( + getFirstCompletedRemoteData() + ).subscribe((itemRD: RemoteData) => { + if (itemRD.hasSucceeded) { + this.item.next(itemRD.payload); + } + }); } } diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html new file mode 100644 index 0000000000..75038d5973 --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html @@ -0,0 +1,98 @@ +
+ + + +
+
+
+
+
+
{{ 'person.page.orcid.synchronization-mode'| translate }}
+
+
+
+ + {{ 'person.page.orcid.synchronization-mode-message' | translate}} + +
+
+ + +
+
+
+
+
+
+
+
+
+
{{ 'person.page.orcid.publications-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
{{ 'person.page.orcid.funding-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
{{ 'person.page.orcid.profile-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.scss b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts new file mode 100644 index 0000000000..ccc00178cb --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts @@ -0,0 +1,257 @@ +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; + +import { getTestScheduler } from 'jasmine-marbles'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { TestScheduler } from 'rxjs/testing'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { Operation } from 'fast-json-patch'; + +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { Item } from '../../../core/shared/item.model'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { OrcidSyncSettingsComponent } from './orcid-sync-settings.component'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; + +describe('OrcidAuthComponent test suite', () => { + let comp: OrcidSyncSettingsComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + let researcherProfileService: jasmine.SpyObj; + let notificationsService; + let formGroup: FormGroup; + + const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: 'test-id', + visible: true, + type: 'profile', + _links: { + item: { + href: 'https://rest.api/rest/api/profiles/test-id/item' + }, + self: { + href: 'https://rest.api/rest/api/profiles/test-id' + }, + } + }); + + const mockItemLinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }], + 'dspace.object.owner': [{ + 'value': 'test person', + 'language': null, + 'authority': 'deced3e7-68e2-495d-bf98-7c44fc33b8ff', + 'confidence': 600, + 'place': 0 + }], + 'dspace.orcid.authenticated': [{ + 'value': '2022-06-10T15:15:12.952872', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'dspace.orcid.scope': [{ + 'value': '/authenticate', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }, { + 'value': '/read-limited', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 1 + }, { + 'value': '/activities/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 2 + }, { + 'value': '/person/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 3 + }], + 'dspace.orcid.sync-mode': [{ + 'value': 'MANUAL', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'dspace.orcid.sync-profile': [{ + 'value': 'BIOGRAPHICAL', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }, { + 'value': 'IDENTIFIERS', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 1 + }], + 'dspace.orcid.sync-publications': [{ + 'value': 'ALL', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'person.identifier.orcid': [{ + 'value': 'orcid-id', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }] + } + }); + + beforeEach(waitForAsync(() => { + researcherProfileService = jasmine.createSpyObj('researcherProfileService', { + findByRelatedItem: jasmine.createSpy('findByRelatedItem'), + updateByOrcidOperations: jasmine.createSpy('updateByOrcidOperations') + }); + + void TestBed.configureTestingModule({ + imports: [ + FormsModule, + NgbAccordionModule, + ReactiveFormsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule.withRoutes([]) + ], + declarations: [OrcidSyncSettingsComponent], + providers: [ + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: ResearcherProfileService, useValue: researcherProfileService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(OrcidSyncSettingsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); + fixture = TestBed.createComponent(OrcidSyncSettingsComponent); + comp = fixture.componentInstance; + comp.item = mockItemLinkedToOrcid; + fixture.detectChanges(); + })); + + it('should create cards properly', () => { + const modes = fixture.debugElement.query(By.css('[data-test="sync-mode"]')); + const publication = fixture.debugElement.query(By.css('[data-test="sync-mode-publication"]')); + const funding = fixture.debugElement.query(By.css('[data-test="sync-mode-funding"]')); + const preferences = fixture.debugElement.query(By.css('[data-test="profile-preferences"]')); + expect(modes).toBeTruthy(); + expect(publication).toBeTruthy(); + expect(funding).toBeTruthy(); + expect(preferences).toBeTruthy(); + }); + + it('should init sync modes properly', () => { + expect(comp.currentSyncMode).toBe('MANUAL'); + expect(comp.currentSyncPublications).toBe('ALL'); + expect(comp.currentSyncFunding).toBe('DISABLED'); + }); + + describe('form submit', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + notificationsService = (comp as any).notificationsService; + formGroup = new FormGroup({ + syncMode: new FormControl('MANUAL'), + syncFundings: new FormControl('ALL'), + syncPublications: new FormControl('ALL'), + syncProfile_BIOGRAPHICAL: new FormControl(true), + syncProfile_IDENTIFIERS: new FormControl(true), + }); + }); + + it('should call updateByOrcidOperations properly', () => { + researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + const expectedOps: Operation[] = [ + { + path: '/orcid/mode', + op: 'replace', + value: 'MANUAL' + }, { + path: '/orcid/publications', + op: 'replace', + value: 'ALL' + }, { + path: '/orcid/fundings', + op: 'replace', + value: 'ALL' + }, { + path: '/orcid/profile', + op: 'replace', + value: 'BIOGRAPHICAL,IDENTIFIERS' + } + ]; + + scheduler.schedule(() => comp.onSubmit(formGroup)); + scheduler.flush(); + + expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(mockResearcherProfile, expectedOps); + }); + + it('should show notification on success', () => { + researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + + scheduler.schedule(() => comp.onSubmit(formGroup)); + scheduler.flush(); + + expect(notificationsService.success).toHaveBeenCalled(); + }); + + it('should show notification on error', () => { + researcherProfileService.findByRelatedItem.and.returnValue(createFailedRemoteDataObject$()); + + scheduler.schedule(() => comp.onSubmit(formGroup)); + scheduler.flush(); + + expect(notificationsService.error).toHaveBeenCalled(); + }); + + it('should show notification on error', () => { + researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + researcherProfileService.updateByOrcidOperations.and.returnValue(createFailedRemoteDataObject$()); + + scheduler.schedule(() => comp.onSubmit(formGroup)); + scheduler.flush(); + + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + +}); diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts similarity index 56% rename from src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts rename to src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts index 45ec48e788..5b5e13a1aa 100644 --- a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts @@ -1,54 +1,79 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; + import { TranslateService } from '@ngx-translate/core'; import { Operation } from 'fast-json-patch'; +import { of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { AuthService } from '../../../core/auth/auth.service'; -import { ItemDataService } from '../../../core/data/item-data.service'; + import { RemoteData } from '../../../core/data/remote-data'; import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; import { Item } from '../../../core/shared/item.model'; -import { getFinishedRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; @Component({ - selector: 'ds-orcid-setting', - templateUrl: './orcid-setting.component.html', - styleUrls: ['./orcid-setting.component.scss'] + selector: 'ds-orcid-sync-setting', + templateUrl: './orcid-sync-settings.component.html', + styleUrls: ['./orcid-sync-settings.component.scss'] }) -export class OrcidSettingComponent implements OnInit { +export class OrcidSyncSettingsComponent implements OnInit { + /** + * The item for which showing the orcid settings + */ + @Input() item: Item; + + /** + * The prefix used for i18n keys + */ messagePrefix = 'person.page.orcid'; + /** + * The current synchronization mode + */ currentSyncMode: string; + /** + * The current synchronization mode for publications + */ currentSyncPublications: string; - currentSyncFundings: string; + /** + * The current synchronization mode for funding + */ + currentSyncFunding: string; + /** + * The synchronization options + */ syncModes: { value: string, label: string }[]; + /** + * The synchronization options for publications + */ syncPublicationOptions: { value: string, label: string }[]; - syncFundingOptions: {value: string, label: string}[]; + /** + * The synchronization options for funding + */ + syncFundingOptions: { value: string, label: string }[]; + /** + * The profile synchronization options + */ syncProfileOptions: { value: string, label: string, checked: boolean }[]; - item: Item; constructor(private researcherProfileService: ResearcherProfileService, - protected translateService: TranslateService, - private notificationsService: NotificationsService, - public authService: AuthService, - private route: ActivatedRoute, - private itemService: ItemDataService - ) { - this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { - this.item = data.payload; - }); + private notificationsService: NotificationsService, + private translateService: TranslateService) { } + /** + * Init orcid settings form + */ ngOnInit() { this.syncModes = [ { @@ -88,12 +113,17 @@ export class OrcidSettingComponent implements OnInit { }; }); - this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH, MANUAL'], 'MANUAL'); + this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL'); this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED'); - this.currentSyncFundings = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); + this.currentSyncFunding = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); } - onSubmit(form: FormGroup) { + /** + * Generate path operations to save orcid synchronization preferences + * + * @param form The form group + */ + onSubmit(form: FormGroup): void { const operations: Operation[] = []; this.fillOperationsFor(operations, '/orcid/mode', form.value.syncMode); this.fillOperationsFor(operations, '/orcid/publications', form.value.syncPublications); @@ -110,10 +140,18 @@ export class OrcidSettingComponent implements OnInit { return; } - this.researcherProfileService.findById(this.item.firstMetadata('dspace.object.owner').authority).pipe( - switchMap((profile) => this.researcherProfileService.patch(profile, operations)), - getFinishedRemoteData() - ).subscribe((remoteData) => { + this.researcherProfileService.findByRelatedItem(this.item).pipe( + getFirstCompletedRemoteData(), + switchMap((profileRD: RemoteData) => { + if (profileRD.hasSucceeded) { + return this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations).pipe( + getFirstCompletedRemoteData() + ); + } else { + return of(profileRD); + } + }), + ).subscribe((remoteData: RemoteData) => { if (remoteData.isSuccess) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); } else { @@ -122,7 +160,27 @@ export class OrcidSettingComponent implements OnInit { }); } - fillOperationsFor(operations: Operation[], path: string, currentValue: string) { + /** + * Retrieve setting saved in the item's metadata + * + * @param metadataField The metadata name that contains setting + * @param allowedValues The allowed values + * @param defaultValue The default value + * @private + */ + private getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string { + const currentPreference = this.item.firstMetadataValue(metadataField); + return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue; + } + + /** + * Generate a replace patch operation + * + * @param operations + * @param path + * @param currentValue + */ + private fillOperationsFor(operations: Operation[], path: string, currentValue: string): void { operations.push({ path: path, op: 'replace', @@ -130,10 +188,4 @@ export class OrcidSettingComponent implements OnInit { }); } - getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string { - const currentPreference = this.item.firstMetadataValue(metadataField); - return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue; - } - - } diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html deleted file mode 100644 index 793e7570ed..0000000000 --- a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html +++ /dev/null @@ -1,82 +0,0 @@ -
- - - -
-
-
- {{ 'person.page.orcid.synchronization-mode-message' | translate}} -
-
-
-
{{ 'person.page.orcid.synchronization-mode'| translate }}
-
-
-
- - -
-
-
-
-
-
-
-
{{ 'person.page.orcid.publications-preferences'| translate }}
-
-
-
-
- - -
-
-
-
-
-
-
{{ 'person.page.orcid.funding-preferences'| translate }}
-
-
-
-
- - -
-
-
-
-
-
-
{{ 'person.page.orcid.profile-preferences'| translate }}
-
-
-
-
- - -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
\ No newline at end of file diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index b3660eb9d7..6e0db386db 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,27 +1,20 @@ -import { map, mergeMap, take, tap } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; - import { Item } from '../../core/shared/item.model'; - import { fadeInOut } from '../../shared/animations/fade'; -import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteData } from '../../core/shared/operators'; +import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators'; import { ViewMode } from '../../core/shared/view-mode.model'; import { AuthService } from '../../core/auth/auth.service'; import { getItemPageRoute } from '../item-page-routing-paths'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; -import { TranslateService } from '@ngx-translate/core'; -import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; -import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model'; -import { isNotUndefined } from '../../shared/empty.util'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; - /** * This component renders a simple item page. diff --git a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.spec.ts b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.spec.ts index eff51b1019..9aeea8b11e 100644 --- a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.spec.ts +++ b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { VersionedItemComponent } from './versioned-item.component'; import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service'; -import { TranslateService } from '@ngx-translate/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { VersionDataService } from '../../../../core/data/version-data.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service'; @@ -19,6 +19,7 @@ import { SearchService } from '../../../../core/shared/search/search.service'; import { ItemDataService } from '../../../../core/data/item-data.service'; import { Version } from '../../../../core/shared/version.model'; import { RouteService } from '../../../../core/services/route.service'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), @@ -57,10 +58,17 @@ describe('VersionedItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [VersionedItemComponent, DummyComponent], - imports: [RouterTestingModule], + imports: [ + RouterTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock, + } + }), + ], providers: [ { provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy }, - { provide: TranslateService, useValue: {} }, { provide: VersionDataService, useValue: versionServiceSpy }, { provide: NotificationsService, useValue: {} }, { provide: ItemVersionsSharedService, useValue: {} }, diff --git a/src/app/menu.resolver.spec.ts b/src/app/menu.resolver.spec.ts new file mode 100644 index 0000000000..db90b7ea00 --- /dev/null +++ b/src/app/menu.resolver.spec.ts @@ -0,0 +1,331 @@ +import { TestBed, waitForAsync } from '@angular/core/testing'; + +import { MenuResolver } from './menu.resolver'; +import { of as observableOf } from 'rxjs'; +import { FeatureID } from './core/data/feature-authorization/feature-id'; +import { TranslateModule } from '@ngx-translate/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MenuService } from './shared/menu/menu.service'; +import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service'; +import { ScriptDataService } from './core/data/processes/script-data.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { MenuServiceStub } from './shared/testing/menu-service.stub'; +import { MenuID } from './shared/menu/menu-id.model'; +import { BrowseService } from './core/browse/browse.service'; +import { cold } from 'jasmine-marbles'; +import createSpy = jasmine.createSpy; +import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils'; +import { createPaginatedList } from './shared/testing/utils.test'; + +const BOOLEAN = { t: true, f: false }; +const MENU_STATE = { + id: 'some menu' +}; +const BROWSE_DEFINITIONS = [ + { id: 'definition1' }, + { id: 'definition2' }, + { id: 'definition3' }, +]; + +describe('MenuResolver', () => { + let resolver: MenuResolver; + + let menuService; + let browseService; + let authorizationService; + let scriptService; + + beforeEach(waitForAsync(() => { + menuService = new MenuServiceStub(); + spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE)); + + browseService = jasmine.createSpyObj('browseService', { + getBrowseDefinitions: createSuccessfulRemoteDataObject$(createPaginatedList(BROWSE_DEFINITIONS)) + }); + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true) + }); + scriptService = jasmine.createSpyObj('scriptService', { + scriptWithNameExistsAndCanExecute: observableOf(true) + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule], + declarations: [AdminSidebarComponent], + providers: [ + { provide: MenuService, useValue: menuService }, + { provide: BrowseService, useValue: browseService }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: ScriptDataService, useValue: scriptService }, + { + provide: NgbModal, useValue: { + open: () => {/*comment*/ + } + } + } + ], + schemas: [NO_ERRORS_SCHEMA] + }); + resolver = TestBed.inject(MenuResolver); + + spyOn(menuService, 'addSection'); + })); + + it('should be created', () => { + expect(resolver).toBeTruthy(); + }); + + describe('resolve', () => { + it('should create all menus', (done) => { + spyOn(resolver, 'createPublicMenu$').and.returnValue(observableOf(true)); + spyOn(resolver, 'createAdminMenu$').and.returnValue(observableOf(true)); + + resolver.resolve(null, null).subscribe(resolved => { + expect(resolved).toBeTrue(); + expect(resolver.createPublicMenu$).toHaveBeenCalled(); + expect(resolver.createAdminMenu$).toHaveBeenCalled(); + done(); + }); + }); + + it('should return an Observable that emits true as soon as all menus are created', () => { + spyOn(resolver, 'createPublicMenu$').and.returnValue(cold('--(t|)', BOOLEAN)); + spyOn(resolver, 'createAdminMenu$').and.returnValue(cold('----(t|)', BOOLEAN)); + + expect(resolver.resolve(null, null)).toBeObservable(cold('----(t|)', BOOLEAN)); + }); + }); + + describe('createPublicMenu$', () => { + it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => { + (menuService as any).getMenu.and.returnValue(cold('--u--m--', { + u: undefined, + m: MENU_STATE, + })); + + expect(resolver.createPublicMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN)); + expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.PUBLIC); + }); + + describe('contents', () => { + beforeEach((done) => { + resolver.createPublicMenu$().subscribe((_) => { + done(); + }); + }); + + it('should include community list link', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({ + id: 'browse_global_communities_and_collections', visible: true, + })); + }); + + it('should include browse dropdown', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({ + id: 'browse_global_by_definition1', parentID: 'browse_global', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({ + id: 'browse_global_by_definition2', parentID: 'browse_global', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({ + id: 'browse_global_by_definition3', parentID: 'browse_global', visible: true, + })); + + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({ + id: 'browse_global', visible: true, + })); + }); + }); + }); + + describe('createAdminMenu$', () => { + it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => { + (menuService as any).getMenu.and.returnValue(cold('--u--m', { + u: undefined, + m: MENU_STATE, + })); + + expect(resolver.createAdminMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN)); + expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.ADMIN); + }); + + describe('for regular user', () => { + beforeEach(() => { + authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => { + return observableOf(false); + }); + }); + + beforeEach((done) => { + resolver.createAdminMenu$().subscribe((_) => { + done(); + }); + }); + + it('should not show site admin section', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'admin_search', visible: false, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'registries', visible: false, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + parentID: 'registries', visible: false, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'curation_tasks', visible: false, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'workflow', visible: false, + })); + }); + + it('should not show edit_community', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'edit_community', visible: false, + })); + + }); + + it('should not show edit_collection', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'edit_collection', visible: false, + })); + }); + + it('should not show access control section', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'access_control', visible: false, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + parentID: 'access_control', visible: false, + })); + }); + + // We check that the menu section has not been called with visible set to true + // The reason why we don't check if it has been called with visible set to false + // Is because the function does not get called unless a user is authorised + it('should not show the import section', () => { + expect(menuService.addSection).not.toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'import', visible: true, + })); + }); + + // We check that the menu section has not been called with visible set to true + // The reason why we don't check if it has been called with visible set to false + // Is because the function does not get called unless a user is authorised + it('should not show the export section', () => { + expect(menuService.addSection).not.toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'export', visible: true, + })); + }); + }); + + describe('for site admin', () => { + beforeEach(() => { + authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => { + return observableOf(featureID === FeatureID.AdministratorOf); + }); + }); + + beforeEach((done) => { + resolver.createAdminMenu$().subscribe((_) => { + done(); + }); + }); + + it('should contain site admin section', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'admin_search', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'registries', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + parentID: 'registries', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'curation_tasks', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'workflow', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'workflow', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'import', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'export', visible: true, + })); + }); + }); + + describe('for community admin', () => { + beforeEach(() => { + authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => { + return observableOf(featureID === FeatureID.IsCommunityAdmin); + }); + }); + + beforeEach((done) => { + resolver.createAdminMenu$().subscribe((_) => { + done(); + }); + }); + + it('should show edit_community', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'edit_community', visible: true, + })); + }); + }); + + describe('for collection admin', () => { + beforeEach(() => { + authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => { + return observableOf(featureID === FeatureID.IsCollectionAdmin); + }); + }); + + beforeEach((done) => { + resolver.createAdminMenu$().subscribe((_) => { + done(); + }); + }); + + it('should show edit_collection', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'edit_collection', visible: true, + })); + }); + }); + + describe('for group admin', () => { + beforeEach(() => { + authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => { + return observableOf(featureID === FeatureID.CanManageGroups); + }); + }); + + beforeEach((done) => { + resolver.createAdminMenu$().subscribe((_) => { + done(); + }); + }); + + it('should show access control section', () => { + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'access_control', visible: true, + })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + parentID: 'access_control', visible: true, + })); + }); + }); + }); +}); diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts new file mode 100644 index 0000000000..4c97d3d1b3 --- /dev/null +++ b/src/app/menu.resolver.ts @@ -0,0 +1,655 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { combineLatest as observableCombineLatest, combineLatest, Observable } from 'rxjs'; +import { MenuID } from './shared/menu/menu-id.model'; +import { MenuState } from './shared/menu/menu-state.model'; +import { MenuItemType } from './shared/menu/menu-item-type.model'; +import { LinkMenuItemModel } from './shared/menu/menu-item/models/link.model'; +import { getFirstCompletedRemoteData } from './core/shared/operators'; +import { PaginatedList } from './core/data/paginated-list.model'; +import { BrowseDefinition } from './core/shared/browse-definition.model'; +import { RemoteData } from './core/data/remote-data'; +import { TextMenuItemModel } from './shared/menu/menu-item/models/text.model'; +import { BrowseService } from './core/browse/browse.service'; +import { MenuService } from './shared/menu/menu.service'; +import { filter, find, map, take } from 'rxjs/operators'; +import { hasValue } from './shared/empty.util'; +import { FeatureID } from './core/data/feature-authorization/feature-id'; +import { + CreateCommunityParentSelectorComponent +} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; +import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model'; +import { + CreateCollectionParentSelectorComponent +} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; +import { + CreateItemParentSelectorComponent +} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +import { + EditCommunitySelectorComponent +} from './shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; +import { + EditCollectionSelectorComponent +} from './shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; +import { + EditItemSelectorComponent +} from './shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; +import { + ExportMetadataSelectorComponent +} from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; +import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { + METADATA_EXPORT_SCRIPT_NAME, + METADATA_IMPORT_SCRIPT_NAME, + ScriptDataService +} from './core/data/processes/script-data.service'; + +/** + * Creates all of the app's menus + */ +@Injectable({ + providedIn: 'root' +}) +export class MenuResolver implements Resolve { + constructor( + protected menuService: MenuService, + protected browseService: BrowseService, + protected authorizationService: AuthorizationDataService, + protected modalService: NgbModal, + protected scriptDataService: ScriptDataService, + ) { + } + + /** + * Initialize all menus + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return combineLatest([ + this.createPublicMenu$(), + this.createAdminMenu$(), + ]).pipe( + map((menusDone: boolean[]) => menusDone.every(Boolean)), + ); + } + + /** + * Wait for a specific menu to appear + * @param id the ID of the menu to wait for + * @return an Observable that emits true as soon as the menu is created + */ + protected waitForMenu$(id: MenuID): Observable { + return this.menuService.getMenu(id).pipe( + find((menu: MenuState) => hasValue(menu)), + map(() => true), + ); + } + + /** + * Initialize all menu sections and items for {@link MenuID.PUBLIC} + */ + createPublicMenu$(): Observable { + const menuList: any[] = [ + /* Communities & Collections tree */ + { + id: `browse_global_communities_and_collections`, + active: false, + visible: true, + index: 0, + model: { + type: MenuItemType.LINK, + text: `menu.section.browse_global_communities_and_collections`, + link: `/community-list` + } as LinkMenuItemModel + } + ]; + // Read the different Browse-By types from config and add them to the browse menu + this.browseService.getBrowseDefinitions() + .pipe(getFirstCompletedRemoteData>()) + .subscribe((browseDefListRD: RemoteData>) => { + if (browseDefListRD.hasSucceeded) { + browseDefListRD.payload.page.forEach((browseDef: BrowseDefinition) => { + menuList.push({ + id: `browse_global_by_${browseDef.id}`, + parentID: 'browse_global', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: `menu.section.browse_global_by_${browseDef.id}`, + link: `/browse/${browseDef.id}` + } as LinkMenuItemModel + }); + }); + menuList.push( + /* Browse */ + { + id: 'browse_global', + active: false, + visible: true, + index: 1, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.browse_global' + } as TextMenuItemModel, + } + ); + } + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.PUBLIC, Object.assign(menuSection, { + shouldPersistOnRouteChange: true + }))); + }); + + return this.waitForMenu$(MenuID.PUBLIC); + } + + /** + * Initialize all menu sections and items for {@link MenuID.ADMIN} + */ + createAdminMenu$() { + this.createMainMenuSections(); + this.createSiteAdministratorMenuSections(); + this.createExportMenuSections(); + this.createImportMenuSections(); + this.createAccessControlMenuSections(); + + return this.waitForMenu$(MenuID.ADMIN); + } + + /** + * Initialize the main menu sections. + * edit_community / edit_collection is only included if the current user is a Community or Collection admin + */ + createMainMenuSections() { + combineLatest([ + this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin), + this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin), + this.authorizationService.isAuthorized(FeatureID.AdministratorOf) + ]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => { + const menuList = [ + /* News */ + { + id: 'new', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.new' + } as TextMenuItemModel, + icon: 'plus', + index: 0 + }, + { + id: 'new_community', + parentID: 'new', + active: false, + visible: isCommunityAdmin, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_community', + function: () => { + this.modalService.open(CreateCommunityParentSelectorComponent); + } + } as OnClickMenuItemModel, + }, + { + id: 'new_collection', + parentID: 'new', + active: false, + visible: isCommunityAdmin, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_collection', + function: () => { + this.modalService.open(CreateCollectionParentSelectorComponent); + } + } as OnClickMenuItemModel, + }, + { + id: 'new_item', + parentID: 'new', + active: false, + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_item', + function: () => { + this.modalService.open(CreateItemParentSelectorComponent); + } + } as OnClickMenuItemModel, + }, + { + id: 'new_process', + parentID: 'new', + active: false, + visible: isCollectionAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.new_process', + link: '/processes/new' + } as LinkMenuItemModel, + }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'new_item_version', + // parentID: 'new', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.new_item_version', + // link: '' + // } as LinkMenuItemModel, + // }, + + /* Edit */ + { + id: 'edit', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.edit' + } as TextMenuItemModel, + icon: 'pencil-alt', + index: 1 + }, + { + id: 'edit_community', + parentID: 'edit', + active: false, + visible: isCommunityAdmin, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.edit_community', + function: () => { + this.modalService.open(EditCommunitySelectorComponent); + } + } as OnClickMenuItemModel, + }, + { + id: 'edit_collection', + parentID: 'edit', + active: false, + visible: isCollectionAdmin, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.edit_collection', + function: () => { + this.modalService.open(EditCollectionSelectorComponent); + } + } as OnClickMenuItemModel, + }, + { + id: 'edit_item', + parentID: 'edit', + active: false, + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.edit_item', + function: () => { + this.modalService.open(EditItemSelectorComponent); + } + } as OnClickMenuItemModel, + }, + + /* Statistics */ + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'statistics_task', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.statistics_task', + // link: '' + // } as LinkMenuItemModel, + // icon: 'chart-bar', + // index: 8 + // }, + + /* Control Panel */ + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'control_panel', + // active: false, + // visible: isSiteAdmin, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.control_panel', + // link: '' + // } as LinkMenuItemModel, + // icon: 'cogs', + // index: 9 + // }, + + /* Processes */ + { + id: 'processes', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.processes', + link: '/processes' + } as LinkMenuItemModel, + icon: 'terminal', + index: 10 + }, + { + id: 'health', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.health', + link: '/health' + } as LinkMenuItemModel, + icon: 'heartbeat', + index: 11 + }, + ]; + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { + shouldPersistOnRouteChange: true + }))); + }); + } + + /** + * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not + * the export scripts exist and the current user is allowed to execute them + */ + createExportMenuSections() { + const menuList = [ + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'export_community', + // parentID: 'export', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.export_community', + // link: '' + // } as LinkMenuItemModel, + // shouldPersistOnRouteChange: true + // }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'export_collection', + // parentID: 'export', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.export_collection', + // link: '' + // } as LinkMenuItemModel, + // shouldPersistOnRouteChange: true + // }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'export_item', + // parentID: 'export', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.export_item', + // link: '' + // } as LinkMenuItemModel, + // shouldPersistOnRouteChange: true + // }, + ]; + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection)); + + observableCombineLatest([ + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME) + ]).pipe( + filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists), + take(1) + ).subscribe(() => { + // Hides the export menu for unauthorised people + // If in the future more sub-menus are added, + // it should be reviewed if they need to be in this subscribe + this.menuService.addSection(MenuID.ADMIN, { + id: 'export', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.export' + } as TextMenuItemModel, + icon: 'file-export', + index: 3, + shouldPersistOnRouteChange: true + }); + this.menuService.addSection(MenuID.ADMIN, { + id: 'export_metadata', + parentID: 'export', + active: true, + visible: true, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.export_metadata', + function: () => { + this.modalService.open(ExportMetadataSelectorComponent); + } + } as OnClickMenuItemModel, + shouldPersistOnRouteChange: true + }); + }); + } + + /** + * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not + * the import scripts exist and the current user is allowed to execute them + */ + createImportMenuSections() { + const menuList = [ + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'import_batch', + // parentID: 'import', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.import_batch', + // link: '' + // } as LinkMenuItemModel, + // } + ]; + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection)); + + observableCombineLatest([ + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME) + ]).pipe( + filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists), + take(1) + ).subscribe(() => { + // Hides the import menu for unauthorised people + // If in the future more sub-menus are added, + // it should be reviewed if they need to be in this subscribe + this.menuService.addSection(MenuID.ADMIN, { + id: 'import', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.import' + } as TextMenuItemModel, + icon: 'file-import', + index: 2, + shouldPersistOnRouteChange: true, + }); + this.menuService.addSection(MenuID.ADMIN, { + id: 'import_metadata', + parentID: 'import', + active: true, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.import_metadata', + link: '/admin/metadata-import' + } as LinkMenuItemModel, + shouldPersistOnRouteChange: true + }); + }); + } + + /** + * Create menu sections dependent on whether or not the current user is a site administrator + */ + createSiteAdministratorMenuSections() { + this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => { + const menuList = [ + /* Admin Search */ + { + id: 'admin_search', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.admin_search', + link: '/admin/search' + } as LinkMenuItemModel, + icon: 'search', + index: 5 + }, + /* Registries */ + { + id: 'registries', + active: false, + visible: authorized, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.registries' + } as TextMenuItemModel, + icon: 'list', + index: 6 + }, + { + id: 'registries_metadata', + parentID: 'registries', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.registries_metadata', + link: 'admin/registries/metadata' + } as LinkMenuItemModel, + }, + { + id: 'registries_format', + parentID: 'registries', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.registries_format', + link: 'admin/registries/bitstream-formats' + } as LinkMenuItemModel, + }, + + /* Curation tasks */ + { + id: 'curation_tasks', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.curation_task', + link: 'admin/curation-tasks' + } as LinkMenuItemModel, + icon: 'filter', + index: 7 + }, + + /* Workflow */ + { + id: 'workflow', + active: false, + visible: authorized, + model: { + type: MenuItemType.LINK, + text: 'menu.section.workflow', + link: '/admin/workflow' + } as LinkMenuItemModel, + icon: 'user-check', + index: 11 + }, + ]; + + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { + shouldPersistOnRouteChange: true + }))); + }); + } + + /** + * Create menu sections dependent on whether or not the current user can manage access control groups + */ + createAccessControlMenuSections() { + observableCombineLatest([ + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.authorizationService.isAuthorized(FeatureID.CanManageGroups) + ]).subscribe(([isSiteAdmin, canManageGroups]) => { + const menuList = [ + /* Access Control */ + { + id: 'access_control_people', + parentID: 'access_control', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.access_control_people', + link: '/access-control/epeople' + } as LinkMenuItemModel, + }, + { + id: 'access_control_groups', + parentID: 'access_control', + active: false, + visible: canManageGroups, + model: { + type: MenuItemType.LINK, + text: 'menu.section.access_control_groups', + link: '/access-control/groups' + } as LinkMenuItemModel, + }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'access_control_authorizations', + // parentID: 'access_control', + // active: false, + // visible: authorized, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.access_control_authorizations', + // link: '' + // } as LinkMenuItemModel, + // }, + { + id: 'access_control', + active: false, + visible: canManageGroups || isSiteAdmin, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.access_control' + } as TextMenuItemModel, + icon: 'key', + index: 4 + }, + ]; + + menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { + shouldPersistOnRouteChange: true, + }))); + }); + } +} diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.html b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.html index b4a114c633..7db6db9a8d 100644 --- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.html +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.html @@ -11,6 +11,7 @@ @@ -26,7 +26,7 @@

{{'researcher.profile.not.associated' | translate}}

- diff --git a/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.scss b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.spec.ts b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.spec.ts new file mode 100644 index 0000000000..168517b47a --- /dev/null +++ b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.spec.ts @@ -0,0 +1,186 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { PersonPageClaimButtonComponent } from './person-page-claim-button.component'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; +import { TranslateLoaderMock } from '../../mocks/translate-loader.mock'; +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { RouteService } from '../../../core/services/route.service'; +import { routeServiceStub } from '../../testing/route-service.stub'; +import { Item } from '../../../core/shared/item.model'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; + +describe('PersonPageClaimButtonComponent', () => { + let scheduler: TestScheduler; + let component: PersonPageClaimButtonComponent; + let fixture: ComponentFixture; + + const mockItem: Item = Object.assign(new Item(), { + metadata: { + 'person.email': [ + { + language: 'en_US', + value: 'fake@email.com' + } + ], + 'person.birthDate': [ + { + language: 'en_US', + value: '1993' + } + ], + 'person.jobTitle': [ + { + language: 'en_US', + value: 'Developer' + } + ], + 'person.familyName': [ + { + language: 'en_US', + value: 'Doe' + } + ], + 'person.givenName': [ + { + language: 'en_US', + value: 'John' + } + ] + }, + _links: { + self: { + href: 'item-href' + } + } + }); + + const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: 'test-id', + visible: true, + type: 'profile', + _links: { + item: { + href: 'https://rest.api/rest/api/profiles/test-id/item' + }, + self: { + href: 'https://rest.api/rest/api/profiles/test-id' + }, + } + }); + + const notificationsService = new NotificationsServiceStub(); + + const authorizationDataService = jasmine.createSpyObj('authorizationDataService', { + isAuthorized: jasmine.createSpy('isAuthorized') + }); + + const researcherProfileService = jasmine.createSpyObj('researcherProfileService', { + createFromExternalSource: jasmine.createSpy('createFromExternalSource'), + findRelatedItemId: jasmine.createSpy('findRelatedItemId'), + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [PersonPageClaimButtonComponent], + providers: [ + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: ResearcherProfileService, useValue: researcherProfileService }, + { provide: RouteService, useValue: routeServiceStub }, + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PersonPageClaimButtonComponent); + component = fixture.componentInstance; + component.object = mockItem; + }); + + describe('when item can be claimed', () => { + beforeEach(() => { + authorizationDataService.isAuthorized.and.returnValue(observableOf(true)); + researcherProfileService.createFromExternalSource.calls.reset(); + researcherProfileService.findRelatedItemId.calls.reset(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create claim button', () => { + const btn = fixture.debugElement.query(By.css('[data-test="item-claim"]')); + expect(btn).toBeTruthy(); + }); + + describe('claim', () => { + describe('when successfully', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + researcherProfileService.createFromExternalSource.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + researcherProfileService.findRelatedItemId.and.returnValue(observableOf('test-id')); + }); + + it('should display success notification', () => { + scheduler.schedule(() => component.claim()); + scheduler.flush(); + + expect(researcherProfileService.findRelatedItemId).toHaveBeenCalled(); + expect(notificationsService.success).toHaveBeenCalled(); + }); + }); + + describe('when not successfully', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + researcherProfileService.createFromExternalSource.and.returnValue(createFailedRemoteDataObject$()); + }); + + it('should display success notification', () => { + scheduler.schedule(() => component.claim()); + scheduler.flush(); + + expect(researcherProfileService.findRelatedItemId).not.toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + }); + + }); + + describe('when item cannot be claimed', () => { + beforeEach(() => { + authorizationDataService.isAuthorized.and.returnValue(observableOf(false)); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create claim button', () => { + const btn = fixture.debugElement.query(By.css('[data-test="item-claim"]')); + expect(btn).toBeFalsy(); + }); + + }); +}); diff --git a/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.ts b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.ts new file mode 100644 index 0000000000..903b9d3679 --- /dev/null +++ b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.ts @@ -0,0 +1,84 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; +import { mergeMap, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + +import { RouteService } from '../../../core/services/route.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; +import { isNotEmpty } from '../../empty.util'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +@Component({ + selector: 'ds-person-page-claim-button', + templateUrl: './person-page-claim-button.component.html', + styleUrls: ['./person-page-claim-button.component.scss'] +}) +export class PersonPageClaimButtonComponent implements OnInit { + + /** + * The target person item to claim + */ + @Input() object: DSpaceObject; + + /** + * A boolean representing if item can be claimed or not + */ + claimable$: BehaviorSubject = new BehaviorSubject(false); + + constructor(protected routeService: RouteService, + protected authorizationService: AuthorizationDataService, + protected notificationsService: NotificationsService, + protected translate: TranslateService, + protected researcherProfileService: ResearcherProfileService) { + } + + ngOnInit(): void { + this.authorizationService.isAuthorized(FeatureID.CanClaimItem, this.object._links.self.href, null, false).pipe( + take(1) + ).subscribe((isAuthorized: boolean) => { + this.claimable$.next(isAuthorized); + }); + + } + + /** + * Create a new researcher profile claiming the current item. + */ + claim() { + this.researcherProfileService.createFromExternalSource(this.object._links.self.href).pipe( + getFirstCompletedRemoteData(), + mergeMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return this.researcherProfileService.findRelatedItemId(rd.payload); + } else { + return observableOf(null); + } + })) + .subscribe((id: string) => { + if (isNotEmpty(id)) { + this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'), + this.translate.get('researcherprofile.success.claim.body')); + this.claimable$.next(false); + } else { + this.notificationsService.error( + this.translate.get('researcherprofile.error.claim.title'), + this.translate.get('researcherprofile.error.claim.body')); + } + }); + } + + /** + * Returns true if the item is claimable, false otherwise. + */ + isClaimable(): Observable { + return this.claimable$; + } + +} diff --git a/src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.spec.ts deleted file mode 100644 index 464f2518ca..0000000000 --- a/src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ActivatedRoute, Router } from '@angular/router'; -/* tslint:disable:no-unused-variable */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { TranslateModule } from '@ngx-translate/core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ClaimItemSelectorComponent } from './claim-item-selector.component'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ProfileClaimService } from '../../../../profile-page/profile-claim/profile-claim.service'; -import { of } from 'rxjs'; - -describe('ClaimItemSelectorComponent', () => { - let component: ClaimItemSelectorComponent; - let fixture: ComponentFixture; - - const profileClaimService = jasmine.createSpyObj('profileClaimService', { - search: of({ payload: {page: []}}) - }); - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [ ClaimItemSelectorComponent ], - providers: [ - { provide: NgbActiveModal, useValue: {} }, - { provide: ActivatedRoute, useValue: {} }, - { provide: Router, useValue: {} }, - { provide: ProfileClaimService, useValue: profileClaimService } - ], - schemas: [NO_ERRORS_SCHEMA] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ClaimItemSelectorComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - -}); diff --git a/src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.ts deleted file mode 100644 index 3744f6d054..0000000000 --- a/src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { BehaviorSubject } from 'rxjs'; -import { PaginatedList } from '../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { Item } from '../../../../core/shared/item.model'; -import { SearchResult } from '../../../search/models/search-result.model'; -import { DSOSelectorModalWrapperComponent } from '../dso-selector-modal-wrapper.component'; -import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths'; -import { EPerson } from '../../../../core/eperson/models/eperson.model'; -import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; -import { ViewMode } from '../../../../core/shared/view-mode.model'; -import { ProfileClaimService } from '../../../../profile-page/profile-claim/profile-claim.service'; -import { CollectionElementLinkType } from '../../../object-collection/collection-element-link.type'; - -/** - * Component - */ -@Component({ - selector: 'ds-claim-item-selector', - templateUrl: './claim-item-selector.component.html' -}) -export class ClaimItemSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { - - @Input() dso: DSpaceObject; - - listEntries$: BehaviorSubject>>> = new BehaviorSubject(null); - - viewMode = ViewMode.ListElement; - - // enum to be exposed - linkTypes = CollectionElementLinkType; - - checked = false; - - @Output() create: EventEmitter = new EventEmitter(); - - constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router, - private profileClaimService: ProfileClaimService) { - super(activeModal, route); - } - - ngOnInit(): void { - this.profileClaimService.search(this.dso as EPerson).subscribe( - (result) => this.listEntries$.next(result) - ); - } - - // triggered when an item is selected - selectItem(dso: DSpaceObject): void { - this.close(); - this.navigate(dso); - } - - navigate(dso: DSpaceObject) { - this.router.navigate([getItemPageRoute(dso as Item)]); - } - - toggleCheckbox() { - this.checked = !this.checked; - } - - createFromScratch() { - this.create.emit(); - this.close(); - } - -} diff --git a/src/app/shared/idle-modal/idle-modal.component.spec.ts b/src/app/shared/idle-modal/idle-modal.component.spec.ts index 847bf6ac4f..7ea0b96d5b 100644 --- a/src/app/shared/idle-modal/idle-modal.component.spec.ts +++ b/src/app/shared/idle-modal/idle-modal.component.spec.ts @@ -46,7 +46,7 @@ describe('IdleModalComponent', () => { describe('extendSessionPressed', () => { beforeEach(fakeAsync(() => { - spyOn(component.response, 'next'); + spyOn(component.response, 'emit'); component.extendSessionPressed(); })); it('should set idle to false', () => { @@ -55,8 +55,8 @@ describe('IdleModalComponent', () => { it('should close the modal', () => { expect(modalStub.close).toHaveBeenCalled(); }); - it('response \'closed\' should have true as next', () => { - expect(component.response.next).toHaveBeenCalledWith(true); + it('response \'closed\' should emit true', () => { + expect(component.response.emit).toHaveBeenCalledWith(true); }); }); @@ -74,7 +74,7 @@ describe('IdleModalComponent', () => { describe('closePressed', () => { beforeEach(fakeAsync(() => { - spyOn(component.response, 'next'); + spyOn(component.response, 'emit'); component.closePressed(); })); it('should set idle to false', () => { @@ -83,8 +83,8 @@ describe('IdleModalComponent', () => { it('should close the modal', () => { expect(modalStub.close).toHaveBeenCalled(); }); - it('response \'closed\' should have true as next', () => { - expect(component.response.next).toHaveBeenCalledWith(true); + it('response \'closed\' should emit true', () => { + expect(component.response.emit).toHaveBeenCalledWith(true); }); }); diff --git a/src/app/shared/idle-modal/idle-modal.component.ts b/src/app/shared/idle-modal/idle-modal.component.ts index 35fafcf5cf..4873137ff1 100644 --- a/src/app/shared/idle-modal/idle-modal.component.ts +++ b/src/app/shared/idle-modal/idle-modal.component.ts @@ -1,8 +1,7 @@ -import { Component, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { environment } from '../../../environments/environment'; import { AuthService } from '../../core/auth/auth.service'; -import { Subject } from 'rxjs'; import { hasValue } from '../empty.util'; import { Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; @@ -29,7 +28,7 @@ export class IdleModalComponent implements OnInit { * An event fired when the modal is closed */ @Output() - response: Subject = new Subject(); + response = new EventEmitter(); constructor(private activeModal: NgbActiveModal, private authService: AuthService, @@ -84,6 +83,6 @@ export class IdleModalComponent implements OnInit { */ closeModal() { this.activeModal.close(); - this.response.next(true); + this.response.emit(true); } } diff --git a/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.html b/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.html index 0c0b72272f..6c54a15162 100644 --- a/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.html +++ b/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.html @@ -8,12 +8,12 @@

{{ "item.version.delete.modal.text" | translate : {version: versionNumber} }}

diff --git a/src/app/shared/log-in/methods/log-in-external-provider.component.ts b/src/app/shared/log-in/methods/log-in-external-provider.component.ts new file mode 100644 index 0000000000..037fc40e90 --- /dev/null +++ b/src/app/shared/log-in/methods/log-in-external-provider.component.ts @@ -0,0 +1,110 @@ +import { Component, Inject, OnInit, } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { select, Store } from '@ngrx/store'; + +import { AuthMethod } from '../../../core/auth/models/auth.method'; + +import { isAuthenticated, isAuthenticationLoading } from '../../../core/auth/selectors'; +import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; +import { isEmpty, isNotNull } from '../../empty.util'; +import { AuthService } from '../../../core/auth/auth.service'; +import { HardRedirectService } from '../../../core/services/hard-redirect.service'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { CoreState } from '../../../core/core-state.model'; + +@Component({ + selector: 'ds-log-in-external-provider', + template: '' + +}) +export abstract class LogInExternalProviderComponent implements OnInit { + + /** + * The authentication method data. + * @type {AuthMethod} + */ + public authMethod: AuthMethod; + + /** + * True if the authentication is loading. + * @type {boolean} + */ + public loading: Observable; + + /** + * The shibboleth authentication location url. + * @type {string} + */ + public location: string; + + /** + * Whether user is authenticated. + * @type {Observable} + */ + public isAuthenticated: Observable; + + /** + * @constructor + * @param {AuthMethod} injectedAuthMethodModel + * @param {boolean} isStandalonePage + * @param {NativeWindowRef} _window + * @param {AuthService} authService + * @param {HardRedirectService} hardRedirectService + * @param {Store} store + */ + constructor( + @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, + @Inject('isStandalonePage') public isStandalonePage: boolean, + @Inject(NativeWindowService) protected _window: NativeWindowRef, + private authService: AuthService, + private hardRedirectService: HardRedirectService, + private store: Store + ) { + this.authMethod = injectedAuthMethodModel; + } + + ngOnInit(): void { + // set isAuthenticated + this.isAuthenticated = this.store.pipe(select(isAuthenticated)); + + // set loading + this.loading = this.store.pipe(select(isAuthenticationLoading)); + + // set location + this.location = decodeURIComponent(this.injectedAuthMethodModel.location); + + } + + /** + * Redirect to the external provider url for login + */ + redirectToExternalProvider() { + this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => { + if (!this.isStandalonePage) { + redirectRoute = this.hardRedirectService.getCurrentRoute(); + } else if (isEmpty(redirectRoute)) { + redirectRoute = '/'; + } + const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString(); + + let externalServerUrl = this.location; + const myRegexp = /\?redirectUrl=(.*)/g; + const match = myRegexp.exec(this.location); + const redirectUrlFromServer = (match && match[1]) ? match[1] : null; + + // Check whether the current page is different from the redirect url received from rest + if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) { + // change the redirect url with the current page url + const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`; + externalServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl); + } + + // redirect to shibboleth authentication url + this.hardRedirectService.redirect(externalServerUrl); + }); + + } + +} diff --git a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts index 38cedf91ec..882996b207 100644 --- a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts +++ b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts @@ -1,110 +1,21 @@ -import { Component, Inject, OnInit, } from '@angular/core'; - -import { Observable } from 'rxjs'; -import { select, Store } from '@ngrx/store'; +import { Component, } from '@angular/core'; import { renderAuthMethodFor } from '../log-in.methods-decorator'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { AuthMethod } from '../../../../core/auth/models/auth.method'; - -import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors'; -import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; -import { isNotNull, isEmpty } from '../../../empty.util'; -import { AuthService } from '../../../../core/auth/auth.service'; -import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; -import { take } from 'rxjs/operators'; -import { URLCombiner } from '../../../../core/url-combiner/url-combiner'; -import { CoreState } from '../../../../core/core-state.model'; +import { LogInExternalProviderComponent } from '../log-in-external-provider.component'; @Component({ selector: 'ds-log-in-oidc', templateUrl: './log-in-oidc.component.html', }) @renderAuthMethodFor(AuthMethodType.Oidc) -export class LogInOidcComponent implements OnInit { +export class LogInOidcComponent extends LogInExternalProviderComponent { /** - * The authentication method data. - * @type {AuthMethod} + * Redirect to orcid authentication url */ - public authMethod: AuthMethod; - - /** - * True if the authentication is loading. - * @type {boolean} - */ - public loading: Observable; - - /** - * The oidc authentication location url. - * @type {string} - */ - public location: string; - - /** - * Whether user is authenticated. - * @type {Observable} - */ - public isAuthenticated: Observable; - - /** - * @constructor - * @param {AuthMethod} injectedAuthMethodModel - * @param {boolean} isStandalonePage - * @param {NativeWindowRef} _window - * @param {AuthService} authService - * @param {HardRedirectService} hardRedirectService - * @param {Store} store - */ - constructor( - @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, - @Inject('isStandalonePage') public isStandalonePage: boolean, - @Inject(NativeWindowService) protected _window: NativeWindowRef, - private authService: AuthService, - private hardRedirectService: HardRedirectService, - private store: Store - ) { - this.authMethod = injectedAuthMethodModel; - } - - ngOnInit(): void { - // set isAuthenticated - this.isAuthenticated = this.store.pipe(select(isAuthenticated)); - - // set loading - this.loading = this.store.pipe(select(isAuthenticationLoading)); - - // set location - this.location = decodeURIComponent(this.injectedAuthMethodModel.location); - - } - redirectToOidc() { - - this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => { - if (!this.isStandalonePage) { - redirectRoute = this.hardRedirectService.getCurrentRoute(); - } else if (isEmpty(redirectRoute)) { - redirectRoute = '/'; - } - const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString(); - - let oidcServerUrl = this.location; - const myRegexp = /\?redirectUrl=(.*)/g; - const match = myRegexp.exec(this.location); - const redirectUrlFromServer = (match && match[1]) ? match[1] : null; - - // Check whether the current page is different from the redirect url received from rest - if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) { - // change the redirect url with the current page url - const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`; - oidcServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl); - } - - // redirect to oidc authentication url - this.hardRedirectService.redirect(oidcServerUrl); - }); - + this.redirectToExternalProvider(); } } diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts index 0bc9e18520..e0b1da3db5 100644 --- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts +++ b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts @@ -1,110 +1,21 @@ -import { Component, Inject, OnInit, } from '@angular/core'; - -import { Observable } from 'rxjs'; -import { select, Store } from '@ngrx/store'; +import { Component, } from '@angular/core'; import { renderAuthMethodFor } from '../log-in.methods-decorator'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { AuthMethod } from '../../../../core/auth/models/auth.method'; - -import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors'; -import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; -import { isNotNull, isEmpty } from '../../../empty.util'; -import { AuthService } from '../../../../core/auth/auth.service'; -import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; -import { take } from 'rxjs/operators'; -import { URLCombiner } from '../../../../core/url-combiner/url-combiner'; -import {CoreState} from "../../../../core/core-state.model"; +import { LogInExternalProviderComponent } from '../log-in-external-provider.component'; @Component({ selector: 'ds-log-in-orcid', templateUrl: './log-in-orcid.component.html', }) @renderAuthMethodFor(AuthMethodType.Orcid) -export class LogInOrcidComponent implements OnInit { +export class LogInOrcidComponent extends LogInExternalProviderComponent { /** - * The authentication method data. - * @type {AuthMethod} + * Redirect to orcid authentication url */ - public authMethod: AuthMethod; - - /** - * True if the authentication is loading. - * @type {boolean} - */ - public loading: Observable; - - /** - * The orcid authentication location url. - * @type {string} - */ - public location: string; - - /** - * Whether user is authenticated. - * @type {Observable} - */ - public isAuthenticated: Observable; - - /** - * @constructor - * @param {AuthMethod} injectedAuthMethodModel - * @param {boolean} isStandalonePage - * @param {NativeWindowRef} _window - * @param {AuthService} authService - * @param {HardRedirectService} hardRedirectService - * @param {Store} store - */ - constructor( - @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, - @Inject('isStandalonePage') public isStandalonePage: boolean, - @Inject(NativeWindowService) protected _window: NativeWindowRef, - private authService: AuthService, - private hardRedirectService: HardRedirectService, - private store: Store - ) { - this.authMethod = injectedAuthMethodModel; - } - - ngOnInit(): void { - // set isAuthenticated - this.isAuthenticated = this.store.pipe(select(isAuthenticated)); - - // set loading - this.loading = this.store.pipe(select(isAuthenticationLoading)); - - // set location - this.location = decodeURIComponent(this.injectedAuthMethodModel.location); - - } - redirectToOrcid() { - - this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => { - if (!this.isStandalonePage) { - redirectRoute = this.hardRedirectService.getCurrentRoute(); - } else if (isEmpty(redirectRoute)) { - redirectRoute = '/'; - } - const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString(); - - let orcidServerUrl = this.location; - const myRegexp = /\?redirectUrl=(.*)/g; - const match = myRegexp.exec(this.location); - const redirectUrlFromServer = (match && match[1]) ? match[1] : null; - - // Check whether the current page is different from the redirect url received from rest - if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) { - // change the redirect url with the current page url - const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`; - orcidServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl); - } - - // redirect to orcid authentication url - this.hardRedirectService.redirect(orcidServerUrl); - }); - + this.redirectToExternalProvider(); } } diff --git a/src/app/shared/log-in/methods/password/log-in-password.component.html b/src/app/shared/log-in/methods/password/log-in-password.component.html index 89e7b9ff1a..c1f1016cb8 100644 --- a/src/app/shared/log-in/methods/password/log-in-password.component.html +++ b/src/app/shared/log-in/methods/password/log-in-password.component.html @@ -10,7 +10,7 @@ placeholder="{{'login.form.email' | translate}}" required type="email" - data-test="email"> + [attr.data-test]="'email' | dsBrowserOnly"> + [attr.data-test]="'password' | dsBrowserOnly"> - diff --git a/src/app/shared/log-in/methods/password/log-in-password.component.spec.ts b/src/app/shared/log-in/methods/password/log-in-password.component.spec.ts index 355d328f3f..5238482770 100644 --- a/src/app/shared/log-in/methods/password/log-in-password.component.spec.ts +++ b/src/app/shared/log-in/methods/password/log-in-password.component.spec.ts @@ -17,6 +17,7 @@ import { storeModuleConfig } from '../../../../app.reducer'; import { AuthMethod } from '../../../../core/auth/models/auth.method'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; +import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe'; describe('LogInPasswordComponent', () => { @@ -57,7 +58,8 @@ describe('LogInPasswordComponent', () => { TranslateModule.forRoot() ], declarations: [ - LogInPasswordComponent + LogInPasswordComponent, + BrowserOnlyMockPipe, ], providers: [ { provide: AuthService, useClass: AuthServiceStub }, diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts index d218a7ca4e..dcfb3ccfc3 100644 --- a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts +++ b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts @@ -1,21 +1,8 @@ -import { Component, Inject, OnInit, } from '@angular/core'; - -import { Observable } from 'rxjs'; -import { select, Store } from '@ngrx/store'; +import { Component, } from '@angular/core'; import { renderAuthMethodFor } from '../log-in.methods-decorator'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { AuthMethod } from '../../../../core/auth/models/auth.method'; - -import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors'; -import { RouteService } from '../../../../core/services/route.service'; -import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; -import { isNotNull, isEmpty } from '../../../empty.util'; -import { AuthService } from '../../../../core/auth/auth.service'; -import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; -import { take } from 'rxjs/operators'; -import { URLCombiner } from '../../../../core/url-combiner/url-combiner'; -import { CoreState } from '../../../../core/core-state.model'; +import { LogInExternalProviderComponent } from '../log-in-external-provider.component'; @Component({ selector: 'ds-log-in-shibboleth', @@ -24,92 +11,13 @@ import { CoreState } from '../../../../core/core-state.model'; }) @renderAuthMethodFor(AuthMethodType.Shibboleth) -export class LogInShibbolethComponent implements OnInit { +export class LogInShibbolethComponent extends LogInExternalProviderComponent { /** - * The authentication method data. - * @type {AuthMethod} + * Redirect to shibboleth authentication url */ - public authMethod: AuthMethod; - - /** - * True if the authentication is loading. - * @type {boolean} - */ - public loading: Observable; - - /** - * The shibboleth authentication location url. - * @type {string} - */ - public location: string; - - /** - * Whether user is authenticated. - * @type {Observable} - */ - public isAuthenticated: Observable; - - /** - * @constructor - * @param {AuthMethod} injectedAuthMethodModel - * @param {boolean} isStandalonePage - * @param {NativeWindowRef} _window - * @param {RouteService} route - * @param {AuthService} authService - * @param {HardRedirectService} hardRedirectService - * @param {Store} store - */ - constructor( - @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, - @Inject('isStandalonePage') public isStandalonePage: boolean, - @Inject(NativeWindowService) protected _window: NativeWindowRef, - private route: RouteService, - private authService: AuthService, - private hardRedirectService: HardRedirectService, - private store: Store - ) { - this.authMethod = injectedAuthMethodModel; - } - - ngOnInit(): void { - // set isAuthenticated - this.isAuthenticated = this.store.pipe(select(isAuthenticated)); - - // set loading - this.loading = this.store.pipe(select(isAuthenticationLoading)); - - // set location - this.location = decodeURIComponent(this.injectedAuthMethodModel.location); - - } - redirectToShibboleth() { - - this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => { - if (!this.isStandalonePage) { - redirectRoute = this.hardRedirectService.getCurrentRoute(); - } else if (isEmpty(redirectRoute)) { - redirectRoute = '/'; - } - const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString(); - - let shibbolethServerUrl = this.location; - const myRegexp = /\?redirectUrl=(.*)/g; - const match = myRegexp.exec(this.location); - const redirectUrlFromServer = (match && match[1]) ? match[1] : null; - - // Check whether the current page is different from the redirect url received from rest - if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) { - // change the redirect url with the current page url - const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`; - shibbolethServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl); - } - - // redirect to shibboleth authentication url - this.hardRedirectService.redirect(shibbolethServerUrl); - }); - + this.redirectToExternalProvider(); } } diff --git a/src/app/shared/log-out/log-out.component.html b/src/app/shared/log-out/log-out.component.html index 9151cb0c2d..83e58b3841 100644 --- a/src/app/shared/log-out/log-out.component.html +++ b/src/app/shared/log-out/log-out.component.html @@ -2,5 +2,5 @@ - + diff --git a/src/app/shared/log-out/log-out.component.spec.ts b/src/app/shared/log-out/log-out.component.spec.ts index f15203ed8b..028738a019 100644 --- a/src/app/shared/log-out/log-out.component.spec.ts +++ b/src/app/shared/log-out/log-out.component.spec.ts @@ -12,6 +12,7 @@ import { Router } from '@angular/router'; import { AppState } from '../../app.reducer'; import { LogOutComponent } from './log-out.component'; import { RouterStub } from '../testing/router.stub'; +import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe'; describe('LogOutComponent', () => { @@ -46,7 +47,8 @@ describe('LogOutComponent', () => { TranslateModule.forRoot() ], declarations: [ - LogOutComponent + LogOutComponent, + BrowserOnlyMockPipe, ], providers: [ { provide: Router, useValue: routerStub }, diff --git a/src/app/shared/menu/menu-item/link-menu-item.component.spec.ts b/src/app/shared/menu/menu-item/link-menu-item.component.spec.ts index e9552f9173..96444a5447 100644 --- a/src/app/shared/menu/menu-item/link-menu-item.component.spec.ts +++ b/src/app/shared/menu/menu-item/link-menu-item.component.spec.ts @@ -4,7 +4,6 @@ import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { LinkMenuItemComponent } from './link-menu-item.component'; import { RouterLinkDirectiveStub } from '../../testing/router-link-directive.stub'; -import { environment } from '../../../../environments/environment'; import { QueryParamsDirectiveStub } from '../../testing/query-params-directive.stub'; import { RouterStub } from '../../testing/router.stub'; import { Router } from '@angular/router'; @@ -58,7 +57,7 @@ describe('LinkMenuItemComponent', () => { const routerLinkQuery = linkDes.map((de) => de.injector.get(RouterLinkDirectiveStub)); expect(routerLinkQuery.length).toBe(1); - expect(routerLinkQuery[0].routerLink).toBe(environment.ui.nameSpace + link); + expect(routerLinkQuery[0].routerLink).toBe(link); }); it('should have the right queryParams attribute', () => { diff --git a/src/app/shared/menu/menu-item/link-menu-item.component.ts b/src/app/shared/menu/menu-item/link-menu-item.component.ts index 09f08c4531..c9a60f0c28 100644 --- a/src/app/shared/menu/menu-item/link-menu-item.component.ts +++ b/src/app/shared/menu/menu-item/link-menu-item.component.ts @@ -2,7 +2,6 @@ import { Component, Inject, Input, OnInit } from '@angular/core'; import { LinkMenuItemModel } from './models/link.model'; import { rendersMenuItemForType } from '../menu-item.decorator'; import { isNotEmpty } from '../../empty.util'; -import { environment } from '../../../../environments/environment'; import { MenuItemType } from '../menu-item-type.model'; import { Router } from '@angular/router'; @@ -30,7 +29,7 @@ export class LinkMenuItemComponent implements OnInit { getRouterLink() { if (this.hasLink) { - return environment.ui.nameSpace + this.item.link; + return this.item.link; } return undefined; } diff --git a/src/app/shared/menu/menu.service.ts b/src/app/shared/menu/menu.service.ts index ef8c0b1fad..f44ddea649 100644 --- a/src/app/shared/menu/menu.service.ts +++ b/src/app/shared/menu/menu.service.ts @@ -39,7 +39,7 @@ const menuByIDSelector = (menuID: MenuID): MemoizedSelector return keySelector(menuID, menusStateSelector); }; -const menuSectionStateSelector = (state: MenuState) => state.sections; +const menuSectionStateSelector = (state: MenuState) => hasValue(state) ? state.sections : {}; const menuSectionByIDSelector = (id: string): MemoizedSelector => { return menuKeySelector(id, menuSectionStateSelector); @@ -166,7 +166,7 @@ export class MenuService { */ isMenuCollapsed(menuID: MenuID): Observable { return this.getMenu(menuID).pipe( - map((state: MenuState) => state.collapsed) + map((state: MenuState) => hasValue(state) ? state.collapsed : undefined) ); } @@ -177,7 +177,7 @@ export class MenuService { */ isMenuPreviewCollapsed(menuID: MenuID): Observable { return this.getMenu(menuID).pipe( - map((state: MenuState) => state.previewCollapsed) + map((state: MenuState) => hasValue(state) ? state.previewCollapsed : undefined) ); } @@ -188,7 +188,7 @@ export class MenuService { */ isMenuVisible(menuID: MenuID): Observable { return this.getMenu(menuID).pipe( - map((state: MenuState) => state.visible) + map((state: MenuState) => hasValue(state) ? state.visible : undefined) ); } diff --git a/src/app/shared/mocks/health-endpoint.mocks.ts b/src/app/shared/mocks/health-endpoint.mocks.ts new file mode 100644 index 0000000000..a9246d91a1 --- /dev/null +++ b/src/app/shared/mocks/health-endpoint.mocks.ts @@ -0,0 +1,160 @@ +import { + HealthComponent, + HealthInfoComponent, + HealthInfoResponse, + HealthResponse, + HealthStatus +} from '../../health-page/models/health-component.model'; + +export const HealthResponseObj: HealthResponse = { + 'status': HealthStatus.UP_WITH_ISSUES, + 'components': { + 'db': { + 'status': HealthStatus.UP, + 'components': { + 'dataSource': { + 'status': HealthStatus.UP, + 'details': { + 'database': 'PostgreSQL', + 'result': 1, + 'validationQuery': 'SELECT 1' + } + }, + 'dspaceDataSource': { + 'status': HealthStatus.UP, + 'details': { + 'database': 'PostgreSQL', + 'result': 1, + 'validationQuery': 'SELECT 1' + } + } + } + }, + 'geoIp': { + 'status': HealthStatus.UP_WITH_ISSUES, + 'details': { + 'reason': 'The GeoLite Database file is missing (/var/lib/GeoIP/GeoLite2-City.mmdb)! Solr Statistics cannot generate location based reports! Please see the DSpace installation instructions for instructions to install this file.' + } + }, + 'solrOaiCore': { + 'status': HealthStatus.UP, + 'details': { + 'status': 0, + 'detectedPathType': 'particular core' + } + }, + 'solrSearchCore': { + 'status': HealthStatus.UP, + 'details': { + 'status': 0, + 'detectedPathType': 'particular core' + } + }, + 'solrStatisticsCore': { + 'status': HealthStatus.UP, + 'details': { + 'status': 0, + 'detectedPathType': 'particular core' + } + } + } +}; + +export const HealthComponentOne: HealthComponent = { + 'status': HealthStatus.UP, + 'components': { + 'dataSource': { + 'status': HealthStatus.UP, + 'details': { + 'database': 'PostgreSQL', + 'result': 1, + 'validationQuery': 'SELECT 1' + } + }, + 'dspaceDataSource': { + 'status': HealthStatus.UP, + 'details': { + 'database': 'PostgreSQL', + 'result': 1, + 'validationQuery': 'SELECT 1' + } + } + } +}; + +export const HealthComponentTwo: HealthComponent = { + 'status': HealthStatus.UP_WITH_ISSUES, + 'details': { + 'reason': 'The GeoLite Database file is missing (/var/lib/GeoIP/GeoLite2-City.mmdb)! Solr Statistics cannot generate location based reports! Please see the DSpace installation instructions for instructions to install this file.' + } +}; + +export const HealthInfoResponseObj: HealthInfoResponse = { + 'app': { + 'name': 'DSpace at My University', + 'dir': '/home/giuseppe/development/java/install/dspace7-review', + 'url': 'http://localhost:8080/server', + 'db': 'jdbc:postgresql://localhost:5432/dspace7', + 'solr': { + 'server': 'http://localhost:8983/solr', + 'prefix': '' + }, + 'mail': { + 'server': 'smtp.example.com', + 'from-address': 'dspace-noreply@myu.edu', + 'feedback-recipient': 'dspace-help@myu.edu', + 'mail-admin': 'dspace-help@myu.edu', + 'mail-helpdesk': 'dspace-help@myu.edu', + 'alert-recipient': 'dspace-help@myu.edu' + }, + 'cors': { + 'allowed-origins': 'http://localhost:4000' + }, + 'ui': { + 'url': 'http://localhost:4000' + } + }, + 'java': { + 'vendor': 'Private Build', + 'version': '11.0.15', + 'runtime': { + 'name': 'OpenJDK Runtime Environment', + 'version': '11.0.15+10-Ubuntu-0ubuntu0.20.04.1' + }, + 'jvm': { + 'name': 'OpenJDK 64-Bit Server VM', + 'vendor': 'Private Build', + 'version': '11.0.15+10-Ubuntu-0ubuntu0.20.04.1' + } + }, + 'version': '7.3-SNAPSHOT' +}; + +export const HealthInfoComponentOne: HealthInfoComponent = { + 'name': 'DSpace at My University', + 'dir': '/home/giuseppe/development/java/install/dspace7-review', + 'url': 'http://localhost:8080/server', + 'db': 'jdbc:postgresql://localhost:5432/dspace7', + 'solr': { + 'server': 'http://localhost:8983/solr', + 'prefix': '' + }, + 'mail': { + 'server': 'smtp.example.com', + 'from-address': 'dspace-noreply@myu.edu', + 'feedback-recipient': 'dspace-help@myu.edu', + 'mail-admin': 'dspace-help@myu.edu', + 'mail-helpdesk': 'dspace-help@myu.edu', + 'alert-recipient': 'dspace-help@myu.edu' + }, + 'cors': { + 'allowed-origins': 'http://localhost:4000' + }, + 'ui': { + 'url': 'http://localhost:4000' + } +}; + +export const HealthInfoComponentTwo = { + 'version': '7.3-SNAPSHOT' +}; diff --git a/src/app/shared/mocks/request.service.mock.ts b/src/app/shared/mocks/request.service.mock.ts index f07aec587e..bce5b2d466 100644 --- a/src/app/shared/mocks/request.service.mock.ts +++ b/src/app/shared/mocks/request.service.mock.ts @@ -13,6 +13,7 @@ export function getMockRequestService(requestEntry$: Observable = isCachedOrPending: false, removeByHrefSubstring: observableOf(true), setStaleByHrefSubstring: observableOf(true), + setStaleByUUID: observableOf(true), hasByHref$: observableOf(false) }); } diff --git a/src/app/shared/mocks/section-sherpa-policies.service.mock.ts b/src/app/shared/mocks/section-sherpa-policies.service.mock.ts new file mode 100644 index 0000000000..9308325682 --- /dev/null +++ b/src/app/shared/mocks/section-sherpa-policies.service.mock.ts @@ -0,0 +1,101 @@ +import { + WorkspaceitemSectionSherpaPoliciesObject +} from '../../core/submission/models/workspaceitem-section-sherpa-policies.model'; + +export const SherpaDataResponse = { + 'id': 'sherpaPolicies', + 'retrievalTime': '2022-04-20T09:44:39.870+00:00', + 'sherpaResponse': + { + 'error': false, + 'message': null, + 'metadata': { + 'id': 23803, + 'uri': 'http://v2.sherpa.ac.uk/id/publication/23803', + 'dateCreated': '2012-11-20 14:51:52', + 'dateModified': '2020-03-06 11:25:54', + 'inDOAJ': false, + 'publiclyVisible': true + }, + 'journals': [{ + 'titles': ['The Lancet', 'Lancet'], + 'url': 'http://www.thelancet.com/journals/lancet/issue/current', + 'issns': ['0140-6736', '1474-547X'], + 'romeoPub': 'Elsevier: The Lancet', + 'zetoPub': 'Elsevier: The Lancet', + 'publisher': { + 'name': 'Elsevier', + 'relationshipType': null, + 'country': null, + 'uri': 'http://www.elsevier.com/', + 'identifier': null, + 'publicationCount': 0, + 'paidAccessDescription': 'Open access', + 'paidAccessUrl': 'https://www.elsevier.com/about/open-science/open-access' + }, + 'publishers': [{ + 'name': 'Elsevier', + 'relationshipType': null, + 'country': null, + 'uri': 'http://www.elsevier.com/', + 'identifier': null, + 'publicationCount': 0, + 'paidAccessDescription': 'Open access', + 'paidAccessUrl': 'https://www.elsevier.com/about/open-science/open-access' + }], + 'policies': [{ + 'id': 0, + 'openAccessPermitted': false, + 'uri': null, + 'internalMoniker': 'Lancet', + 'permittedVersions': [{ + 'articleVersion': 'submitted', + 'option': 1, + 'conditions': ['Upon publication publisher copyright and source must be acknowledged', 'Upon publication must link to publisher version'], + 'prerequisites': [], + 'locations': ['Author\'s Homepage', 'Preprint Repository'], + 'licenses': [], + 'embargo': null + }, { + 'articleVersion': 'accepted', + 'option': 1, + 'conditions': ['Publisher copyright and source must be acknowledged', 'Must link to publisher version'], + 'prerequisites': [], + 'locations': ['Author\'s Homepage', 'Institutional Website'], + 'licenses': ['CC BY-NC-ND'], + 'embargo': null + }, { + 'articleVersion': 'accepted', + 'option': 2, + 'conditions': ['Publisher copyright and source must be acknowledged', 'Must link to publisher version'], + 'prerequisites': ['If Required by Funder'], + 'locations': ['Non-Commercial Repository'], + 'licenses': ['CC BY-NC-ND'], + 'embargo': { amount: 6, units: 'Months' } + }, { + 'articleVersion': 'accepted', + 'option': 3, + 'conditions': ['Publisher copyright and source must be acknowledged', 'Must link to publisher version'], + 'prerequisites': [], + 'locations': ['Non-Commercial Repository'], + 'licenses': [], + 'embargo': null + }], + 'urls': { + 'http://download.thelancet.com/flatcontentassets/authors/lancet-information-for-authors.pdf': 'Guidelines for Authors', + 'http://www.thelancet.com/journals/lancet/article/PIIS0140-6736%2813%2960720-5/fulltext': 'The Lancet journals welcome a new open access policy', + 'http://www.thelancet.com/lancet-information-for-authors/after-publication': 'What happens after publication?', + 'http://www.thelancet.com/lancet/information-for-authors/disclosure-of-results': 'Disclosure of results before publication', + 'https://www.elsevier.com/__data/assets/pdf_file/0005/78476/external-embargo-list.pdf': 'Journal Embargo Period List', + 'https://www.elsevier.com/__data/assets/pdf_file/0011/78473/UK-Embargo-Periods.pdf': 'Journal Embargo List for UK Authors' + }, + 'openAccessProhibited': false, + 'publicationCount': 0, + 'preArchiving': 'can', + 'postArchiving': 'can', + 'pubArchiving': 'cannot' + }], + 'inDOAJ': false + }] + } +} as WorkspaceitemSectionSherpaPoliciesObject; diff --git a/src/app/shared/notifications/notification/notification.component.spec.ts b/src/app/shared/notifications/notification/notification.component.spec.ts index 8101f51329..f6de303726 100644 --- a/src/app/shared/notifications/notification/notification.component.spec.ts +++ b/src/app/shared/notifications/notification/notification.component.spec.ts @@ -98,7 +98,7 @@ describe('NotificationComponent', () => { it('should have html content', () => { fixture = TestBed.createComponent(NotificationComponent); comp = fixture.componentInstance; - const htmlContent = 'test'; + const htmlContent = 'test'; comp.notification = { id: '1', type: NotificationType.Info, diff --git a/src/app/shared/object-grid/object-grid.component.html b/src/app/shared/object-grid/object-grid.component.html index 07dbcec805..c988e76803 100644 --- a/src/app/shared/object-grid/object-grid.component.html +++ b/src/app/shared/object-grid/object-grid.component.html @@ -18,7 +18,7 @@ >
-
+
diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index 492cb4cf07..927f2b9d2a 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -17,7 +17,7 @@ (next)="goNext()" >
    -
  • +
  • ) => { - this.processing$.next(false); - if (responseRD && responseRD.hasSucceeded) { - this.notificationsService.success(null, this.translate.get('resource-policies.edit.page.success.content')); - this.redirectToAuthorizationsPage(); - } else { - this.notificationsService.error(null, this.translate.get('resource-policies.edit.page.failure.content')); + map((responseRD) => responseRD && responseRD.hasSucceeded) + ) : of(true); + + const updateResourcePolicySucceeded$ = this.resourcePolicyService.update(updatedObject).pipe( + getFirstCompletedRemoteData(), + map((responseRD) => responseRD && responseRD.hasSucceeded) + ); + + observableCombineLatest([updateTargetSucceeded$, updateResourcePolicySucceeded$]).subscribe( + ([updateTargetSucceeded, updateResourcePolicySucceeded]) => { + this.processing$.next(false); + if (updateTargetSucceeded && updateResourcePolicySucceeded) { + this.notificationsService.success(null, this.translate.get('resource-policies.edit.page.success.content')); + this.redirectToAuthorizationsPage(); + } else if (updateResourcePolicySucceeded) { // everything except target has been updated + this.notificationsService.error(null, this.translate.get('resource-policies.edit.page.target-failure.content')); + } else if (updateTargetSucceeded) { // only target has been updated + this.notificationsService.error(null, this.translate.get('resource-policies.edit.page.other-failure.content')); + } else { // nothing has been updated + this.notificationsService.error(null, this.translate.get('resource-policies.edit.page.failure.content')); + } } - }); + ); } } diff --git a/src/app/shared/resource-policies/form/resource-policy-form.component.html b/src/app/shared/resource-policies/form/resource-policy-form.component.html index 42475a955b..66c1fc400e 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.component.html +++ b/src/app/shared/resource-policies/form/resource-policy-form.component.html @@ -7,25 +7,23 @@ [displayCancel]="false"> + + + + + + + diff --git a/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts b/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts index 5cc6397118..e555522c79 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts @@ -242,6 +242,7 @@ describe('ResourcePolicyFormComponent test suite', () => { fixture = TestBed.createComponent(ResourcePolicyFormComponent); comp = fixture.componentInstance; compAsAny = fixture.componentInstance; + compAsAny.resourcePolicy = resourcePolicy; comp.isProcessing = observableOf(false); }); @@ -253,6 +254,8 @@ describe('ResourcePolicyFormComponent test suite', () => { }); it('should init form model properly', () => { + epersonService.findByHref.and.returnValue(observableOf(undefined)); + groupService.findByHref.and.returnValue(observableOf(undefined)); spyOn(compAsAny, 'isFormValid').and.returnValue(observableOf(false)); spyOn(compAsAny, 'initModelsValue').and.callThrough(); spyOn(compAsAny, 'buildResourcePolicyForm').and.callThrough(); @@ -261,12 +264,12 @@ describe('ResourcePolicyFormComponent test suite', () => { expect(compAsAny.buildResourcePolicyForm).toHaveBeenCalled(); expect(compAsAny.initModelsValue).toHaveBeenCalled(); expect(compAsAny.formModel.length).toBe(5); - expect(compAsAny.subs.length).toBe(0); + expect(compAsAny.subs.length).toBe(1); }); it('should can set grant', () => { - expect(comp.canSetGrant()).toBeTruthy(); + expect(comp.isBeingEdited()).toBeTruthy(); }); it('should not have a target name', () => { @@ -279,7 +282,7 @@ describe('ResourcePolicyFormComponent test suite', () => { expect(compAsAny.reset.emit).toHaveBeenCalled(); }); - it('should update resource policy grant object properly', () => { + it('should update resource policy grant object properly', () => { comp.updateObjectSelected(EPersonMock, true); expect(comp.resourcePolicyGrant).toEqual(EPersonMock); @@ -301,6 +304,7 @@ describe('ResourcePolicyFormComponent test suite', () => { comp = fixture.componentInstance; compAsAny = fixture.componentInstance; comp.resourcePolicy = resourcePolicy; + compAsAny.resourcePolicy = resourcePolicy; comp.isProcessing = observableOf(false); compAsAny.ePersonService.findByHref.and.returnValue( observableOf(createSuccessfulRemoteDataObject({})).pipe(delay(100)) @@ -343,8 +347,8 @@ describe('ResourcePolicyFormComponent test suite', () => { }); }); - it('should not can set grant', () => { - expect(comp.canSetGrant()).toBeFalsy(); + it('should be being edited', () => { + expect(comp.isBeingEdited()).toBeTrue(); }); it('should have a target name', () => { @@ -398,6 +402,7 @@ describe('ResourcePolicyFormComponent test suite', () => { type: 'group', uuid: GroupMock.id }; + eventPayload.updateTarget = false; scheduler = getTestScheduler(); scheduler.schedule(() => comp.onSubmit()); diff --git a/src/app/shared/resource-policies/form/resource-policy-form.component.ts b/src/app/shared/resource-policies/form/resource-policy-form.component.ts index 7ae699340e..223e610908 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.component.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { Observable, @@ -41,6 +41,7 @@ import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { RequestService } from '../../../core/data/request.service'; +import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'; export interface ResourcePolicyEvent { object: ResourcePolicy; @@ -48,6 +49,7 @@ export interface ResourcePolicyEvent { type: string, uuid: string }; + updateTarget: boolean; } @Component({ @@ -83,6 +85,8 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { */ @Output() submit: EventEmitter = new EventEmitter(); + @ViewChild('content') content: ElementRef; + /** * The form id * @type {string} @@ -125,6 +129,10 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { */ private subs: Subscription[] = []; + navActiveId: string; + + resourcePolicyTargetUpdated = false; + /** * Initialize instance variables * @@ -133,6 +141,7 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { * @param {FormService} formService * @param {GroupDataService} groupService * @param {RequestService} requestService + * @param modalService */ constructor( private dsoNameService: DSONameService, @@ -140,6 +149,7 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { private formService: FormService, private groupService: GroupDataService, private requestService: RequestService, + private modalService: NgbModal, ) { } @@ -151,7 +161,7 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { this.formId = this.formService.getUniqueId('resource-policy-form'); this.formModel = this.buildResourcePolicyForm(); - if (!this.canSetGrant()) { + if (this.isBeingEdited()) { const epersonRD$ = this.ePersonService.findByHref(this.resourcePolicy._links.eperson.href, false).pipe( getFirstSucceededRemoteData() ); @@ -169,6 +179,7 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { filter(() => this.isActive), ).subscribe((dsoRD: RemoteData) => { this.resourcePolicyGrant = dsoRD.payload; + this.navActiveId = String(dsoRD.payload.type); this.resourcePolicyTargetName$.next(this.getResourcePolicyTargetName()); }) ); @@ -193,19 +204,12 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { */ private buildResourcePolicyForm(): DynamicFormControlModel[] { const formModel: DynamicFormControlModel[] = []; - // TODO to be removed when https://jira.lyrasis.org/browse/DS-4477 will be implemented - const policyTypeConf = Object.assign({}, RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG, { - disabled: isNotEmpty(this.resourcePolicy) - }); - // TODO to be removed when https://jira.lyrasis.org/browse/DS-4477 will be implemented - const actionConf = Object.assign({}, RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG, { - disabled: isNotEmpty(this.resourcePolicy) - }); + formModel.push( new DsDynamicInputModel(RESOURCE_POLICY_FORM_NAME_CONFIG), new DsDynamicTextAreaModel(RESOURCE_POLICY_FORM_DESCRIPTION_CONFIG), - new DynamicSelectModel(policyTypeConf), - new DynamicSelectModel(actionConf) + new DynamicSelectModel(RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG), + new DynamicSelectModel(RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG) ); const startDateModel = new DynamicDatePickerModel( @@ -255,8 +259,8 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { * * @return true if is possible, false otherwise */ - canSetGrant(): boolean { - return isEmpty(this.resourcePolicy); + isBeingEdited(): boolean { + return !isEmpty(this.resourcePolicy); } /** @@ -272,8 +276,10 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { * Update reference to the eperson or group that will be granted the permission */ updateObjectSelected(object: DSpaceObject, isEPerson: boolean): void { + this.resourcePolicyTargetUpdated = true; this.resourcePolicyGrant = object; this.resourcePolicyGrantType = isEPerson ? 'eperson' : 'group'; + this.resourcePolicyTargetName$.next(this.getResourcePolicyTargetName()); } /** @@ -297,6 +303,7 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { type: this.resourcePolicyGrantType, uuid: this.resourcePolicyGrant.id }; + eventPayload.updateTarget = this.resourcePolicyTargetUpdated; this.submit.emit(eventPayload); }); } @@ -329,4 +336,12 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()); } + + onNavChange(changeEvent: NgbNavChangeEvent) { + // if a policy is being edited it should not be possible to switch between group and eperson + if (this.isBeingEdited()) { + changeEvent.preventDefault(); + this.modalService.open(this.content); + } + } } diff --git a/src/app/shared/resource-policies/resource-policies.component.spec.ts b/src/app/shared/resource-policies/resource-policies.component.spec.ts index 894949ba47..fc60229ff4 100644 --- a/src/app/shared/resource-policies/resource-policies.component.spec.ts +++ b/src/app/shared/resource-policies/resource-policies.component.spec.ts @@ -343,6 +343,16 @@ describe('ResourcePoliciesComponent test suite', () => { fixture.detectChanges(); }); + it('should call ResourcePolicyService.delete for the checked policies', () => { + resourcePolicyService.delete.and.returnValue(observableOf(true)); + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.deleteSelectedResourcePolicies()); + scheduler.flush(); + + // only the first one is checked + expect(resourcePolicyService.delete).toHaveBeenCalledWith(resourcePolicy.id); + }); + it('should notify success when delete is successful', () => { resourcePolicyService.delete.and.returnValue(observableOf(true)); diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts index 94c928438a..9313d23c0b 100644 --- a/src/app/shared/resource-policies/resource-policies.component.ts +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -157,7 +157,6 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { } else { this.notificationsService.error(null, this.translate.get('resource-policies.delete.failure.content')); } - this.requestService.setStaleByHrefSubstring(this.resourceUUID); this.processingDelete$.next(false); }) ); diff --git a/src/app/shared/search-form/search-form.component.html b/src/app/shared/search-form/search-form.component.html index 354e30233a..226962cc61 100644 --- a/src/app/shared/search-form/search-form.component.html +++ b/src/app/shared/search-form/search-form.component.html @@ -4,10 +4,10 @@
    - - +
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 6f485ec77e..4b5844f660 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -13,6 +13,7 @@ import { SearchConfigurationService } from '../../core/shared/search/search-conf import { PaginationServiceStub } from '../testing/pagination-service.stub'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe'; import { SearchServiceStub } from '../testing/search-service.stub'; import { Router } from '@angular/router'; import { RouterStub } from '../testing/router.stub'; @@ -41,7 +42,10 @@ describe('SearchFormComponent', () => { { provide: SearchConfigurationService, useValue: searchConfigService }, { provide: DSpaceObjectDataService, useValue: dspaceObjectService }, ], - declarations: [SearchFormComponent] + declarations: [ + SearchFormComponent, + BrowserOnlyMockPipe, + ] }).compileComponents(); })); diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-filter.component.html index 230f072772..452433e165 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.html @@ -4,6 +4,7 @@ class="filter-name d-flex" [attr.aria-controls]="regionId" [id]="toggleId" [attr.aria-expanded]="false" [attr.aria-label]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate" + [attr.data-test]="'filter-toggle' | dsBrowserOnly" >
{{'search.filters.filter.' + filter.name + '.head'| translate}} diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts index 662b380ce8..7abe45ca8c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts @@ -13,6 +13,7 @@ import { FilterType } from '../../models/filter-type.model'; import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub'; import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component'; import { SequenceService } from '../../../../core/shared/sequence.service'; +import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe'; describe('SearchFilterComponent', () => { let comp: SearchFilterComponent; @@ -62,7 +63,10 @@ describe('SearchFilterComponent', () => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule], - declarations: [SearchFilterComponent], + declarations: [ + SearchFilterComponent, + BrowserOnlyMockPipe, + ], providers: [ { provide: SearchService, useValue: searchServiceStub }, { diff --git a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts index 94e05b64fe..fadde46e53 100644 --- a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts +++ b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts @@ -84,7 +84,7 @@ describe('SearchSwitchConfigurationComponent', () => { expect(childElements.length).toEqual(comp.configurationList.length); }); - it('should call onSelect method when selecting an option', () => { + it('should call onSelect method when selecting an option', waitForAsync(() => { fixture.whenStable().then(() => { spyOn(comp, 'onSelect'); select = fixture.debugElement.query(By.css('select')); @@ -94,8 +94,7 @@ describe('SearchSwitchConfigurationComponent', () => { fixture.detectChanges(); expect(comp.onSelect).toHaveBeenCalled(); }); - - }); + })); it('should navigate to the route when selecting an option', () => { spyOn((comp as any), 'getSearchLinkParts').and.returnValue([MYDSPACE_ROUTE]); diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 051468c089..d9cc08182a 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -7,7 +7,12 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { NouisliderModule } from 'ng2-nouislider'; import { - NgbDatepickerModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbTimepickerModule, NgbTooltipModule, + NgbDatepickerModule, + NgbDropdownModule, + NgbNavModule, + NgbPaginationModule, + NgbTimepickerModule, + NgbTooltipModule, NgbTypeaheadModule, } from '@ng-bootstrap/ng-bootstrap'; import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core'; @@ -16,7 +21,9 @@ import { FileUploadModule } from 'ng2-file-upload'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MomentModule } from 'ngx-moment'; import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component'; -import { ExportMetadataSelectorComponent } from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; +import { + ExportMetadataSelectorComponent +} from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component'; import { ItemListElementComponent } from './object-list/item-list-element/item-types/item/item-list-element.component'; import { EnumKeysPipe } from './utils/enum-keys-pipe'; @@ -24,13 +31,21 @@ import { FileSizePipe } from './utils/file-size-pipe'; import { MetadataFieldValidator } from './utils/metadatafield-validator.directive'; import { SafeUrlPipe } from './utils/safe-url-pipe'; import { ConsolePipe } from './utils/console.pipe'; -import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component'; +import { + CollectionListElementComponent +} from './object-list/collection-list-element/collection-list-element.component'; import { CommunityListElementComponent } from './object-list/community-list-element/community-list-element.component'; -import { SearchResultListElementComponent } from './object-list/search-result-list-element/search-result-list-element.component'; +import { + SearchResultListElementComponent +} from './object-list/search-result-list-element/search-result-list-element.component'; import { ObjectListComponent } from './object-list/object-list.component'; -import { CollectionGridElementComponent } from './object-grid/collection-grid-element/collection-grid-element.component'; +import { + CollectionGridElementComponent +} from './object-grid/collection-grid-element/collection-grid-element.component'; import { CommunityGridElementComponent } from './object-grid/community-grid-element/community-grid-element.component'; -import { AbstractListableElementComponent } from './object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { + AbstractListableElementComponent +} from './object-collection/shared/object-collection-element/abstract-listable-element.component'; import { ObjectGridComponent } from './object-grid/object-grid.component'; import { ObjectCollectionComponent } from './object-collection/object-collection.component'; import { ErrorComponent } from './error/error.component'; @@ -38,7 +53,9 @@ import { LoadingComponent } from './loading/loading.component'; import { PaginationComponent } from './pagination/pagination.component'; import { ThumbnailComponent } from '../thumbnail/thumbnail.component'; import { SearchFormComponent } from './search-form/search-form.component'; -import { SearchResultGridElementComponent } from './object-grid/search-result-grid-element/search-result-grid-element.component'; +import { + SearchResultGridElementComponent +} from './object-grid/search-result-grid-element/search-result-grid-element.component'; import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component'; import { VarDirective } from './utils/var.directive'; import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component'; @@ -53,21 +70,33 @@ import { ChipsComponent } from './chips/chips.component'; import { NumberPickerComponent } from './number-picker/number-picker.component'; import { MockAdminGuard } from './mocks/admin-guard.service.mock'; import { AlertComponent } from './alert/alert.component'; -import { SearchResultDetailElementComponent } from './object-detail/my-dspace-result-detail-element/search-result-detail-element.component'; +import { + SearchResultDetailElementComponent +} from './object-detail/my-dspace-result-detail-element/search-result-detail-element.component'; import { ClaimedTaskActionsComponent } from './mydspace-actions/claimed-task/claimed-task-actions.component'; import { PoolTaskActionsComponent } from './mydspace-actions/pool-task/pool-task-actions.component'; import { ObjectDetailComponent } from './object-detail/object-detail.component'; -import { ItemDetailPreviewComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component'; -import { MyDSpaceItemStatusComponent } from './object-collection/shared/mydspace-item-status/my-dspace-item-status.component'; +import { + ItemDetailPreviewComponent +} from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component'; +import { + MyDSpaceItemStatusComponent +} from './object-collection/shared/mydspace-item-status/my-dspace-item-status.component'; import { WorkspaceitemActionsComponent } from './mydspace-actions/workspaceitem/workspaceitem-actions.component'; import { WorkflowitemActionsComponent } from './mydspace-actions/workflowitem/workflowitem-actions.component'; import { ItemSubmitterComponent } from './object-collection/shared/mydspace-item-submitter/item-submitter.component'; import { ItemActionsComponent } from './mydspace-actions/item/item-actions.component'; -import { ClaimedTaskActionsApproveComponent } from './mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component'; -import { ClaimedTaskActionsRejectComponent } from './mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component'; +import { + ClaimedTaskActionsApproveComponent +} from './mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component'; +import { + ClaimedTaskActionsRejectComponent +} from './mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component'; import { ObjNgFor } from './utils/object-ngfor.pipe'; import { BrowseByComponent } from './browse-by/browse-by.component'; -import { BrowseEntryListElementComponent } from './object-list/browse-entry-list-element/browse-entry-list-element.component'; +import { + BrowseEntryListElementComponent +} from './object-list/browse-entry-list-element/browse-entry-list-element.component'; import { DebounceDirective } from './utils/debounce.directive'; import { ClickOutsideDirective } from './utils/click-outside.directive'; import { EmphasizePipe } from './utils/emphasize.pipe'; @@ -76,54 +105,106 @@ import { CapitalizePipe } from './utils/capitalize.pipe'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; import { AuthorityConfidenceStateDirective } from './authority-confidence/authority-confidence-state.directive'; import { LangSwitchComponent } from './lang-switch/lang-switch.component'; -import { PlainTextMetadataListElementComponent } from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component'; -import { ItemMetadataListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component'; -import { MetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/metadata-representation-list-element.component'; +import { + PlainTextMetadataListElementComponent +} from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component'; +import { + ItemMetadataListElementComponent +} from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component'; +import { + MetadataRepresentationListElementComponent +} from './object-list/metadata-representation-list-element/metadata-representation-list-element.component'; import { ObjectValuesPipe } from './utils/object-values-pipe'; import { InListValidator } from './utils/in-list-validator.directive'; import { AutoFocusDirective } from './utils/auto-focus.directive'; import { StartsWithDateComponent } from './starts-with/date/starts-with-date.component'; import { StartsWithTextComponent } from './starts-with/text/starts-with-text.component'; import { DSOSelectorComponent } from './dso-selector/dso-selector/dso-selector.component'; -import { CreateCommunityParentSelectorComponent } from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; -import { CreateItemParentSelectorComponent } from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; -import { CreateCollectionParentSelectorComponent } from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; -import { CommunitySearchResultListElementComponent } from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; -import { CollectionSearchResultListElementComponent } from './object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component'; -import { EditItemSelectorComponent } from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; -import { EditCommunitySelectorComponent } from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; -import { EditCollectionSelectorComponent } from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; -import { ItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; -import { MetadataFieldWrapperComponent } from '../item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component'; +import { + CreateCommunityParentSelectorComponent +} from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; +import { + CreateItemParentSelectorComponent +} from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +import { + CreateCollectionParentSelectorComponent +} from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; +import { + CommunitySearchResultListElementComponent +} from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; +import { + CollectionSearchResultListElementComponent +} from './object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component'; +import { + EditItemSelectorComponent +} from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; +import { + EditCommunitySelectorComponent +} from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; +import { + EditCollectionSelectorComponent +} from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; +import { + ItemListPreviewComponent +} from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; +import { + MetadataFieldWrapperComponent +} from '../item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component'; import { MetadataValuesComponent } from '../item-page/field-components/metadata-values/metadata-values.component'; import { RoleDirective } from './roles/role.directive'; import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component'; -import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component'; -import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; -import { CollectionSearchResultGridElementComponent } from './object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; -import { CommunitySearchResultGridElementComponent } from './object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'; +import { + ClaimedTaskActionsReturnToPoolComponent +} from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component'; +import { + ItemDetailPreviewFieldComponent +} from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; +import { + CollectionSearchResultGridElementComponent +} from './object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; +import { + CommunitySearchResultGridElementComponent +} from './object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'; import { PageSizeSelectorComponent } from './page-size-selector/page-size-selector.component'; import { AbstractTrackableComponent } from './trackable/abstract-trackable.component'; -import { ComcolMetadataComponent } from './comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; +import { + ComcolMetadataComponent +} from './comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; import { ItemSelectComponent } from './object-select/item-select/item-select.component'; import { CollectionSelectComponent } from './object-select/collection-select/collection-select.component'; -import { FilterInputSuggestionsComponent } from './input-suggestions/filter-suggestions/filter-input-suggestions.component'; -import { DsoInputSuggestionsComponent } from './input-suggestions/dso-input-suggestions/dso-input-suggestions.component'; +import { + FilterInputSuggestionsComponent +} from './input-suggestions/filter-suggestions/filter-input-suggestions.component'; +import { + DsoInputSuggestionsComponent +} from './input-suggestions/dso-input-suggestions/dso-input-suggestions.component'; import { ItemGridElementComponent } from './object-grid/item-grid-element/item-types/item/item-grid-element.component'; import { TypeBadgeComponent } from './object-list/type-badge/type-badge.component'; import { AccessStatusBadgeComponent } from './object-list/access-status-badge/access-status-badge.component'; -import { MetadataRepresentationLoaderComponent } from './metadata-representation/metadata-representation-loader.component'; +import { + MetadataRepresentationLoaderComponent +} from './metadata-representation/metadata-representation-loader.component'; import { MetadataRepresentationDirective } from './metadata-representation/metadata-representation.directive'; -import { ListableObjectComponentLoaderComponent } from './object-collection/shared/listable-object/listable-object-component-loader.component'; -import { ItemSearchResultListElementComponent } from './object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; +import { + ListableObjectComponentLoaderComponent +} from './object-collection/shared/listable-object/listable-object-component-loader.component'; +import { + ItemSearchResultListElementComponent +} from './object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; import { ListableObjectDirective } from './object-collection/shared/listable-object/listable-object.directive'; -import { ItemMetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; +import { + ItemMetadataRepresentationListElementComponent +} from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; import { PageWithSidebarComponent } from './sidebar/page-with-sidebar.component'; import { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component'; import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component'; import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component'; -import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; -import { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component'; +import { + SelectableListItemControlComponent +} from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; +import { + ImportableListItemControlComponent +} from './object-collection/shared/importable-list-item-control/importable-list-item-control.component'; import { ItemVersionsComponent } from './item/item-versions/item-versions.component'; import { SortablejsModule } from 'ngx-sortablejs'; import { LogInContainerComponent } from './log-in/container/log-in-container.component'; @@ -136,10 +217,16 @@ import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-ve import { FileValidator } from './utils/require-file.validator'; import { FileValueAccessorDirective } from './utils/file-value-accessor.directive'; import { FileSectionComponent } from '../item-page/simple/field-components/file-section/file-section.component'; -import { ModifyItemOverviewComponent } from '../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; -import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component'; +import { + ModifyItemOverviewComponent +} from '../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; +import { + ClaimedTaskActionsLoaderComponent +} from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component'; import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive'; -import { ClaimedTaskActionsEditMetadataComponent } from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; +import { + ClaimedTaskActionsEditMetadataComponent +} from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; import { ImpersonateNavbarComponent } from './impersonate-navbar/impersonate-navbar.component'; import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive'; import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component'; @@ -147,37 +234,62 @@ import { CollectionDropdownComponent } from './collection-dropdown/collection-dr import { EntityDropdownComponent } from './entity-dropdown/entity-dropdown.component'; import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component'; import { CurationFormComponent } from '../curation-form/curation-form.component'; -import { PublicationSidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component'; -import { SidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/sidebar-search-list-element.component'; -import { CollectionSidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component'; -import { CommunitySidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component'; -import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component'; +import { + PublicationSidebarSearchListElementComponent +} from './object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component'; +import { + SidebarSearchListElementComponent +} from './object-list/sidebar-search-list-element/sidebar-search-list-element.component'; +import { + CollectionSidebarSearchListElementComponent +} from './object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component'; +import { + CommunitySidebarSearchListElementComponent +} from './object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component'; +import { + AuthorizedCollectionSelectorComponent +} from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component'; import { DsoPageEditButtonComponent } from './dso-page/dso-page-edit-button/dso-page-edit-button.component'; import { DsoPageVersionButtonComponent } from './dso-page/dso-page-version-button/dso-page-version-button.component'; import { HoverClassDirective } from './hover-class.directive'; -import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component'; +import { + ValidationSuggestionsComponent +} from './input-suggestions/validation-suggestions/validation-suggestions.component'; import { ItemAlertsComponent } from './item/item-alerts/item-alerts.component'; -import { ItemSearchResultGridElementComponent } from './object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; +import { + ItemSearchResultGridElementComponent +} from './object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; 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 { + 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'; import { LinkMenuItemComponent } from './menu/menu-item/link-menu-item.component'; import { OnClickMenuItemComponent } from './menu/menu-item/onclick-menu-item.component'; import { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component'; import { SearchNavbarComponent } from '../search-navbar/search-navbar.component'; -import { ItemVersionsSummaryModalComponent } from './item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component'; -import { ItemVersionsDeleteModalComponent } from './item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component'; +import { + ItemVersionsSummaryModalComponent +} from './item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component'; +import { + ItemVersionsDeleteModalComponent +} from './item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component'; import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component'; -import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component'; +import { + BitstreamRequestACopyPageComponent +} from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component'; import { DsSelectComponent } from './ds-select/ds-select.component'; import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'; import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; -import { ClaimItemSelectorComponent } from './dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component'; import { RSSComponent } from './rss-feed/rss.component'; import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component'; import { DsoPageOrcidButtonComponent } from './dso-page/dso-page-orcid-button/dso-page-orcid-button.component'; import { LogInOrcidComponent } from './log-in/methods/orcid/log-in-orcid.component'; +import { BrowserOnlyPipe } from './utils/browser-only.pipe'; +import { PersonPageClaimButtonComponent } from './dso-page/person-page-claim-button/person-page-claim-button.component'; const MODULES = [ CommonModule, @@ -219,7 +331,8 @@ const PIPES = [ ObjectKeysPipe, ObjectValuesPipe, ConsolePipe, - ObjNgFor + ObjNgFor, + BrowserOnlyPipe, ]; const COMPONENTS = [ @@ -348,9 +461,7 @@ const COMPONENTS = [ CollectionSidebarSearchListElementComponent, CommunitySidebarSearchListElementComponent, SearchNavbarComponent, - ScopeSelectorModalComponent, - - ClaimItemSelectorComponent + ScopeSelectorModalComponent ]; const ENTRY_COMPONENTS = [ @@ -408,7 +519,6 @@ const ENTRY_COMPONENTS = [ OnClickMenuItemComponent, TextMenuItemComponent, ScopeSelectorModalComponent, - ClaimItemSelectorComponent, ExternalLinkMenuItemComponent ]; @@ -417,6 +527,7 @@ const SHARED_ITEM_PAGE_COMPONENTS = [ MetadataValuesComponent, DsoPageEditButtonComponent, DsoPageVersionButtonComponent, + PersonPageClaimButtonComponent, ItemAlertsComponent, GenericItemPageFieldComponent, MetadataRepresentationListComponent, diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.html b/src/app/shared/sidebar/filter/sidebar-filter.component.html index bd392aa715..79afaa7583 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.component.html +++ b/src/app/shared/sidebar/filter/sidebar-filter.component.html @@ -1,5 +1,5 @@
-
+
{{ label | translate }}
diff --git a/src/app/shared/testing/auth-request-service.stub.ts b/src/app/shared/testing/auth-request-service.stub.ts index 0dc57427dd..0094324518 100644 --- a/src/app/shared/testing/auth-request-service.stub.ts +++ b/src/app/shared/testing/auth-request-service.stub.ts @@ -34,6 +34,9 @@ export class AuthRequestServiceStub { }, eperson: { href: this.mockUser._links.self.href + }, + specialGroups: { + href: this.mockUser._links.self.href } }; } else { @@ -62,6 +65,9 @@ export class AuthRequestServiceStub { }, eperson: { href: this.mockUser._links.self.href + }, + specialGroups: { + href: this.mockUser._links.self.href } }; } else { diff --git a/src/app/shared/testing/browser-only-mock.pipe.ts b/src/app/shared/testing/browser-only-mock.pipe.ts new file mode 100644 index 0000000000..12e5a7b2d7 --- /dev/null +++ b/src/app/shared/testing/browser-only-mock.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/** + * Support dsBrowserOnly in unit tests. + */ +@Pipe({ + name: 'dsBrowserOnly' +}) +export class BrowserOnlyMockPipe implements PipeTransform { + transform(value: string): string | undefined { + return value; + } +} diff --git a/src/app/shared/testing/menu-service.stub.ts b/src/app/shared/testing/menu-service.stub.ts index 14eccfda67..926232bad0 100644 --- a/src/app/shared/testing/menu-service.stub.ts +++ b/src/app/shared/testing/menu-service.stub.ts @@ -1,5 +1,6 @@ import { Observable, of as observableOf } from 'rxjs'; import { MenuSection } from '../menu/menu-section.model'; +import { MenuState } from '../menu/menu-state.model'; import { MenuID } from '../menu/menu-id.model'; export class MenuServiceStub { @@ -77,6 +78,10 @@ export class MenuServiceStub { return observableOf(true); } + getMenu(id: MenuID): Observable { // todo: resolve import + return observableOf({} as MenuState); + } + getMenuTopSections(id: MenuID): Observable { return observableOf([this.visibleSection1, this.visibleSection2]); } diff --git a/src/app/shared/testing/sections-service.stub.ts b/src/app/shared/testing/sections-service.stub.ts index 1628453bc8..b687c512c2 100644 --- a/src/app/shared/testing/sections-service.stub.ts +++ b/src/app/shared/testing/sections-service.stub.ts @@ -20,4 +20,5 @@ export class SectionsServiceStub { computeSectionConfiguredMetadata = jasmine.createSpy('computeSectionConfiguredMetadata'); getShownSectionErrors = jasmine.createSpy('getShownSectionErrors'); getSectionServerErrors = jasmine.createSpy('getSectionServerErrors'); + getIsInformational = jasmine.createSpy('getIsInformational'); } diff --git a/src/app/shared/testing/special-group.mock.ts b/src/app/shared/testing/special-group.mock.ts new file mode 100644 index 0000000000..f1102e0584 --- /dev/null +++ b/src/app/shared/testing/special-group.mock.ts @@ -0,0 +1,56 @@ +import { Observable } from 'rxjs'; + +import { EPersonMock } from './eperson.mock'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { Group } from '../../core/eperson/models/group.model'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { RemoteData } from '../../core/data/remote-data'; + +export const SpecialGroupMock2: Group = Object.assign(new Group(), { + handle: null, + subgroups: [], + epersons: [], + permanent: true, + selfRegistered: false, + _links: { + self: { + href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2', + }, + subgroups: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2/subgroups' }, + object: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2/object' }, + epersons: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2/epersons' } + }, + _name: 'testgroupname2', + id: 'testgroupid2', + uuid: 'testgroupid2', + type: 'specialGroups', + // object: createSuccessfulRemoteDataObject$({ name: 'testspecialGroupsid2objectName'}) +}); + +export const SpecialGroupMock: Group = Object.assign(new Group(), { + handle: null, + subgroups: [SpecialGroupMock2], + epersons: [EPersonMock], + selfRegistered: false, + permanent: false, + _links: { + self: { + href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid', + }, + subgroups: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid/subgroups' }, + object: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid2/object' }, + epersons: { href: 'https://rest.api/server/api/eperson/specialGroups/testgroupid/epersons' } + }, + _name: 'testgroupname', + id: 'testgroupid', + uuid: 'testgroupid', + type: 'specialGroups', +}); + +export const SpecialGroupDataMock: RemoteData> = createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), [SpecialGroupMock2,SpecialGroupMock])); +export const SpecialGroupDataMock$: Observable>> = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [SpecialGroupMock2,SpecialGroupMock])); +export const EmptySpecialGroupDataMock$: Observable>> = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); + + + diff --git a/src/app/shared/testing/test-module.ts b/src/app/shared/testing/test-module.ts index b43adde8ae..7d45dd41f3 100644 --- a/src/app/shared/testing/test-module.ts +++ b/src/app/shared/testing/test-module.ts @@ -5,6 +5,7 @@ import { SharedModule } from '../shared.module'; import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive.stub'; import { QueryParamsDirectiveStub } from './query-params-directive.stub'; import { RouterLinkDirectiveStub } from './router-link-directive.stub'; +import { BrowserOnlyMockPipe } from './browser-only-mock.pipe'; /** * This module isn't used. It serves to prevent the AoT compiler @@ -21,7 +22,8 @@ import { RouterLinkDirectiveStub } from './router-link-directive.stub'; QueryParamsDirectiveStub, MySimpleItemActionComponent, RouterLinkDirectiveStub, - NgComponentOutletDirectiveStub + NgComponentOutletDirectiveStub, + BrowserOnlyMockPipe, ], exports: [ QueryParamsDirectiveStub diff --git a/src/app/shared/utils/browser-only.pipe.ts b/src/app/shared/utils/browser-only.pipe.ts new file mode 100644 index 0000000000..e3ee3df69d --- /dev/null +++ b/src/app/shared/utils/browser-only.pipe.ts @@ -0,0 +1,35 @@ +import { Inject, Pipe, PipeTransform, PLATFORM_ID } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; + +/** + * A pipe that only returns its input when run in the browser. + * Used to distinguish client-side rendered content from server-side rendered content. + * + * When used with attributes as in + * ``` + * [attr.data-test]="'something' | dsBrowserOnly" + * ``` + * the server-side rendered HTML will not contain the `data-test` attribute. + * When rendered client-side, the HTML will contain `data-test="something"` + * + * This can be useful for end-to-end testing elements that need JS (that isn't included in SSR HTML) to function: + * By depending on `dsBrowserOnly` attributes in tests we can make sure we wait + * until such components are fully interactive before trying to interact with them. + */ +@Pipe({ + name: 'dsBrowserOnly' +}) +export class BrowserOnlyPipe implements PipeTransform { + constructor( + @Inject(PLATFORM_ID) private platformID: any, + ) { + } + + transform(value: string): string | undefined { + if (isPlatformBrowser((this.platformID))) { + return value; + } else { + return undefined; + } + } +} diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.html b/src/app/shared/view-mode-switch/view-mode-switch.component.html index 2863a4832b..5f70bc699c 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.html +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.html @@ -7,7 +7,7 @@ routerLinkActive="active" [class.active]="currentMode === viewModeEnum.ListElement" class="btn btn-secondary" - data-test="list-view"> + [attr.data-test]="'list-view' | dsBrowserOnly"> + [attr.data-test]="'grid-view' | dsBrowserOnly"> + [attr.data-test]="'detail-view' | dsBrowserOnly">
diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts index d361857413..5780ea5cf3 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts @@ -9,6 +9,7 @@ import { SearchService } from '../../core/shared/search/search.service'; import { ViewModeSwitchComponent } from './view-mode-switch.component'; import { SearchServiceStub } from '../testing/search-service.stub'; import { ViewMode } from '../../core/shared/view-mode.model'; +import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe'; @Component({ template: '' }) class DummyComponent { @@ -36,7 +37,8 @@ describe('ViewModeSwitchComponent', () => { ], declarations: [ ViewModeSwitchComponent, - DummyComponent + DummyComponent, + BrowserOnlyMockPipe, ], providers: [ { provide: SearchService, useValue: searchService }, diff --git a/src/app/submission/form/footer/submission-form-footer.component.html b/src/app/submission/form/footer/submission-form-footer.component.html index 7013c55667..e898e1c220 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.html +++ b/src/app/submission/form/footer/submission-form-footer.component.html @@ -3,6 +3,7 @@ +
+ {{version.embargo.amount}} + {{version.embargo?.units[0]}} + + {{ + 'submission.sections.sherpa.publisher.policy.noembargo' | translate }} + + + + + + {{version.locations[0]}} +{{version.locations.length-1}} + + + {{ + 'submission.sections.sherpa.publisher.policy.nolocation' | translate }} + + +
+
+
+ + +
+
+
+
+
+
+

{{ + 'submission.sections.sherpa.publisher.policy.embargo' | translate }}

+
+
+

{{version.embargo.amount}} + {{version.embargo.units}}

+ +

{{ 'submission.sections.sherpa.publisher.policy.noembargo' | translate }}

+
+
+
+
+
+

{{ + 'submission.sections.sherpa.publisher.policy.license' | translate }}

+
+
+

{{license}}

+
+
+
+
+

{{ + 'submission.sections.sherpa.publisher.policy.prerequisites' | translate }}

+
+
+

{{prerequisite}}

+
+
+
+
+

{{ + 'submission.sections.sherpa.publisher.policy.location' | translate }}

+
+
+

{{location}}

+
+
+
+
+

{{ + 'submission.sections.sherpa.publisher.policy.conditions' | translate }}

+
+
+

{{condition}}

+
+
+
+
+ \ No newline at end of file diff --git a/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.scss b/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.spec.ts b/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.spec.ts new file mode 100644 index 0000000000..b65cb5e00f --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.spec.ts @@ -0,0 +1,57 @@ +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContentAccordionComponent } from './content-accordion.component'; + +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { SherpaDataResponse } from '../../../../shared/mocks/section-sherpa-policies.service.mock'; + +describe('ContentAccordionComponent', () => { + let component: ContentAccordionComponent; + let fixture: ComponentFixture; + let de: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + NgbCollapseModule + ], + declarations: [ContentAccordionComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ContentAccordionComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + component.isCollapsed = false; + component.version = SherpaDataResponse.sherpaResponse.journals[0].policies[0].permittedVersions[0]; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show 2 rows', () => { + component.version = SherpaDataResponse.sherpaResponse.journals[0].policies[0].permittedVersions[0]; + fixture.detectChanges(); + expect(de.queryAll(By.css('.row')).length).toEqual(2); + }); + + it('should show 5 rows', () => { + component.version = SherpaDataResponse.sherpaResponse.journals[0].policies[0].permittedVersions[2]; + fixture.detectChanges(); + expect(de.queryAll(By.css('.row')).length).toEqual(5); + }); +}); diff --git a/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.ts b/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.ts new file mode 100644 index 0000000000..0e7bc863ad --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; + +import { PermittedVersions } from '../../../../core/submission/models/sherpa-policies-details.model'; + +/** + * This component represents a section that contains the inner accordions for the publisher policy versions. + */ +@Component({ + selector: 'ds-content-accordion', + templateUrl: './content-accordion.component.html', + styleUrls: ['./content-accordion.component.scss'] +}) +export class ContentAccordionComponent { + /** + * PermittedVersions to show information from + */ + @Input() version: PermittedVersions; + + /** + * A boolean representing if div should start collapsed + */ + public isCollapsed = true; +} diff --git a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.html b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.html new file mode 100644 index 0000000000..15dd4d7286 --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.html @@ -0,0 +1,39 @@ +
+
+
+

{{ 'submission.sections.sherpa.record.information.id' | translate }}

+
+
+

{{metadata.id}} +

+
+
+
+
+

{{ 'submission.sections.sherpa.record.information.date.created' | translate }}

+
+
+

{{metadata.dateCreated | date: 'd MMMM Y H:mm:ss zzzz' }} +

+
+
+
+
+

{{ 'submission.sections.sherpa.record.information.date.modified' | translate }}

+
+
+

{{metadata.dateModified| date: 'd MMMM Y H:mm:ss zzzz' }} +

+
+
+
+
+

{{ 'submission.sections.sherpa.record.information.uri' | translate }}

+
+ +
+
\ No newline at end of file diff --git a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.scss b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.spec.ts b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.spec.ts new file mode 100644 index 0000000000..9a60a6d010 --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.spec.ts @@ -0,0 +1,47 @@ +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MetadataInformationComponent } from './metadata-information.component'; + +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { SherpaDataResponse } from '../../../../shared/mocks/section-sherpa-policies.service.mock'; + +describe('MetadataInformationComponent', () => { + let component: MetadataInformationComponent; + let fixture: ComponentFixture; + let de: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [MetadataInformationComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MetadataInformationComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + component.metadata = SherpaDataResponse.sherpaResponse.metadata; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show 4 rows', () => { + expect(de.queryAll(By.css('.row')).length).toEqual(4); + }); + +}); diff --git a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.ts b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.ts new file mode 100644 index 0000000000..307c18d8cc --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; + +import { Metadata } from '../../../../core/submission/models/sherpa-policies-details.model'; + +/** + * This component represents a section that contains the matadata informations. + */ +@Component({ + selector: 'ds-metadata-information', + templateUrl: './metadata-information.component.html', + styleUrls: ['./metadata-information.component.scss'] +}) +export class MetadataInformationComponent { + /** + * Metadata to show information from + */ + @Input() metadata: Metadata; + +} diff --git a/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.html b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.html new file mode 100644 index 0000000000..3c35da8f08 --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.html @@ -0,0 +1,64 @@ +
+
+
+

{{'submission.sections.sherpa.publication.information.title' | translate}}

+
+
+

{{title}} +

+
+
+
+
+

{{'submission.sections.sherpa.publication.information.issns' | translate}}

+
+
+

{{issn}} +

+
+
+
+
+

{{'submission.sections.sherpa.publication.information.url' | translate}}

+
+ +
+
+
+

{{'submission.sections.sherpa.publication.information.publishers' | translate}}

+
+ +
+
+
+

{{'submission.sections.sherpa.publication.information.romeoPub' | translate}}

+
+
+

+ {{journal.romeoPub}} +

+
+
+
+
+

{{'submission.sections.sherpa.publication.information.zetoPub' | translate}}

+
+
+

+ {{journal.zetoPub}} +

+
+
+
\ No newline at end of file diff --git a/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.scss b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.spec.ts b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.spec.ts new file mode 100644 index 0000000000..c5dc896858 --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.spec.ts @@ -0,0 +1,47 @@ +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PublicationInformationComponent } from './publication-information.component'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { SherpaDataResponse } from '../../../../shared/mocks/section-sherpa-policies.service.mock'; + +describe('PublicationInformationComponent', () => { + let component: PublicationInformationComponent; + let fixture: ComponentFixture; + let de: DebugElement; + + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [PublicationInformationComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PublicationInformationComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + component.journal = SherpaDataResponse.sherpaResponse.journals[0]; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show 6 rows', () => { + expect(de.queryAll(By.css('.row')).length).toEqual(6); + }); + +}); diff --git a/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.ts b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.ts new file mode 100644 index 0000000000..cfe42adf7b --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; + +import { Journal } from '../../../../core/submission/models/sherpa-policies-details.model'; + +/** + * This component represents a section that contains the journal publication information. + */ +@Component({ + selector: 'ds-publication-information', + templateUrl: './publication-information.component.html', + styleUrls: ['./publication-information.component.scss'] +}) +export class PublicationInformationComponent { + /** + * Journal to show information from + */ + @Input() journal: Journal; + +} diff --git a/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.html b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.html new file mode 100644 index 0000000000..87370cf7e3 --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.html @@ -0,0 +1,16 @@ +
+ + +
+
+

+ {{'submission.sections.sherpa.publisher.policy.more.information' | translate}} +

+ +
+
+
\ No newline at end of file diff --git a/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.scss b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.spec.ts b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.spec.ts new file mode 100644 index 0000000000..3e2c33481a --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.spec.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PublisherPolicyComponent } from './publisher-policy.component'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { SherpaDataResponse } from '../../../../shared/mocks/section-sherpa-policies.service.mock'; +import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; + +describe('PublisherPolicyComponent', () => { + let component: PublisherPolicyComponent; + let fixture: ComponentFixture; + let de: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [PublisherPolicyComponent], + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PublisherPolicyComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + component.policy = SherpaDataResponse.sherpaResponse.journals[0].policies[0]; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show content accordion', () => { + expect(de.query(By.css('ds-content-accordion'))).toBeTruthy(); + }); + + it('should show 1 row', () => { + expect(de.queryAll(By.css('.row')).length).toEqual(1); + }); +}); diff --git a/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.ts b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.ts new file mode 100644 index 0000000000..96ada3904c --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.ts @@ -0,0 +1,28 @@ +import { Component, Input } from '@angular/core'; + +import { Policy } from '../../../../core/submission/models/sherpa-policies-details.model'; +import { AlertType } from '../../../../shared/alert/aletr-type'; + +/** + * This component represents a section that contains the publisher policy informations. + */ +@Component({ + selector: 'ds-publisher-policy', + templateUrl: './publisher-policy.component.html', + styleUrls: ['./publisher-policy.component.scss'] +}) +export class PublisherPolicyComponent { + + /** + * Policy to show information from + */ + @Input() policy: Policy; + + + /** + * The AlertType enumeration + * @type {AlertType} + */ + public AlertTypeEnum = AlertType; + +} diff --git a/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.html b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.html new file mode 100644 index 0000000000..4b636ee46e --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.html @@ -0,0 +1,72 @@ + + + +
+ + {{'submission.sections.sherpa.publisher.policy.description' | translate}} + +
+ +
+
+ + + + +
+
+ +
+ + +
+
+
+ +
+
+
+
+ +
+ + +
+
+
+ +
+
+
+ +
+
+ +
+ + +
+
+
+ +
+
+
+ + + + + +
diff --git a/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.scss b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.spec.ts b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.spec.ts new file mode 100644 index 0000000000..76a980ed3c --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.spec.ts @@ -0,0 +1,117 @@ +import { SharedModule } from '../../../shared/shared.module'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; +import { SherpaDataResponse } from '../../../shared/mocks/section-sherpa-policies.service.mock'; +import { ComponentFixture, inject, TestBed } from '@angular/core/testing'; + +import { SectionsService } from '../sections.service'; +import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { BrowserModule, By } from '@angular/platform-browser'; + +import { Store } from '@ngrx/store'; +import { AppState } from '../../../app.reducer'; +import { SubmissionSectionSherpaPoliciesComponent } from './section-sherpa-policies.component'; +import { SubmissionService } from '../../submission.service'; +import { DebugElement } from '@angular/core'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { of as observableOf } from 'rxjs'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +describe('SubmissionSectionSherpaPoliciesComponent', () => { + let component: SubmissionSectionSherpaPoliciesComponent; + let fixture: ComponentFixture; + let de: DebugElement; + + const sectionsServiceStub = new SectionsServiceStub(); + + const operationsBuilder = jasmine.createSpyObj('operationsBuilder', { + add: undefined, + remove: undefined, + replace: undefined, + }); + + const storeStub = jasmine.createSpyObj('store', ['dispatch']); + + const sectionData = { + header: 'submit.progressbar.sherpaPolicies', + config: 'http://localhost:8080/server/api/config/submissionaccessoptions/SherpaPoliciesDefaultConfiguration', + mandatory: true, + sectionType: 'sherpaPolicies', + collapsed: false, + enabled: true, + data: SherpaDataResponse, + errorsToShow: [], + serverValidationErrors: [], + isLoading: false, + isValid: true + }; + + describe('SubmissionSectionSherpaPoliciesComponent', () => { + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + BrowserModule, + NoopAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + NgbCollapseModule, + SharedModule + ], + declarations: [SubmissionSectionSherpaPoliciesComponent], + providers: [ + { provide: SectionsService, useValue: sectionsServiceStub }, + { provide: JsonPatchOperationsBuilder, useValue: operationsBuilder }, + { provide: SubmissionService, useValue: SubmissionServiceStub }, + { provide: Store, useValue: storeStub }, + { provide: 'sectionDataProvider', useValue: sectionData }, + { provide: 'submissionIdProvider', useValue: '1508' }, + ] + }) + .compileComponents(); + }); + + beforeEach(inject([Store], (store: Store) => { + fixture = TestBed.createComponent(SubmissionSectionSherpaPoliciesComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + sectionsServiceStub.getSectionData.and.returnValue(observableOf(SherpaDataResponse)); + fixture.detectChanges(); + })); + + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show refresh button', () => { + expect(de.query(By.css('[data-test="refresh-btn"]'))).toBeTruthy(); + }); + + it('should show publisher information', () => { + expect(de.query(By.css('ds-publication-information'))).toBeTruthy(); + }); + + it('should show publisher policy', () => { + expect(de.query(By.css('ds-publisher-policy'))).toBeTruthy(); + }); + + it('should show metadata information', () => { + expect(de.query(By.css('ds-metadata-information'))).toBeTruthy(); + }); + + it('when refresh button click operationsBuilder.remove should have been called', () => { + de.query(By.css('[data-test="refresh-btn"]')).nativeElement.click(); + expect(operationsBuilder.remove).toHaveBeenCalled(); + }); + + + }); + +}); diff --git a/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.ts b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.ts new file mode 100644 index 0000000000..e55b75146f --- /dev/null +++ b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.ts @@ -0,0 +1,127 @@ +import { AlertType } from '../../../shared/alert/aletr-type'; +import { Component, Inject } from '@angular/core'; + +import { BehaviorSubject, Observable, of, Subscription } from 'rxjs'; + +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { + WorkspaceitemSectionSherpaPoliciesObject +} from '../../../core/submission/models/workspaceitem-section-sherpa-policies.model'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionsType } from '../sections-type'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsService } from '../sections.service'; +import { SectionModelComponent } from '../models/section.model'; +import { SubmissionService } from '../../submission.service'; +import { hasValue, isEmpty } from '../../../shared/empty.util'; + +/** + * This component represents a section for the sherpa policy informations structure. + */ +@Component({ + selector: 'ds-section-sherpa-policies', + templateUrl: './section-sherpa-policies.component.html', + styleUrls: ['./section-sherpa-policies.component.scss'] +}) +@renderSectionFor(SectionsType.SherpaPolicies) +export class SubmissionSectionSherpaPoliciesComponent extends SectionModelComponent { + + /** + * The accesses section data + * @type {WorkspaceitemSectionAccessesObject} + */ + public sherpaPoliciesData$: BehaviorSubject = new BehaviorSubject(null); + + /** + * The [[JsonPatchOperationPathCombiner]] object + * @type {JsonPatchOperationPathCombiner} + */ + protected pathCombiner: JsonPatchOperationPathCombiner; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + protected subs: Subscription[] = []; + + /** + * A boolean representing if div should start collapsed + */ + public isCollapsed = false; + + + /** + * The AlertType enumeration + * @type {AlertType} + */ + public AlertTypeEnum = AlertType; + + /** + * Initialize instance variables + * + * @param {SectionsService} sectionService + * @param {SectionDataObject} injectedSectionData + * @param {JsonPatchOperationsBuilder} operationsBuilder + * @param {SubmissionService} submissionService + * @param {string} injectedSubmissionId + */ + constructor( + protected sectionService: SectionsService, + protected operationsBuilder: JsonPatchOperationsBuilder, + private submissionService: SubmissionService, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(undefined, injectedSectionData, injectedSubmissionId); + } + + /** + * Unsubscribe from all subscriptions + */ + onSectionDestroy() { + + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + + + /** + * Initialize all instance variables and retrieve collection default access conditions + */ + protected onSectionInit(): void { + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); + this.subs.push( + this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) + .subscribe((sherpaPolicies: WorkspaceitemSectionSherpaPoliciesObject) => { + this.sherpaPoliciesData$.next(sherpaPolicies); + }) + ); + } + + /** + * Get section status + * + * @return Observable + * the section status + */ + protected getSectionStatus(): Observable { + return of(true); + } + + /** + * Check if section has no data + */ + hasNoData(): boolean { + return isEmpty(this.sherpaPoliciesData$.value); + } + + /** + * Refresh sherpa information + */ + refresh() { + this.operationsBuilder.remove(this.pathCombiner.getPath('retrievalTime')); + this.submissionService.dispatchSaveSection(this.submissionId, this.sectionData.id); + } + +} diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index 05aa765054..eb6bde35ad 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -22,15 +22,27 @@ import { SubmissionSectionLicenseComponent } from './sections/license/section-li import { SubmissionUploadsConfigService } from '../core/config/submission-uploads-config.service'; import { SubmissionEditComponent } from './edit/submission-edit.component'; import { SubmissionSectionUploadFileComponent } from './sections/upload/file/section-upload-file.component'; -import { SubmissionSectionUploadFileEditComponent } from './sections/upload/file/edit/section-upload-file-edit.component'; -import { SubmissionSectionUploadFileViewComponent } from './sections/upload/file/view/section-upload-file-view.component'; -import { SubmissionSectionUploadAccessConditionsComponent } from './sections/upload/accessConditions/submission-section-upload-access-conditions.component'; +import { + SubmissionSectionUploadFileEditComponent +} from './sections/upload/file/edit/section-upload-file-edit.component'; +import { + SubmissionSectionUploadFileViewComponent +} from './sections/upload/file/view/section-upload-file-view.component'; +import { + SubmissionSectionUploadAccessConditionsComponent +} from './sections/upload/accessConditions/submission-section-upload-access-conditions.component'; import { SubmissionSubmitComponent } from './submit/submission-submit.component'; import { storeModuleConfig } from '../app.reducer'; import { SubmissionImportExternalComponent } from './import-external/submission-import-external.component'; -import { SubmissionImportExternalSearchbarComponent } from './import-external/import-external-searchbar/submission-import-external-searchbar.component'; -import { SubmissionImportExternalPreviewComponent } from './import-external/import-external-preview/submission-import-external-preview.component'; -import { SubmissionImportExternalCollectionComponent } from './import-external/import-external-collection/submission-import-external-collection.component'; +import { + SubmissionImportExternalSearchbarComponent +} from './import-external/import-external-searchbar/submission-import-external-searchbar.component'; +import { + SubmissionImportExternalPreviewComponent +} from './import-external/import-external-preview/submission-import-external-preview.component'; +import { + SubmissionImportExternalCollectionComponent +} from './import-external/import-external-collection/submission-import-external-collection.component'; import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/submission-section-cc-licenses.component'; import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; @@ -38,10 +50,19 @@ import { ThemedSubmissionEditComponent } from './edit/themed-submission-edit.com import { ThemedSubmissionSubmitComponent } from './submit/themed-submission-submit.component'; import { ThemedSubmissionImportExternalComponent } from './import-external/themed-submission-import-external.component'; import { FormModule } from '../shared/form/form.module'; -import { NgbAccordionModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbCollapseModule, NgbModalModule, NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { SubmissionSectionAccessesComponent } from './sections/accesses/section-accesses.component'; import { SubmissionAccessesConfigService } from '../core/config/submission-accesses-config.service'; import { SectionAccessesService } from './sections/accesses/section-accesses.service'; +import { SubmissionSectionSherpaPoliciesComponent } from './sections/sherpa-policies/section-sherpa-policies.component'; +import { ContentAccordionComponent } from './sections/sherpa-policies/content-accordion/content-accordion.component'; +import { PublisherPolicyComponent } from './sections/sherpa-policies/publisher-policy/publisher-policy.component'; +import { + PublicationInformationComponent +} from './sections/sherpa-policies/publication-information/publication-information.component'; +import { + MetadataInformationComponent +} from './sections/sherpa-policies/metadata-information/metadata-information.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -50,7 +71,8 @@ const ENTRY_COMPONENTS = [ SubmissionSectionLicenseComponent, SubmissionSectionCcLicensesComponent, SubmissionSectionAccessesComponent, - SubmissionSectionUploadFileEditComponent + SubmissionSectionUploadFileEditComponent, + SubmissionSectionSherpaPoliciesComponent, ]; const DECLARATIONS = [ @@ -75,6 +97,10 @@ const DECLARATIONS = [ SubmissionImportExternalSearchbarComponent, SubmissionImportExternalPreviewComponent, SubmissionImportExternalCollectionComponent, + ContentAccordionComponent, + PublisherPolicyComponent, + PublicationInformationComponent, + MetadataInformationComponent, ]; @NgModule({ @@ -87,8 +113,9 @@ const DECLARATIONS = [ JournalEntitiesModule.withEntryComponents(), ResearchEntitiesModule.withEntryComponents(), FormModule, - NgbAccordionModule, - NgbModalModule + NgbModalModule, + NgbCollapseModule, + NgbAccordionModule ], declarations: DECLARATIONS, exports: DECLARATIONS, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 0f240899cd..bddc705cb8 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -27,6 +27,16 @@ "404.page-not-found": "page not found", + "error-page.description.401": "unauthorized", + + "error-page.description.403": "forbidden", + + "error-page.description.500": "Service Unavailable", + + "error-page.description.404": "page not found", + + "error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator", + "access-status.embargo.listelement.badge": "Embargo", "access-status.metadata.only.listelement.badge": "Metadata only", @@ -1105,10 +1115,14 @@ "comcol-role.edit.create": "Create", + "comcol-role.edit.create.error.title": "Failed to create a group for the '{{ role }}' role", + "comcol-role.edit.restrict": "Restrict", "comcol-role.edit.delete": "Delete", + "comcol-role.edit.delete.error.title": "Failed to delete the '{{ role }}' role's group", + "comcol-role.edit.community-admin.name": "Administrators", @@ -1357,6 +1371,14 @@ "confirmation-modal.delete-eperson.confirm": "Delete", + "confirmation-modal.delete-profile.header": "Delete Profile", + + "confirmation-modal.delete-profile.info": "Are you sure you want to delete your profile", + + "confirmation-modal.delete-profile.cancel": "Cancel", + + "confirmation-modal.delete-profile.confirm": "Delete", + "error.bitstream": "Error fetching bitstream", @@ -1589,6 +1611,46 @@ "grant-request-copy.success": "Successfully granted item request", + "health.breadcrumbs": "Health", + + "health-page.heading" : "Health", + + "health-page.info-tab" : "Info", + + "health-page.status-tab" : "Status", + + "health-page.error.msg": "The health check service is temporarily unavailable", + + "health-page.property.status": "Status code", + + "health-page.section.db.title": "Database", + + "health-page.section.geoIp.title": "GeoIp", + + "health-page.section.solrAuthorityCore.title": "Sor: authority core", + + "health-page.section.solrOaiCore.title": "Sor: oai core", + + "health-page.section.solrSearchCore.title": "Sor: search core", + + "health-page.section.solrStatisticsCore.title": "Sor: statistics core", + + "health-page.section-info.app.title": "Application Backend", + + "health-page.section-info.java.title": "Java", + + "health-page.status": "Status", + + "health-page.status.ok.info": "Operational", + + "health-page.status.error.info": "Problems detected", + + "health-page.status.warning.info": "Possible issues detected", + + "health-page.title": "Health", + + "health-page.section.no-issues": "No issues detected", + "home.description": "", @@ -2067,6 +2129,7 @@ "item.edit.withdraw.success": "The item was withdrawn successfully", + "item.orcid.return": "Back", "item.listelement.badge": "Item", @@ -2123,6 +2186,10 @@ "item.page.link.simple": "Simple item page", + "item.page.orcid.title": "ORCID", + + "item.page.orcid.tooltip": "Open ORCID setting page", + "item.page.person.search.title": "Articles by this author", "item.page.related-items.view-more": "Show {{ amount }} more", @@ -2157,6 +2224,8 @@ "item.page.claim.button": "Claim", + "item.page.claim.tooltip": "Claim this item as profile", + "item.preview.dc.identifier.uri": "Identifier:", "item.preview.dc.contributor.author": "Authors:", @@ -2173,6 +2242,22 @@ "item.preview.dc.title": "Title:", + "item.preview.dc.type": "Type:", + + "item.preview.oaire.citation.issue" : "Issue", + + "item.preview.oaire.citation.volume" : "Volume", + + "item.preview.dc.relation.issn" : "ISSN", + + "item.preview.dc.identifier.isbn" : "ISBN", + + "item.preview.dc.identifier": "Identifier:", + + "item.preview.dc.relation.ispartof" : "Journal or Serie", + + "item.preview.dc.identifier.doi" : "DOI", + "item.preview.person.familyName": "Surname:", "item.preview.person.givenName": "Name:", @@ -2530,13 +2615,15 @@ "menu.section.icon.find": "Find menu section", + "menu.section.icon.health": "Health check menu section", + "menu.section.icon.import": "Import menu section", "menu.section.icon.new": "New menu section", "menu.section.icon.pin": "Pin sidebar", - "menu.section.icon.processes": "Processes menu section", + "menu.section.icon.processes": "Processes Health", "menu.section.icon.registries": "Registries menu section", @@ -2578,6 +2665,8 @@ "menu.section.processes": "Processes", + "menu.section.health": "Health", + "menu.section.registries": "Registries", @@ -2781,6 +2870,8 @@ "person.page.lastname": "Last Name", + "person.page.name": "Name", + "person.page.link.full": "Show all metadata", "person.page.orcid": "ORCID", @@ -2896,6 +2987,8 @@ "profile.groups.head": "Authorization groups you belong to", + "profile.special.groups.head": "Authorization special groups you belong to", + "profile.head": "Update Profile", "profile.metadata.form.error.firstname.required": "First Name is required", @@ -3149,6 +3242,10 @@ "resource-policies.edit.page.failure.content": "An error occurred while editing the resource policy.", + "resource-policies.edit.page.target-failure.content": "An error occurred while editing the target (ePerson or group) of the resource policy.", + + "resource-policies.edit.page.other-failure.content": "An error occurred while editing the resource policy. The target (ePerson or group) has been successfully updated.", + "resource-policies.edit.page.success.content": "Operation successful", "resource-policies.edit.page.title": "Edit resource policy", @@ -3171,6 +3268,16 @@ "resource-policies.form.eperson-group-list.table.headers.name": "Name", + "resource-policies.form.eperson-group-list.modal.header": "Cannot change type", + + "resource-policies.form.eperson-group-list.modal.text1.toGroup": "It is not possible to replace an ePerson with a group.", + + "resource-policies.form.eperson-group-list.modal.text1.toEPerson": "It is not possible to replace a group with an ePerson.", + + "resource-policies.form.eperson-group-list.modal.text2": "Delete the current resource policy and create a new one with the desired type.", + + "resource-policies.form.eperson-group-list.modal.close": "Ok", + "resource-policies.form.date.end.label": "End Date", "resource-policies.form.date.start.label": "Start Date", @@ -3585,6 +3692,22 @@ "submission.import-external.source.arxiv": "arXiv", + "submission.import-external.source.ads": "NASA/ADS", + + "submission.import-external.source.cinii": "CiNii", + + "submission.import-external.source.crossref": "CrossRef", + + "submission.import-external.source.scielo": "SciELO", + + "submission.import-external.source.scopus": "Scopus", + + "submission.import-external.source.vufind": "VuFind", + + "submission.import-external.source.wos": "Web Of Science", + + "submission.import-external.source.epo": "European Patent Office (EPO)", + "submission.import-external.source.loading": "Loading ...", "submission.import-external.source.sherpaJournal": "SHERPA Journals", @@ -3599,10 +3722,24 @@ "submission.import-external.source.pubmed": "Pubmed", + "submission.import-external.source.pubmedeu": "Pubmed Europe", + "submission.import-external.source.lcname": "Library of Congress Names", "submission.import-external.preview.title": "Item Preview", + "submission.import-external.preview.title.Publication": "Publication Preview", + + "submission.import-external.preview.title.none": "Item Preview", + + "submission.import-external.preview.title.Journal": "Journal Preview", + + "submission.import-external.preview.title.OrgUnit": "Organizational Unit Preview", + + "submission.import-external.preview.title.Person": "Person Preview", + + "submission.import-external.preview.title.Project": "Project Preview", + "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.button.import": "Start submission", @@ -3625,6 +3762,26 @@ "submission.sections.describe.relationship-lookup.external-source.import-button-title.isProjectOfPublication": "Project", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.none": "Import remote item", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Event": "Import remote event", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Product": "Import remote product", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Equipment": "Import remote equipment", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.OrgUnit": "Import remote organizational unit", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Funding": "Import remote fund", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Person": "Import remote person", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Patent": "Import remote patent", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Project": "Import remote project", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Publication": "Import remote publication", + "submission.sections.describe.relationship-lookup.external-source.import-modal.isProjectOfPublication.added.new-entity": "New Entity Added!", "submission.sections.describe.relationship-lookup.external-source.import-modal.isProjectOfPublication.title": "Project", @@ -3841,6 +3998,18 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.arxiv": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.crossref": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.epo": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.scopus": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.scielo": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.wos": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title": "Search Results", + "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.confirm": "Save a new name variant", @@ -3909,10 +4078,15 @@ "submission.sections.submit.progressbar.license": "Deposit license", + "submission.sections.submit.progressbar.sherpapolicy": "Sherpa policies", + "submission.sections.submit.progressbar.upload": "Upload files", + "submission.sections.submit.progressbar.sherpaPolicies": "Publisher open access policy information", + "submission.sections.sherpa-policy.title-empty": "No publisher policy information available. If your work has an associated ISSN, please enter it above to see any related publisher open access policies.", + "submission.sections.status.errors.title": "Errors", "submission.sections.status.valid.title": "Valid", @@ -3925,6 +4099,10 @@ "submission.sections.status.warnings.aria": "has warnings", + "submission.sections.status.info.title": "Additional Information", + + "submission.sections.status.info.aria": "Additional Information", + "submission.sections.toggle.open": "Open section", "submission.sections.toggle.close": "Close section", @@ -4024,6 +4202,60 @@ "submission.sections.accesses.form.until-placeholder": "Until", + "submission.sections.sherpa.publication.information": "Publication information", + + "submission.sections.sherpa.publication.information.title": "Title", + + "submission.sections.sherpa.publication.information.issns": "ISSNs", + + "submission.sections.sherpa.publication.information.url": "URL", + + "submission.sections.sherpa.publication.information.publishers": "Publisher", + + "submission.sections.sherpa.publication.information.romeoPub": "Romeo Pub", + + "submission.sections.sherpa.publication.information.zetoPub": "Zeto Pub", + + "submission.sections.sherpa.publisher.policy": "Publisher Policy", + + "submission.sections.sherpa.publisher.policy.description": "The below information was found via Sherpa Romeo. Based on the policies of your publisher, it provides advice regarding whether an embargo may be necessary and/or which files you are allowed to upload. If you have questions, please contact your site administrator via the feedback form in the footer.", + + "submission.sections.sherpa.publisher.policy.openaccess": "Open Access pathways permitted by this journal's policy are listed below by article version. Click on a pathway for a more detailed view", + + "submission.sections.sherpa.publisher.policy.more.information": "For more information, please see the following links:", + + "submission.sections.sherpa.publisher.policy.version": "Version", + + "submission.sections.sherpa.publisher.policy.embargo": "Embargo", + + "submission.sections.sherpa.publisher.policy.noembargo": "No Embargo", + + "submission.sections.sherpa.publisher.policy.nolocation": "None", + + "submission.sections.sherpa.publisher.policy.license": "License", + + "submission.sections.sherpa.publisher.policy.prerequisites": "Prerequisites", + + "submission.sections.sherpa.publisher.policy.location": "Location", + + "submission.sections.sherpa.publisher.policy.conditions": "Conditions", + + "submission.sections.sherpa.publisher.policy.refresh": "Refresh", + + "submission.sections.sherpa.record.information": "Record Information", + + "submission.sections.sherpa.record.information.id": "ID", + + "submission.sections.sherpa.record.information.date.created": "Date Created", + + "submission.sections.sherpa.record.information.date.modified": "Last Modified", + + "submission.sections.sherpa.record.information.uri": "URI", + + "submission.sections.sherpa.error.message": "There was an error retrieving sherpa informations", + + + "submission.submit.breadcrumbs": "New submission", "submission.submit.title": "New submission", @@ -4215,6 +4447,8 @@ "researcher.profile.associated": "Researcher profile associated", + "researcher.profile.change-visibility.fail": "An unexpected error occurs while changing the profile visibility", + "researcher.profile.create.new": "Create new", "researcher.profile.create.success": "Researcher profile created successfully", @@ -4411,7 +4645,7 @@ "person.page.orcid.synchronization-mode.label": "Synchronization mode", - "person.page.orcid.synchronization-mode-message": "Enable 'Manual' Synchronization mode to disable batch synchronization, so you must send your data to ORCID Registry manually", + "person.page.orcid.synchronization-mode-message": "Please select how you would like synchronization with orcid to occur. It can be set as 'Manual' so you must send your data to ORCID Registry manually, or as 'Batch' so the synchronization on ORCID will be done by the system with a scheduled script", "person.page.orcid.synchronization-settings-update.success": "The synchronization settings have been updated successfully", diff --git a/src/assets/i18n/lv.json5 b/src/assets/i18n/lv.json5 index 738baca186..d6c409b783 100644 --- a/src/assets/i18n/lv.json5 +++ b/src/assets/i18n/lv.json5 @@ -1,5625 +1,5625 @@ { - + // "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.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.unauthorized": "unauthorized", // TODO New key - Add a translation "401.unauthorized": "unauthorized", - - - + + + // "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.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.forbidden": "forbidden", // TODO New key - Add a translation "403.forbidden": "forbidden", - - - + + + // "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": "Meklēto lapu nav iespējams atrast. Iespējams, lapa ir pārvietota vai izdzēsta. Lai atgrieztos sākumlapā, varat izmantot zemāk esošo pogu. ", - + // "404.link.home-page": "Take me to the home page", "404.link.home-page": "Argriezties uz sākumu", - + // "404.page-not-found": "page not found", "404.page-not-found": "lapa nav atrasta", - + // "admin.curation-tasks.breadcrumbs": "System curation tasks", // TODO New key - Add a translation "admin.curation-tasks.breadcrumbs": "System curation tasks", - + // "admin.curation-tasks.title": "System curation tasks", // TODO New key - Add a translation "admin.curation-tasks.title": "System curation tasks", - + // "admin.curation-tasks.header": "System curation tasks", // TODO New key - Add a translation "admin.curation-tasks.header": "System curation tasks", - + // "admin.registries.bitstream-formats.breadcrumbs": "Format registry", // TODO New key - Add a translation "admin.registries.bitstream-formats.breadcrumbs": "Format registry", - + // "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.failure.content": "An error occurred while creating the new bitstream format.", "admin.registries.bitstream-formats.create.failure.content": "Veidojot jauno bitu straumes formātu, radās kļūda.", - + // "admin.registries.bitstream-formats.create.failure.head": "Failure", "admin.registries.bitstream-formats.create.failure.head": "Kļūda", - + // "admin.registries.bitstream-formats.create.head": "Create Bitstream format", "admin.registries.bitstream-formats.create.head": "Izveidot Bitu straumes formātu", - + // "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", "admin.registries.bitstream-formats.create.new": "Pievienot jaunu bitu straumes formātu", - + // "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", "admin.registries.bitstream-formats.create.success.content": "Jauns bitu straumes formāts tika veiksmīgi izveidots.", - + // "admin.registries.bitstream-formats.create.success.head": "Success", "admin.registries.bitstream-formats.create.success.head": "Veiksmīgi izpildīts", - + // "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", "admin.registries.bitstream-formats.delete.failure.amount": "Neizdevās dzēst {{ amount }} formātu(s)", - + // "admin.registries.bitstream-formats.delete.failure.head": "Failure", "admin.registries.bitstream-formats.delete.failure.head": "Kļūda", - + // "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", "admin.registries.bitstream-formats.delete.success.amount": "Veiksmīgi dzēsti {{ amount }} formats(i)", - + // "admin.registries.bitstream-formats.delete.success.head": "Success", "admin.registries.bitstream-formats.delete.success.head": "Veiksmīgi izpildīts", - + // "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", "admin.registries.bitstream-formats.description": "Šis bitu straumes formātu saraksts sniedz informāciju par zināmajiem formātiem un to atbalsta līmeni.", - + // "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.description.hint": "", "admin.registries.bitstream-formats.edit.description.hint": "", - + // "admin.registries.bitstream-formats.edit.description.label": "Description", "admin.registries.bitstream-formats.edit.description.label": "Apraksts", - + // "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": "Paplašinājumi ir failu paplašinājumi, kurus izmanto, lai automātiski identificētu augšupielādēto failu formātu. Katram formātam var ievadīt vairākus paplašinājumus.", - + // "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", "admin.registries.bitstream-formats.edit.extensions.label": "Faila paplašinājums", - + // "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extension without the dot", "admin.registries.bitstream-formats.edit.extensions.placeholder": "Ievadiet faila paplašinājumu bez punkta", - + // "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", "admin.registries.bitstream-formats.edit.failure.content": "Rediģējot bitu straumes formātu, radās kļūda.", - + // "admin.registries.bitstream-formats.edit.failure.head": "Failure", "admin.registries.bitstream-formats.edit.failure.head": "Kļūda", - + // "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", "admin.registries.bitstream-formats.edit.head": "Bitu straumes formāts: {{ 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": "Formāti, kas atzīmēti kā iekšēji, tiek paslēpti no lietotāja un tiek izmantoti administratīviem mērķiem.", - + // "admin.registries.bitstream-formats.edit.internal.label": "Internal", "admin.registries.bitstream-formats.edit.internal.label": "Iekšējais", - + // "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": "MIME tipam, kas saistīts ar šo formātu, nav jābūt unikālam.", - + // "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Tips", - + // "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": "Šim formātam ir unikāls nosaukums, (piem. Microsoft Word XP or Microsoft Word 2000)", - + // "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", "admin.registries.bitstream-formats.edit.shortDescription.label": "Nosaukums", - + // "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", "admin.registries.bitstream-formats.edit.success.content": "Bitu straumes formāts tika veiksmīgi rediģēts.", - + // "admin.registries.bitstream-formats.edit.success.head": "Success", "admin.registries.bitstream-formats.edit.success.head": "Veiksmīgi izpildīts", - + // "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", "admin.registries.bitstream-formats.edit.supportLevel.hint": "Atbalsta līmenis, ko jūsu iestāde apņemas nodrošināt šim formātam.", - + // "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", "admin.registries.bitstream-formats.edit.supportLevel.label": "Atbalsta līmenis", - + // "admin.registries.bitstream-formats.head": "Bitstream Format Registry", "admin.registries.bitstream-formats.head": "Bitu straumes Formatu Reģistrs", - + // "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", "admin.registries.bitstream-formats.no-items": "Nav rādāmi bitu straumes formāti.", - + // "admin.registries.bitstream-formats.table.delete": "Delete selected", "admin.registries.bitstream-formats.table.delete": "Dzēst atlasīto", - + // "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", "admin.registries.bitstream-formats.table.deselect-all": "Noņemt atlasi", - + // "admin.registries.bitstream-formats.table.internal": "internal", "admin.registries.bitstream-formats.table.internal": "Iekšējais", - + // "admin.registries.bitstream-formats.table.mimetype": "MIME Type", "admin.registries.bitstream-formats.table.mimetype": "MIME Tips", - + // "admin.registries.bitstream-formats.table.name": "Name", "admin.registries.bitstream-formats.table.name": "Nosaukums", - + // "admin.registries.bitstream-formats.table.return": "Return", "admin.registries.bitstream-formats.table.return": "Atgriezties", - + // "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Zināms", - + // "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Atbalstīts", - + // "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Nezināms", - + // "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", "admin.registries.bitstream-formats.table.supportLevel.head": "Atbalsta Līmenis", - + // "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitu straumes Formatu Reģistrs", - - - + + + // "admin.registries.metadata.breadcrumbs": "Metadata registry", // TODO New key - Add a translation "admin.registries.metadata.breadcrumbs": "Metadata registry", - + // "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": "Metadatu reģistrā tiek uzturēts visu repozitorijā pieejamo metadatu lauku saraksts. Šos laukus var sadalīt starp vairākām shēmām. Tomēr DSpace ir nepieciešama kvalificēta Dublin Core shēma.", - + // "admin.registries.metadata.form.create": "Create metadata schema", "admin.registries.metadata.form.create": "Izveidot metadatu shēmu", - + // "admin.registries.metadata.form.edit": "Edit metadata schema", "admin.registries.metadata.form.edit": "Rediģēt metadatu shēmu", - + // "admin.registries.metadata.form.name": "Name", "admin.registries.metadata.form.name": "Nosaukums", - + // "admin.registries.metadata.form.namespace": "Namespace", "admin.registries.metadata.form.namespace": "Nosaukumvieta", - + // "admin.registries.metadata.head": "Metadata Registry", "admin.registries.metadata.head": "Metadatu reģistrs", - + // "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", "admin.registries.metadata.schemas.no-items": "Nav rādāmas metadatu shēmas.", - + // "admin.registries.metadata.schemas.table.delete": "Delete selected", "admin.registries.metadata.schemas.table.delete": "Dzēst izvēlēto", - + // "admin.registries.metadata.schemas.table.id": "ID", "admin.registries.metadata.schemas.table.id": "ID", - + // "admin.registries.metadata.schemas.table.name": "Name", "admin.registries.metadata.schemas.table.name": "Nosaukums", - + // "admin.registries.metadata.schemas.table.namespace": "Namespace", "admin.registries.metadata.schemas.table.namespace": "Nosaukumvieta", - + // "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", "admin.registries.metadata.title": "DSpace Angular :: Metadatu Registry", - - - + + + // "admin.registries.schema.breadcrumbs": "Metadata schema", // TODO New key - Add a translation "admin.registries.schema.breadcrumbs": "Metadata schema", - + // "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", "admin.registries.schema.description": "Metadu shēma priekš \"{{namespace}}\".", - + // "admin.registries.schema.fields.head": "Schema metadata fields", "admin.registries.schema.fields.head": "Shēmas metadatu lauki", - + // "admin.registries.schema.fields.no-items": "No metadata fields to show.", "admin.registries.schema.fields.no-items": "Nav pieejami metadatu lauki.", - + // "admin.registries.schema.fields.table.delete": "Delete selected", "admin.registries.schema.fields.table.delete": "Dzēst izvēlēto", - + // "admin.registries.schema.fields.table.field": "Field", "admin.registries.schema.fields.table.field": "Lauks", - + // "admin.registries.schema.fields.table.scopenote": "Scope Note", "admin.registries.schema.fields.table.scopenote": "Jomas Piezīme", - + // "admin.registries.schema.form.create": "Create metadata field", "admin.registries.schema.form.create": "Izveidot matadatu lauku", - + // "admin.registries.schema.form.edit": "Edit metadata field", "admin.registries.schema.form.edit": "Rediģēt metadatu lauku", - + // "admin.registries.schema.form.element": "Element", "admin.registries.schema.form.element": "Elements", - + // "admin.registries.schema.form.qualifier": "Qualifier", "admin.registries.schema.form.qualifier": "Kvalifikācija", - + // "admin.registries.schema.form.scopenote": "Scope Note", "admin.registries.schema.form.scopenote": "Jomas Piezīme", - + // "admin.registries.schema.head": "Metadata Schema", "admin.registries.schema.head": "Metadatu Shēma", - + // "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", "admin.registries.schema.notification.created": "Veiksmīgi izveidota metadatu shēma \"{{prefix}}\"", - + // "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", "admin.registries.schema.notification.deleted.failure": "Neizdevās izdzēst {{amount}} metadatu shēmas", - + // "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", "admin.registries.schema.notification.deleted.success": "{{amount}} metadatu shēmas ir veiksmīgi izdzēstas", - + // "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", "admin.registries.schema.notification.edited": "Veiksmīgi rediģēta metadatu shēma \"{{prefix}}\"", - + // "admin.registries.schema.notification.failure": "Error", "admin.registries.schema.notification.failure": "Kļūda", - + // "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", "admin.registries.schema.notification.field.created": "Metadatu lauks ir veiksmīgi izveidots \"{{field}}\"", - + // "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", "admin.registries.schema.notification.field.deleted.failure": "Neizdevās izdzēst {{amount}} metadatu laukus", - + // "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", "admin.registries.schema.notification.field.deleted.success": "{{amount}} metadatu lauki ir veiksmīgi izdzēsti", - + // "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", "admin.registries.schema.notification.field.edited": "Metadatu lauks ir veiksmīgi rediģēts \"{{field}}\"", - + // "admin.registries.schema.notification.success": "Success", "admin.registries.schema.notification.success": "Veiksmīgi izpildīts", - + // "admin.registries.schema.return": "Return", "admin.registries.schema.return": "Atgriezties", - + // "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", "admin.registries.schema.title": "DSpace Angular :: Metadatu shēmas reģistrs", - - - + + + // "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.impersonate": "Impersonate EPerson", // TODO New key - Add a translation "admin.access-control.epeople.actions.impersonate": "Impersonate EPerson", - + // "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.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.title": "DSpace Angular :: EPeople", "admin.access-control.epeople.title": "DSpace Angular :: EPersonas", - + // "admin.access-control.epeople.head": "EPeople", "admin.access-control.epeople.head": "EPersonas", - + // "admin.access-control.epeople.search.head": "Search", "admin.access-control.epeople.search.head": "Meklēt", - + // "admin.access-control.epeople.button.see-all": "Browse All", "admin.access-control.epeople.button.see-all": "Skatīt visus", - + // "admin.access-control.epeople.search.scope.metadata": "Metadata", "admin.access-control.epeople.search.scope.metadata": "Metadati", - + // "admin.access-control.epeople.search.scope.email": "E-mail (exact)", "admin.access-control.epeople.search.scope.email": "E-pasts (pilns)", - + // "admin.access-control.epeople.search.button": "Search", "admin.access-control.epeople.search.button": "Meklēt", - + // "admin.access-control.epeople.button.add": "Add EPerson", "admin.access-control.epeople.button.add": "Pievienot EPersonu", - + // "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": "Lietotājs", - + // "admin.access-control.epeople.table.email": "E-mail (exact)", "admin.access-control.epeople.table.email": "E-pasts (precīzs)", - + // "admin.access-control.epeople.table.edit": "Edit", "admin.access-control.epeople.table.edit": "Rediģēt", - + // "admin.access-control.epeople.table.edit.buttons.edit": "Edit \"{{name}}\"", "admin.access-control.epeople.table.edit.buttons.edit": "Rediģēt \"{{name}}\"", - + // "admin.access-control.epeople.table.edit.buttons.remove": "Delete \"{{name}}\"", "admin.access-control.epeople.table.edit.buttons.remove": "Dzēst \"{{name}}\"", - + // "admin.access-control.epeople.no-items": "No EPeople to show.", "admin.access-control.epeople.no-items": "Nav pieejamas EPersonas.", - + // "admin.access-control.epeople.form.create": "Create EPerson", "admin.access-control.epeople.form.create": "Izveidot EPersonu", - + // "admin.access-control.epeople.form.edit": "Edit EPerson", "admin.access-control.epeople.form.edit": "Rediģēt EPersonu", - + // "admin.access-control.epeople.form.firstName": "First name", "admin.access-control.epeople.form.firstName": "Vārds", - + // "admin.access-control.epeople.form.lastName": "Last name", "admin.access-control.epeople.form.lastName": "Uzvārds", - + // "admin.access-control.epeople.form.email": "E-mail", "admin.access-control.epeople.form.email": "E-pasts", - + // "admin.access-control.epeople.form.emailHint": "Must be valid e-mail address", "admin.access-control.epeople.form.emailHint": "Jābūt derīgai e-pasta adresei", - + // "admin.access-control.epeople.form.canLogIn": "Can log in", "admin.access-control.epeople.form.canLogIn": "Var pierakstīties", - + // "admin.access-control.epeople.form.requireCertificate": "Requires certificate", "admin.access-control.epeople.form.requireCertificate": "Nepieciešams sertifikāts", - + // "admin.access-control.epeople.form.notification.created.success": "Successfully created EPerson \"{{name}}\"", "admin.access-control.epeople.form.notification.created.success": "Veiksmīgi izveidota EPersona \"{{name}}\"", - + // "admin.access-control.epeople.form.notification.created.failure": "Failed to create EPerson \"{{name}}\"", "admin.access-control.epeople.form.notification.created.failure": "Neizdevās izveidot EPersonu \"{{name}}\"", - + // "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": "Neizdevās izveidot EPersonu \"{{name}}\", e-pasts \"{{email}}\" jau tiek lietots.", - + // "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": "Neizdevās rediģēt EPersonu \"{{name}}\", e-pasts \"{{email}}\" jau tiek lietots.", - + // "admin.access-control.epeople.form.notification.edited.success": "Successfully edited EPerson \"{{name}}\"", "admin.access-control.epeople.form.notification.edited.success": "Veiksmīgi rediģēta EPersona \"{{name}}\"", - + // "admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"", "admin.access-control.epeople.form.notification.edited.failure": "Neizdevās rediģēt EPerosnu \"{{name}}\"", - + // "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.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.groupsEPersonIsMemberOf": "Member of these groups:", "admin.access-control.epeople.form.groupsEPersonIsMemberOf": "Grupas lietotāji:", - + // "admin.access-control.epeople.form.table.id": "ID", "admin.access-control.epeople.form.table.id": "ID", - + // "admin.access-control.epeople.form.table.name": "Name", "admin.access-control.epeople.form.table.name": "Vārds", - + // "admin.access-control.epeople.form.memberOfNoGroups": "This EPerson is not a member of any groups", "admin.access-control.epeople.form.memberOfNoGroups": "EPersona nepieder nevienai grupai", - + // "admin.access-control.epeople.form.goToGroups": "Add to groups", "admin.access-control.epeople.form.goToGroups": "Pievienot grupām", - + // "admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"", "admin.access-control.epeople.notification.deleted.failure": "Neizdevās dzēst EPersonu: \"{{name}}\"", - + // "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"", "admin.access-control.epeople.notification.deleted.success": "Veiksmīgi dzēsta EPersona: \"{{name}}\"", - - - + + + // "admin.access-control.groups.title": "DSpace Angular :: Groups", "admin.access-control.groups.title": "DSpace Angular :: Grupas", - + // "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.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.head": "Groups", "admin.access-control.groups.head": "Grupas", - + // "admin.access-control.groups.button.add": "Add group", "admin.access-control.groups.button.add": "Pievienot grupu", - + // "admin.access-control.groups.search.head": "Search groups", "admin.access-control.groups.search.head": "Meklēt grupas", - + // "admin.access-control.groups.button.see-all": "Browse all", "admin.access-control.groups.button.see-all": "Skatīt visas", - + // "admin.access-control.groups.search.button": "Search", "admin.access-control.groups.search.button": "Meklēt", - + // "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": "Vārds", - + // "admin.access-control.groups.table.members": "Members", "admin.access-control.groups.table.members": "Lietotāji", - + // "admin.access-control.groups.table.edit": "Edit", "admin.access-control.groups.table.edit": "Rediģēt", - + // "admin.access-control.groups.table.edit.buttons.edit": "Edit \"{{name}}\"", "admin.access-control.groups.table.edit.buttons.edit": "Rediģēt \"{{name}}\"", - + // "admin.access-control.groups.table.edit.buttons.remove": "Delete \"{{name}}\"", "admin.access-control.groups.table.edit.buttons.remove": "Dzēst \"{{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": "Netika atrasta grupa ar šādu nosaukumu vai identifikatoru", - + // "admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"", "admin.access-control.groups.notification.deleted.success": "Veiksmīgi dzēsta grupa \"{{name}}\"", - + // "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.content": "Cause: \"{{cause}}\"", // TODO New key - Add a translation "admin.access-control.groups.notification.deleted.failure.content": "Cause: \"{{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.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.head.create": "Create group", "admin.access-control.groups.form.head.create": "Izveidot grupu", - + // "admin.access-control.groups.form.head.edit": "Edit group", "admin.access-control.groups.form.head.edit": "Rediģēt grupu", - + // "admin.access-control.groups.form.groupName": "Group name", "admin.access-control.groups.form.groupName": "Grupas nosaukums", - + // "admin.access-control.groups.form.groupDescription": "Description", "admin.access-control.groups.form.groupDescription": "Apraksts", - + // "admin.access-control.groups.form.notification.created.success": "Successfully created Group \"{{name}}\"", "admin.access-control.groups.form.notification.created.success": "Izveidota grupa \"{{name}}\"", - + // "admin.access-control.groups.form.notification.created.failure": "Failed to create Group \"{{name}}\"", "admin.access-control.groups.form.notification.created.failure": "Neizdevās izveidot grupu \"{{name}}\"", - + // "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": "Neizdevās izveidot grupu ar nosaukumu: \"{{name}}\", pārliecinies vai grupa ar šādu nosaukumu jau netiek izmantota.", - + // "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.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.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.actions.delete": "Delete Group", // TODO New key - Add a translation "admin.access-control.groups.form.actions.delete": "Delete Group", - + // "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.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.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.confirm": "Delete", // TODO New key - Add a translation "admin.access-control.groups.form.delete-group.modal.confirm": "Delete", - + // "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.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.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.members-list.head": "EPeople", "admin.access-control.groups.form.members-list.head": "EPersona", - + // "admin.access-control.groups.form.members-list.search.head": "Add EPeople", "admin.access-control.groups.form.members-list.search.head": "Pievienot EPersonu", - + // "admin.access-control.groups.form.members-list.button.see-all": "Browse All", "admin.access-control.groups.form.members-list.button.see-all": "Skatīt visus", - + // "admin.access-control.groups.form.members-list.headMembers": "Current Members", "admin.access-control.groups.form.members-list.headMembers": "Pašreizējie lietotāji", - + // "admin.access-control.groups.form.members-list.search.scope.metadata": "Metadata", "admin.access-control.groups.form.members-list.search.scope.metadata": "Metadats", - + // "admin.access-control.groups.form.members-list.search.scope.email": "E-mail (exact)", "admin.access-control.groups.form.members-list.search.scope.email": "E-pasts (precīzi)", - + // "admin.access-control.groups.form.members-list.search.button": "Search", "admin.access-control.groups.form.members-list.search.button": "Meklēt", - + // "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": "Nosaukums", - + // "admin.access-control.groups.form.members-list.table.edit": "Remove / Add", "admin.access-control.groups.form.members-list.table.edit": "Noņemt / Pievienot", - + // "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": "Noņemt lietotāju ar vārdu \"{{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": "Veiksmīgi pievienots lietotājs: \"{{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": "Neizdevās pievienot lietotāju: \"{{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": "Veiksmīgi dzēsts lietotājs: \"{{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": "Neizdevās dzēst lietotāju: \"{{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": "Pievienot lietotāju ar vārdu \"{{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": "Pašlaik nav aktīvu grupu, iesniegt sākumā vārdu.", - + // "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": "Grupā nav lietotāju, atrast un pievienot.", - + // "admin.access-control.groups.form.members-list.no-items": "No EPeople found in that search", "admin.access-control.groups.form.members-list.no-items": "Netika atrasta neviena EPersona", - + // "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.head": "Groups", "admin.access-control.groups.form.subgroups-list.head": "Grupas", - + // "admin.access-control.groups.form.subgroups-list.search.head": "Add Subgroup", "admin.access-control.groups.form.subgroups-list.search.head": "Pievienot apakšgrupu", - + // "admin.access-control.groups.form.subgroups-list.button.see-all": "Browse All", "admin.access-control.groups.form.subgroups-list.button.see-all": "Skatīt visus", - + // "admin.access-control.groups.form.subgroups-list.headSubgroups": "Current Subgroups", "admin.access-control.groups.form.subgroups-list.headSubgroups": "Pašreizējās apakšgrupas", - + // "admin.access-control.groups.form.subgroups-list.search.button": "Search", "admin.access-control.groups.form.subgroups-list.search.button": "Meklēt", - + // "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": "Vārds", - + // "admin.access-control.groups.form.subgroups-list.table.edit": "Remove / Add", "admin.access-control.groups.form.subgroups-list.table.edit": "Noņemt / Pievienot", - + // "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": "Noņemt apakšgrupu ar nosaukumu \"{{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": "Pievienot apakšgrupu ar nosaukumu \"{{name}}\"", - + // "admin.access-control.groups.form.subgroups-list.table.edit.currentGroup": "Current group", "admin.access-control.groups.form.subgroups-list.table.edit.currentGroup": "Pašreizējā grupa", - + // "admin.access-control.groups.form.subgroups-list.notification.success.addSubgroup": "Successfully added subgroup: \"{{name}}\"", "admin.access-control.groups.form.subgroups-list.notification.success.addSubgroup": "Veiksmīgi pievienota apakšgrupa: \"{{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": "Neizdevās pievienot apakšgrupu: \"{{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": "Veiksmīgi dzēsta apakšgrupa: \"{{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": "Neizdevās dzēst apakšgrupu: \"{{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": "Pašlaik nav aktīvu grupu, iesniegt sākumā vārdu.", - + // "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": "Šī ir pašreizēja grupa, nevar pievienot", - + // "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": "Netika atrasta grupa ar šādu nosaukumu vai identifikatoru", - + // "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": "Grupā pašreiz nav apakšgrupu.", - + // "admin.access-control.groups.form.return": "Return to groups", "admin.access-control.groups.form.return": "Atgriezties pie grupām", - - - + + + // "admin.search.breadcrumbs": "Administrative Search", "admin.search.breadcrumbs": "Administratīvā Meklēšana", - + // "admin.search.collection.edit": "Edit", "admin.search.collection.edit": "Rediģēt", - + // "admin.search.community.edit": "Edit", "admin.search.community.edit": "Rediģēt", - + // "admin.search.item.delete": "Delete", "admin.search.item.delete": "Dzēst", - + // "admin.search.item.edit": "Edit", "admin.search.item.edit": "Rediģēt", - + // "admin.search.item.make-private": "Make Private", "admin.search.item.make-private": "Padarīt Privātu", - + // "admin.search.item.make-public": "Make Public", "admin.search.item.make-public": "Padarīt Publisku", - + // "admin.search.item.move": "Move", "admin.search.item.move": "Pārvietot", - + // "admin.search.item.reinstate": "Reinstate", "admin.search.item.reinstate": "Atjaunot", - + // "admin.search.item.withdraw": "Withdraw", "admin.search.item.withdraw": "Atsaukt", - + // "admin.search.title": "Administrative Search", "admin.search.title": "Administratīvā Meklēšana", - + // "administrativeView.search.results.head": "Administrative Search", "administrativeView.search.results.head": "Administratīvā meklēšana", - - - - + + + + // "admin.workflow.breadcrumbs": "Administer Workflow", "admin.workflow.breadcrumbs": "Administrēt darba plūsmu", - + // "admin.workflow.title": "Administer Workflow", "admin.workflow.title": "Administrēt darba plūsmu", - + // "admin.workflow.item.workflow": "Workflow", "admin.workflow.item.workflow": "Darba plūsma", - + // "admin.workflow.item.delete": "Delete", "admin.workflow.item.delete": "Dzēst", - + // "admin.workflow.item.send-back": "Send back", "admin.workflow.item.send-back": "Sūtīt atpakaļ", - - - + + + // "admin.metadata-import.breadcrumbs": "Import Metadata", // TODO New key - Add a translation "admin.metadata-import.breadcrumbs": "Import Metadata", - + // "admin.metadata-import.title": "Import Metadata", // TODO New key - Add a translation "admin.metadata-import.title": "Import 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.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.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.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.button.return": "Return", // TODO New key - Add a translation "admin.metadata-import.page.button.return": "Return", - + // "admin.metadata-import.page.button.proceed": "Proceed", // TODO New key - Add a translation "admin.metadata-import.page.button.proceed": "Proceed", - + // "admin.metadata-import.page.error.addFile": "Select file first!", // TODO New key - Add a translation "admin.metadata-import.page.error.addFile": "Select file first!", - - - - + + + + // "auth.errors.invalid-user": "Invalid email address or password.", "auth.errors.invalid-user": "Nepareizs e-pasta adrese vai parole.", - + // "auth.messages.expired": "Your session has expired. Please log in again.", "auth.messages.expired": "Jūsu sesija ir beigusies. Lūdzu pieslēdzieties vēlreiz", - - - + + + // "bitstream.edit.bitstream": "Bitstream: ", "bitstream.edit.bitstream": "Bitu straume: ", - + // "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": "Pēc izvēles sniedziet īsu faila aprakstu, piemēram, \"Galvanais raksts\" vai \"Eksperimenta datu nolasījumi\".", - + // "bitstream.edit.form.description.label": "Description", "bitstream.edit.form.description.label": "Apraksts", - + // "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": "Pirmā diena, no kuras atļauta pieeja. Šai formai nevarēs mainīt datumu. Lai uzstādītu embargo datumu šai bitu straumei, doties uz Materiāla statuss, noklikšķiniet uz Autorizācijas..., izveidot vai rediģēt bitu straumes LASĪT politiku, un uzstādīt Sākuma datumu kā vēlaties.", - + // "bitstream.edit.form.embargo.label": "Embargo until specific date", "bitstream.edit.form.embargo.label": "Embargo līdz noteiktam datumam", - + // "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": "Mainīt faila nosaukumu bitu straumei. Ņemiet vērā, ka tas mainīs redzamo bitu straumes URL, bet vecās saites joprojām būš pieejamas, kamēr ID secība nemainīsies.", - + // "bitstream.edit.form.fileName.label": "Filename", "bitstream.edit.form.fileName.label": "Faila nosaukums", - + // "bitstream.edit.form.newFormat.label": "Describe new format", "bitstream.edit.form.newFormat.label": "Aprakstīt jauno formātu", - + // "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": "Lietojumprogramma, kuru izmantojāt faila izveidošanai, un versijas numurs (piemēram, \" ACMESoft SuperApp versija 1.5 \").", - + // "bitstream.edit.form.primaryBitstream.label": "Primary bitstream", "bitstream.edit.form.primaryBitstream.label": "Galvenā bitu straume", - + // "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": "Ja formāts nav iepriekš minētajā sarakstā, atlasiet “formāts, kurš nav sarakstā” iepriekš un aprakstiet to sadaļā “Aprakstīt jauno formātu”.", - + // "bitstream.edit.form.selectedFormat.label": "Selected Format", "bitstream.edit.form.selectedFormat.label": "Izvēlētais formāts", - + // "bitstream.edit.form.selectedFormat.unknown": "Format not in list", "bitstream.edit.form.selectedFormat.unknown": "Formāts, kurš nav sarakstā", - + // "bitstream.edit.notifications.error.format.title": "An error occurred saving the bitstream's format", "bitstream.edit.notifications.error.format.title": "Saglabājot bitu straumes formātu, radās kļūda", - + // "bitstream.edit.notifications.saved.content": "Your changes to this bitstream were saved.", "bitstream.edit.notifications.saved.content": "Jūsu izmaiņas šajā bitu straumē tika saglabātas.", - + // "bitstream.edit.notifications.saved.title": "Bitstream saved", "bitstream.edit.notifications.saved.title": "Bitu straume saglabāta", - + // "bitstream.edit.title": "Edit bitstream", "bitstream.edit.title": "Rediģēt bitu straumi", - - - + + + // "browse.comcol.by.author": "By Author", "browse.comcol.by.author": "Pēc Autora", - + // "browse.comcol.by.dateissued": "By Issue Date", "browse.comcol.by.dateissued": "Pēc Izdošanas Datuma", - + // "browse.comcol.by.subject": "By Subject", "browse.comcol.by.subject": "Pēc Priekšmeta", - + // "browse.comcol.by.title": "By Title", "browse.comcol.by.title": "Pēc Nosaukuma", - + // "browse.comcol.head": "Browse", "browse.comcol.head": "Pārlūkot", - + // "browse.empty": "No items to show.", "browse.empty": "Ieraksti nav atrasti.", - + // "browse.metadata.author": "Author", "browse.metadata.author": "Autors", - + // "browse.metadata.dateissued": "Issue Date", "browse.metadata.dateissued": "Izdošanas datums", - + // "browse.metadata.subject": "Subject", "browse.metadata.subject": "Priekšmets", - + // "browse.metadata.title": "Title", "browse.metadata.title": "Nosaukums", - + // "browse.metadata.author.breadcrumbs": "Browse by Author", "browse.metadata.author.breadcrumbs": "Meklēt pēc Autora", - + // "browse.metadata.dateissued.breadcrumbs": "Browse by Date", "browse.metadata.dateissued.breadcrumbs": "Meklēt pēc Datuma", - + // "browse.metadata.subject.breadcrumbs": "Browse by Subject", "browse.metadata.subject.breadcrumbs": "Meklēt pēc Priekšmeta", - + // "browse.metadata.title.breadcrumbs": "Browse by Title", "browse.metadata.title.breadcrumbs": "Meklēt pēc Nosaukuma", - + // "browse.startsWith.choose_start": "(Choose start)", "browse.startsWith.choose_start": "(Izvēlieties sākumu)", - + // "browse.startsWith.choose_year": "(Choose year)", "browse.startsWith.choose_year": "(Izvēlieties gadu)", - + // "browse.startsWith.jump": "Jump to a point in the index:", "browse.startsWith.jump": "Pāriet uz punktu indeksā:", - + // "browse.startsWith.months.april": "April", "browse.startsWith.months.april": "Aprīlis", - + // "browse.startsWith.months.august": "August", "browse.startsWith.months.august": "Augusts", - + // "browse.startsWith.months.december": "December", "browse.startsWith.months.december": "Decembris", - + // "browse.startsWith.months.february": "February", "browse.startsWith.months.february": "Februāris", - + // "browse.startsWith.months.january": "January", "browse.startsWith.months.january": "Janvāris", - + // "browse.startsWith.months.july": "July", "browse.startsWith.months.july": "Jūlijs", - + // "browse.startsWith.months.june": "June", "browse.startsWith.months.june": "Jūnijs", - + // "browse.startsWith.months.march": "March", "browse.startsWith.months.march": "Marts", - + // "browse.startsWith.months.may": "May", "browse.startsWith.months.may": "Maijs", - + // "browse.startsWith.months.none": "(Choose month)", "browse.startsWith.months.none": "(Izvēlieties mēnesi)", - + // "browse.startsWith.months.november": "November", "browse.startsWith.months.november": "Novembris", - + // "browse.startsWith.months.october": "October", "browse.startsWith.months.october": "Oktobris", - + // "browse.startsWith.months.september": "September", "browse.startsWith.months.september": "Septembris", - + // "browse.startsWith.submit": "Go", "browse.startsWith.submit": "Izpildīt", - + // "browse.startsWith.type_date": "Or type in a date (year-month):", "browse.startsWith.type_date": "Vai ievadiet datumu (gads-mēnesis):", - + // "browse.startsWith.type_text": "Or enter first few letters:", "browse.startsWith.type_text": "Vai arī ievadiet pirmos burtus:", - + // "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", "browse.title": "Meklē {{ collection }} pēc {{ field }} {{ value }}", - - + + // "chips.remove": "Remove chip", "chips.remove": "Dzēst daļu", - - - + + + // "collection.create.head": "Create a Collection", "collection.create.head": "Izveidot Kolekciju", - + // "collection.create.notifications.success": "Successfully created the Collection", "collection.create.notifications.success": "Kolekcija tika veiksmīgi izveidota", - + // "collection.create.sub-head": "Create a Collection for Community {{ parent }}", "collection.create.sub-head": "Izveidot Kolekciju priekš Kategorijas {{ parent }}", - + // "collection.curate.header": "Curate Collection: {{collection}}", // TODO New key - Add a translation "collection.curate.header": "Curate Collection: {{collection}}", - + // "collection.delete.cancel": "Cancel", "collection.delete.cancel": "Atcelt", - + // "collection.delete.confirm": "Confirm", "collection.delete.confirm": "Apstiprināt", - + // "collection.delete.head": "Delete Collection", "collection.delete.head": "Dzēst Kolekciju", - + // "collection.delete.notification.fail": "Collection could not be deleted", "collection.delete.notification.fail": "Kolekciju nevar izdzēst", - + // "collection.delete.notification.success": "Successfully deleted collection", "collection.delete.notification.success": "Kolekcija ir veiksmīgi izdzēsta", - + // "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", "collection.delete.text": "Vai esat pārliecināts, ka vēlaties izdzēst kolekciju \"{{ dso }}\"", - - - + + + // "collection.edit.delete": "Delete this collection", "collection.edit.delete": "Dzēst šo kolekciju", - + // "collection.edit.head": "Edit Collection", "collection.edit.head": "Rediģēt Kolekciju", - + // "collection.edit.breadcrumbs": "Edit Collection", "collection.edit.breadcrumbs": "Rediģēt Kolekciju", - - - + + + // "collection.edit.tabs.mapper.head": "Item Mapper", // TODO New key - Add a translation "collection.edit.tabs.mapper.head": "Item Mapper", - + // "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.item-mapper.cancel": "Cancel", "collection.edit.item-mapper.cancel": "Atcelt", - + // "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", "collection.edit.item-mapper.collection": "Kolekcija: \"{{name}}\"", - + // "collection.edit.item-mapper.confirm": "Map selected items", "collection.edit.item-mapper.confirm": "Piesaistīt izvēlētos materiālus", - + // "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": "Šis ir materiālu piesaistīšanas rīks, kas kolekciju administratoriem ļauj šajā kolekcijā piesaistīt citu kolekciju materiālus. Jūs varat meklēt materiālus no citām kolekcijām un piesaistīt tos vai pārlūkot pašlaik piesaistīto materiālu sarakstu.", - + // "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", "collection.edit.item-mapper.head": "Materiālu piesaistīšana - Materiālu piesaistīšana no citas kolekcijas", - + // "collection.edit.item-mapper.no-search": "Please enter a query to search", "collection.edit.item-mapper.no-search": "Lūdzu ievadiet vaicājumu,lai meklētu", - + // "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", "collection.edit.item-mapper.notifications.map.error.content": "{{amount}} materiālu piesaistīšanā radās kļūdas.", - + // "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", "collection.edit.item-mapper.notifications.map.error.head": "Piesaistīšanas kļūdas", - + // "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", "collection.edit.item-mapper.notifications.map.success.content": "Veiksmīgi piesaistīti {{amount}} materiāli.", - + // "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", "collection.edit.item-mapper.notifications.map.success.head": "Piesaistīšana pabeigta", - + // "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": "Noņemot piesaistītos materiālus {{amount}}, radās kļūda.", - + // "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", "collection.edit.item-mapper.notifications.unmap.error.head": "Dzēst piesaistes kļūdas", - + // "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", "collection.edit.item-mapper.notifications.unmap.success.content": "Veiksmīgi dzēsta piesaiste {{amount}} materiāliem.", - + // "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", "collection.edit.item-mapper.notifications.unmap.success.head": "Piesaistes dzēšana izpildīta", - + // "collection.edit.item-mapper.remove": "Remove selected item mappings", "collection.edit.item-mapper.remove": "Dzēst izvēlēto materiālu piesaistes", - + // "collection.edit.item-mapper.tabs.browse": "Browse mapped items", "collection.edit.item-mapper.tabs.browse": "Pārlūkot piesaistītos materiālus", - + // "collection.edit.item-mapper.tabs.map": "Map new items", "collection.edit.item-mapper.tabs.map": "Piesaistīt jaunus materiālus", - - - + + + // "collection.edit.logo.label": "Collection logo", "collection.edit.logo.label": "Kolekcijas logotips", - + // "collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.", "collection.edit.logo.notifications.add.error": "Kolekcijas logotipa augšupielāde neizdevās. Pirms mēģināt vēlreiz, lūdzu, pārbaudiet saturu.", - + // "collection.edit.logo.notifications.add.success": "Upload Collection logo successful.", "collection.edit.logo.notifications.add.success": "Kolekcijas logotipa augšupielāde ir veiksmīga.", - + // "collection.edit.logo.notifications.delete.success.title": "Logo deleted", "collection.edit.logo.notifications.delete.success.title": "Logotips dzēsts", - + // "collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo", "collection.edit.logo.notifications.delete.success.content": "Kolekcijas logotips ir veiksmīgi izdzēsts", - + // "collection.edit.logo.notifications.delete.error.title": "Error deleting logo", "collection.edit.logo.notifications.delete.error.title": "Kļūda dzēšot logotopu", - + // "collection.edit.logo.upload": "Drop a Collection Logo to upload", "collection.edit.logo.upload": "Ievietot Kolekcijas logotipu, lai augšupielādētu", - - - + + + // "collection.edit.notifications.success": "Successfully edited the Collection", "collection.edit.notifications.success": "Kolekcija ir veiksmīgi rediģēta", - + // "collection.edit.return": "Return", "collection.edit.return": "Atgriezties", - - - + + + // "collection.edit.tabs.curate.head": "Curate", "collection.edit.tabs.curate.head": "Pārvaldīt", - + // "collection.edit.tabs.curate.title": "Collection Edit - Curate", "collection.edit.tabs.curate.title": "Rediģēt kolekciju - Pārvaldība", - + // "collection.edit.tabs.authorizations.head": "Authorizations", // TODO New key - Add a translation "collection.edit.tabs.authorizations.head": "Authorizations", - + // "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.metadata.head": "Edit Metadata", "collection.edit.tabs.metadata.head": "Rediģēt Metadatus", - + // "collection.edit.tabs.metadata.title": "Collection Edit - Metadata", "collection.edit.tabs.metadata.title": "Rediģēt Kolekciju - Metadati", - + // "collection.edit.tabs.roles.head": "Assign Roles", "collection.edit.tabs.roles.head": "Piešķirt Lomu", - + // "collection.edit.tabs.roles.title": "Collection Edit - Roles", "collection.edit.tabs.roles.title": "Rediģēt Kolekciju - Lomas", - + // "collection.edit.tabs.source.external": "This collection harvests its content from an external source", "collection.edit.tabs.source.external": "Šīs kolekcijas saturs tiek iegūts no ārēja avota", - + // "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": "Jums ir jānorāda noteikts id no mērķa kolekcijas", - + // "collection.edit.tabs.source.form.harvestType": "Content being harvested", "collection.edit.tabs.source.form.harvestType": "Saturs tiek iegūts", - + // "collection.edit.tabs.source.form.head": "Configure an external source", "collection.edit.tabs.source.form.head": "Konfigurēt ārējo avotu", - + // "collection.edit.tabs.source.form.metadataConfigId": "Metadata Format", "collection.edit.tabs.source.form.metadataConfigId": "Matadatu formāts", - + // "collection.edit.tabs.source.form.oaiSetId": "OAI specific set id", "collection.edit.tabs.source.form.oaiSetId": "OAI konkrētas kopas id", - + // "collection.edit.tabs.source.form.oaiSource": "OAI Provider", "collection.edit.tabs.source.form.oaiSource": "OAI Sniedzējs", - + // "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": "Ievākt metadatus un bitu straumes (nepieciešams ORE atbalsts)", - + // "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": "Ievākt metadatus un atsauces uz bitu straumēm (requires ORE support)", - + // "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Harvest metadata only", "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Iegūt tikai metadatus", - + // "collection.edit.tabs.source.head": "Content Source", "collection.edit.tabs.source.head": "Satura Avots", - + // "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": "Jūsu veiktās izmaiņas tika atmestas. Lai atjaunotu izmaiņas, noklikšķiniet uz pogas 'Atsaukt'", - + // "collection.edit.tabs.source.notifications.discarded.title": "Changed discarded", "collection.edit.tabs.source.notifications.discarded.title": "Mainīts atmests", - + // "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": "Jūsu veiktās izmaiņas netika saglabātas. Pirms saglabāšanas, lūdzu, pārliecinieties, vai visi lauki ir derīgi.", - + // "collection.edit.tabs.source.notifications.invalid.title": "Metadata invalid", "collection.edit.tabs.source.notifications.invalid.title": "Metadati nav derīgi", - + // "collection.edit.tabs.source.notifications.saved.content": "Your changes to this collection's content source were saved.", "collection.edit.tabs.source.notifications.saved.content": "Jūsu veiktās izmaiņas šīs kolekcijas satura avotā tika saglabātas.", - + // "collection.edit.tabs.source.notifications.saved.title": "Content Source saved", "collection.edit.tabs.source.notifications.saved.title": "Satura avots ir saglabāts", - + // "collection.edit.tabs.source.title": "Collection Edit - Content Source", "collection.edit.tabs.source.title": "Rediģēt Kolekciju - Satura Avots", - - - + + + // "collection.edit.template.add-button": "Add", // TODO New key - Add a translation "collection.edit.template.add-button": "Add", - + // "collection.edit.template.breadcrumbs": "Item template", // TODO New key - Add a translation "collection.edit.template.breadcrumbs": "Item template", - + // "collection.edit.template.cancel": "Cancel", // TODO New key - Add a translation "collection.edit.template.cancel": "Cancel", - + // "collection.edit.template.delete-button": "Delete", // TODO New key - Add a translation "collection.edit.template.delete-button": "Delete", - + // "collection.edit.template.edit-button": "Edit", // TODO New key - Add a translation "collection.edit.template.edit-button": "Edit", - + // "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.label": "Template item", // TODO New key - Add a translation "collection.edit.template.label": "Template item", - + // "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.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.title": "Edit Template Item", // TODO New key - Add a translation "collection.edit.template.title": "Edit Template Item", - - - + + + // "collection.form.abstract": "Short Description", "collection.form.abstract": "Īss apraksts", - + // "collection.form.description": "Introductory text (HTML)", "collection.form.description": "Ievadteksts (HTML)", - + // "collection.form.errors.title.required": "Please enter a collection name", "collection.form.errors.title.required": "Lūdzu ievadiet kolekcijas nosaukumu", - + // "collection.form.license": "License", "collection.form.license": "Licence", - + // "collection.form.provenance": "Provenance", "collection.form.provenance": "Izcelsmes avots", - + // "collection.form.rights": "Copyright text (HTML)", "collection.form.rights": "Autortiesību teksts (HTML)", - + // "collection.form.tableofcontents": "News (HTML)", "collection.form.tableofcontents": "Jaunumi (HTML)", - + // "collection.form.title": "Name", "collection.form.title": "Nosaukums", - - - + + + // "collection.listelement.badge": "Collection", // TODO New key - Add a translation "collection.listelement.badge": "Collection", - - - + + + // "collection.page.browse.recent.head": "Recent Submissions", "collection.page.browse.recent.head": "Nesenie iesniegumi", - + // "collection.page.browse.recent.empty": "No items to show", "collection.page.browse.recent.empty": "Ieraksti nav atrasti", - + // "collection.page.edit": "Edit this collection", // TODO New key - Add a translation "collection.page.edit": "Edit this collection", - + // "collection.page.handle": "Permanent URI for this collection", "collection.page.handle": "Kolekcijas nemainīgs URI", - + // "collection.page.license": "License", "collection.page.license": "Licence", - + // "collection.page.news": "News", "collection.page.news": "Jaunumi", - - - + + + // "collection.select.confirm": "Confirm selected", "collection.select.confirm": "Apsitprināt izvēlēto", - + // "collection.select.empty": "No collections to show", "collection.select.empty": "Kolekcijas nav pieejamas", - + // "collection.select.table.title": "Title", "collection.select.table.title": "Nosaukums", - - - + + + // "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.", "collection.source.update.notifications.error.content": "Norādītie iestatījumi ir pārbaudīti un nedarbojas.", - + // "collection.source.update.notifications.error.title": "Server Error", "collection.source.update.notifications.error.title": "Servera Kļūda", - - - + + + // "communityList.tabTitle": "DSpace - Community List", "communityList.tabTitle": "DSpace - Kategoriju Saraksts", - + // "communityList.title": "List of Communities", "communityList.title": "Kategoriju saraksts", - + // "communityList.showMore": "Show More", "communityList.showMore": "Rādīt Vairāk", - - - + + + // "community.create.head": "Create a Community", "community.create.head": "Izveidot Kategoriju", - + // "community.create.notifications.success": "Successfully created the Community", "community.create.notifications.success": "Kategorija tika veiksmīgi izveidota", - + // "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", "community.create.sub-head": "Izveidot Apakškategorija priekš Kategorijas {{ parent }}", - + // "community.curate.header": "Curate Community: {{community}}", // TODO New key - Add a translation "community.curate.header": "Curate Community: {{community}}", - + // "community.delete.cancel": "Cancel", "community.delete.cancel": "Atcelt", - + // "community.delete.confirm": "Confirm", "community.delete.confirm": "Apstiprināt", - + // "community.delete.head": "Delete Community", "community.delete.head": "Dzēst Kategoriju", - + // "community.delete.notification.fail": "Community could not be deleted", "community.delete.notification.fail": "Kategriju nav iespējams izdzēst", - + // "community.delete.notification.success": "Successfully deleted community", "community.delete.notification.success": "Veiksmīgi izdzēsta kategorija", - + // "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", "community.delete.text": "Vai tiešām vēlaties dzēst kategoriju \"{{ dso }}\"", - + // "community.edit.delete": "Delete this community", "community.edit.delete": "Dzēst šo kategoriju", - + // "community.edit.head": "Edit Community", "community.edit.head": "Rediģēt Kategoriju", - + // "community.edit.breadcrumbs": "Edit Community", "community.edit.breadcrumbs": "Rediģēt Kategoriju", - - + + // "community.edit.logo.label": "Community logo", "community.edit.logo.label": "Kategorijas logotips", - + // "community.edit.logo.notifications.add.error": "Uploading Community logo failed. Please verify the content before retrying.", "community.edit.logo.notifications.add.error": "Kategorijas logotipa augšupielāde neizdevās. Pirms mēģināt vēlreiz, lūdzu, pārbaudiet saturu.", - + // "community.edit.logo.notifications.add.success": "Upload Community logo successful.", "community.edit.logo.notifications.add.success": "Kategorijas logotipa augšupielāde ir veiksmīga.", - + // "community.edit.logo.notifications.delete.success.title": "Logo deleted", "community.edit.logo.notifications.delete.success.title": "logotips ir izdzēsts", - + // "community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo", "community.edit.logo.notifications.delete.success.content": "Kategorijas logotips ir veiksmīgi izdzēsts", - + // "community.edit.logo.notifications.delete.error.title": "Error deleting logo", "community.edit.logo.notifications.delete.error.title": "Izdzēšot logotipu, radās kļūda", - + // "community.edit.logo.upload": "Drop a Community Logo to upload", "community.edit.logo.upload": "Ievietot Kategorijas logotipu, lai augšupielādētu", - - - + + + // "community.edit.notifications.success": "Successfully edited the Community", "community.edit.notifications.success": "Kategorija tika veiksmīgi rediģēta", - + // "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.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.return": "Return", "community.edit.return": "Atgriezties", - - - + + + // "community.edit.tabs.curate.head": "Curate", "community.edit.tabs.curate.head": "Pārvaldīt", - + // "community.edit.tabs.curate.title": "Community Edit - Curate", "community.edit.tabs.curate.title": "Rdiģēt Kategoriju - Pārvaldība", - + // "community.edit.tabs.metadata.head": "Edit Metadata", "community.edit.tabs.metadata.head": "Rediģēt Metadatus", - + // "community.edit.tabs.metadata.title": "Community Edit - Metadata", "community.edit.tabs.metadata.title": "Rediģēt Kategoriju - Metadati", - + // "community.edit.tabs.roles.head": "Assign Roles", "community.edit.tabs.roles.head": "Piešķirt Lomas", - + // "community.edit.tabs.roles.title": "Community Edit - Roles", "community.edit.tabs.roles.title": "Rediģēt Kategorju - Lomas", - + // "community.edit.tabs.authorizations.head": "Authorizations", // TODO New key - Add a translation "community.edit.tabs.authorizations.head": "Authorizations", - + // "community.edit.tabs.authorizations.title": "Community Edit - Authorizations", // TODO New key - Add a translation "community.edit.tabs.authorizations.title": "Community Edit - Authorizations", - - - + + + // "community.listelement.badge": "Community", // TODO New key - Add a translation "community.listelement.badge": "Community", - - - + + + // "comcol-role.edit.no-group": "None", // TODO New key - Add a translation "comcol-role.edit.no-group": "None", - + // "comcol-role.edit.create": "Create", // TODO New key - Add a translation "comcol-role.edit.create": "Create", - + // "comcol-role.edit.restrict": "Restrict", // TODO New key - Add a translation "comcol-role.edit.restrict": "Restrict", - + // "comcol-role.edit.delete": "Delete", // TODO New key - Add a translation "comcol-role.edit.delete": "Delete", - - + + // "comcol-role.edit.community-admin.name": "Administrators", // TODO New key - Add a translation "comcol-role.edit.community-admin.name": "Administrators", - + // "comcol-role.edit.collection-admin.name": "Administrators", // TODO New key - Add a translation "comcol-role.edit.collection-admin.name": "Administrators", - - + + // "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.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.submitters.name": "Submitters", // TODO New key - Add a translation "comcol-role.edit.submitters.name": "Submitters", - + // "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.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.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.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.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.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.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.editor.name": "Editors", // TODO New key - Add a translation "comcol-role.edit.editor.name": "Editors", - + // "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.finaleditor.name": "Final editors", // TODO New key - Add a translation "comcol-role.edit.finaleditor.name": "Final editors", - + // "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.reviewer.name": "Reviewers", // TODO New key - Add a translation "comcol-role.edit.reviewer.name": "Reviewers", - + // "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.", - - - + + + // "community.form.abstract": "Short Description", "community.form.abstract": "Īss apraksts", - + // "community.form.description": "Introductory text (HTML)", "community.form.description": "Ievadteksts (HTML)", - + // "community.form.errors.title.required": "Please enter a community name", "community.form.errors.title.required": "Lūdzu ievadiet kategorijas nosaukumu", - + // "community.form.rights": "Copyright text (HTML)", "community.form.rights": "Autortiesību teksts (HTML)", - + // "community.form.tableofcontents": "News (HTML)", "community.form.tableofcontents": "Jaunumi (HTML)", - + // "community.form.title": "Name", "community.form.title": "Nosaukums", - + // "community.page.edit": "Edit this community", // TODO New key - Add a translation "community.page.edit": "Edit this community", - + // "community.page.handle": "Permanent URI for this community", "community.page.handle": "Kategorijas nemainīgs URI", - + // "community.page.license": "License", "community.page.license": "Licence", - + // "community.page.news": "News", "community.page.news": "Jaunumi", - + // "community.all-lists.head": "Subcommunities and Collections", "community.all-lists.head": "Apakškategorijas un Kolekcijas", - + // "community.sub-collection-list.head": "Collections of this Community", "community.sub-collection-list.head": "Kolekcijas no šīs kategorijas", - + // "community.sub-community-list.head": "Communities of this Community", "community.sub-community-list.head": "Šīs kategorijas apakš-kategorijas", - - - + + + // "cookies.consent.accept-all": "Accept all", // TODO New key - Add a translation "cookies.consent.accept-all": "Accept all", - + // "cookies.consent.accept-selected": "Accept selected", // TODO New key - Add a translation "cookies.consent.accept-selected": "Accept selected", - + // "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.title": "(opt-out)", // TODO New key - Add a translation "cookies.consent.app.opt-out.title": "(opt-out)", - + // "cookies.consent.app.purpose": "purpose", // TODO New key - Add a translation "cookies.consent.app.purpose": "purpose", - + // "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.title": "(always required)", // TODO New key - Add a translation "cookies.consent.app.required.title": "(always required)", - + // "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.close": "Close", // TODO New key - Add a translation "cookies.consent.close": "Close", - + // "cookies.consent.decline": "Decline", // TODO New key - Add a translation "cookies.consent.decline": "Decline", - + // "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.learnMore": "Customize", // TODO New key - Add a translation "cookies.consent.content-notice.learnMore": "Customize", - + // "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.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.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.title": "Information that we collect", // TODO New key - Add a translation "cookies.consent.content-modal.title": "Information that we collect", - - - + + + // "cookies.consent.app.title.authentication": "Authentication", // TODO New key - Add a translation "cookies.consent.app.title.authentication": "Authentication", - + // "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.title.preferences": "Preferences", // TODO New key - Add a translation "cookies.consent.app.title.preferences": "Preferences", - + // "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.title.acknowledgement": "Acknowledgement", // TODO New key - Add a translation "cookies.consent.app.title.acknowledgement": "Acknowledgement", - + // "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.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.purpose.functional": "Functional", // TODO New key - Add a translation "cookies.consent.purpose.functional": "Functional", - + // "cookies.consent.purpose.statistical": "Statistical", // TODO New key - Add a translation "cookies.consent.purpose.statistical": "Statistical", - - + + // "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.noop.label": "NOOP", // TODO New key - Add a translation "curation-task.task.noop.label": "NOOP", - + // "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.requiredmetadata.label": "Check for Required Metadata", // TODO New key - Add a translation "curation-task.task.requiredmetadata.label": "Check for Required Metadata", - + // "curation-task.task.translate.label": "Microsoft Translator", // TODO New key - Add a translation "curation-task.task.translate.label": "Microsoft Translator", - + // "curation-task.task.vscan.label": "Virus Scan", // TODO New key - Add a translation "curation-task.task.vscan.label": "Virus Scan", - - - + + + // "curation.form.task-select.label": "Task:", // TODO New key - Add a translation "curation.form.task-select.label": "Task:", - + // "curation.form.submit": "Start", // TODO New key - Add a translation "curation.form.submit": "Start", - + // "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.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.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.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.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)", - - - + + + // "dso-selector.create.collection.head": "New collection", "dso-selector.create.collection.head": "Jauna kolekcija", - + // "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.community.head": "New community", "dso-selector.create.community.head": "Jauna kategorija", - + // "dso-selector.create.community.sub-level": "Create a new community in", "dso-selector.create.community.sub-level": "Izveidot jaunu kategroiju iekš", - + // "dso-selector.create.community.top-level": "Create a new top-level community", "dso-selector.create.community.top-level": "Izveidot jaunu augstākā līmeņa kategoriju", - + // "dso-selector.create.item.head": "New item", "dso-selector.create.item.head": "Jauns materiāls", - + // "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.submission.head": "New submission", // TODO New key - Add a translation "dso-selector.create.submission.head": "New submission", - + // "dso-selector.edit.collection.head": "Edit collection", "dso-selector.edit.collection.head": "Rediģēt kolekciju", - + // "dso-selector.edit.community.head": "Edit community", "dso-selector.edit.community.head": "Rediģēt kategoriju", - + // "dso-selector.edit.item.head": "Edit item", "dso-selector.edit.item.head": "Rediģēt matriālu", - + // "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.no-results": "No {{ type }} found", "dso-selector.no-results": "Nav atrasts {{ type }}", - + // "dso-selector.placeholder": "Search for a {{ type }}", "dso-selector.placeholder": "Meklēt {{ type }}", - - - + + + // "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.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.cancel": "Cancel", // TODO New key - Add a translation "confirmation-modal.export-metadata.cancel": "Cancel", - + // "confirmation-modal.export-metadata.confirm": "Export", // TODO New key - Add a translation "confirmation-modal.export-metadata.confirm": "Export", - + // "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.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.cancel": "Cancel", // TODO New key - Add a translation "confirmation-modal.delete-eperson.cancel": "Cancel", - + // "confirmation-modal.delete-eperson.confirm": "Delete", // TODO New key - Add a translation "confirmation-modal.delete-eperson.confirm": "Delete", - - + + // "error.bitstream": "Error fetching bitstream", "error.bitstream": "Radās kļūda, saņemot bitu straumi", - + // "error.browse-by": "Error fetching items", "error.browse-by": "Materiālu ielasīšanas kļūda", - + // "error.collection": "Error fetching collection", "error.collection": "Kolekciju ielasīšanas kļūda", - + // "error.collections": "Error fetching collections", "error.collections": "Kolekciju ielasīšanas kļūda", - + // "error.community": "Error fetching community", "error.community": "Kategorijas ielasīšanas kļūda", - + // "error.identifier": "No item found for the identifier", "error.identifier": "Netika atrasti materiāli dotajam identifikatoram", - + // "error.default": "Error", "error.default": "Kļūda", - + // "error.item": "Error fetching item", "error.item": "Materiāla ielasīšanas kļūda", - + // "error.items": "Error fetching items", "error.items": "Materiālu ielasīšanas kļūda", - + // "error.objects": "Error fetching objects", "error.objects": "Objektu ielasīšanas kļūda", - + // "error.recent-submissions": "Error fetching recent submissions", "error.recent-submissions": "Neseno iesniegumu ielasīšanas kļūda", - + // "error.search-results": "Error fetching search results", "error.search-results": "Meklēšanas rezultātu ielasīšanas kļūda", - + // "error.sub-collections": "Error fetching sub-collections", "error.sub-collections": "Kļūda ielasot apakškolekcijas", - + // "error.sub-communities": "Error fetching sub-communities", "error.sub-communities": "Apakškategorijas ielasīšanas kļūda", - + // "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": "Sadaļas inicializācijas laikā radās kļūda. Lūdzu, pārbaudiet ievades formas konfigurāciju. Sīkāka informācija ir sniegta zemāk :

", - + // "error.top-level-communities": "Error fetching top-level communities", "error.top-level-communities": "Kļūda ielasot augstākā līmeņa kategorijas", - + // "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": "Jums ir jānodrošina šī licence, lai pabeigtu iesniegšanu. Ja šobrīd nevarat piešķirt šo licenci, varat saglabāt savu darbu un atgriezties vēlāk vai noņemt iesniegšanu.", - + // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", "error.validation.pattern": "Šo ievadi ierobežo pašreizējais modelis: {{ pattern }}.", - + // "error.validation.filerequired": "The file upload is mandatory", "error.validation.filerequired": "Faila augšupielāde ir obligāta", - - - + + + // "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", - - - + + + // "footer.copyright": "copyright © 2002-{{ year }}", "footer.copyright": "copyright © 2002-{{ year }}", - + // "footer.link.dspace": "DSpace software", "footer.link.dspace": "DSpace software", - + // "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.privacy-policy": "Privacy policy", // TODO New key - Add a translation "footer.link.privacy-policy": "Privacy policy", - + // "footer.link.end-user-agreement":"End User Agreement", // TODO New key - Add a translation "footer.link.end-user-agreement":"End User Agreement", - - - + + + // "forgot-email.form.header": "Forgot Password", // TODO New key - Add a translation "forgot-email.form.header": "Forgot Password", - + // "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.email": "Email Address *", // TODO New key - Add a translation "forgot-email.form.email": "Email Address *", - + // "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.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.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.submit": "Submit", // TODO New key - Add a translation "forgot-email.form.submit": "Submit", - + // "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.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.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.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-password.title": "Forgot Password", // TODO New key - Add a translation "forgot-password.title": "Forgot Password", - + // "forgot-password.form.head": "Forgot Password", // TODO New key - Add a translation "forgot-password.form.head": "Forgot Password", - + // "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.card.security": "Security", // TODO New key - Add a translation "forgot-password.form.card.security": "Security", - + // "forgot-password.form.identification.header": "Identify", // TODO New key - Add a translation "forgot-password.form.identification.header": "Identify", - + // "forgot-password.form.identification.email": "Email address: ", // TODO New key - Add a translation "forgot-password.form.identification.email": "Email address: ", - + // "forgot-password.form.label.password": "Password", // TODO New key - Add a translation "forgot-password.form.label.password": "Password", - + // "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.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.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.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.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.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.title": "Password reset completed", // TODO New key - Add a translation "forgot-password.form.notification.success.title": "Password reset completed", - + // "forgot-password.form.submit": "Submit password", // TODO New key - Add a translation "forgot-password.form.submit": "Submit password", - - - + + + // "form.add": "Add", "form.add": "Pievienot", - + // "form.add-help": "Click here to add the current entry and to add another one", "form.add-help": "Noklikšķiniet šeit, lai pievienotu tekošo elementu un lai pievienotu vēl vienu", - + // "form.cancel": "Cancel", "form.cancel": "Atcelt", - + // "form.clear": "Clear", "form.clear": "Notīrīt", - + // "form.clear-help": "Click here to remove the selected value", "form.clear-help": "Noklikšķiniet šeit, lai dzēstu atlasīto vērtību", - + // "form.edit": "Edit", "form.edit": "Rediģēt", - + // "form.edit-help": "Click here to edit the selected value", "form.edit-help": "Noklikšķiniet šeit, lai rediģēt atlasīto vērtību", - + // "form.first-name": "First name", "form.first-name": "Vārds", - + // "form.group-collapse": "Collapse", "form.group-collapse": "Sakļaut", - + // "form.group-collapse-help": "Click here to collapse", "form.group-collapse-help": "Noklikšķiniet šeit, lai sakļautu", - + // "form.group-expand": "Expand", "form.group-expand": "Izvērst", - + // "form.group-expand-help": "Click here to expand and add more elements", "form.group-expand-help": "Noklikšķiniet šeit, lai izvērstu un pievienotu citus elementus", - + // "form.last-name": "Last name", "form.last-name": "Uzvārds", - + // "form.loading": "Loading...", "form.loading": "Notiek ielāde...", - + // "form.lookup": "Lookup", "form.lookup": "Atrast", - + // "form.lookup-help": "Click here to look up an existing relation", "form.lookup-help": "Noklikšķiniet šeit, lai meklētu esošu saistību", - + // "form.no-results": "No results found", "form.no-results": "Rezultāti netika atrasti", - + // "form.no-value": "No value entered", "form.no-value": "Vērtība netika ievadīta", - + // "form.other-information": {}, "form.other-information": {}, - + // "form.remove": "Remove", "form.remove": "Dzēst", - + // "form.save": "Save", "form.save": "Saglabāt", - + // "form.save-help": "Save changes", "form.save-help": "Saglabāt izmaiņas", - + // "form.search": "Search", "form.search": "Meklēt", - + // "form.search-help": "Click here to look for an existing correspondence", // TODO Source message changed - Revise the translation "form.search-help": "Noklikšķiniet šeit, lai meklētu esošu korespondenci", - + // "form.submit": "Submit", "form.submit": "Iesniegt", - - - + + + // "home.description": "", "home.description": "", - + // "home.breadcrumbs": "Home", // TODO New key - Add a translation "home.breadcrumbs": "Home", - + // "home.title": "DSpace Angular :: Home", "home.title": "DSpace Angular :: Sākums", - + // "home.top-level-communities.head": "Communities in DSpace", "home.top-level-communities.head": "DSpace Kategorijas", - + // "home.top-level-communities.help": "Select a community to browse its collections.", "home.top-level-communities.help": "Izvēlieties kategoriju, lai pārlūkotu tās kolekcijas.", - - - + + + // "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.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.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.breadcrumbs": "End User Agreement", // TODO New key - Add a translation "info.end-user-agreement.breadcrumbs": "End User Agreement", - + // "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.save": "Save", // TODO New key - Add a translation "info.end-user-agreement.buttons.save": "Save", - + // "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.title": "End User Agreement", // TODO New key - Add a translation "info.end-user-agreement.title": "End User Agreement", - + // "info.privacy.breadcrumbs": "Privacy Statement", // TODO New key - Add a translation "info.privacy.breadcrumbs": "Privacy Statement", - + // "info.privacy.head": "Privacy Statement", // TODO New key - Add a translation "info.privacy.head": "Privacy Statement", - + // "info.privacy.title": "Privacy Statement", // TODO New key - Add a translation "info.privacy.title": "Privacy Statement", - - - + + + // "item.alerts.private": "This item is private", // TODO New key - Add a translation "item.alerts.private": "This item is private", - + // "item.alerts.withdrawn": "This item has been withdrawn", // TODO New key - Add a translation "item.alerts.withdrawn": "This item has been withdrawn", - - - + + + // "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.title": "Edit item's Policies", // TODO New key - Add a translation "item.edit.authorizations.title": "Edit item's Policies", - - - + + + // "item.badge.private": "Private", // TODO New key - Add a translation "item.badge.private": "Private", - + // "item.badge.withdrawn": "Withdrawn", // TODO New key - Add a translation "item.badge.withdrawn": "Withdrawn", - - - + + + // "item.bitstreams.upload.bundle": "Bundle", "item.bitstreams.upload.bundle": "Kopiens", - + // "item.bitstreams.upload.bundle.placeholder": "Select a bundle", "item.bitstreams.upload.bundle.placeholder": "Izvēlēties kopienu", - + // "item.bitstreams.upload.bundle.new": "Create bundle", "item.bitstreams.upload.bundle.new": "Izveidot kopienu", - + // "item.bitstreams.upload.bundles.empty": "This item doesn\'t contain any bundles to upload a bitstream to.", "item.bitstreams.upload.bundles.empty": "Materiālā nav neviena kopiena ko augšupielādēt bitu straumē.", - + // "item.bitstreams.upload.cancel": "Cancel", "item.bitstreams.upload.cancel": "Atcelt", - + // "item.bitstreams.upload.drop-message": "Drop a file to upload", "item.bitstreams.upload.drop-message": "Nomest failu augšupielādei", - + // "item.bitstreams.upload.item": "Item: ", "item.bitstreams.upload.item": "Materiāls: ", - + // "item.bitstreams.upload.notifications.bundle.created.content": "Successfully created new bundle.", "item.bitstreams.upload.notifications.bundle.created.content": "Veiksmīgi izveidota kopiena.", - + // "item.bitstreams.upload.notifications.bundle.created.title": "Created bundle", "item.bitstreams.upload.notifications.bundle.created.title": "Izveidot kopienu", - + // "item.bitstreams.upload.notifications.upload.failed": "Upload failed. Please verify the content before retrying.", "item.bitstreams.upload.notifications.upload.failed": "Augšupielāde neizdevās. Lūdzu pārbaudi saturu pirms mēģini vēlreiz.", - + // "item.bitstreams.upload.title": "Upload bitstream", "item.bitstreams.upload.title": "Augšupielādēt bitu straumi", - - - + + + // "item.edit.bitstreams.bundle.edit.buttons.upload": "Upload", "item.edit.bitstreams.bundle.edit.buttons.upload": "Augšupielādēt", - + // "item.edit.bitstreams.bundle.displaying": "Currently displaying {{ amount }} bitstreams of {{ total }}.", "item.edit.bitstreams.bundle.displaying": "Šobrīd rāda {{ amount }} bitu straumes no {{ total }}.", - + // "item.edit.bitstreams.bundle.load.all": "Load all ({{ total }})", "item.edit.bitstreams.bundle.load.all": "Ielādēt visus ({{ total }})", - + // "item.edit.bitstreams.bundle.load.more": "Load more", "item.edit.bitstreams.bundle.load.more": "Ielādēt vēl", - + // "item.edit.bitstreams.bundle.name": "BUNDLE: {{ name }}", "item.edit.bitstreams.bundle.name": "KOPIENA: {{ name }}", - + // "item.edit.bitstreams.discard-button": "Discard", "item.edit.bitstreams.discard-button": "Atsaukt", - + // "item.edit.bitstreams.edit.buttons.download": "Download", "item.edit.bitstreams.edit.buttons.download": "Lejupielādēt", - + // "item.edit.bitstreams.edit.buttons.drag": "Drag", "item.edit.bitstreams.edit.buttons.drag": "Vilkt", - + // "item.edit.bitstreams.edit.buttons.edit": "Edit", "item.edit.bitstreams.edit.buttons.edit": "Rediģēt", - + // "item.edit.bitstreams.edit.buttons.remove": "Remove", "item.edit.bitstreams.edit.buttons.remove": "Noņemt", - + // "item.edit.bitstreams.edit.buttons.undo": "Undo changes", "item.edit.bitstreams.edit.buttons.undo": "Atlikt izmaiņas", - + // "item.edit.bitstreams.empty": "This item doesn't contain any bitstreams. Click the upload button to create one.", "item.edit.bitstreams.empty": "Šis materiāls nesatur bitu straumi. Nospied augšupielādes pogu, lai izveidotu jaunu.", - + // "item.edit.bitstreams.headers.actions": "Actions", "item.edit.bitstreams.headers.actions": "Darbības", - + // "item.edit.bitstreams.headers.bundle": "Bundle", "item.edit.bitstreams.headers.bundle": "Kopiena", - + // "item.edit.bitstreams.headers.description": "Description", "item.edit.bitstreams.headers.description": "Apraksts", - + // "item.edit.bitstreams.headers.format": "Format", "item.edit.bitstreams.headers.format": "Formāts", - + // "item.edit.bitstreams.headers.name": "Name", "item.edit.bitstreams.headers.name": "Nosaukums", - + // "item.edit.bitstreams.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.bitstreams.notifications.discarded.content": "Jūsu veiktās izmaiņas tika atsaukt. Lai atjaunotu izmaiņas, noklikšķiniet uz pogas Atsaukt", - + // "item.edit.bitstreams.notifications.discarded.title": "Changes discarded", "item.edit.bitstreams.notifications.discarded.title": "Izmaiņas atsauktas", - + // "item.edit.bitstreams.notifications.move.failed.title": "Error moving bitstreams", "item.edit.bitstreams.notifications.move.failed.title": "Radās kļūda pārvietojot bitu straumi", - + // "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": "Jūsu pārvietošanas izmaiņas materiāla bitu straumē un kopienā ir saglabātas.", - + // "item.edit.bitstreams.notifications.move.saved.title": "Move changes saved", "item.edit.bitstreams.notifications.move.saved.title": "Pārvietošanas izmaiņas saglabātas", - + // "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": "Materiāls, kurā pašlaik strādājat, ir mainījis kāds cits lietotājs. Jūsu pašreizējās izmaiņas tiek atmestas, lai novērstu konfliktus", - + // "item.edit.bitstreams.notifications.outdated.title": "Changes outdated", "item.edit.bitstreams.notifications.outdated.title": "Novecojušas izmaiņas", - + // "item.edit.bitstreams.notifications.remove.failed.title": "Error deleting bitstream", "item.edit.bitstreams.notifications.remove.failed.title": "Radās kļūda dzēšot bitu straumi", - + // "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": "Jūsu materiāla bitu straumes izņemšanas izmaiņas ir saglabātas.", - + // "item.edit.bitstreams.notifications.remove.saved.title": "Removal changes saved", "item.edit.bitstreams.notifications.remove.saved.title": "Izņemšanas izmaiņas ir saglabātas", - + // "item.edit.bitstreams.reinstate-button": "Undo", "item.edit.bitstreams.reinstate-button": "Atlikt", - + // "item.edit.bitstreams.save-button": "Save", "item.edit.bitstreams.save-button": "Saglabāt", - + // "item.edit.bitstreams.upload-button": "Upload", "item.edit.bitstreams.upload-button": "Augšupielādēt", - - - + + + // "item.edit.delete.cancel": "Cancel", "item.edit.delete.cancel": "Atcelt", - + // "item.edit.delete.confirm": "Delete", "item.edit.delete.confirm": "Dzēst", - + // "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": "Vai esat drošs, ka pilnībā vēlaties izdzēst šo ierakstu? Uzmanību: Ieraksta kopija netiks saglabāta.", - + // "item.edit.delete.error": "An error occurred while deleting the item", "item.edit.delete.error": "Radās kļūda dzēšot materiālus", - + // "item.edit.delete.header": "Delete item: {{ id }}", "item.edit.delete.header": "Dzēst materiālu: {{ id }}", - + // "item.edit.delete.success": "The item has been deleted", "item.edit.delete.success": "Materiāls ir izdzēsts", - + // "item.edit.head": "Edit Item", "item.edit.head": "Rediģēt Materiālu", - + // "item.edit.breadcrumbs": "Edit Item", "item.edit.breadcrumbs": "Rediģēt Materiālu", - - + + // "item.edit.tabs.mapper.head": "Collection Mapper", // TODO New key - Add a translation "item.edit.tabs.mapper.head": "Collection Mapper", - + // "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.item-mapper.buttons.add": "Map item to selected collections", "item.edit.item-mapper.buttons.add": "Piesaistīt materiālu piesaistītajai kolekcijai", - + // "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", "item.edit.item-mapper.buttons.remove": "Dzēst materiālus izvēlētajai kolekcijai", - + // "item.edit.item-mapper.cancel": "Cancel", "item.edit.item-mapper.cancel": "Atcelt", - + // "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": "Šis ir materiālu piesaistīšanas rīks, kas ļauj administratoriem piesaistīt šo materiālu citās kolekcijās. Jūs varat meklēt kolekcijas un piesaistīt tās vai pārlūkot kolekciju sarakstu, uz kuru materiāls pašlaik ir piesaistīts.", - + // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", "item.edit.item-mapper.head": "Materiāla piesaistīšana - Piesaistīt Materiālu Kolekcijai", - + // "item.edit.item-mapper.item": "Item: \"{{name}}\"", "item.edit.item-mapper.item": "Materiāls: \"{{name}}\"", - + // "item.edit.item-mapper.no-search": "Please enter a query to search", "item.edit.item-mapper.no-search": "Lūdzu ievadiet vaicājumu, lai meklētu", - + // "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": "Radās kļūda piesaistot materiālus {{amount}} kolekcijai.", - + // "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", "item.edit.item-mapper.notifications.add.error.head": "Piesaistīšanas kļūdas", - + // "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", "item.edit.item-mapper.notifications.add.success.content": "Veiksmīgi piesaistīto materiāli {{amount}} kolekcijai.", - + // "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", "item.edit.item-mapper.notifications.add.success.head": "Piesaistīšana pabeigta", - + // "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": "Rādās kļūdas noņemot piesaistes {{amount}} kolekcijām.", - + // "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", "item.edit.item-mapper.notifications.remove.error.head": "Piesaistīšanas kļūdu noņemšana", - + // "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", "item.edit.item-mapper.notifications.remove.success.content": "Veiksmīgi noņemta materiālu piesaiste {{amount}} kolekcijās.", - + // "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", "item.edit.item-mapper.notifications.remove.success.head": "Piesaistes noņemšana ir pabeigta", - + // "item.edit.item-mapper.tabs.browse": "Browse mapped collections", "item.edit.item-mapper.tabs.browse": "Pārlūkot piesaistīās kolekcijas", - + // "item.edit.item-mapper.tabs.map": "Map new collections", "item.edit.item-mapper.tabs.map": "Piesaistīt jaunu kolekciju", - - - + + + // "item.edit.metadata.add-button": "Add", "item.edit.metadata.add-button": "Pievienot", - + // "item.edit.metadata.discard-button": "Discard", "item.edit.metadata.discard-button": "Atmest", - + // "item.edit.metadata.edit.buttons.edit": "Edit", "item.edit.metadata.edit.buttons.edit": "Rediģēt", - + // "item.edit.metadata.edit.buttons.remove": "Remove", "item.edit.metadata.edit.buttons.remove": "Dzēst", - + // "item.edit.metadata.edit.buttons.undo": "Undo changes", "item.edit.metadata.edit.buttons.undo": "Atsaukt izmaiņas", - + // "item.edit.metadata.edit.buttons.unedit": "Stop editing", "item.edit.metadata.edit.buttons.unedit": "Pārtraukt rediģēšanu", - + // "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.headers.edit": "Edit", "item.edit.metadata.headers.edit": "Rediģēt", - + // "item.edit.metadata.headers.field": "Field", "item.edit.metadata.headers.field": "Lauks", - + // "item.edit.metadata.headers.language": "Lang", "item.edit.metadata.headers.language": "Valoda", - + // "item.edit.metadata.headers.value": "Value", "item.edit.metadata.headers.value": "Vērtība", - + // "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", "item.edit.metadata.metadatafield.invalid": "Lūdzu, izvēlieties derīgu metadatu lauku", - + // "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.metadata.notifications.discarded.content": "Jūsu veiktās izmaiņas tika atmestas. Lai atjaunotu izmaiņas, noklikšķiniet uz pogas 'Atsaukt'", - + // "item.edit.metadata.notifications.discarded.title": "Changed discarded", "item.edit.metadata.notifications.discarded.title": "Mainīts atmests", - + // "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.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", "item.edit.metadata.notifications.invalid.content": "Jūsu veiktās izmaiņas netika saglabātas. Pirms saglabāšanas, lūdzu, pārliecinieties, vai visi lauki ir derīgi.", - + // "item.edit.metadata.notifications.invalid.title": "Metadata invalid", "item.edit.metadata.notifications.invalid.title": "Metadati nav derīgi", - + // "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": "Materiāls, kurā pašlaik strādājat, ir mainījis cits lietotājs. Jūsu pašreizējās izmaiņas tiek atmestas, lai novērstu konfliktus", - + // "item.edit.metadata.notifications.outdated.title": "Changed outdated", "item.edit.metadata.notifications.outdated.title": "Mainīts novecojis", - + // "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", "item.edit.metadata.notifications.saved.content": "Jūsu veiktās izmaiņas šī materiāla metadatos tika saglabātas.", - + // "item.edit.metadata.notifications.saved.title": "Metadata saved", "item.edit.metadata.notifications.saved.title": "Metadati saglabāti", - + // "item.edit.metadata.reinstate-button": "Undo", "item.edit.metadata.reinstate-button": "Atsaukt", - + // "item.edit.metadata.save-button": "Save", "item.edit.metadata.save-button": "Saglabāt", - - - + + + // "item.edit.modify.overview.field": "Field", "item.edit.modify.overview.field": "Lauks", - + // "item.edit.modify.overview.language": "Language", "item.edit.modify.overview.language": "Valoda", - + // "item.edit.modify.overview.value": "Value", "item.edit.modify.overview.value": "Vērtība", - - - + + + // "item.edit.move.cancel": "Cancel", "item.edit.move.cancel": "Atcelt", - + // "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": "Atlasiet kolekciju, uz kuru vēlaties pārvietot šo materiālu. Lai sašaurinātu parādīto kolekciju sarakstu, lodziņā varat ievadīt meklēšanas vaicājumu.", - + // "item.edit.move.error": "An error occurred when attempting to move the item", "item.edit.move.error": "Mēģinot pārvietot materiālu, radās kļūda", - + // "item.edit.move.head": "Move item: {{id}}", "item.edit.move.head": "Pārvietot materiālu: {{id}}", - + // "item.edit.move.inheritpolicies.checkbox": "Inherit policies", "item.edit.move.inheritpolicies.checkbox": "Mantot nosacījumus", - + // "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", "item.edit.move.inheritpolicies.description": "Mantot mērķa kolekcijas noklusējuma politikas", - + // "item.edit.move.move": "Move", "item.edit.move.move": "Pārvietot", - + // "item.edit.move.processing": "Moving...", "item.edit.move.processing": "Pārvieto...", - + // "item.edit.move.search.placeholder": "Enter a search query to look for collections", "item.edit.move.search.placeholder": "Ievadiet meklēšanas vaicājumu, lai meklētu kolekcijas", - + // "item.edit.move.success": "The item has been moved successfully", "item.edit.move.success": "Materiāls ir veiksmīgi pārvietots", - + // "item.edit.move.title": "Move item", "item.edit.move.title": "Pārvietot materiālu", - - - + + + // "item.edit.private.cancel": "Cancel", "item.edit.private.cancel": "Atcelt", - + // "item.edit.private.confirm": "Make it Private", "item.edit.private.confirm": "Padarīt privātu", - + // "item.edit.private.description": "Are you sure this item should be made private in the archive?", "item.edit.private.description": "Vai esat pārliecināts, ka šim materiālam arhīvā jābūt privātam?", - + // "item.edit.private.error": "An error occurred while making the item private", "item.edit.private.error": "Padarot elementu privātu radās kļūda", - + // "item.edit.private.header": "Make item private: {{ id }}", "item.edit.private.header": "Padarīt materiālu privātu: {{ id }}", - + // "item.edit.private.success": "The item is now private", "item.edit.private.success": "Materiāls tagad ir privāts", - - - + + + // "item.edit.public.cancel": "Cancel", "item.edit.public.cancel": "Atcelt", - + // "item.edit.public.confirm": "Make it Public", "item.edit.public.confirm": "Padarīt Publisku", - + // "item.edit.public.description": "Are you sure this item should be made public in the archive?", "item.edit.public.description": "Vai tiešām vēlaties šo vienumu publiskot arhīvā?", - + // "item.edit.public.error": "An error occurred while making the item public", "item.edit.public.error": "Padarot elementu publisku radās kļūda", - + // "item.edit.public.header": "Make item public: {{ id }}", "item.edit.public.header": "Padarīt materiālu publisku: {{ id }}", - + // "item.edit.public.success": "The item is now public", "item.edit.public.success": "Materiāls tagad ir publisks", - - - + + + // "item.edit.reinstate.cancel": "Cancel", "item.edit.reinstate.cancel": "Atcelt", - + // "item.edit.reinstate.confirm": "Reinstate", "item.edit.reinstate.confirm": "Atjaunot", - + // "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", "item.edit.reinstate.description": "Vai esat pārliecināts, ka šo materiālu nepieciešams atjaunot arhīvā?", - + // "item.edit.reinstate.error": "An error occurred while reinstating the item", "item.edit.reinstate.error": "Atjaunot materiālu, radās kļūda", - + // "item.edit.reinstate.header": "Reinstate item: {{ id }}", "item.edit.reinstate.header": "Atjaunot materiālu: {{ id }}", - + // "item.edit.reinstate.success": "The item was reinstated successfully", "item.edit.reinstate.success": "Materiāls tika veiksmīgi atjaunots", - - - + + + // "item.edit.relationships.discard-button": "Discard", "item.edit.relationships.discard-button": "Atcelt", - + // "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.remove": "Remove", "item.edit.relationships.edit.buttons.remove": "Dzēst", - + // "item.edit.relationships.edit.buttons.undo": "Undo changes", "item.edit.relationships.edit.buttons.undo": "Atsaukt izmaiņas", - + // "item.edit.relationships.no-relationships": "No relationships", // TODO New key - Add a translation "item.edit.relationships.no-relationships": "No relationships", - + // "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.relationships.notifications.discarded.content": "Jūsu veiktās izmaiņas tika atmestas. Lai atjaunotu izmaiņas, noklikšķiniet uz pogas 'Atsaukt'", - + // "item.edit.relationships.notifications.discarded.title": "Changes discarded", "item.edit.relationships.notifications.discarded.title": "Mainīts atmests", - + // "item.edit.relationships.notifications.failed.title": "Error editing relationships", // TODO Source message changed - Revise the translation "item.edit.relationships.notifications.failed.title": "Kļūda dzēšot saikni", - + // "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": "Materiāls, kurā pašlaik strādājat, ir mainījis cits lietotājs. Jūsu pašreizējās izmaiņas tiek atmestas, lai novērstu konfliktus", - + // "item.edit.relationships.notifications.outdated.title": "Changes outdated", "item.edit.relationships.notifications.outdated.title": "Novecojušas izmaiņas", - + // "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", "item.edit.relationships.notifications.saved.content": "Jūsu veiktās izmaiņas šī materiāla saiknēs tika saglabātas.", - + // "item.edit.relationships.notifications.saved.title": "Relationships saved", "item.edit.relationships.notifications.saved.title": "Saiknes saglabātas", - + // "item.edit.relationships.reinstate-button": "Undo", "item.edit.relationships.reinstate-button": "Atsaukt", - + // "item.edit.relationships.save-button": "Save", "item.edit.relationships.save-button": "Saglabāt", - + // "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.tabs.bitstreams.head": "Bitstreams", "item.edit.tabs.bitstreams.head": "Bitu straume", - + // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", "item.edit.tabs.bitstreams.title": "Rediģēt Materiālu - Bitu straumes", - + // "item.edit.tabs.curate.head": "Curate", "item.edit.tabs.curate.head": "Pārvaldīt", - + // "item.edit.tabs.curate.title": "Item Edit - Curate", "item.edit.tabs.curate.title": "Rediģēt Materiālu - Pārvaldība", - + // "item.edit.tabs.metadata.head": "Metadata", "item.edit.tabs.metadata.head": "Metadati", - + // "item.edit.tabs.metadata.title": "Item Edit - Metadata", "item.edit.tabs.metadata.title": "Rediģēt Materiālu - Metadati", - + // "item.edit.tabs.relationships.head": "Relationships", "item.edit.tabs.relationships.head": "Attiecības", - + // "item.edit.tabs.relationships.title": "Item Edit - Relationships", "item.edit.tabs.relationships.title": "Rediģēt Materiālu - Attiecības", - + // "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", "item.edit.tabs.status.buttons.authorizations.button": "Tiesības...", - + // "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", "item.edit.tabs.status.buttons.authorizations.label": "Rediģēt materiāla tiesības", - + // "item.edit.tabs.status.buttons.delete.button": "Permanently delete", "item.edit.tabs.status.buttons.delete.button": "Neatgriezeniski izdzēst", - + // "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", "item.edit.tabs.status.buttons.delete.label": "Pilnīgi izņemt materiālu", - + // "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", "item.edit.tabs.status.buttons.mappedCollections.button": "Piesaistītās kolekcijas", - + // "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", "item.edit.tabs.status.buttons.mappedCollections.label": "Pārvaldīt piesaistītās kolekcijas", - + // "item.edit.tabs.status.buttons.move.button": "Move...", "item.edit.tabs.status.buttons.move.button": "Pārvieto...", - + // "item.edit.tabs.status.buttons.move.label": "Move item to another collection", "item.edit.tabs.status.buttons.move.label": "Pārvietot materiālu uz citu kolekciju", - + // "item.edit.tabs.status.buttons.private.button": "Make it private...", "item.edit.tabs.status.buttons.private.button": "Padarīt to privātu...", - + // "item.edit.tabs.status.buttons.private.label": "Make item private", "item.edit.tabs.status.buttons.private.label": "Padarīt materiālu privātu", - + // "item.edit.tabs.status.buttons.public.button": "Make it public...", "item.edit.tabs.status.buttons.public.button": "Padarīt to publisku...", - + // "item.edit.tabs.status.buttons.public.label": "Make item public", "item.edit.tabs.status.buttons.public.label": "Padarīt materiālu publisku", - + // "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", "item.edit.tabs.status.buttons.reinstate.button": "Atjaunot...", - + // "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", "item.edit.tabs.status.buttons.reinstate.label": "Atjaunot materiālu repozitorijā", - + // "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", "item.edit.tabs.status.buttons.withdraw.button": "Atsaukt...", - + // "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", "item.edit.tabs.status.buttons.withdraw.label": "Izņemiet materiālu no repozitorijas", - + // "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": "Laipni lūdzam vienumu pārvaldības lapā. Šeit jūs varat materiālu izņemt, atjaunot, pārvietot vai izdzēst. Citās cilnēs varat arī atjaunināt vai pievienot jaunus metadatus / bitu straumes.", - + // "item.edit.tabs.status.head": "Status", "item.edit.tabs.status.head": "Status", - + // "item.edit.tabs.status.labels.handle": "Handle", "item.edit.tabs.status.labels.handle": "Apstrāde", - + // "item.edit.tabs.status.labels.id": "Item Internal ID", "item.edit.tabs.status.labels.id": "Materiāla Iekšējais ID", - + // "item.edit.tabs.status.labels.itemPage": "Item Page", "item.edit.tabs.status.labels.itemPage": "Materiāla Lapa", - + // "item.edit.tabs.status.labels.lastModified": "Last Modified", "item.edit.tabs.status.labels.lastModified": "Pēdējo reizi modificēts", - + // "item.edit.tabs.status.title": "Item Edit - Status", "item.edit.tabs.status.title": "Rediģēt Materiālu - Statuss", - + // "item.edit.tabs.versionhistory.head": "Version History", "item.edit.tabs.versionhistory.head": "Versiju Vēsture", - + // "item.edit.tabs.versionhistory.title": "Item Edit - Version History", "item.edit.tabs.versionhistory.title": "Rediģēt Materiālu - Versiju Vēsture", - + // "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": "Šajā lietotāja saskarnē vēl nav iespējams ienākt vai pievienot jaunas versijas.", - + // "item.edit.tabs.view.head": "View Item", "item.edit.tabs.view.head": "Skatīt Materiālu", - + // "item.edit.tabs.view.title": "Item Edit - View", "item.edit.tabs.view.title": "Materiāla Rediģēšana - Skats", - - - + + + // "item.edit.withdraw.cancel": "Cancel", "item.edit.withdraw.cancel": "Atcelt", - + // "item.edit.withdraw.confirm": "Withdraw", "item.edit.withdraw.confirm": "Atsaukt", - + // "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", "item.edit.withdraw.description": "Vai esat pārliecināts, ka šo materiālu vajadzētu izņemt no arhīva?", - + // "item.edit.withdraw.error": "An error occurred while withdrawing the item", "item.edit.withdraw.error": "Izņemot materiālu, radās kļūda", - + // "item.edit.withdraw.header": "Withdraw item: {{ id }}", "item.edit.withdraw.header": "Izņemt materiālu: {{ id }}", - + // "item.edit.withdraw.success": "The item was withdrawn successfully", "item.edit.withdraw.success": "Materiāls tika veiksmīgi izņemts", - - - + + + // "item.listelement.badge": "Item", // TODO New key - Add a translation "item.listelement.badge": "Item", - + // "item.page.description": "Description", // TODO New key - Add a translation "item.page.description": "Description", - + // "item.page.edit": "Edit this item", // TODO New key - Add a translation "item.page.edit": "Edit this item", - + // "item.page.journal-issn": "Journal ISSN", // TODO New key - Add a translation "item.page.journal-issn": "Journal ISSN", - + // "item.page.journal-title": "Journal Title", // TODO New key - Add a translation "item.page.journal-title": "Journal Title", - + // "item.page.publisher": "Publisher", // TODO New key - Add a translation "item.page.publisher": "Publisher", - + // "item.page.titleprefix": "Item: ", // TODO New key - Add a translation "item.page.titleprefix": "Item: ", - + // "item.page.volume-title": "Volume Title", // TODO New key - Add a translation "item.page.volume-title": "Volume Title", - + // "item.search.results.head": "Item Search Results", // TODO New key - Add a translation "item.search.results.head": "Item Search Results", - + // "item.search.title": "DSpace Angular :: Item Search", // TODO New key - Add a translation "item.search.title": "DSpace Angular :: Item Search", - - - + + + // "item.page.abstract": "Abstract", "item.page.abstract": "Kopsavilkums", - + // "item.page.author": "Authors", "item.page.author": "Autori", - + // "item.page.citation": "Citation", "item.page.citation": "Citēšana", - + // "item.page.collections": "Collections", "item.page.collections": "Kolekcijas", - + // "item.page.date": "Date", "item.page.date": "Datums", - + // "item.page.edit": "Edit this item", // TODO New key - Add a translation "item.page.edit": "Edit this item", - + // "item.page.files": "Files", "item.page.files": "Faili", - + // "item.page.filesection.description": "Description:", "item.page.filesection.description": "Apraksts:", - + // "item.page.filesection.download": "Download", "item.page.filesection.download": "Lejupielādēt", - + // "item.page.filesection.format": "Format:", "item.page.filesection.format": "Formāts:", - + // "item.page.filesection.name": "Name:", "item.page.filesection.name": "Nosaukums:", - + // "item.page.filesection.size": "Size:", "item.page.filesection.size": "Izmērs:", - + // "item.page.journal.search.title": "Articles in this journal", "item.page.journal.search.title": "Raksti šajā žurnālā", - + // "item.page.link.full": "Full item page", "item.page.link.full": "Pilna materiālu lapa", - + // "item.page.link.simple": "Simple item page", "item.page.link.simple": "Vienkārša materiālu lapa", - + // "item.page.person.search.title": "Articles by this author", "item.page.person.search.title": "Šī autora raksti", - + // "item.page.related-items.view-more": "Show {{ amount }} more", "item.page.related-items.view-more": "Parādīt {{ amount }} vairāk", - + // "item.page.related-items.view-less": "Hide last {{ amount }}", "item.page.related-items.view-less": "Paslēpt pēdējo {{ amount }}", - + // "item.page.relationships.isAuthorOfPublication": "Publications", "item.page.relationships.isAuthorOfPublication": "Publikācijas", - + // "item.page.relationships.isJournalOfPublication": "Publications", "item.page.relationships.isJournalOfPublication": "Publikācijas", - + // "item.page.relationships.isOrgUnitOfPerson": "Authors", "item.page.relationships.isOrgUnitOfPerson": "Autori", - + // "item.page.relationships.isOrgUnitOfProject": "Research Projects", "item.page.relationships.isOrgUnitOfProject": "Pētniecības projekti", - + // "item.page.subject": "Keywords", "item.page.subject": "Atslēgas vārdi", - + // "item.page.uri": "URI", "item.page.uri": "URI", - + // "item.page.bitstreams.view-more": "Show more", // TODO New key - Add a translation "item.page.bitstreams.view-more": "Show more", - + // "item.page.bitstreams.collapse": "Collapse", // TODO New key - Add a translation "item.page.bitstreams.collapse": "Collapse", - + // "item.page.filesection.original.bundle" : "Original bundle", // TODO New key - Add a translation "item.page.filesection.original.bundle" : "Original bundle", - + // "item.page.filesection.license.bundle" : "License bundle", // TODO New key - Add a translation "item.page.filesection.license.bundle" : "License bundle", - + // "item.preview.dc.identifier.uri": "Identifier:", // TODO New key - Add a translation "item.preview.dc.identifier.uri": "Identifier:", - + // "item.preview.dc.contributor.author": "Authors:", // TODO New key - Add a translation "item.preview.dc.contributor.author": "Authors:", - + // "item.preview.dc.date.issued": "Published date:", // TODO New key - Add a translation "item.preview.dc.date.issued": "Published date:", - + // "item.preview.dc.description.abstract": "Abstract:", // TODO New key - Add a translation "item.preview.dc.description.abstract": "Abstract:", - + // "item.preview.dc.identifier.other": "Other identifier:", // TODO New key - Add a translation "item.preview.dc.identifier.other": "Other identifier:", - + // "item.preview.dc.language.iso": "Language:", // TODO New key - Add a translation "item.preview.dc.language.iso": "Language:", - + // "item.preview.dc.subject": "Subjects:", // TODO New key - Add a translation "item.preview.dc.subject": "Subjects:", - + // "item.preview.dc.title": "Title:", // TODO New key - Add a translation "item.preview.dc.title": "Title:", - + // "item.preview.person.familyName": "Surname:", // TODO New key - Add a translation "item.preview.person.familyName": "Surname:", - + // "item.preview.person.givenName": "Name:", // TODO New key - Add a translation "item.preview.person.givenName": "Name:", - + // "item.preview.person.identifier.orcid": "ORCID:", // TODO New key - Add a translation "item.preview.person.identifier.orcid": "ORCID:", - - + + // "item.select.confirm": "Confirm selected", "item.select.confirm": "Apstiprināt izvēlētos", - + // "item.select.empty": "No items to show", "item.select.empty": "Ieraksti nav atrasti", - + // "item.select.table.author": "Author", "item.select.table.author": "Autors", - + // "item.select.table.collection": "Collection", "item.select.table.collection": "Kolekcija", - + // "item.select.table.title": "Title", "item.select.table.title": "Nosaukums", - - + + // "item.version.history.empty": "There are no other versions for this item yet.", "item.version.history.empty": "Šim materiālam vēl nav citu versiju.", - + // "item.version.history.head": "Version History", "item.version.history.head": "Versiju Vēsture", - + // "item.version.history.return": "Return", "item.version.history.return": "Atgriezties", - + // "item.version.history.selected": "Selected version", "item.version.history.selected": "Izvēlētā versija", - + // "item.version.history.table.version": "Version", "item.version.history.table.version": "Versija", - + // "item.version.history.table.item": "Item", "item.version.history.table.item": "Materiāls", - + // "item.version.history.table.editor": "Editor", "item.version.history.table.editor": "Redaktors", - + // "item.version.history.table.date": "Date", "item.version.history.table.date": "Datums", - + // "item.version.history.table.summary": "Summary", "item.version.history.table.summary": "Kopsavilkums", - - - + + + // "item.version.notice": "This is not the latest version of this item. The latest version can be found here.", "item.version.notice": "Šī nav jaunākā šī materiāla versija. Jaunāko versiju var atrast here.", - - - + + + // "journal.listelement.badge": "Journal", "journal.listelement.badge": "Žurnāls", - + // "journal.page.description": "Description", "journal.page.description": "Apraksts", - + // "journal.page.edit": "Edit this item", // TODO New key - Add a translation "journal.page.edit": "Edit this item", - + // "journal.page.editor": "Editor-in-Chief", "journal.page.editor": "Galvenais Radaktors", - + // "journal.page.issn": "ISSN", "journal.page.issn": "ISSN", - + // "journal.page.publisher": "Publisher", "journal.page.publisher": "Izdevējs", - + // "journal.page.titleprefix": "Journal: ", "journal.page.titleprefix": "Žurnāls: ", - + // "journal.search.results.head": "Journal Search Results", "journal.search.results.head": "Žurnālu meklēšanas rezultāti", - + // "journal.search.title": "DSpace Angular :: Journal Search", "journal.search.title": "DSpace Angular :: Žurnālu Meklēšana", - - - + + + // "journalissue.listelement.badge": "Journal Issue", "journalissue.listelement.badge": "Žurnāla izdevums", - + // "journalissue.page.description": "Description", "journalissue.page.description": "Apraksts", - + // "journalissue.page.edit": "Edit this item", // TODO New key - Add a translation "journalissue.page.edit": "Edit this item", - + // "journalissue.page.issuedate": "Issue Date", "journalissue.page.issuedate": "Izdošanas Datums", - + // "journalissue.page.journal-issn": "Journal ISSN", "journalissue.page.journal-issn": "Žurnāla ISSN", - + // "journalissue.page.journal-title": "Journal Title", "journalissue.page.journal-title": "Žurnāla nosaukums", - + // "journalissue.page.keyword": "Keywords", "journalissue.page.keyword": "Atslēgas vārdi", - + // "journalissue.page.number": "Number", "journalissue.page.number": "Numurs", - + // "journalissue.page.titleprefix": "Journal Issue: ", "journalissue.page.titleprefix": "Žurnāla izdevums: ", - - - + + + // "journalvolume.listelement.badge": "Journal Volume", "journalvolume.listelement.badge": "Žurnāla sējums", - + // "journalvolume.page.description": "Description", "journalvolume.page.description": "Apraksts", - + // "journalvolume.page.edit": "Edit this item", // TODO New key - Add a translation "journalvolume.page.edit": "Edit this item", - + // "journalvolume.page.issuedate": "Issue Date", "journalvolume.page.issuedate": "Izdošanas Datums", - + // "journalvolume.page.titleprefix": "Journal Volume: ", "journalvolume.page.titleprefix": "Žurnāla sējums: ", - + // "journalvolume.page.volume": "Volume", "journalvolume.page.volume": "Sējums", - - - + + + // "loading.bitstream": "Loading bitstream...", "loading.bitstream": "Notiek bitu straumes ielāde...", - + // "loading.bitstreams": "Loading bitstreams...", "loading.bitstreams": "Notiek bitu straumes ielāde...", - + // "loading.browse-by": "Loading items...", "loading.browse-by": "Notiek materiālu ielāde...", - + // "loading.browse-by-page": "Loading page...", "loading.browse-by-page": "Notiek lapas ielāde...", - + // "loading.collection": "Loading collection...", "loading.collection": "Notiek kolekcijas ielāde...", - + // "loading.collections": "Loading collections...", "loading.collections": "Notiek kolekciju ielāde...", - + // "loading.content-source": "Loading content source...", "loading.content-source": "Notiek satura avota ielāde...", - + // "loading.community": "Loading community...", "loading.community": "Notiek kategoriju ielāde...", - + // "loading.default": "Loading...", "loading.default": "Notiek ielāde...", - + // "loading.item": "Loading item...", "loading.item": "Notiek materiāla ielāde...", - + // "loading.items": "Loading items...", "loading.items": "Notiek materiālu ielāde...", - + // "loading.mydspace-results": "Loading items...", "loading.mydspace-results": "Notiek materiālu ielāde..", - + // "loading.objects": "Loading...", "loading.objects": "Notiek ielāde...", - + // "loading.recent-submissions": "Loading recent submissions...", "loading.recent-submissions": "Notiek neseno iesniegumu ielāde...", - + // "loading.search-results": "Loading search results...", "loading.search-results": "Notiek maklēšanas rezulātu ielāde...", - + // "loading.sub-collections": "Loading sub-collections...", "loading.sub-collections": "Notiek apakškolekciju ielāde...", - + // "loading.sub-communities": "Loading sub-communities...", "loading.sub-communities": "Notiek apakškategoriju ielāde...", - + // "loading.top-level-communities": "Loading top-level communities...", "loading.top-level-communities": "Notiek augstākā līmeņa kategorju ielāde...", - - - + + + // "login.form.email": "Email address", "login.form.email": "E-pasta adrese", - + // "login.form.forgot-password": "Have you forgotten your password?", "login.form.forgot-password": "Vai esat aizmirsis paroli?", - + // "login.form.header": "Please log in to DSpace", "login.form.header": "Lūdzu pieslēdzieties DSpace", - + // "login.form.new-user": "New user? Click here to register.", "login.form.new-user": "Jauns lietotājs? Noklikšķiniet šeit, lai reģistrētos.", - + // "login.form.or-divider": "or", "login.form.or-divider": "vai", - + // "login.form.password": "Password", "login.form.password": "Parole", - + // "login.form.shibboleth": "Log in with Shibboleth", "login.form.shibboleth": "Pieslēgties ar Shibboleth", - + // "login.form.submit": "Log in", "login.form.submit": "Pieslēgties", - + // "login.title": "Login", "login.title": "Pierakstīties", - + // "login.breadcrumbs": "Login", "login.breadcrumbs": "Pierakstīties", - - - + + + // "logout.form.header": "Log out from DSpace", "logout.form.header": "Izrakstīties no DSpace", - + // "logout.form.submit": "Log out", "logout.form.submit": "Izrakstīties", - + // "logout.title": "Logout", "logout.title": "Izrakstīties", - - - + + + // "menu.header.admin": "Admin", "menu.header.admin": "Administrators", - + // "menu.header.image.logo": "Repository logo", "menu.header.image.logo": "Repozitorijas logotips", - - - + + + // "menu.section.access_control": "Access Control", "menu.section.access_control": "Piekļuves kontrole", - + // "menu.section.access_control_authorizations": "Authorizations", "menu.section.access_control_authorizations": "Pilnvaras", - + // "menu.section.access_control_groups": "Groups", "menu.section.access_control_groups": "Grupas", - + // "menu.section.access_control_people": "People", "menu.section.access_control_people": "Personas", - - - + + + // "menu.section.admin_search": "Admin Search", "menu.section.admin_search": "Administratora Meklēšana", - - - + + + // "menu.section.browse_community": "This Community", "menu.section.browse_community": "Šī kategorija", - + // "menu.section.browse_community_by_author": "By Author", "menu.section.browse_community_by_author": "Pēc Autora", - + // "menu.section.browse_community_by_issue_date": "By Issue Date", "menu.section.browse_community_by_issue_date": "Pēc Izdošanas Datuma", - + // "menu.section.browse_community_by_title": "By Title", "menu.section.browse_community_by_title": "Pēc Nosaukuma", - + // "menu.section.browse_global": "All of DSpace", "menu.section.browse_global": "Viss no DSpace", - + // "menu.section.browse_global_by_author": "By Author", "menu.section.browse_global_by_author": "Pēc Autora", - + // "menu.section.browse_global_by_dateissued": "By Issue Date", "menu.section.browse_global_by_dateissued": "Pēc Izdošanas Datuma", - + // "menu.section.browse_global_by_subject": "By Subject", "menu.section.browse_global_by_subject": "Pēc Priekšmeta", - + // "menu.section.browse_global_by_title": "By Title", "menu.section.browse_global_by_title": "Pēc Nosaukuma", - + // "menu.section.browse_global_communities_and_collections": "Communities & Collections", "menu.section.browse_global_communities_and_collections": "Kategorijas & Kolekcijas", - - - + + + // "menu.section.control_panel": "Control Panel", "menu.section.control_panel": "Vadības Panelis", - + // "menu.section.curation_task": "Curation Task", "menu.section.curation_task": "Kuratora Uzdevums", - - - + + + // "menu.section.edit": "Edit", "menu.section.edit": "Rediģēt", - + // "menu.section.edit_collection": "Collection", "menu.section.edit_collection": "Kolekcija", - + // "menu.section.edit_community": "Community", "menu.section.edit_community": "Kategorija", - + // "menu.section.edit_item": "Item", "menu.section.edit_item": "Materiāls", - - - + + + // "menu.section.export": "Export", "menu.section.export": "Eksportēt", - + // "menu.section.export_collection": "Collection", "menu.section.export_collection": "Kolekcija", - + // "menu.section.export_community": "Community", "menu.section.export_community": "Kategorija", - + // "menu.section.export_item": "Item", "menu.section.export_item": "Materiāls", - + // "menu.section.export_metadata": "Metadata", "menu.section.export_metadata": "Metadati", - - - + + + // "menu.section.icon.access_control": "Access Control menu section", "menu.section.icon.access_control": "Piekļuves kontroles izvēlnes sadaļa", - + // "menu.section.icon.admin_search": "Admin search menu section", "menu.section.icon.admin_search": "Administratoru meklēšanas izvēlnes sadaļa", - + // "menu.section.icon.control_panel": "Control Panel menu section", "menu.section.icon.control_panel": "Vadības paneļa izvēlnes sadaļa", - + // "menu.section.icon.curation_task": "Curation Task menu section", "menu.section.icon.curation_task": "Kuratora uzdevumu izvēlnes sadaļa", - + // "menu.section.icon.edit": "Edit menu section", "menu.section.icon.edit": "Labot izvēlnes sadaļu", - + // "menu.section.icon.export": "Export menu section", "menu.section.icon.export": "Eksportēt izvēlnes sadaļu", - + // "menu.section.icon.find": "Find menu section", "menu.section.icon.find": "Atrast izvēlnes sadaļu", - + // "menu.section.icon.import": "Import menu section", "menu.section.icon.import": "Importēt izvēlnes sadaļa", - + // "menu.section.icon.new": "New menu section", "menu.section.icon.new": "Jauna izvēlnes sadaļa", - + // "menu.section.icon.pin": "Pin sidebar", "menu.section.icon.pin": "Piespraust sānjoslu", - + // "menu.section.icon.processes": "Processes menu section", // TODO New key - Add a translation "menu.section.icon.processes": "Processes menu section", - + // "menu.section.icon.registries": "Registries menu section", "menu.section.icon.registries": "Reģistru izvēlnes sadaļa", - + // "menu.section.icon.statistics_task": "Statistics Task menu section", "menu.section.icon.statistics_task": "Statistkas uzdevumu izvēlnes sadaļa", - + // "menu.section.icon.unpin": "Unpin sidebar", "menu.section.icon.unpin": "Atspraust sānjoslu", - - - + + + // "menu.section.import": "Import", "menu.section.import": "Importēt", - + // "menu.section.import_batch": "Batch Import (ZIP)", "menu.section.import_batch": "Importēt (ZIP)", - + // "menu.section.import_metadata": "Metadata", "menu.section.import_metadata": "Metadati", - - - + + + // "menu.section.new": "New", "menu.section.new": "Jauns", - + // "menu.section.new_collection": "Collection", "menu.section.new_collection": "Kolekcija", - + // "menu.section.new_community": "Community", "menu.section.new_community": "Kategorija", - + // "menu.section.new_item": "Item", "menu.section.new_item": "Materiāls", - + // "menu.section.new_item_version": "Item Version", "menu.section.new_item_version": "Materiāla Versija", - + // "menu.section.new_process": "Process", // TODO New key - Add a translation "menu.section.new_process": "Process", - - - + + + // "menu.section.pin": "Pin sidebar", "menu.section.pin": "Piespraust sānjoslu", - + // "menu.section.unpin": "Unpin sidebar", "menu.section.unpin": "Atspraust sānjoslu", - - - + + + // "menu.section.processes": "Processes", "menu.section.processes": "Procesi", - - - + + + // "menu.section.registries": "Registries", "menu.section.registries": "Reģistri", - + // "menu.section.registries_format": "Format", "menu.section.registries_format": "Formāts", - + // "menu.section.registries_metadata": "Metadata", "menu.section.registries_metadata": "Metadati", - - - + + + // "menu.section.statistics": "Statistics", "menu.section.statistics": "Statistika", - + // "menu.section.statistics_task": "Statistics Task", "menu.section.statistics_task": "Statistikas Uzdevumi", - - - + + + // "menu.section.toggle.access_control": "Toggle Access Control section", "menu.section.toggle.access_control": "Pārslēgt Piekļuvas Kontronles sadaļu", - + // "menu.section.toggle.control_panel": "Toggle Control Panel section", "menu.section.toggle.control_panel": "Pārslēgt Vadības Paneļa sadaļu", - + // "menu.section.toggle.curation_task": "Toggle Curation Task section", "menu.section.toggle.curation_task": "Pārslēgt Kuratora Uzdevumu sadaļu", - + // "menu.section.toggle.edit": "Toggle Edit section", "menu.section.toggle.edit": "Pārslēgt Rediģēt sadaļu", - + // "menu.section.toggle.export": "Toggle Export section", "menu.section.toggle.export": "Pārslēgt Eksportēt sadaļu", - + // "menu.section.toggle.find": "Toggle Find section", "menu.section.toggle.find": "Pārslēgt Meklēt sadaļu", - + // "menu.section.toggle.import": "Toggle Import section", "menu.section.toggle.import": "Pārslēgt Importēt sadaļu", - + // "menu.section.toggle.new": "Toggle New section", "menu.section.toggle.new": "Pārslēgt Jauns sadaļu", - + // "menu.section.toggle.registries": "Toggle Registries section", "menu.section.toggle.registries": "Pārslēgt Reģistru sadaļu", - + // "menu.section.toggle.statistics_task": "Toggle Statistics Task section", "menu.section.toggle.statistics_task": "Pārslēgt Statistikas Uzdevumu sadaļu", - - + + // "menu.section.workflow": "Administer Workflow", // TODO Source message changed - Revise the translation "menu.section.workflow": "Administrēt darba plūsmu", - - + + // "mydspace.description": "", "mydspace.description": "", - + // "mydspace.general.text-here": "here", // TODO Source message changed - Revise the translation "mydspace.general.text-here": "ŠEIT", - + // "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", "mydspace.messages.controller-help": "Atlasiet šo opciju, lai nosūtītu materiāla objekta iesniedzējam.", - + // "mydspace.messages.description-placeholder": "Insert your message here...", "mydspace.messages.description-placeholder": "Ievietojiet savu ziņojumu šeit ...", - + // "mydspace.messages.hide-msg": "Hide message", "mydspace.messages.hide-msg": "Paslēpt ziņojumu", - + // "mydspace.messages.mark-as-read": "Mark as read", "mydspace.messages.mark-as-read": "Atzīmēt kā lasītu", - + // "mydspace.messages.mark-as-unread": "Mark as unread", "mydspace.messages.mark-as-unread": "Atzīmēt kā nelasītu", - + // "mydspace.messages.no-content": "No content.", "mydspace.messages.no-content": "Nav satura.", - + // "mydspace.messages.no-messages": "No messages yet.", "mydspace.messages.no-messages": "Nav paziņojumu.", - + // "mydspace.messages.send-btn": "Send", "mydspace.messages.send-btn": "Sūtīt", - + // "mydspace.messages.show-msg": "Show message", "mydspace.messages.show-msg": "Parādīt ziņu", - + // "mydspace.messages.subject-placeholder": "Subject...", "mydspace.messages.subject-placeholder": "Priekšmets...", - + // "mydspace.messages.submitter-help": "Select this option to send a message to controller.", "mydspace.messages.submitter-help": "Atlasiet šo opciju, lai nosūtītu ziņojumu kontrolierim.", - + // "mydspace.messages.title": "Messages", "mydspace.messages.title": "Vēstules", - + // "mydspace.messages.to": "To", "mydspace.messages.to": "Uz", - + // "mydspace.new-submission": "New submission", "mydspace.new-submission": "Jauns iesniegums", - + // "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-short": "Import metadata", // TODO New key - Add a translation "mydspace.new-submission-external-short": "Import metadata", - + // "mydspace.results.head": "Your submissions", "mydspace.results.head": "Jūsu iesniegumi", - + // "mydspace.results.no-abstract": "No Abstract", "mydspace.results.no-abstract": "Nav Kopsavilkums", - + // "mydspace.results.no-authors": "No Authors", "mydspace.results.no-authors": "Nav Autoru", - + // "mydspace.results.no-collections": "No Collections", "mydspace.results.no-collections": "Nav Kolekcijas", - + // "mydspace.results.no-date": "No Date", "mydspace.results.no-date": "Nav Datuma", - + // "mydspace.results.no-files": "No Files", "mydspace.results.no-files": "Nav Failu", - + // "mydspace.results.no-results": "There were no items to show", "mydspace.results.no-results": "Nav materiālu, ko parādīt", - + // "mydspace.results.no-title": "No title", "mydspace.results.no-title": "Nav Nosaukuma", - + // "mydspace.results.no-uri": "No Uri", "mydspace.results.no-uri": "Nav Uri", - + // "mydspace.show.workflow": "All tasks", "mydspace.show.workflow": "Visi uzdevumi", - + // "mydspace.show.workspace": "Your Submissions", "mydspace.show.workspace": "Jūsu Iesniegumi", - + // "mydspace.status.archived": "Archived", "mydspace.status.archived": "Arhivēts", - + // "mydspace.status.validation": "Validation", "mydspace.status.validation": "Validācija", - + // "mydspace.status.waiting-for-controller": "Waiting for controller", "mydspace.status.waiting-for-controller": "Gaida kontrolieri", - + // "mydspace.status.workflow": "Workflow", "mydspace.status.workflow": "Darba plūsma", - + // "mydspace.status.workspace": "Workspace", "mydspace.status.workspace": "Darbavieta", - + // "mydspace.title": "MyDSpace", "mydspace.title": "Mans DSpace", - + // "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", "mydspace.upload.upload-failed": "Veidojot jaunu darbvietu, radās kļūda. Lūdzu pārbaudiet augšupielādēto saturu pirms mēģiniet vēlreiz.", - + // "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-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-multiple-successful": "{{qty}} new workspace items created.", "mydspace.upload.upload-multiple-successful": "{{qty}} izveidoti jauni darbvietas materiāli.", - + // "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", "mydspace.upload.upload-successful": "Ir izveidots jauns darbvietas materiāls. Noklikšķiniet {{here}}, lai to rediģētu.", - + // "mydspace.view-btn": "View", "mydspace.view-btn": "Skatīt", - - - + + + // "nav.browse.header": "All of DSpace", "nav.browse.header": "Viss no DSpace", - + // "nav.community-browse.header": "By Community", "nav.community-browse.header": "Pēc Kategorijas", - + // "nav.language": "Language switch", "nav.language": "Valodas maiņa", - + // "nav.login": "Log In", "nav.login": "Pieslēgties", - + // "nav.logout": "Log Out", "nav.logout": "Izrakstīties", - + // "nav.mydspace": "MyDSpace", "nav.mydspace": "Mans DSpace", - + // "nav.profile": "Profile", "nav.profile": "Profils", - + // "nav.search": "Search", "nav.search": "Meklēt", - + // "nav.statistics.header": "Statistics", "nav.statistics.header": "Statistika", - + // "nav.stop-impersonating": "Stop impersonating EPerson", // TODO New key - Add a translation "nav.stop-impersonating": "Stop impersonating EPerson", - - - + + + // "orgunit.listelement.badge": "Organizational Unit", "orgunit.listelement.badge": "Struktūrvienība", - + // "orgunit.page.city": "City", "orgunit.page.city": "Pilsēta", - + // "orgunit.page.country": "Country", "orgunit.page.country": "Valsts", - + // "orgunit.page.dateestablished": "Date established", "orgunit.page.dateestablished": "Dibināšanas datums", - + // "orgunit.page.description": "Description", "orgunit.page.description": "Apraksts", - + // "orgunit.page.edit": "Edit this item", // TODO New key - Add a translation "orgunit.page.edit": "Edit this item", - + // "orgunit.page.id": "ID", "orgunit.page.id": "ID", - + // "orgunit.page.titleprefix": "Organizational Unit: ", "orgunit.page.titleprefix": "Struktūrvienība: ", - - - + + + // "pagination.results-per-page": "Results Per Page", "pagination.results-per-page": "Rezultāti vienā lapā", - + // "pagination.showing.detail": "{{ range }} of {{ total }}", "pagination.showing.detail": "{{ range }} no {{ total }}", - + // "pagination.showing.label": "Now showing ", "pagination.showing.label": "Tagad rāda ", - + // "pagination.sort-direction": "Sort Options", "pagination.sort-direction": "Kārtošanas iespējas", - - - + + + // "person.listelement.badge": "Person", "person.listelement.badge": "Persona", - + // "person.listelement.no-title": "No name found", // TODO New key - Add a translation "person.listelement.no-title": "No name found", - + // "person.page.birthdate": "Birth Date", "person.page.birthdate": "Dzimšanas datums", - + // "person.page.edit": "Edit this item", // TODO New key - Add a translation "person.page.edit": "Edit this item", - + // "person.page.email": "Email Address", "person.page.email": "E-pasta adrese", - + // "person.page.firstname": "First Name", "person.page.firstname": "Vārds", - + // "person.page.jobtitle": "Job Title", "person.page.jobtitle": "Ieņemamais amats", - + // "person.page.lastname": "Last Name", "person.page.lastname": "Uzvārds", - + // "person.page.link.full": "Show all metadata", "person.page.link.full": "Parādīt visus metadatus", - + // "person.page.orcid": "ORCID", "person.page.orcid": "ORCID", - + // "person.page.staffid": "Staff ID", "person.page.staffid": "Personāla ID", - + // "person.page.titleprefix": "Person: ", "person.page.titleprefix": "Persona: ", - + // "person.search.results.head": "Person Search Results", "person.search.results.head": "Personas meklēšanas rezultāti", - + // "person.search.title": "DSpace Angular :: Person Search", "person.search.title": "DSpace Angular :: Personas Meklēšana", - - - + + + // "process.new.select-parameters": "Parameters", // TODO New key - Add a translation "process.new.select-parameters": "Parameters", - + // "process.new.cancel": "Cancel", // TODO New key - Add a translation "process.new.cancel": "Cancel", - + // "process.new.submit": "Submit", // TODO New key - Add a translation "process.new.submit": "Submit", - + // "process.new.select-script": "Script", // TODO New key - Add a translation "process.new.select-script": "Script", - + // "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.required": "Script is required", // TODO New key - Add a translation "process.new.select-script.required": "Script is required", - + // "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.required": "Please select a file", // TODO New key - Add a translation "process.new.parameter.file.required": "Please select a file", - + // "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.type.value": "value", // TODO New key - Add a translation "process.new.parameter.type.value": "value", - + // "process.new.parameter.type.file": "file", // TODO New key - Add a translation "process.new.parameter.type.file": "file", - + // "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.notification.success.title": "Success", // TODO New key - Add a translation "process.new.notification.success.title": "Success", - + // "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.error.title": "Error", // TODO New key - Add a translation "process.new.notification.error.title": "Error", - + // "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.header": "Create a new process", // TODO New key - Add a translation "process.new.header": "Create a new process", - + // "process.new.title": "Create a new process", // TODO New key - Add a translation "process.new.title": "Create a new process", - + // "process.new.breadcrumbs": "Create a new process", // TODO New key - Add a translation "process.new.breadcrumbs": "Create a new process", - - - + + + // "process.detail.arguments" : "Arguments", // TODO New key - Add a translation "process.detail.arguments" : "Arguments", - + // "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.back" : "Back", // TODO New key - Add a translation "process.detail.back" : "Back", - + // "process.detail.output" : "Process Output", // TODO New key - Add a translation "process.detail.output" : "Process Output", - + // "process.detail.logs.button": "Retrieve process output", // TODO New key - Add a translation "process.detail.logs.button": "Retrieve process output", - + // "process.detail.logs.loading": "Retrieving", // TODO New key - Add a translation "process.detail.logs.loading": "Retrieving", - + // "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.output-files" : "Output Files", // TODO New key - Add a translation "process.detail.output-files" : "Output Files", - + // "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.script" : "Script", // TODO New key - Add a translation "process.detail.script" : "Script", - + // "process.detail.title" : "Process: {{ id }} - {{ name }}", // TODO New key - Add a translation "process.detail.title" : "Process: {{ id }} - {{ name }}", - + // "process.detail.start-time" : "Start time", // TODO New key - Add a translation "process.detail.start-time" : "Start time", - + // "process.detail.end-time" : "Finish time", // TODO New key - Add a translation "process.detail.end-time" : "Finish time", - + // "process.detail.status" : "Status", // TODO New key - Add a translation "process.detail.status" : "Status", - + // "process.detail.create" : "Create similar process", // TODO New key - Add a translation "process.detail.create" : "Create similar process", - - - + + + // "process.overview.table.finish" : "Finish time", // TODO New key - Add a translation "process.overview.table.finish" : "Finish time", - + // "process.overview.table.id" : "Process ID", // TODO New key - Add a translation "process.overview.table.id" : "Process ID", - + // "process.overview.table.name" : "Name", // TODO New key - Add a translation "process.overview.table.name" : "Name", - + // "process.overview.table.start" : "Start time", // TODO New key - Add a translation "process.overview.table.start" : "Start time", - + // "process.overview.table.status" : "Status", // TODO New key - Add a translation "process.overview.table.status" : "Status", - + // "process.overview.table.user" : "User", // TODO New key - Add a translation "process.overview.table.user" : "User", - + // "process.overview.title": "Processes Overview", // TODO New key - Add a translation "process.overview.title": "Processes Overview", - + // "process.overview.breadcrumbs": "Processes Overview", // TODO New key - Add a translation "process.overview.breadcrumbs": "Processes Overview", - + // "process.overview.new": "New", // TODO New key - Add a translation "process.overview.new": "New", - - + + // "profile.breadcrumbs": "Update Profile", "profile.breadcrumbs": "Atjaunot Profilu", - + // "profile.card.identify": "Identify", "profile.card.identify": "Identificēt", - + // "profile.card.security": "Security", "profile.card.security": "Drošība", - + // "profile.form.submit": "Update Profile", "profile.form.submit": "Atjaunot Profilu", - + // "profile.groups.head": "Authorization groups you belong to", "profile.groups.head": "Autorizācijas grupas, kurām jūs piederat", - + // "profile.head": "Update Profile", "profile.head": "Atjaunot Profilu", - + // "profile.metadata.form.error.firstname.required": "First Name is required", "profile.metadata.form.error.firstname.required": "Vārds ir nepieciešams", - + // "profile.metadata.form.error.lastname.required": "Last Name is required", "profile.metadata.form.error.lastname.required": "Uzvārds ir nepieciešams", - + // "profile.metadata.form.label.email": "Email Address", "profile.metadata.form.label.email": "E-pasta adrese", - + // "profile.metadata.form.label.firstname": "First Name", "profile.metadata.form.label.firstname": "Vārds", - + // "profile.metadata.form.label.language": "Language", "profile.metadata.form.label.language": "Valoda", - + // "profile.metadata.form.label.lastname": "Last Name", "profile.metadata.form.label.lastname": "Uzvārds", - + // "profile.metadata.form.label.phone": "Contact Telephone", "profile.metadata.form.label.phone": "Kontakttālrunis", - + // "profile.metadata.form.notifications.success.content": "Your changes to the profile were saved.", "profile.metadata.form.notifications.success.content": "Profila izmaiņas tika saglabātas.", - + // "profile.metadata.form.notifications.success.title": "Profile saved", "profile.metadata.form.notifications.success.title": "Profils saglabāts", - + // "profile.notifications.warning.no-changes.content": "No changes were made to the Profile.", "profile.notifications.warning.no-changes.content": "Profilā izmaiņas netika veiktas.", - + // "profile.notifications.warning.no-changes.title": "No changes", "profile.notifications.warning.no-changes.title": "Bez izmaiņām", - + // "profile.security.form.error.matching-passwords": "The passwords do not match.", "profile.security.form.error.matching-passwords": "Paroles nesakrīt.", - + // "profile.security.form.error.password-length": "The password should be at least 6 characters long.", "profile.security.form.error.password-length": "Parolei jābūt vismaz 6 rakstzīmju garai.", - + // "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": "Pēc izvēles zemāk esošajā lodziņā varat ievadīt jaunu paroli un apstiprināt to, atkārtoti ierakstot to otrajā lodziņā. Tam jābūt vismaz sešu rakstzīmju garam.", - + // "profile.security.form.label.password": "Password", "profile.security.form.label.password": "Parole", - + // "profile.security.form.label.passwordrepeat": "Retype to confirm", "profile.security.form.label.passwordrepeat": "Atkārtojiet, lai apstiprinātu", - + // "profile.security.form.notifications.success.content": "Your changes to the password were saved.", "profile.security.form.notifications.success.content": "Jūsu paroles izmaiņas tika saglabātas.", - + // "profile.security.form.notifications.success.title": "Password saved", "profile.security.form.notifications.success.title": "Parole saglabāta", - + // "profile.security.form.notifications.error.title": "Error changing passwords", "profile.security.form.notifications.error.title": "Kļūda mainot paroles", - + // "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": "Parolei jābūt vismaz 6 rakstzīmju garai.", - + // "profile.security.form.notifications.error.not-same": "The provided passwords are not the same.", "profile.security.form.notifications.error.not-same": "Norādītās paroles nav vienādas.", - + // "profile.title": "Update Profile", "profile.title": "Atjaunot profilu", - - - + + + // "project.listelement.badge": "Research Project", "project.listelement.badge": "Izpētes projekts", - + // "project.page.contributor": "Contributors", "project.page.contributor": "Līdzautori", - + // "project.page.description": "Description", "project.page.description": "Apraksts", - + // "project.page.edit": "Edit this item", // TODO New key - Add a translation "project.page.edit": "Edit this item", - + // "project.page.expectedcompletion": "Expected Completion", "project.page.expectedcompletion": "Sagaidāmais pabeigšanas datums", - + // "project.page.funder": "Funders", "project.page.funder": "Izveidotāji", - + // "project.page.id": "ID", "project.page.id": "ID", - + // "project.page.keyword": "Keywords", "project.page.keyword": "Atslēgas vārdi", - + // "project.page.status": "Status", "project.page.status": "Status", - + // "project.page.titleprefix": "Research Project: ", "project.page.titleprefix": "Izpētes projekts: ", - + // "project.search.results.head": "Project Search Results", "project.search.results.head": "Projekta meklēšanas rezultāti", - - - + + + // "publication.listelement.badge": "Publication", "publication.listelement.badge": "Publikācija", - + // "publication.page.description": "Description", "publication.page.description": "Apraksts", - + // "publication.page.edit": "Edit this item", // TODO New key - Add a translation "publication.page.edit": "Edit this item", - + // "publication.page.journal-issn": "Journal ISSN", "publication.page.journal-issn": "Žurnāla ISSN", - + // "publication.page.journal-title": "Journal Title", "publication.page.journal-title": "Žurnāla Nosaukums", - + // "publication.page.publisher": "Publisher", "publication.page.publisher": "Izdevējs", - + // "publication.page.titleprefix": "Publication: ", "publication.page.titleprefix": "Publikācija: ", - + // "publication.page.volume-title": "Volume Title", "publication.page.volume-title": "Sējuma Nosaukums", - + // "publication.search.results.head": "Publication Search Results", "publication.search.results.head": "Publikāciju meklēšanas rezultāti", - + // "publication.search.title": "DSpace Angular :: Publication Search", "publication.search.title": "DSpace Angular :: Publikācijas meklēšana", - - + + // "register-email.title": "New user registration", // TODO New key - Add a translation "register-email.title": "New user registration", - + // "register-page.create-profile.header": "Create Profile", // TODO New key - Add a translation "register-page.create-profile.header": "Create Profile", - + // "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.email": "Email Address", // TODO New key - Add a translation "register-page.create-profile.identification.email": "Email Address", - + // "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.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.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.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.contact": "Contact Telephone", // TODO New key - Add a translation "register-page.create-profile.identification.contact": "Contact Telephone", - + // "register-page.create-profile.identification.language": "Language", // TODO New key - Add a translation "register-page.create-profile.identification.language": "Language", - + // "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.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.label.password": "Password *", // TODO New key - Add a translation "register-page.create-profile.security.label.password": "Password *", - + // "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.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.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.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.submit": "Complete Registration", // TODO New key - Add a translation "register-page.create-profile.submit": "Complete Registration", - + // "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.head": "Registration failed", // TODO New key - Add a translation "register-page.create-profile.submit.error.head": "Registration failed", - + // "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.head": "Registration completed", // TODO New key - Add a translation "register-page.create-profile.submit.success.head": "Registration completed", - - + + // "register-page.registration.header": "New user registration", // TODO New key - Add a translation "register-page.registration.header": "New user registration", - + // "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.email": "Email Address *", // TODO New key - Add a translation "register-page.registration.email": "Email Address *", - + // "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.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.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.submit": "Register", // TODO New key - Add a translation "register-page.registration.submit": "Register", - + // "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.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.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.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 }}", - - - + + + // "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.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.title": "Unable to add relationship", // TODO New key - Add a translation "relationships.add.error.title": "Unable to add relationship", - + // "relationships.isAuthorOf": "Authors", "relationships.isAuthorOf": "Autori", - + // "relationships.isAuthorOf.Person": "Authors (persons)", // TODO New key - Add a translation "relationships.isAuthorOf.Person": "Authors (persons)", - + // "relationships.isAuthorOf.OrgUnit": "Authors (organizational units)", // TODO New key - Add a translation "relationships.isAuthorOf.OrgUnit": "Authors (organizational units)", - + // "relationships.isIssueOf": "Journal Issues", "relationships.isIssueOf": "Žurnāla Izdevumi", - + // "relationships.isJournalIssueOf": "Journal Issue", "relationships.isJournalIssueOf": "Žurnāla Izdevums", - + // "relationships.isJournalOf": "Journals", "relationships.isJournalOf": "Žurnāli", - + // "relationships.isOrgUnitOf": "Organizational Units", "relationships.isOrgUnitOf": "Struktūrvienības", - + // "relationships.isPersonOf": "Authors", "relationships.isPersonOf": "Autori", - + // "relationships.isProjectOf": "Research Projects", "relationships.isProjectOf": "Pētniecības Projekti", - + // "relationships.isPublicationOf": "Publications", "relationships.isPublicationOf": "Publikācijas", - + // "relationships.isPublicationOfJournalIssue": "Articles", "relationships.isPublicationOfJournalIssue": "Raksti", - + // "relationships.isSingleJournalOf": "Journal", "relationships.isSingleJournalOf": "Žurnāls", - + // "relationships.isSingleVolumeOf": "Journal Volume", "relationships.isSingleVolumeOf": "Žurnāla Sējums", - + // "relationships.isVolumeOf": "Journal Volumes", "relationships.isVolumeOf": "Žurnāla Sējums", - + // "relationships.isContributorOf": "Contributors", "relationships.isContributorOf": "Līdzautori", - - - + + + // "resource-policies.add.button": "Add", // TODO New key - Add a translation "resource-policies.add.button": "Add", - + // "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.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.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.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.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.collection": "Add a new Collection policy", // TODO New key - Add a translation "resource-policies.add.for.collection": "Add a new Collection policy", - + // "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.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.success.content": "Operation successful", // TODO New key - Add a translation "resource-policies.create.page.success.content": "Operation successful", - + // "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.delete.btn": "Delete selected", // TODO New key - Add a translation "resource-policies.delete.btn": "Delete selected", - + // "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.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.success.content": "Operation successful", // TODO New key - Add a translation "resource-policies.delete.success.content": "Operation successful", - + // "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.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.success.content": "Operation successful", // TODO New key - Add a translation "resource-policies.edit.page.success.content": "Operation successful", - + // "resource-policies.edit.page.title": "Edit resource policy", // TODO New key - Add a translation "resource-policies.edit.page.title": "Edit resource policy", - + // "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.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.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.select.btn": "Select", // TODO New key - Add a translation "resource-policies.form.eperson-group-list.select.btn": "Select", - + // "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.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.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.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.name": "Name", // TODO New key - Add a translation "resource-policies.form.eperson-group-list.table.headers.name": "Name", - + // "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.description.label": "Description", // TODO New key - Add a translation "resource-policies.form.description.label": "Description", - + // "resource-policies.form.name.label": "Name", // TODO New key - Add a translation "resource-policies.form.name.label": "Name", - + // "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.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.table.headers.action": "Action", // TODO New key - Add a translation "resource-policies.table.headers.action": "Action", - + // "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.start": "Start Date", // TODO New key - Add a translation "resource-policies.table.headers.date.start": "Start Date", - + // "resource-policies.table.headers.edit": "Edit", // TODO New key - Add a translation "resource-policies.table.headers.edit": "Edit", - + // "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.policy": "Edit policy", // TODO New key - Add a translation "resource-policies.table.headers.edit.policy": "Edit policy", - + // "resource-policies.table.headers.eperson": "EPerson", // TODO New key - Add a translation "resource-policies.table.headers.eperson": "EPerson", - + // "resource-policies.table.headers.group": "Group", // TODO New key - Add a translation "resource-policies.table.headers.group": "Group", - + // "resource-policies.table.headers.id": "ID", // TODO New key - Add a translation "resource-policies.table.headers.id": "ID", - + // "resource-policies.table.headers.name": "Name", // TODO New key - Add a translation "resource-policies.table.headers.name": "Name", - + // "resource-policies.table.headers.policyType": "type", // TODO New key - Add a translation "resource-policies.table.headers.policyType": "type", - + // "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.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.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.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.collection": "Policies for Collection", // TODO New key - Add a translation "resource-policies.table.headers.title.for.collection": "Policies for Collection", - - - + + + // "search.description": "", "search.description": "", - + // "search.switch-configuration.title": "Show", "search.switch-configuration.title": "Rādīt", - + // "search.title": "DSpace Angular :: Search", "search.title": "DSpace Angular :: Meklēt", - + // "search.breadcrumbs": "Search", "search.breadcrumbs": "Meklēt", - - + + // "search.filters.applied.f.author": "Author", "search.filters.applied.f.author": "Autors", - + // "search.filters.applied.f.dateIssued.max": "End date", "search.filters.applied.f.dateIssued.max": "Beigu datums", - + // "search.filters.applied.f.dateIssued.min": "Start date", "search.filters.applied.f.dateIssued.min": "Sākuma datums", - + // "search.filters.applied.f.dateSubmitted": "Date submitted", "search.filters.applied.f.dateSubmitted": "Iesniegšanas datums", - + // "search.filters.applied.f.discoverable": "Private", "search.filters.applied.f.discoverable": "Privāts", - + // "search.filters.applied.f.entityType": "Item Type", "search.filters.applied.f.entityType": "Materiāla tips", - + // "search.filters.applied.f.has_content_in_original_bundle": "Has files", "search.filters.applied.f.has_content_in_original_bundle": "Ir faili", - + // "search.filters.applied.f.itemtype": "Type", "search.filters.applied.f.itemtype": "Tips", - + // "search.filters.applied.f.namedresourcetype": "Status", "search.filters.applied.f.namedresourcetype": "Status", - + // "search.filters.applied.f.subject": "Subject", "search.filters.applied.f.subject": "Priekšmets", - + // "search.filters.applied.f.submitter": "Submitter", "search.filters.applied.f.submitter": "Iesniedzējs", - + // "search.filters.applied.f.jobTitle": "Job Title", "search.filters.applied.f.jobTitle": "Ieņemamais Amats", - + // "search.filters.applied.f.birthDate.max": "End birth date", "search.filters.applied.f.birthDate.max": "Dzimšanas beigu datums", - + // "search.filters.applied.f.birthDate.min": "Start birth date", "search.filters.applied.f.birthDate.min": "Dzimšanas sākuma datums", - + // "search.filters.applied.f.withdrawn": "Withdrawn", "search.filters.applied.f.withdrawn": "Atsaukts", - - - + + + // "search.filters.filter.author.head": "Author", "search.filters.filter.author.head": "Autors", - + // "search.filters.filter.author.placeholder": "Author name", "search.filters.filter.author.placeholder": "Autora vārds", - + // "search.filters.filter.birthDate.head": "Birth Date", "search.filters.filter.birthDate.head": "Dzimšanas datums", - + // "search.filters.filter.birthDate.placeholder": "Birth Date", "search.filters.filter.birthDate.placeholder": "Dzimšanas datums", - + // "search.filters.filter.creativeDatePublished.head": "Date Published", "search.filters.filter.creativeDatePublished.head": "Publicēšanas datums", - + // "search.filters.filter.creativeDatePublished.placeholder": "Date Published", "search.filters.filter.creativeDatePublished.placeholder": "Publicēšanas datums", - + // "search.filters.filter.creativeWorkEditor.head": "Editor", "search.filters.filter.creativeWorkEditor.head": "Redaktors", - + // "search.filters.filter.creativeWorkEditor.placeholder": "Editor", "search.filters.filter.creativeWorkEditor.placeholder": "Redaktors", - + // "search.filters.filter.creativeWorkKeywords.head": "Subject", "search.filters.filter.creativeWorkKeywords.head": "Priekšmets", - + // "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", "search.filters.filter.creativeWorkKeywords.placeholder": "Priekšmets", - + // "search.filters.filter.creativeWorkPublisher.head": "Publisher", "search.filters.filter.creativeWorkPublisher.head": "Izdevējs", - + // "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", "search.filters.filter.creativeWorkPublisher.placeholder": "Izdevējs", - + // "search.filters.filter.dateIssued.head": "Date", "search.filters.filter.dateIssued.head": "Datums", - + // "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", "search.filters.filter.dateIssued.max.placeholder": "Minimālais datums", - + // "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", "search.filters.filter.dateIssued.min.placeholder": "Maksimālais datums", - + // "search.filters.filter.dateSubmitted.head": "Date submitted", "search.filters.filter.dateSubmitted.head": "Iesniegšanas datums", - + // "search.filters.filter.dateSubmitted.placeholder": "Date submitted", "search.filters.filter.dateSubmitted.placeholder": "Iesniegšanas datums", - + // "search.filters.filter.discoverable.head": "Private", "search.filters.filter.discoverable.head": "Privāts", - + // "search.filters.filter.withdrawn.head": "Withdrawn", "search.filters.filter.withdrawn.head": "Atsaukts", - + // "search.filters.filter.entityType.head": "Item Type", "search.filters.filter.entityType.head": "Materiāla tips", - + // "search.filters.filter.entityType.placeholder": "Item Type", "search.filters.filter.entityType.placeholder": "Materiāla tips", - + // "search.filters.filter.has_content_in_original_bundle.head": "Has files", "search.filters.filter.has_content_in_original_bundle.head": "Ir faili", - + // "search.filters.filter.itemtype.head": "Type", "search.filters.filter.itemtype.head": "Tips", - + // "search.filters.filter.itemtype.placeholder": "Type", "search.filters.filter.itemtype.placeholder": "Tips", - + // "search.filters.filter.jobTitle.head": "Job Title", "search.filters.filter.jobTitle.head": "Ieņemamais Amats", - + // "search.filters.filter.jobTitle.placeholder": "Job Title", "search.filters.filter.jobTitle.placeholder": "Ieņemamais Amats", - + // "search.filters.filter.knowsLanguage.head": "Known language", "search.filters.filter.knowsLanguage.head": "Pārvalda valodu", - + // "search.filters.filter.knowsLanguage.placeholder": "Known language", "search.filters.filter.knowsLanguage.placeholder": "Pārvalda valodu", - + // "search.filters.filter.namedresourcetype.head": "Status", "search.filters.filter.namedresourcetype.head": "Status", - + // "search.filters.filter.namedresourcetype.placeholder": "Status", "search.filters.filter.namedresourcetype.placeholder": "Status", - + // "search.filters.filter.objectpeople.head": "People", "search.filters.filter.objectpeople.head": "Personas", - + // "search.filters.filter.objectpeople.placeholder": "People", "search.filters.filter.objectpeople.placeholder": "Personas", - + // "search.filters.filter.organizationAddressCountry.head": "Country", "search.filters.filter.organizationAddressCountry.head": "Valsts", - + // "search.filters.filter.organizationAddressCountry.placeholder": "Country", "search.filters.filter.organizationAddressCountry.placeholder": "Valsts", - + // "search.filters.filter.organizationAddressLocality.head": "City", "search.filters.filter.organizationAddressLocality.head": "Pilsēta", - + // "search.filters.filter.organizationAddressLocality.placeholder": "City", "search.filters.filter.organizationAddressLocality.placeholder": "Pilsēta", - + // "search.filters.filter.organizationFoundingDate.head": "Date Founded", "search.filters.filter.organizationFoundingDate.head": "Dibināšanas datums", - + // "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", "search.filters.filter.organizationFoundingDate.placeholder": "Dibināšanas datums", - + // "search.filters.filter.scope.head": "Scope", "search.filters.filter.scope.head": "Joma", - + // "search.filters.filter.scope.placeholder": "Scope filter", "search.filters.filter.scope.placeholder": "Jomas filtrs", - + // "search.filters.filter.show-less": "Collapse", "search.filters.filter.show-less": "Sakļaut", - + // "search.filters.filter.show-more": "Show more", "search.filters.filter.show-more": "Rādīt vairāk", - + // "search.filters.filter.subject.head": "Subject", "search.filters.filter.subject.head": "Priekšmets", - + // "search.filters.filter.subject.placeholder": "Subject", "search.filters.filter.subject.placeholder": "Priekšmets", - + // "search.filters.filter.submitter.head": "Submitter", "search.filters.filter.submitter.head": "Iesniedzējs", - + // "search.filters.filter.submitter.placeholder": "Submitter", "search.filters.filter.submitter.placeholder": "Iesniedzējs", - - - + + + // "search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalIssue": "Žurnāla Izdevums", - + // "search.filters.entityType.JournalVolume": "Journal Volume", "search.filters.entityType.JournalVolume": "Žurnāla Sējum", - + // "search.filters.entityType.OrgUnit": "Organizational Unit", "search.filters.entityType.OrgUnit": "Struktūrvienība", - + // "search.filters.has_content_in_original_bundle.true": "Yes", "search.filters.has_content_in_original_bundle.true": "Jā", - + // "search.filters.has_content_in_original_bundle.false": "No", "search.filters.has_content_in_original_bundle.false": "Nē", - + // "search.filters.discoverable.true": "No", "search.filters.discoverable.true": "Nē", - + // "search.filters.discoverable.false": "Yes", "search.filters.discoverable.false": "Jā", - + // "search.filters.withdrawn.true": "Yes", "search.filters.withdrawn.true": "Jā", - + // "search.filters.withdrawn.false": "No", "search.filters.withdrawn.false": "Nē", - - + + // "search.filters.head": "Filters", "search.filters.head": "Filtri", - + // "search.filters.reset": "Reset filters", "search.filters.reset": "Atiestatīt filtrus", - - - + + + // "search.form.search": "Search", "search.form.search": "Meklēt", - + // "search.form.search_dspace": "Search DSpace", "search.form.search_dspace": "Meklēt DSpace", - + // "search.form.search_mydspace": "Search MyDSpace", "search.form.search_mydspace": "Meklēt manā DSpace", - - - + + + // "search.results.head": "Search Results", "search.results.head": "Meklēšanas rezultāti", - + // "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", "search.results.no-results": "Rezultāti netika atrasti. Vai jums ir grūtības atrast meklēto? Mēģiniet ievietot", - + // "search.results.no-results-link": "quotes around it", "search.results.no-results-link": "pēdiņas ap to", - + // "search.results.empty": "Your search returned no results.", "search.results.empty": "Rezultāti netika atrasti.", - - - + + + // "search.sidebar.close": "Back to results", "search.sidebar.close": "Atpakaļ pie rezultātiem", - + // "search.sidebar.filters.title": "Filters", "search.sidebar.filters.title": "Filtri", - + // "search.sidebar.open": "Search Tools", "search.sidebar.open": "Meklēšanas Rīki", - + // "search.sidebar.results": "results", "search.sidebar.results": "rezultāti", - + // "search.sidebar.settings.rpp": "Results per page", "search.sidebar.settings.rpp": "Rezultāti vienā lapā", - + // "search.sidebar.settings.sort-by": "Sort By", "search.sidebar.settings.sort-by": "Kārtot Pēc", - + // "search.sidebar.settings.title": "Settings", "search.sidebar.settings.title": "Iestatījumi", - - - + + + // "search.view-switch.show-detail": "Show detail", "search.view-switch.show-detail": "Attēlot detaļas", - + // "search.view-switch.show-grid": "Show as grid", "search.view-switch.show-grid": "Attēlot matricā", - + // "search.view-switch.show-list": "Show as list", "search.view-switch.show-list": "Attēlot kā sarakstu", - - - + + + // "sorting.ASC": "Ascending", // TODO New key - Add a translation "sorting.ASC": "Ascending", - + // "sorting.DESC": "Descending", // TODO New key - Add a translation "sorting.DESC": "Descending", - + // "sorting.dc.title.ASC": "Title Ascending", "sorting.dc.title.ASC": "Nosaukums augošā secībā", - + // "sorting.dc.title.DESC": "Title Descending", "sorting.dc.title.DESC": "Nosaukums dilstošā secībā", - + // "sorting.score.DESC": "Relevance", "sorting.score.DESC": "Atbilstība", - - - + + + // "statistics.title": "Statistics", // TODO New key - Add a translation "statistics.title": "Statistics", - + // "statistics.header": "Statistics for {{ scope }}", // TODO New key - Add a translation "statistics.header": "Statistics for {{ scope }}", - + // "statistics.breadcrumbs": "Statistics", // TODO New key - Add a translation "statistics.breadcrumbs": "Statistics", - + // "statistics.page.no-data": "No data available", // TODO New key - Add a translation "statistics.page.no-data": "No data available", - + // "statistics.table.no-data": "No data available", // TODO New key - Add a translation "statistics.table.no-data": "No data available", - + // "statistics.table.title.TotalVisits": "Total visits", // TODO New key - Add a translation "statistics.table.title.TotalVisits": "Total visits", - + // "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.TotalDownloads": "File Visits", // TODO New key - Add a translation "statistics.table.title.TotalDownloads": "File Visits", - + // "statistics.table.title.TopCountries": "Top country views", // TODO New key - Add a translation "statistics.table.title.TopCountries": "Top country views", - + // "statistics.table.title.TopCities": "Top city views", // TODO New key - Add a translation "statistics.table.title.TopCities": "Top city views", - + // "statistics.table.header.views": "Views", // TODO New key - Add a translation "statistics.table.header.views": "Views", - - - + + + // "submission.edit.title": "Edit Submission", "submission.edit.title": "Rediģēt Iesniegumu", - + // "submission.general.cannot_submit": "You have not the privilege to make a new submission.", "submission.general.cannot_submit": "Jums nav tiesību iesniegt jaunu iesniegumu.", - + // "submission.general.deposit": "Deposit", "submission.general.deposit": "Ievietot", - + // "submission.general.discard.confirm.cancel": "Cancel", "submission.general.discard.confirm.cancel": "Atcelt", - + // "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", "submission.general.discard.confirm.info": "Šo darbību nevar atsaukt. Vai tu esat pārliecināts?", - + // "submission.general.discard.confirm.submit": "Yes, I'm sure", "submission.general.discard.confirm.submit": "Jā, esmu pārliecināts", - + // "submission.general.discard.confirm.title": "Discard submission", "submission.general.discard.confirm.title": "Atcelt ievietošanu", - + // "submission.general.discard.submit": "Discard", "submission.general.discard.submit": "Atcelt", - + // "submission.general.save": "Save", "submission.general.save": "Saglabāt", - + // "submission.general.save-later": "Save for later", "submission.general.save-later": "Saglabāt vēlākam", - - + + // "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.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.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.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.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.button": "Search", // TODO New key - Add a translation "submission.import-external.search.button": "Search", - + // "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.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.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.sherpaJournal": "SHERPA Journals", // TODO New key - Add a translation "submission.import-external.source.sherpaJournal": "SHERPA Journals", - + // "submission.import-external.source.sherpaPublisher": "SHERPA Publishers", // TODO New key - Add a translation "submission.import-external.source.sherpaPublisher": "SHERPA Publishers", - + // "submission.import-external.source.orcid": "ORCID", // TODO New key - Add a translation "submission.import-external.source.orcid": "ORCID", - + // "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.preview.title": "Item Preview", // TODO New key - Add a translation "submission.import-external.preview.title": "Item Preview", - + // "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.button.import": "Start submission", // TODO New key - Add a translation "submission.import-external.preview.button.import": "Start submission", - + // "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.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.sections.describe.relationship-lookup.close": "Close", "submission.sections.describe.relationship-lookup.close": "Aizvērt", - + // "submission.sections.describe.relationship-lookup.external-source.added": "Successfully added local entry to the selection", "submission.sections.describe.relationship-lookup.external-source.added": "Lokālais ieraksts veiksmigi pievienots izvēlei", - + // "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.Journal": "Import remote journal", "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal": "Importēt attālinātu žurnālu", - + // "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": "Importēt attālināta žurnāla izdevumu", - + // "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": "Importēt attālinātā žurnāla sējumu", - + // "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.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.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.authority": "Authority", "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Autoritāte", - + // "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": "Importēt kā jaunu lokālās autoritātes ierakstu", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Cancel", "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Atcelt", - + // "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": "Atlasiet kolekciju, kurā importēt jaunus ierakstus", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Entities", "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Vienības", - + // "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": "Importēt kā jaunu vietējo vienību", - + // "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": "Importē no LC Nosaukuma", - + // "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": "Importē no ORCID", - + // "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": "Importē no Sherpa Žurnāla", - + // "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": "Importēt no Sherpa Izdevēja", - + // "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.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.import": "Import", "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Importēt", - + // "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": "Importēt Attālinātu Žurnālu", - + // "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": "Lokālais žurnāls veiksmīgi pievienots izlasei", - + // "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": "Veiksmīgi importēts un pievienots ārējais žurnāls atlasē", - + // "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": "Importēt Attālināta Žurnāla Izdevumu", - + // "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": "Lokālā žurnāla izdevums veiksmīgi pievienots izlasei", - + // "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": "Veiksmīgi importēts un atlasē pievienots ārēja žurnāla izdevums", - + // "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": "Importēt Attālinātā Žurnāla Sējumu", - + // "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": "Lokālā žurnāla sējums veiksmīgi pievienots atlasē", - + // "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": "Veiksmīgi importēts un atlasē pievienots ārējā žurnāla sējums", - + // "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Select a local match:", "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Atlasīt lokālo sakritību:", - + // "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all", "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Atcelt izvēli", - + // "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page", "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Atcelt izvēlēto lapu", - + // "submission.sections.describe.relationship-lookup.search-tab.loading": "Loading...", "submission.sections.describe.relationship-lookup.search-tab.loading": "Notiek ielāde...", - + // "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Search query", "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Meklēšanas vaicājums", - + // "submission.sections.describe.relationship-lookup.search-tab.search": "Go", "submission.sections.describe.relationship-lookup.search-tab.search": "Izpildīt", - + // "submission.sections.describe.relationship-lookup.search-tab.select-all": "Select all", "submission.sections.describe.relationship-lookup.search-tab.select-all": "Izvēlēties visus", - + // "submission.sections.describe.relationship-lookup.search-tab.select-page": "Select page", "submission.sections.describe.relationship-lookup.search-tab.select-page": "Izvēlēties lapu", - + // "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", "submission.sections.describe.relationship-lookup.selected": "Izvēlētie {{ size }} materiāli", - + // "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.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.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.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.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.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.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.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.Journal": "Local Journals ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Vietējie Žurnāli ({{ 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.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.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.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.sherpaJournal": "Sherpa Journals ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Žurnāli ({{ 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 Izdevēji ({{ count }})", - + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})", - + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Nosaukumi ({{ 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.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.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.selection-tab.tab-title": "Current Selection ({{ count }})", "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Pašreiz Izvēlēti ({{ 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.JournalIssue": "Journal Issues", // TODO New key - Add a translation "submission.sections.describe.relationship-lookup.title.JournalIssue": "Journal Issues", - + // "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.JournalVolume": "Journal Volumes", // TODO New key - Add a translation "submission.sections.describe.relationship-lookup.title.JournalVolume": "Journal Volumes", - + // "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.isAuthorOfPublication": "Authors", // TODO New key - Add a translation "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "Authors", - + // "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.Project": "Projects", // TODO New key - Add a translation "submission.sections.describe.relationship-lookup.title.Project": "Projects", - + // "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.Person": "Authors", // TODO New key - Add a translation "submission.sections.describe.relationship-lookup.title.Person": "Authors", - + // "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.DataPackage": "Data Packages", // TODO New key - Add a translation "submission.sections.describe.relationship-lookup.title.DataPackage": "Data Packages", - + // "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.Funding Agency": "Funding Agency", "submission.sections.describe.relationship-lookup.title.Funding Agency": "Finansējošā aģēntūra", - + // "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.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.search-tab.toggle-dropdown": "Toggle dropdown", "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Pārslēgšanas nolaižamā izvēlne", - + // "submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings", "submission.sections.describe.relationship-lookup.selection-tab.settings": "Iestatījumi", - + // "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Your selection is currently empty.", "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "ūsu atlase pašlaik ir tukša.", - + // "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.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.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.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.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.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.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.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.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.Journal": "Selected Journals", "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Izvēlētais Žurnāls", - + // "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.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.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.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.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.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.sherpaJournal": "Search Results", "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Meklēšanas rezultāti", - + // "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results", "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Meklēšanas rezultāti", - + // "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results", "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Meklēšanas rezultāti", - + // "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.lcname": "Search Results", "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Meklēšanas rezultāti", - + // "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.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.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": "Vai vēlaties saglabāt \"{{ value }}\" kā vārda variantu šai personai, lai jūs un citi varētu to izmantot turpmākai iesniegšanai? Ja nē, jūs joprojām varat to izmantot šai iesniegšanai.", - + // "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant", "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Saglabājiet jauna nosaukuma variantu", - + // "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Use only for this submission", "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Izmantojiet tikai pašreizējai iesniegšanai", - + // "submission.sections.ccLicense.type": "License Type", // TODO New key - Add a translation "submission.sections.ccLicense.type": "License Type", - + // "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.change": "Change your license type…", // TODO New key - Add a translation "submission.sections.ccLicense.change": "Change your license type…", - + // "submission.sections.ccLicense.none": "No licenses available", // TODO New key - Add a translation "submission.sections.ccLicense.none": "No licenses available", - + // "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.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.confirmation": "I grant the license above", // TODO New key - Add a translation "submission.sections.ccLicense.confirmation": "I grant the license above", - + // "submission.sections.general.add-more": "Add more", "submission.sections.general.add-more": "Pievienot vēl", - + // "submission.sections.general.collection": "Collection", "submission.sections.general.collection": "Kolekcija", - + // "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", "submission.sections.general.deposit_error_notice": "Iesniedzot materiālu, radās problēma. Lūdzu, vēlāk mēģiniet vēlreiz.", - + // "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", "submission.sections.general.deposit_success_notice": "Iesniegums ir veiksmīgi iesniegts.", - + // "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", "submission.sections.general.discard_error_notice": "Izmetot vienumu, radās kļūda. Lūdzu, vēlāk mēģiniet vēlreiz", - + // "submission.sections.general.discard_success_notice": "Submission discarded successfully.", "submission.sections.general.discard_success_notice": "Iesniegums atmests.", - + // "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", "submission.sections.general.metadata-extracted": "Jauni metadati ir iegūti un pievienoti {{sectionId}} sadaļā.", - + // "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", "submission.sections.general.metadata-extracted-new-section": "Jauna {{sectionId}} sadaļa ir pievienota iesniegšanai.", - + // "submission.sections.general.no-collection": "No collection found", "submission.sections.general.no-collection": "Kolekcijas nav atrastas", - + // "submission.sections.general.no-sections": "No options available", "submission.sections.general.no-sections": "Opcijas nav pieejamas", - + // "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", "submission.sections.general.save_error_notice": "Saglabājot materiālu, radās problēma. Lūdzu, vēlāk mēģiniet vēlreiz.", - + // "submission.sections.general.save_success_notice": "Submission saved successfully.", "submission.sections.general.save_success_notice": "Iesniegums veiksmīgi saglabāts.", - + // "submission.sections.general.search-collection": "Search for a collection", "submission.sections.general.search-collection": "Meklēt kolekciju", - + // "submission.sections.general.sections_not_valid": "There are incomplete sections.", "submission.sections.general.sections_not_valid": "Ir nepilnīgas sadaļas.", - - - + + + // "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.describe.recycle": "Recycle", "submission.sections.submit.progressbar.describe.recycle": "Pārstrādāt", - + // "submission.sections.submit.progressbar.describe.stepcustom": "Describe", "submission.sections.submit.progressbar.describe.stepcustom": "Aprakstiet", - + // "submission.sections.submit.progressbar.describe.stepone": "Describe", "submission.sections.submit.progressbar.describe.stepone": "Aprakstiet", - + // "submission.sections.submit.progressbar.describe.steptwo": "Describe", "submission.sections.submit.progressbar.describe.steptwo": "Aprakstiet", - + // "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", "submission.sections.submit.progressbar.detect-duplicate": "Potenciālie dublikāti", - + // "submission.sections.submit.progressbar.license": "Deposit license", "submission.sections.submit.progressbar.license": "Ievietot licenci", - + // "submission.sections.submit.progressbar.upload": "Upload files", "submission.sections.submit.progressbar.upload": "Augšupielādēt failus", - - - + + + // "submission.sections.upload.delete.confirm.cancel": "Cancel", "submission.sections.upload.delete.confirm.cancel": "Atcelt", - + // "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", "submission.sections.upload.delete.confirm.info": "Šo darbību nevar atsaukt. Vai tu esi pārliecināts?", - + // "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", "submission.sections.upload.delete.confirm.submit": "Jā, esmu pārliecināts", - + // "submission.sections.upload.delete.confirm.title": "Delete bitstream", "submission.sections.upload.delete.confirm.title": "Dzēst bitu straumes", - + // "submission.sections.upload.delete.submit": "Delete", "submission.sections.upload.delete.submit": "Dzēst", - + // "submission.sections.upload.drop-message": "Drop files to attach them to the item", "submission.sections.upload.drop-message": "Ievietojiet failu, lai pievienotu materiālam", - + // "submission.sections.upload.form.access-condition-label": "Access condition type", "submission.sections.upload.form.access-condition-label": "Piekļuves nosacījuma tips", - + // "submission.sections.upload.form.date-required": "Date is required.", "submission.sections.upload.form.date-required": "Datums ir nepieciešams.", - + // "submission.sections.upload.form.from-label": "Grant access from", // TODO Source message changed - Revise the translation "submission.sections.upload.form.from-label": "Iespēja izmantot no", - + // "submission.sections.upload.form.from-placeholder": "From", "submission.sections.upload.form.from-placeholder": "No", - + // "submission.sections.upload.form.group-label": "Group", "submission.sections.upload.form.group-label": "Grupa", - + // "submission.sections.upload.form.group-required": "Group is required.", "submission.sections.upload.form.group-required": "Nepieciešama grupa.", - + // "submission.sections.upload.form.until-label": "Grant access until", // TODO Source message changed - Revise the translation "submission.sections.upload.form.until-label": "Piekļuvas atļauja līdz", - + // "submission.sections.upload.form.until-placeholder": "Until", "submission.sections.upload.form.until-placeholder": "Līdz", - + // "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": "Augšupielādētie faili kolekcijā {{collectionName}} būs pieejami atbilstoši šīm grupām:", - + // "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": "Lūdzu, ņemiet vērā, ka augšupielādētie faili kolekcijā {{collectionName}} būs pieejami papildus tam, kas ir skaidri noteikts par atsevišķu failu, ar šādām grupām:", - + // "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": "Šeit atradīsit visus failus, kas pašlaik atrodas materiālā. Varat atjaunināt failu metadatus un piekļuves nosacījumus vai augšupielādēt papildu failus, vienkārši ievelkot un atstājot tos visur lapā", - + // "submission.sections.upload.no-entry": "No", "submission.sections.upload.no-entry": "Nē", - + // "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", "submission.sections.upload.no-file-uploaded": "Vēl nav augšupielādēts neviens fails.", - + // "submission.sections.upload.save-metadata": "Save metadata", "submission.sections.upload.save-metadata": "Saglabāt metadatus", - + // "submission.sections.upload.undo": "Cancel", "submission.sections.upload.undo": "Atcelt", - + // "submission.sections.upload.upload-failed": "Upload failed", "submission.sections.upload.upload-failed": "Augšupielāde neizdevās", - + // "submission.sections.upload.upload-successful": "Upload successful", "submission.sections.upload.upload-successful": "Augšupielāde veiksmīga", - - - + + + // "submission.submit.title": "Submission", "submission.submit.title": "Iesniegums", - - - + + + // "submission.workflow.generic.delete": "Delete", "submission.workflow.generic.delete": "Dzēst", - + // "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": "Ja vēlaties atmest šo materiālu izvēlieties \"Dzēst\". Pēc tam jums tiks lūgts to apstiprināt.", - + // "submission.workflow.generic.edit": "Edit", "submission.workflow.generic.edit": "Rediģēt", - + // "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", "submission.workflow.generic.edit-help": "Izvēlieties šo opciju, lai mainītu materiāla metadatus.", - + // "submission.workflow.generic.view": "View", "submission.workflow.generic.view": "Skatīt", - + // "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", "submission.workflow.generic.view-help": "Izvēlieties šo opciju, lai skatītu materiāla metadatus.", - - - + + + // "submission.workflow.tasks.claimed.approve": "Approve", "submission.workflow.tasks.claimed.approve": "Apstiprināt", - + // "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": "Ja esat pārskatījis materiālu un tas ir piemērots iekļaušanai kolekcijā, atlasiet \"Apstiprināt\".", - + // "submission.workflow.tasks.claimed.edit": "Edit", "submission.workflow.tasks.claimed.edit": "Rediģēt", - + // "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", "submission.workflow.tasks.claimed.edit_help": "Izvēlieties šo opciju, lai mainītu materiāla metadatus.", - + // "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": "Lūdzu, zemāk esošajā lodziņā ievadiet iesnieguma noraidīšanas iemeslu, norādot, vai iesniedzējs var novērst problēmu un atkārtoti iesniegt.", - + // "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", "submission.workflow.tasks.claimed.reject.reason.placeholder": "Aprakstiet noraidījuma iemeslu", - + // "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", "submission.workflow.tasks.claimed.reject.reason.submit": "Noraidīt materiālu", - + // "submission.workflow.tasks.claimed.reject.reason.title": "Reason", "submission.workflow.tasks.claimed.reject.reason.title": "Iemesls", - + // "submission.workflow.tasks.claimed.reject.submit": "Reject", "submission.workflow.tasks.claimed.reject.submit": "Noraidīt", - + // "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": "Ja esat pārskatījis vienumu un secinājis, ka tas nav piemērots iekļaušanai kolekcijā, atlasiet \"Noraidīt\". Pēc tam jums tiks lūgts ievadīt ziņojumu, norādot, kāpēc materiāls nav piemērots, un vai iesniedzējam vajadzētu kaut ko mainīt un atkārtoti iesniegt.", - + // "submission.workflow.tasks.claimed.return": "Return to pool", "submission.workflow.tasks.claimed.return": "Atgriezt kopnē", - + // "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": "Atgrieziet uzdevumu kopnē, lai uzdevumu varētu veikt cits lietotājs.", - - - + + + // "submission.workflow.tasks.generic.error": "Error occurred during operation...", "submission.workflow.tasks.generic.error": "Apstrādes laikā radās kļūda...", - + // "submission.workflow.tasks.generic.processing": "Processing...", "submission.workflow.tasks.generic.processing": "Apstrāde...", - + // "submission.workflow.tasks.generic.submitter": "Submitter", "submission.workflow.tasks.generic.submitter": "Iesniedzējs", - + // "submission.workflow.tasks.generic.success": "Operation successful", "submission.workflow.tasks.generic.success": "Darbībā veiksmīga", - - - + + + // "submission.workflow.tasks.pool.claim": "Claim", "submission.workflow.tasks.pool.claim": "Apstrādāt", - + // "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", "submission.workflow.tasks.pool.claim_help": "Piešķirt šo uzdevumu sev.", - + // "submission.workflow.tasks.pool.hide-detail": "Hide detail", "submission.workflow.tasks.pool.hide-detail": "Paslēpt detaļas", - + // "submission.workflow.tasks.pool.show-detail": "Show detail", "submission.workflow.tasks.pool.show-detail": "Parādīt detaļas", - - - + + + // "title": "DSpace", "title": "DSpace", - - - + + + // "vocabulary-treeview.header": "Hierarchical tree view", // TODO New key - Add a translation "vocabulary-treeview.header": "Hierarchical tree view", - + // "vocabulary-treeview.load-more": "Load more", // TODO New key - Add a translation "vocabulary-treeview.load-more": "Load more", - + // "vocabulary-treeview.search.form.reset": "Reset", // TODO New key - Add a translation "vocabulary-treeview.search.form.reset": "Reset", - + // "vocabulary-treeview.search.form.search": "Search", // TODO New key - Add a translation "vocabulary-treeview.search.form.search": "Search", - + // "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.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", - - - + + + // "uploader.browse": "browse", "uploader.browse": "pārlūkot", - + // "uploader.drag-message": "Drag & Drop your files here", "uploader.drag-message": "Velciet failu šeit", - + // "uploader.or": ", or ", // TODO Source message changed - Revise the translation "uploader.or": ", vai", - + // "uploader.processing": "Processing", "uploader.processing": "Datu apstrāde", - + // "uploader.queue-length": "Queue length", "uploader.queue-length": "Rindas garums", - + // "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": "Izēlieties tipu, kam vēlaties saglabāt virtuālos metadatus kā reālus metadatus", - + // "virtual-metadata.delete-item.modal-head": "The virtual metadata of this relation", "virtual-metadata.delete-item.modal-head": "Šīs saiknes virtuālie metadati", - + // "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": "Izēlieties materiālus, kam vēlaties saglabāt virtuālos metadatus kā reālus metadatus", - - - + + + // "workflowAdmin.search.results.head": "Administer Workflow", - "workflowAdmin.search.results.head": "Administrēt darba plūsmu" - - - + "workflowAdmin.search.results.head": "Administrēt darba plūsmu", + + + // "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 + +} diff --git a/src/config/actuators.config.ts b/src/config/actuators.config.ts new file mode 100644 index 0000000000..8f59a13c98 --- /dev/null +++ b/src/config/actuators.config.ts @@ -0,0 +1,11 @@ +import { Config } from './config.interface'; + +/** + * Config that determines the spring Actuators options + */ +export class ActuatorsConfig implements Config { + /** + * The endpoint path + */ + public endpointPath: string; +} diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 8bfa1f66f1..649efacb7b 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -15,6 +15,7 @@ import { UIServerConfig } from './ui-server-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface'; import { BrowseByConfig } from './browse-by-config.interface'; import { BundleConfig } from './bundle-config.interface'; +import { ActuatorsConfig } from './actuators.config'; interface AppConfig extends Config { ui: UIServerConfig; @@ -34,6 +35,7 @@ interface AppConfig extends Config { themes: ThemeConfig[]; mediaViewer: MediaViewerConfig; bundle: BundleConfig; + actuators: ActuatorsConfig } const APP_CONFIG = new InjectionToken('APP_CONFIG'); diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 476351b403..383b92cf73 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -15,6 +15,7 @@ import { SubmissionConfig } from './submission-config.interface'; import { ThemeConfig } from './theme.model'; import { UIServerConfig } from './ui-server-config.interface'; import { BundleConfig } from './bundle-config.interface'; +import { ActuatorsConfig } from './actuators.config'; export class DefaultAppConfig implements AppConfig { production = false; @@ -48,6 +49,10 @@ export class DefaultAppConfig implements AppConfig { nameSpace: '/', }; + actuators: ActuatorsConfig = { + endpointPath: '/actuator/health' + }; + // Caching settings cache: CacheConfig = { // NOTE: how long should objects be cached for by default diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 4fb68521fc..8ad842c33e 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -38,6 +38,10 @@ export const environment: BuildConfig = { baseUrl: 'https://rest.com/api' }, + actuators: { + endpointPath: '/actuator/health' + }, + // Caching settings cache: { // NOTE: how long should objects be cached for by default diff --git a/src/index.csr.html b/src/index.csr.html index d23cb2cae3..b1ef4343b1 100644 --- a/src/index.csr.html +++ b/src/index.csr.html @@ -13,6 +13,6 @@ - + diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index 252227b056..dc3be0de30 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -2,11 +2,10 @@ import { HttpClient, HttpClientModule } from '@angular/common/http'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterModule, NoPreloading } from '@angular/router'; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { TranslateJson5HttpLoader } from '../../ngx-translate-loaders/translate-json5-http.loader'; +import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader'; import { IdlePreloadModule } from 'angular-idle-preload'; @@ -42,8 +41,8 @@ import { environment } from '../../environments/environment'; export const REQ_KEY = makeStateKey('req'); -export function createTranslateLoader(http: HttpClient) { - return new TranslateJson5HttpLoader(http, 'assets/i18n/', '.json5'); +export function createTranslateLoader(transferState: TransferState, http: HttpClient) { + return new TranslateBrowserLoader(transferState, http, 'assets/i18n/', '.json5'); } export function getRequest(transferState: TransferState): any { @@ -59,13 +58,6 @@ export function getRequest(transferState: TransferState): any { HttpClientModule, // forRoot ensures the providers are only created once IdlePreloadModule.forRoot(), - RouterModule.forRoot([], { - // enableTracing: true, - useHash: false, - scrollPositionRestoration: 'enabled', - anchorScrolling: 'enabled', - preloadingStrategy: NoPreloading - }), StatisticsModule.forRoot(), Angulartics2RouterlessModule.forRoot(), BrowserAnimationsModule, @@ -74,7 +66,7 @@ export function getRequest(transferState: TransferState): any { loader: { provide: TranslateLoader, useFactory: (createTranslateLoader), - deps: [HttpClient] + deps: [TransferState, HttpClient] } }), AppModule @@ -92,9 +84,11 @@ export function getRequest(transferState: TransferState): any { // extend environment with app config for browser extendEnvironmentWithAppConfig(environment, appConfig); } - dspaceTransferState.transfer(); - correlationIdService.initCorrelationId(); - return () => true; + return () => + dspaceTransferState.transfer().then((b: boolean) => { + correlationIdService.initCorrelationId(); + return b; + }); }, deps: [TransferState, DSpaceTransferState, CorrelationIdService], multi: true diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 52f0048f4d..236b7bc5a0 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -3,7 +3,6 @@ import { APP_INITIALIZER, NgModule } from '@angular/core'; import { BrowserModule, TransferState } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ServerModule } from '@angular/platform-server'; -import { RouterModule } from '@angular/router'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -15,7 +14,7 @@ import { AppComponent } from '../../app/app.component'; import { AppModule } from '../../app/app.module'; import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server-transfer-state.module'; import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; -import { TranslateJson5UniversalLoader } from '../../ngx-translate-loaders/translate-json5-universal.loader'; +import { TranslateServerLoader } from '../../ngx-translate-loaders/translate-server.loader'; import { CookieService } from '../../app/core/services/cookie.service'; import { ServerCookieService } from '../../app/core/services/server-cookie.service'; import { AuthService } from '../../app/core/auth/auth.service'; @@ -37,8 +36,8 @@ import { AppConfig, APP_CONFIG_STATE } from '../../config/app-config.interface'; import { environment } from '../../environments/environment'; -export function createTranslateLoader() { - return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5'); +export function createTranslateLoader(transferState: TransferState) { + return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json5'); } @NgModule({ @@ -47,16 +46,13 @@ export function createTranslateLoader() { BrowserModule.withServerTransition({ appId: 'dspace-angular' }), - RouterModule.forRoot([], { - useHash: false - }), NoopAnimationsModule, DSpaceServerTransferStateModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: (createTranslateLoader), - deps: [] + deps: [TransferState] } }), AppModule, diff --git a/src/modules/transfer-state/dspace-browser-transfer-state.service.ts b/src/modules/transfer-state/dspace-browser-transfer-state.service.ts index ae3306c3fb..512d6aeb71 100644 --- a/src/modules/transfer-state/dspace-browser-transfer-state.service.ts +++ b/src/modules/transfer-state/dspace-browser-transfer-state.service.ts @@ -1,12 +1,19 @@ import { Injectable } from '@angular/core'; +import { coreSelector } from 'src/app/core/core.selectors'; import { StoreAction, StoreActionTypes } from '../../app/store.actions'; import { DSpaceTransferState } from './dspace-transfer-state.service'; +import { find, map } from 'rxjs/operators'; +import { isNotEmpty } from '../../app/shared/empty.util'; @Injectable() export class DSpaceBrowserTransferState extends DSpaceTransferState { - transfer() { + transfer(): Promise { const state = this.transferState.get(DSpaceTransferState.NGRX_STATE, null); this.transferState.remove(DSpaceTransferState.NGRX_STATE); this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state)); + return this.store.select(coreSelector).pipe( + find((core: any) => isNotEmpty(core)), + map(() => true) + ).toPromise(); } } diff --git a/src/modules/transfer-state/dspace-server-transfer-state.service.ts b/src/modules/transfer-state/dspace-server-transfer-state.service.ts index ac8c817d84..96b1e4be38 100644 --- a/src/modules/transfer-state/dspace-server-transfer-state.service.ts +++ b/src/modules/transfer-state/dspace-server-transfer-state.service.ts @@ -5,7 +5,7 @@ import { DSpaceTransferState } from './dspace-transfer-state.service'; @Injectable() export class DSpaceServerTransferState extends DSpaceTransferState { - transfer() { + transfer(): Promise { this.transferState.onSerialize(DSpaceTransferState.NGRX_STATE, () => { let state; this.store.pipe(take(1)).subscribe((saveState: any) => { @@ -14,5 +14,7 @@ export class DSpaceServerTransferState extends DSpaceTransferState { return state; }); + + return new Promise(() => true); } } diff --git a/src/modules/transfer-state/dspace-transfer-state.service.ts b/src/modules/transfer-state/dspace-transfer-state.service.ts index 05b1109f17..32761866fb 100644 --- a/src/modules/transfer-state/dspace-transfer-state.service.ts +++ b/src/modules/transfer-state/dspace-transfer-state.service.ts @@ -14,5 +14,5 @@ export abstract class DSpaceTransferState { ) { } - abstract transfer(): void; + abstract transfer(): Promise; } diff --git a/src/ngx-translate-loaders/ngx-translate-state.ts b/src/ngx-translate-loaders/ngx-translate-state.ts new file mode 100644 index 0000000000..4e6c2f496b --- /dev/null +++ b/src/ngx-translate-loaders/ngx-translate-state.ts @@ -0,0 +1,15 @@ +import { makeStateKey } from '@angular/platform-browser'; + +/** + * Represents ngx-translate messages in different languages in the TransferState + */ +export class NgxTranslateState { + [lang: string]: { + [key: string]: string + } +} + +/** + * The key to store the NgxTranslateState as part of the TransferState + */ +export const NGX_TRANSLATE_STATE = makeStateKey('NGX_TRANSLATE_STATE'); diff --git a/src/ngx-translate-loaders/translate-browser.loader.ts b/src/ngx-translate-loaders/translate-browser.loader.ts new file mode 100644 index 0000000000..217f301bd5 --- /dev/null +++ b/src/ngx-translate-loaders/translate-browser.loader.ts @@ -0,0 +1,44 @@ +import { TranslateLoader } from '@ngx-translate/core'; +import { HttpClient } from '@angular/common/http'; +import { TransferState } from '@angular/platform-browser'; +import { NGX_TRANSLATE_STATE, NgxTranslateState } from './ngx-translate-state'; +import { hasValue } from '../app/shared/empty.util'; +import { map } from 'rxjs/operators'; +import { of as observableOf, Observable } from 'rxjs'; +import * as JSON5 from 'json5'; + +/** + * A TranslateLoader for ngx-translate to retrieve i18n messages from the TransferState, or download + * them if they're not available there + */ +export class TranslateBrowserLoader implements TranslateLoader { + constructor( + protected transferState: TransferState, + protected http: HttpClient, + protected prefix?: string, + protected suffix?: string + ) { + } + + /** + * Return the i18n messages for a given language, first try to find them in the TransferState + * retrieve them using HttpClient if they're not available there + * + * @param lang the language code + */ + getTranslation(lang: string): Observable { + // Get the ngx-translate messages from the transfer state, to speed up the initial page load + // client side + const state = this.transferState.get(NGX_TRANSLATE_STATE, {}); + const messages = state[lang]; + if (hasValue(messages)) { + return observableOf(messages); + } else { + // If they're not available on the transfer state (e.g. when running in dev mode), retrieve + // them using HttpClient + return this.http.get('' + this.prefix + lang + this.suffix, { responseType: 'text' }).pipe( + map((json: any) => JSON5.parse(json)) + ); + } + } +} diff --git a/src/ngx-translate-loaders/translate-json5-http.loader.ts b/src/ngx-translate-loaders/translate-json5-http.loader.ts deleted file mode 100644 index b6759408ce..0000000000 --- a/src/ngx-translate-loaders/translate-json5-http.loader.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { TranslateLoader } from '@ngx-translate/core'; -import { map } from 'rxjs/operators'; -import * as JSON5 from 'json5'; - -export class TranslateJson5HttpLoader implements TranslateLoader { - constructor(private http: HttpClient, public prefix?: string, public suffix?: string) { - } - - getTranslation(lang: string): any { - return this.http.get('' + this.prefix + lang + this.suffix, {responseType: 'text'}).pipe( - map((json: any) => JSON5.parse(json)) - ); - } -} diff --git a/src/ngx-translate-loaders/translate-json5-universal.loader.ts b/src/ngx-translate-loaders/translate-json5-universal.loader.ts deleted file mode 100644 index 657be1012d..0000000000 --- a/src/ngx-translate-loaders/translate-json5-universal.loader.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TranslateLoader } from '@ngx-translate/core'; -import { Observable } from 'rxjs'; -import * as fs from 'fs'; - -const JSON5 = require('json5').default; - -export class TranslateJson5UniversalLoader implements TranslateLoader { - - constructor(private prefix: string = 'dist/assets/i18n/', private suffix: string = '.json') { } - - public getTranslation(lang: string): Observable { - return Observable.create((observer: any) => { - observer.next(JSON5.parse(fs.readFileSync(`${this.prefix}${lang}${this.suffix}`, 'utf8'))); - observer.complete(); - }); - } - -} diff --git a/src/ngx-translate-loaders/translate-server.loader.ts b/src/ngx-translate-loaders/translate-server.loader.ts new file mode 100644 index 0000000000..4ba6ccd5e9 --- /dev/null +++ b/src/ngx-translate-loaders/translate-server.loader.ts @@ -0,0 +1,52 @@ +import { TranslateLoader } from '@ngx-translate/core'; +import { Observable, of as observableOf } from 'rxjs'; +import * as fs from 'fs'; +import { TransferState } from '@angular/platform-browser'; +import { NGX_TRANSLATE_STATE, NgxTranslateState } from './ngx-translate-state'; + +const JSON5 = require('json5').default; + +/** + * A TranslateLoader for ngx-translate to parse json5 files server-side, and store them in the + * TransferState + */ +export class TranslateServerLoader implements TranslateLoader { + + constructor( + protected transferState: TransferState, + protected prefix: string = 'dist/assets/i18n/', + protected suffix: string = '.json' + ) { + } + + /** + * Return the i18n messages for a given language, and store them in the TransferState + * + * @param lang the language code + */ + public getTranslation(lang: string): Observable { + // Retrieve the file for the given language, and parse it + const messages = JSON5.parse(fs.readFileSync(`${this.prefix}${lang}${this.suffix}`, 'utf8')); + // Store the parsed messages in the transfer state so they'll be available immediately when the + // app loads on the client + this.storeInTransferState(lang, messages); + // Return the parsed messages to translate things server side + return observableOf(messages); + } + + /** + * Store the i18n messages for the given language code in the transfer state, so they can be + * retrieved client side + * + * @param lang the language code + * @param messages the i18n messages + * @protected + */ + protected storeInTransferState(lang: string, messages) { + const prevState = this.transferState.get(NGX_TRANSLATE_STATE, {}); + const nextState = Object.assign({}, prevState, { + [lang]: messages + }); + this.transferState.set(NGX_TRANSLATE_STATE, nextState); + } +} diff --git a/src/styles/_bootstrap_variables.scss b/src/styles/_bootstrap_variables.scss index 4c631a294a..20c0b3a679 100644 --- a/src/styles/_bootstrap_variables.scss +++ b/src/styles/_bootstrap_variables.scss @@ -6,7 +6,9 @@ $sidebar-items-width: 250px !default; $total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width !default; /* Fonts */ -$fa-font-path: "/assets/fonts" !default; +// Starting this url with a caret (^) allows it to be a relative path based on UI's deployment path +// See https://github.com/angular/angular-cli/issues/12797#issuecomment-598534241 +$fa-font-path: "^assets/fonts" !default; /* Images */ $image-path: "../assets/images" !default; diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss index 1bd2ec7b0a..89d1d76e9a 100644 --- a/src/styles/_global-styles.scss +++ b/src/styles/_global-styles.scss @@ -133,3 +133,8 @@ ds-dynamic-form-control-container.d-none { */ visibility: collapse; } + +/* Used for dso administrative functionality */ +.btn-dark { + background-color: var(--ds-admin-sidebar-bg); +} diff --git a/src/styles/base-theme.scss b/src/styles/base-theme.scss index bde50bcfd7..a88057fec1 100644 --- a/src/styles/base-theme.scss +++ b/src/styles/base-theme.scss @@ -5,3 +5,4 @@ @import './bootstrap_variables_mapping.scss'; @import './_truncatable-part.component.scss'; @import './_global-styles.scss'; +@import '../../node_modules/ngx-ui-switch/ui-switch.component.scss'; diff --git a/src/test.ts b/src/test.ts index dfafd98807..477195418b 100644 --- a/src/test.ts +++ b/src/test.ts @@ -7,6 +7,7 @@ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; declare const require: any; @@ -17,9 +18,11 @@ getTestBed().initTestEnvironment( { teardown: { destroyAfterEach: false } } ); -// If store is mocked, reset state after each test (see https://ngrx.io/guide/migration/v13) jasmine.getEnv().afterEach(() => { + // If store is mocked, reset state after each test (see https://ngrx.io/guide/migration/v13) getTestBed().inject(MockStore, null)?.resetSelectors(); + // Close any leftover modals + getTestBed().inject(NgbModal, null)?.dismissAll?.(); }); // Then we find all the tests. diff --git a/src/themes/custom/eager-theme.module.ts b/src/themes/custom/eager-theme.module.ts new file mode 100644 index 0000000000..fdbdba1038 --- /dev/null +++ b/src/themes/custom/eager-theme.module.ts @@ -0,0 +1,58 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { SharedModule } from '../../app/shared/shared.module'; +import { HomeNewsComponent } from './app/home-page/home-news/home-news.component'; +import { NavbarComponent } from './app/navbar/navbar.component'; +import { HeaderComponent } from './app/header/header.component'; +import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component'; +import { SearchModule } from '../../app/shared/search/search.module'; +import { RootModule } from '../../app/root.module'; +import { NavbarModule } from '../../app/navbar/navbar.module'; +import { PublicationComponent } from './app/item-page/simple/item-types/publication/publication.component'; +import { ItemPageModule } from '../../app/item-page/item-page.module'; +import { FooterComponent } from './app/footer/footer.component'; + +/** + * Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS. + * This will ensure that decorator gets picked up when the app loads + */ +const ENTRY_COMPONENTS = [ + PublicationComponent, +]; + +const DECLARATIONS = [ + ...ENTRY_COMPONENTS, + HomeNewsComponent, + HeaderComponent, + HeaderNavbarWrapperComponent, + NavbarComponent, + FooterComponent, +]; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + SearchModule, + FormsModule, + RootModule, + NavbarModule, + ItemPageModule, + ], + declarations: DECLARATIONS, + providers: [ + ...ENTRY_COMPONENTS.map((component) => ({ provide: component })) + ], +}) +/** + * This module is included in the main bundle that gets downloaded at first page load. So it should + * contain only the themed components that have to be available immediately for the first page load, + * and the minimal set of imports required to make them work. Anything you can cut from it will make + * the initial page load faster, but may cause the page to flicker as components that were already + * rendered server side need to be lazy-loaded again client side + * + * Themed EntryComponents should also be added here + */ +export class EagerThemeModule { +} diff --git a/src/themes/custom/entry-components.ts b/src/themes/custom/entry-components.ts deleted file mode 100644 index b518e4cc45..0000000000 --- a/src/themes/custom/entry-components.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PublicationComponent } from './app/item-page/simple/item-types/publication/publication.component'; - -export const ENTRY_COMPONENTS = [ - PublicationComponent -]; diff --git a/src/themes/custom/theme.module.ts b/src/themes/custom/lazy-theme.module.ts similarity index 91% rename from src/themes/custom/theme.module.ts rename to src/themes/custom/lazy-theme.module.ts index dea61daf0a..874dff90d3 100644 --- a/src/themes/custom/theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -28,20 +28,28 @@ 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 { HomePageComponent } from './app/home-page/home-page.component'; import { HomePageModule } from '../../app/home-page/home-page.module'; -import { RootComponent } from './app/root/root.component'; import { AppModule } from '../../app/app.module'; -import { PublicationComponent } from './app/item-page/simple/item-types/publication/publication.component'; import { ItemPageModule } from '../../app/item-page/item-page.module'; import { RouterModule } from '@angular/router'; -import { AccessControlModule } from '../../app/access-control/access-control.module'; +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 { SearchModule } from '../../app/shared/search/search.module'; +import { ResourcePoliciesModule } from '../../app/shared/resource-policies/resource-policies.module'; +import { ComcolModule } from '../../app/shared/comcol/comcol.module'; +import { RootModule } from '../../app/root.module'; +import { FileSectionComponent } from './app/item-page/simple/field-components/file-section/file-section.component'; +import { HomePageComponent } from './app/home-page/home-page.component'; +import { RootComponent } from './app/root/root.component'; import { BrowseBySwitcherComponent } from './app/browse-by/browse-by-switcher/browse-by-switcher.component'; import { CommunityListPageComponent } from './app/community-list-page/community-list-page.component'; -import { CommunityListPageModule } from '../../app/community-list-page/community-list-page.module'; import { SearchPageComponent } from './app/search-page/search-page.component'; -import { InfoModule } from '../../app/info/info.module'; +import { ConfigurationSearchPageComponent } from './app/search-page/configuration-search-page.component'; import { EndUserAgreementComponent } from './app/info/end-user-agreement/end-user-agreement.component'; import { PageNotFoundComponent } from './app/pagenotfound/pagenotfound.component'; import { ObjectNotFoundComponent } from './app/lookup-by-id/objectnotfound/objectnotfound.component'; @@ -49,14 +57,10 @@ import { ForbiddenComponent } from './app/forbidden/forbidden.component'; import { PrivacyComponent } from './app/info/privacy/privacy.component'; import { CollectionStatisticsPageComponent } from './app/statistics-page/collection-statistics-page/collection-statistics-page.component'; import { CommunityStatisticsPageComponent } from './app/statistics-page/community-statistics-page/community-statistics-page.component'; -import { StatisticsPageModule } from '../../app/statistics-page/statistics-page.module'; import { ItemStatisticsPageComponent } from './app/statistics-page/item-statistics-page/item-statistics-page.component'; import { SiteStatisticsPageComponent } from './app/statistics-page/site-statistics-page/site-statistics-page.component'; import { CommunityPageComponent } from './app/community-page/community-page.component'; import { CollectionPageComponent } from './app/collection-page/collection-page.component'; -import { CommunityPageModule } from '../../app/community-page/community-page.module'; -import { CollectionPageModule } from '../../app/collection-page/collection-page.module'; -import { ConfigurationSearchPageComponent } from './app/search-page/configuration-search-page.component'; import { ItemPageComponent } from './app/item-page/simple/item-page.component'; import { FullItemPageComponent } from './app/item-page/full/full-item-page.component'; import { LoginPageComponent } from './app/login-page/login-page.component'; @@ -66,32 +70,21 @@ import { ForgotEmailComponent } from './app/forgot-password/forgot-password-emai import { ForgotPasswordFormComponent } from './app/forgot-password/forgot-password-form/forgot-password-form.component'; import { ProfilePageComponent } from './app/profile-page/profile-page.component'; import { RegisterEmailComponent } from './app/register-page/register-email/register-email.component'; +import { MyDSpacePageComponent } from './app/my-dspace-page/my-dspace-page.component'; import { SubmissionEditComponent } from './app/submission/edit/submission-edit.component'; import { SubmissionImportExternalComponent } from './app/submission/import-external/submission-import-external.component'; import { SubmissionSubmitComponent } from './app/submission/submit/submission-submit.component'; -import { MyDSpacePageComponent } from './app/my-dspace-page/my-dspace-page.component'; -import { WorkflowItemSendBackComponent } from './app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component'; import { WorkflowItemDeleteComponent } from './app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component'; -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'; -import { HeaderComponent } from './app/header/header.component'; -import { FooterComponent } from './app/footer/footer.component'; +import { WorkflowItemSendBackComponent } from './app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component'; import { BreadcrumbsComponent } from './app/breadcrumbs/breadcrumbs.component'; -import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component'; -import { FileSectionComponent } from './app/item-page/simple/field-components/file-section/file-section.component'; -import { SearchModule } from '../../app/shared/search/search.module'; -import { ResourcePoliciesModule } from '../../app/shared/resource-policies/resource-policies.module'; -import { ComcolModule } from '../../app/shared/comcol/comcol.module'; import { FeedbackComponent } from './app/info/feedback/feedback.component'; import { CommunityListComponent } from './app/community-list-page/community-list/community-list.component'; + const DECLARATIONS = [ FileSectionComponent, HomePageComponent, - HomeNewsComponent, RootComponent, - PublicationComponent, BrowseBySwitcherComponent, CommunityListPageComponent, SearchPageComponent, @@ -122,22 +115,18 @@ const DECLARATIONS = [ SubmissionSubmitComponent, WorkflowItemDeleteComponent, WorkflowItemSendBackComponent, - FooterComponent, - HeaderComponent, - NavbarComponent, - HeaderNavbarWrapperComponent, BreadcrumbsComponent, FeedbackComponent, - CommunityListComponent + CommunityListComponent, ]; @NgModule({ imports: [ - AccessControlModule, AdminRegistriesModule, AdminSearchModule, AdminWorkflowModuleModule, AppModule, + RootModule, BitstreamFormatsModule, BrowseByModule, CollectionFormModule, @@ -178,9 +167,9 @@ const DECLARATIONS = [ SearchModule, FormsModule, ResourcePoliciesModule, - ComcolModule + ComcolModule, ], - declarations: DECLARATIONS + declarations: DECLARATIONS, }) /** @@ -190,5 +179,5 @@ const DECLARATIONS = [ * 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 { +class LazyThemeModule { } diff --git a/src/themes/custom/styles/theme.scss b/src/themes/custom/styles/theme.scss index 35810b15a6..ad2c0de24e 100644 --- a/src/themes/custom/styles/theme.scss +++ b/src/themes/custom/styles/theme.scss @@ -11,3 +11,4 @@ @import '../../../styles/bootstrap_variables_mapping.scss'; @import '../../../styles/_truncatable-part.component.scss'; @import './_global-styles.scss'; +@import '../../../../node_modules/ngx-ui-switch/ui-switch.component.scss'; 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 index 92ce1ba020..ef576ed99c 100644 --- 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 @@ -1,4 +1,4 @@ -
+
@@ -30,5 +30,10 @@
+ + + + + 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 index 5e89f6b62f..93ec1763f3 100644 --- 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 @@ -2,12 +2,21 @@ display: block; margin-top: calc(var(--ds-content-spacing) * -1); - div.background-image { + div.background-image-container { color: white; - background-color: var(--bs-info); position: relative; - background-image: url('/assets/dspace/images/banner.jpg'); - background-size: cover; + + .background-image > img { + background-color: var(--bs-info); + position: absolute; + z-index: -1; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: top; + } .container { position: relative; diff --git a/src/themes/dspace/assets/images/banner-half.jpg b/src/themes/dspace/assets/images/banner-half.jpg new file mode 100644 index 0000000000..31610cc350 Binary files /dev/null and b/src/themes/dspace/assets/images/banner-half.jpg differ diff --git a/src/themes/dspace/assets/images/banner-half.webp b/src/themes/dspace/assets/images/banner-half.webp new file mode 100644 index 0000000000..f11cfb7859 Binary files /dev/null and b/src/themes/dspace/assets/images/banner-half.webp differ diff --git a/src/themes/dspace/assets/images/banner-tall.jpg b/src/themes/dspace/assets/images/banner-tall.jpg new file mode 100644 index 0000000000..d310311296 Binary files /dev/null and b/src/themes/dspace/assets/images/banner-tall.jpg differ diff --git a/src/themes/dspace/assets/images/banner-tall.webp b/src/themes/dspace/assets/images/banner-tall.webp new file mode 100644 index 0000000000..a4ec97f2bc Binary files /dev/null and b/src/themes/dspace/assets/images/banner-tall.webp differ diff --git a/src/themes/dspace/assets/images/banner.webp b/src/themes/dspace/assets/images/banner.webp new file mode 100644 index 0000000000..7745766f05 Binary files /dev/null and b/src/themes/dspace/assets/images/banner.webp differ diff --git a/src/themes/dspace/eager-theme.module.ts b/src/themes/dspace/eager-theme.module.ts new file mode 100644 index 0000000000..5dd114cd72 --- /dev/null +++ b/src/themes/dspace/eager-theme.module.ts @@ -0,0 +1,52 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { SharedModule } from '../../app/shared/shared.module'; +import { HomeNewsComponent } from './app/home-page/home-news/home-news.component'; +import { NavbarComponent } from './app/navbar/navbar.component'; +import { HeaderComponent } from './app/header/header.component'; +import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component'; +import { SearchModule } from '../../app/shared/search/search.module'; +import { RootModule } from '../../app/root.module'; +import { NavbarModule } from '../../app/navbar/navbar.module'; + +/** + * Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS. + * This will ensure that decorator gets picked up when the app loads + */ +const ENTRY_COMPONENTS = [ +]; + +const DECLARATIONS = [ + ...ENTRY_COMPONENTS, + HomeNewsComponent, + HeaderComponent, + HeaderNavbarWrapperComponent, + NavbarComponent, +]; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + SearchModule, + FormsModule, + RootModule, + NavbarModule, + ], + declarations: DECLARATIONS, + providers: [ + ...ENTRY_COMPONENTS.map((component) => ({ provide: component })) + ], +}) +/** + * This module is included in the main bundle that gets downloaded at first page load. So it should + * contain only the themed components that have to be available immediately for the first page load, + * and the minimal set of imports required to make them work. Anything you can cut from it will make + * the initial page load faster, but may cause the page to flicker as components that were already + * rendered server side need to be lazy-loaded again client side + * + * Themed EntryComponents should also be added here + */ +export class EagerThemeModule { +} diff --git a/src/themes/dspace/entry-components.ts b/src/themes/dspace/entry-components.ts deleted file mode 100644 index 2386ecb130..0000000000 --- a/src/themes/dspace/entry-components.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const ENTRY_COMPONENTS = [ -]; diff --git a/src/themes/dspace/theme.module.ts b/src/themes/dspace/lazy-theme.module.ts similarity index 70% rename from src/themes/dspace/theme.module.ts rename to src/themes/dspace/lazy-theme.module.ts index 2a774eb9c8..a4e8027a15 100644 --- a/src/themes/dspace/theme.module.ts +++ b/src/themes/dspace/lazy-theme.module.ts @@ -2,10 +2,16 @@ 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 { + 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 { + 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'; @@ -13,14 +19,18 @@ import { EditItemPageModule } from '../../app/item-page/edit-item-page/edit-item 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 { + 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 { + 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'; @@ -28,7 +38,6 @@ 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'; @@ -40,18 +49,14 @@ import { CommunityPageModule } from '../../app/community-page/community-page.mod 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'; -import { HeaderComponent } from './app/header/header.component'; -import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component'; import { SearchModule } from '../../app/shared/search/search.module'; -import { ResourcePoliciesModule } from '../../app/shared/resource-policies/resource-policies.module'; +import { + ResourcePoliciesModule +} from '../../app/shared/resource-policies/resource-policies.module'; import { ComcolModule } from '../../app/shared/comcol/comcol.module'; +import { RootModule } from '../../app/root.module'; const DECLARATIONS = [ - HomeNewsComponent, - HeaderComponent, - HeaderNavbarWrapperComponent, - NavbarComponent ]; @NgModule({ @@ -60,6 +65,7 @@ const DECLARATIONS = [ AdminSearchModule, AdminWorkflowModuleModule, AppModule, + RootModule, BitstreamFormatsModule, BrowseByModule, CollectionFormModule, @@ -100,17 +106,17 @@ const DECLARATIONS = [ SearchModule, FormsModule, ResourcePoliciesModule, - ComcolModule + ComcolModule, ], - declarations: DECLARATIONS + 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 { +/** + * 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 LazyThemeModule { } diff --git a/src/themes/dspace/styles/theme.scss b/src/themes/dspace/styles/theme.scss index 35810b15a6..ad2c0de24e 100644 --- a/src/themes/dspace/styles/theme.scss +++ b/src/themes/dspace/styles/theme.scss @@ -11,3 +11,4 @@ @import '../../../styles/bootstrap_variables_mapping.scss'; @import '../../../styles/_truncatable-part.component.scss'; @import './_global-styles.scss'; +@import '../../../../node_modules/ngx-ui-switch/ui-switch.component.scss'; diff --git a/src/themes/eager-themes.module.ts b/src/themes/eager-themes.module.ts new file mode 100644 index 0000000000..4a46595f35 --- /dev/null +++ b/src/themes/eager-themes.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { EagerThemeModule as DSpaceEagerThemeModule } from './dspace/eager-theme.module'; +// import { EagerThemeModule as CustomEagerThemeModule } from './custom/eager-theme.module'; + +/** + * This module bundles the eager theme modules for all available themes. + * Eager modules contain components that are present on every page (to speed up initial loading) + * and entry components (to ensure their decorators get picked up). + * + * Themes that aren't in use should not be imported here so they don't take up unnecessary space in the main bundle. + */ +@NgModule({ + imports: [ + DSpaceEagerThemeModule, + // CustomEagerThemeModule, + ], +}) +export class EagerThemesModule { +} diff --git a/src/themes/themed-entry-component.module.ts b/src/themes/themed-entry-component.module.ts deleted file mode 100644 index 41cdf62269..0000000000 --- a/src/themes/themed-entry-component.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NgModule } from '@angular/core'; -import { ENTRY_COMPONENTS as CUSTOM } from './custom/entry-components'; - -const ENTRY_COMPONENTS = [ - ...CUSTOM, -]; - - -/** - * This module only serves to ensure themed entry components are discoverable. It's kept separate - * from the theme modules to ensure only the minimal number of theme components is loaded ahead of - * time - */ -@NgModule() -export class ThemedEntryComponentModule { - static withEntryComponents() { - return { - ngModule: ThemedEntryComponentModule, - providers: ENTRY_COMPONENTS.map((component) => ({provide: component})) - }; - } - -} diff --git a/webpack/webpack.browser.ts b/webpack/webpack.browser.ts index 5ed4f27e41..2c33d91afd 100644 --- a/webpack/webpack.browser.ts +++ b/webpack/webpack.browser.ts @@ -3,12 +3,37 @@ import { join } from 'path'; import { buildAppConfig } from '../src/config/config.server'; import { commonExports } from './webpack.common'; +const CompressionPlugin = require('compression-webpack-plugin'); +const zlib = require('zlib'); + module.exports = Object.assign({}, commonExports, { target: 'web', + plugins: [ + ...commonExports.plugins, + new CompressionPlugin({ + filename: '[path][base].gz', + algorithm: 'gzip', + test: /\.(js|css|html|svg|json5)$/, + threshold: 10240, + minRatio: 0.8, + }), + new CompressionPlugin({ + filename: '[path][base].br', + algorithm: 'brotliCompress', + test: /\.(js|css|html|svg|json5)$/, + compressionOptions: { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 11, + }, + }, + threshold: 10240, + minRatio: 0.8, + }), + ], devServer: { setupMiddlewares(middlewares, server) { buildAppConfig(join(process.cwd(), 'src/assets/config.json')); return middlewares; } - } + } }); diff --git a/yarn.lock b/yarn.lock index dd2798eb49..f9352e8f05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3155,11 +3155,6 @@ async-each-series@0.1.1: resolved "https://registry.yarnpkg.com/async-each-series/-/async-each-series-0.1.1.tgz#7617c1917401fd8ca4a28aadce3dbae98afeb432" integrity sha1-dhfBkXQB/Yykooqtzj266Yr+tDI= -async@0.9.x: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - async@1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -3172,7 +3167,7 @@ async@^2.6.2: dependencies: lodash "^4.17.14" -async@^3.2.0: +async@^3.2.0, async@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== @@ -3226,6 +3221,14 @@ axios@0.21.4: dependencies: follow-redirects "^1.14.0" +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -3460,6 +3463,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -4111,17 +4121,13 @@ compressible@~2.0.16: dependencies: mime-db ">= 1.43.0 < 2" -compression-webpack-plugin@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-3.1.0.tgz#9f510172a7b5fae5aad3b670652e8bd7997aeeca" - integrity sha512-iqTHj3rADN4yHwXMBrQa/xrncex/uEQy8QHlaTKxGchT/hC0SdlJlmL/5eRqffmWq2ep0/Romw6Ld39JjTR/ug== +compression-webpack-plugin@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-9.2.0.tgz#57fd539d17c5907eebdeb4e83dcfe2d7eceb9ef6" + integrity sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw== dependencies: - cacache "^13.0.1" - find-cache-dir "^3.0.0" - neo-async "^2.5.0" - schema-utils "^2.6.1" - serialize-javascript "^2.1.2" - webpack-sources "^1.0.1" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" compression@^1.7.4: version "1.7.4" @@ -4139,7 +4145,7 @@ compression@^1.7.4: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== configstore@^5.0.1: version "5.0.1" @@ -4916,6 +4922,11 @@ dependency-graph@^0.11.0: resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -5138,11 +5149,11 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= ejs@^3.1.5: - version "3.1.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" - integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== + version "3.1.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" + integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== dependencies: - jake "^10.6.1" + jake "^10.8.5" electron-to-chromium@^1.4.71: version "1.4.75" @@ -5819,6 +5830,13 @@ express-rate-limit@^5.1.3: resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.5.1.tgz#110c23f6a65dfa96ab468eda95e71697bc6987a2" integrity sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg== +express-static-gzip@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-2.1.5.tgz#e45b512ef5596a068c45f729d7e0cc0b429b08b4" + integrity sha512-bgiQ1fY7ltuUrSzg0WoN7ycoAd7r2VEw7durn/3k0jCMUC5wydF0K36ouIuJPE+MNDwK5uoSaVgIBVNemwxWgw== + dependencies: + serve-static "^1.14.1" + express@^4.17.1: version "4.17.3" resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" @@ -6017,11 +6035,11 @@ file-saver@^2.0.5: integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== filelist@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" - integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== dependencies: - minimatch "^3.0.4" + minimatch "^5.0.1" filesize@^6.1.0: version "6.4.0" @@ -6071,7 +6089,7 @@ finalhandler@1.1.2, finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@^3.0.0, find-cache-dir@^3.3.1: +find-cache-dir@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== @@ -6108,7 +6126,7 @@ flatted@^3.1.0, flatted@^3.2.5: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== -follow-redirects@^1.0.0, follow-redirects@^1.14.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.9: version "1.14.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== @@ -7516,12 +7534,12 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jake@^10.6.1: - version "10.8.4" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.4.tgz#f6a8b7bf90c6306f768aa82bb7b98bf4ca15e84a" - integrity sha512-MtWeTkl1qGsWUtbl/Jsca/8xSoK3x0UmS82sNbjqxxG/de/M/3b1DntdjHgPMC50enlTNwXOCRqPXLLt5cCfZA== +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== dependencies: - async "0.9.x" + async "^3.2.3" chalk "^4.0.2" filelist "^1.0.1" minimatch "^3.0.4" @@ -8554,10 +8572,17 @@ minimatch@^3.0.2, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== minipass-collect@^1.0.2: version "1.0.2" @@ -8716,10 +8741,10 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment@^2.29.1: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== +moment@^2.29.2: + version "2.29.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" + integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== morgan@^1.10.0: version "1.10.0" @@ -8823,7 +8848,7 @@ negotiator@0.6.3, negotiator@^0.6.2, negotiator@^0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -neo-async@^2.5.0, neo-async@^2.6.2: +neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== @@ -8884,11 +8909,6 @@ ngx-ui-switch@^11.0.1: resolved "https://registry.yarnpkg.com/ngx-ui-switch/-/ngx-ui-switch-11.0.1.tgz#c7f1e97ebe698f827a26f49951b50492b22c7839" integrity sha512-N8QYT/wW+xJdyh/aeebTSLPA6Sgrwp69H6KAcW0XZueg/LF+FKiqyG6Po/gFHq2gDhLikwyJEMpny8sudTI08w== -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - nice-napi@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nice-napi/-/nice-napi-1.0.2.tgz#dc0ab5a1eac20ce548802fc5686eaa6bc654927b" @@ -8918,9 +8938,9 @@ node-fetch@^2.6.1: whatwg-url "^5.0.0" node-forge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" - integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== node-gyp-build@^4.2.2: version "4.3.0" @@ -9203,6 +9223,13 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -11275,7 +11302,7 @@ schema-utils@2.7.0: ajv "^6.12.2" ajv-keywords "^3.4.1" -schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6: +schema-utils@^2.6.5, schema-utils@^2.6.6: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== @@ -11399,10 +11426,24 @@ send@0.17.2: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" - integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" serialize-javascript@^4.0.0: version "4.0.0" @@ -11458,6 +11499,16 @@ serve-static@1.14.2: parseurl "~1.3.3" send "0.17.2" +serve-static@^1.14.1: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + server-destroy@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/server-destroy/-/server-destroy-1.0.1.tgz#f13bf928e42b9c3e79383e61cc3998b5d14e6cdd" @@ -13028,7 +13079,7 @@ webpack-merge@5.8.0, webpack-merge@^5.7.3: clone-deep "^4.0.1" wildcard "^2.0.0" -webpack-sources@^1.0.1, webpack-sources@^1.4.3: +webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==