diff --git a/Dockerfile.dist b/Dockerfile.dist index 4c47b0cb406..98e2e32ab54 100644 --- a/Dockerfile.dist +++ b/Dockerfile.dist @@ -2,7 +2,7 @@ # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details # Test build: -# docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist . +# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-8_x-dist . FROM node:18-alpine AS build diff --git a/angular.json b/angular.json index 5f0204249b1..02fd69b1e1b 100644 --- a/angular.json +++ b/angular.json @@ -30,7 +30,6 @@ "lodash", "jwt-decode", "uuid", - "webfontloader", "zone.js" ], "outputPath": "dist/browser", diff --git a/config/config.example.yml b/config/config.example.yml index 6c3c900f6ab..25949ba4918 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1,7 +1,7 @@ # NOTE: will log all redux actions and transfers in console debug: false -# Angular Universal server settings +# Angular User Inteface settings # NOTE: these settings define where Node.js will start your UI application. Therefore, these # "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar) ui: @@ -17,12 +17,12 @@ ui: # Trust X-FORWARDED-* headers from proxies (default = true) useProxies: true -universal: - # Whether to inline "critical" styles into the server-side rendered HTML. - # Determining which styles are critical is a relatively expensive operation; - # this option can be disabled to boost server performance at the expense of - # loading smoothness. - inlineCriticalCss: true +# Angular Server Side Rendering (SSR) settings +ssr: + # Whether to tell Angular to inline "critical" styles into the server-side rendered HTML. + # Determining which styles are critical is a relatively expensive operation; this option is + # disabled (false) by default to boost server performance at the expense of loading smoothness. + inlineCriticalCss: false # The REST API server settings # NOTE: these settings define which (publicly available) REST API to use. They are usually diff --git a/cypress/e2e/header.cy.ts b/cypress/e2e/header.cy.ts index 043d67dd2b9..1471e5ae6c5 100644 --- a/cypress/e2e/header.cy.ts +++ b/cypress/e2e/header.cy.ts @@ -10,4 +10,29 @@ describe('Header', () => { // Analyze for accessibility testA11y('ds-header'); }); + + it('should allow for changing language to German (for example)', () => { + cy.visit('/'); + + // Click the language switcher (globe) in header + cy.get('a[data-test="lang-switch"]').click(); + // Click on the "Deusch" language in dropdown + cy.get('#language-menu-list li').contains('Deutsch').click(); + + // HTML "lang" attribute should switch to "de" + cy.get('html').invoke('attr', 'lang').should('eq', 'de'); + + // Login menu should now be in German + cy.get('a[data-test="login-menu"]').contains('Anmelden'); + + // Change back to English from language switcher + cy.get('a[data-test="lang-switch"]').click(); + cy.get('#language-menu-list li').contains('English').click(); + + // HTML "lang" attribute should switch to "en" + cy.get('html').invoke('attr', 'lang').should('eq', 'en'); + + // Login menu should now be in English + cy.get('a[data-test="login-menu"]').contains('Log In'); + }); }); diff --git a/docker/README.md b/docker/README.md index 3dc5fd50550..6360124b601 100644 --- a/docker/README.md +++ b/docker/README.md @@ -23,14 +23,14 @@ the Docker compose scripts in this 'docker' folder. This Dockerfile is used to build a *development* DSpace Angular UI image, published as 'dspace/dspace-angular' ``` -docker build -t dspace/dspace-angular:latest . +docker build -t dspace/dspace-angular:dspace-8_x . ``` This image is built *automatically* after each commit is made to the `main` branch. Admins to our DockerHub repo can manually publish with the following command. ``` -docker push dspace/dspace-angular:latest +docker push dspace/dspace-angular:dspace-8_x ``` ### Dockerfile.dist @@ -39,7 +39,7 @@ The `Dockerfile.dist` is used to generate a *production* build and runtime envir ```bash # build the latest image -docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist . +docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-8_x-dist . ``` A default/demo version of this image is built *automatically*. diff --git a/docker/cli.yml b/docker/cli.yml index 9b1973426f3..93fb0f5ee2c 100644 --- a/docker/cli.yml +++ b/docker/cli.yml @@ -14,14 +14,14 @@ # Therefore, it should be kept in sync with that file networks: # Default to using network named 'dspacenet' from docker-compose-rest.yml. - # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet") + # Its full name will be prepended with the project name (e.g. "-p d8" means it will be named "d8_dspacenet") # If COMPOSITE_PROJECT_NAME is missing, default value will be "docker" (name of folder this file is in) default: name: ${COMPOSE_PROJECT_NAME:-docker}_dspacenet external: true services: dspace-cli: - image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}" + image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-8_x}" container_name: dspace-cli environment: # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. diff --git a/docker/db.entities.yml b/docker/db.entities.yml index b3cf5bd86f4..17414eb9920 100644 --- a/docker/db.entities.yml +++ b/docker/db.entities.yml @@ -14,10 +14,11 @@ # # Therefore, it should be kept in sync with that file services: dspacedb: - image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql" + image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql" environment: # This LOADSQL should be kept in sync with the URL in DSpace/DSpace # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data + # NOTE: currently there is no dspace8 version - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql dspace: ### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' #### diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index c5c419a4a74..6ff2a509c9f 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -33,7 +33,7 @@ services: # This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly. solr__D__statistics__P__autoCommit: 'false' LOGGING_CONFIG: /dspace/config/log4j2-container.xml - image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" + image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-8_x-test}" depends_on: - dspacedb networks: @@ -60,11 +60,12 @@ services: # NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data dspacedb: container_name: dspacedb - image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql" + image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql" environment: # This LOADSQL should be kept in sync with the LOADSQL in # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data + # NOTE: currently there is no dspace8 version LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql PGDATA: /pgdata POSTGRES_PASSWORD: dspace @@ -81,7 +82,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" + image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-8_x}" networks: - dspacenet ports: diff --git a/docker/docker-compose-dist.yml b/docker/docker-compose-dist.yml index 67eba167852..de28699c013 100644 --- a/docker/docker-compose-dist.yml +++ b/docker/docker-compose-dist.yml @@ -26,7 +26,7 @@ services: DSPACE_REST_HOST: sandbox.dspace.org DSPACE_REST_PORT: 443 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist + image: dspace/dspace-angular:${DSPACE_VER:-dspace-8_x}-dist build: context: .. dockerfile: Dockerfile.dist diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index 09dfcf2a5f7..a35bfdfd099 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -40,7 +40,7 @@ services: # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. proxies__P__trusted__P__ipranges: '172.23.0' LOGGING_CONFIG: /dspace/config/log4j2-container.xml - image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" + image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-8_x-test}" depends_on: - dspacedb networks: @@ -68,7 +68,7 @@ services: dspacedb: container_name: dspacedb # Uses a custom Postgres image with pgcrypto installed - image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}" + image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}" environment: PGDATA: /pgdata POSTGRES_PASSWORD: dspace @@ -85,7 +85,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" + image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-8_x}" networks: - dspacenet ports: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1c268b84b7b..e98f6f9599d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -23,7 +23,7 @@ services: DSPACE_REST_HOST: localhost DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:${DSPACE_VER:-latest} + image: dspace/dspace-angular:${DSPACE_VER:-dspace-8_x} build: context: .. dockerfile: Dockerfile diff --git a/package.json b/package.json index e20aceacdd6..47494d87a4e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "preserve": "yarn base-href", "serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts", "serve:ssr": "node dist/server/main", - "analyze": "webpack-bundle-analyzer dist/browser/stats.json", "build": "ng build --configuration development", "build:stats": "ng build --stats-json", "build:prod": "cross-env NODE_ENV=production yarn run build:ssr", @@ -55,11 +54,6 @@ "https": false }, "private": true, - "resolutions": { - "minimist": "^1.2.5", - "webdriver-manager": "^12.1.8", - "ts-node": "10.2.1" - }, "dependencies": { "@angular/animations": "^17.3.11", "@angular/cdk": "^17.3.10", @@ -67,16 +61,14 @@ "@angular/compiler": "^17.3.11", "@angular/core": "^17.3.11", "@angular/forms": "^17.3.11", - "@angular/localize": "17.3.11", + "@angular/localize": "17.3.12", "@angular/platform-browser": "^17.3.11", "@angular/platform-browser-dynamic": "^17.3.11", "@angular/platform-server": "^17.3.11", "@angular/router": "^17.3.11", - "@angular/ssr": "^17.3.8", - "@babel/runtime": "7.21.0", + "@angular/ssr": "^17.3.11", + "@babel/runtime": "7.26.0", "@kolkov/ngx-gallery": "^2.0.1", - "@material-ui/core": "^4.11.0", - "@material-ui/icons": "^4.11.3", "@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-dynamic-forms/core": "^16.0.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0", @@ -85,26 +77,24 @@ "@ngrx/store": "^17.1.1", "@ngx-translate/core": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0", - "@types/grecaptcha": "^3.0.4", - "angular-idle-preload": "3.0.0", "angulartics2": "^12.2.0", "axios": "^1.7.4", "bootstrap": "^4.6.1", "cerialize": "0.1.18", "cli-progress": "^3.12.0", "colors": "^1.4.0", - "compression": "^1.7.4", - "cookie-parser": "1.4.6", + "compression": "^1.7.5", + "cookie-parser": "1.4.7", "core-js": "^3.30.1", "date-fns": "^2.29.3", "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", "ejs": "^3.1.10", - "express": "^4.20.0", + "express": "^4.21.1", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", "filesize": "^6.1.0", - "http-proxy-middleware": "^1.0.5", + "http-proxy-middleware": "^2.0.7", "http-terminator": "^3.2.0", "isbot": "^5.1.17", "js-cookie": "2.2.1", @@ -118,9 +108,8 @@ "markdown-it": "^13.0.1", "mirador": "^3.3.0", "mirador-dl-plugin": "^0.13.0", - "mirador-share-plugin": "^0.11.0", + "mirador-share-plugin": "^0.16.0", "morgan": "^1.10.0", - "ng-mocks": "^14.10.0", "ng2-file-upload": "5.0.0", "ng2-nouislider": "^2.0.0", "ngx-infinite-scroll": "^16.0.0", @@ -128,58 +117,55 @@ "ngx-skeleton-loader": "^9.0.0", "ngx-ui-switch": "^14.1.0", "nouislider": "^15.7.1", - "pem": "1.14.7", - "prop-types": "^15.8.1", - "react-copy-to-clipboard": "^5.1.0", - "reflect-metadata": "^0.1.13", + "pem": "1.14.8", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.0", - "sanitize-html": "^2.12.1", - "sortablejs": "1.15.0", "uuid": "^8.3.2", - "webfontloader": "1.6.28", - "zone.js": "~0.14.4" + "zone.js": "~0.15.0" }, "devDependencies": { "@angular-builders/custom-webpack": "~17.0.2", - "@angular-devkit/build-angular": "^17.3.8", - "@angular-eslint/builder": "17.2.1", - "@angular-eslint/bundled-angular-compiler": "17.2.1", - "@angular-eslint/eslint-plugin": "17.2.1", - "@angular-eslint/eslint-plugin-template": "17.2.1", - "@angular-eslint/schematics": "17.2.1", - "@angular-eslint/template-parser": "17.2.1", - "@angular/cli": "^17.3.8", + "@angular-devkit/build-angular": "^17.3.11", + "@angular-eslint/builder": "17.5.3", + "@angular-eslint/bundled-angular-compiler": "17.5.3", + "@angular-eslint/eslint-plugin": "17.5.3", + "@angular-eslint/eslint-plugin-template": "17.5.3", + "@angular-eslint/schematics": "17.5.3", + "@angular-eslint/template-parser": "17.5.3", + "@angular/cli": "^17.3.11", "@angular/compiler-cli": "^17.3.11", "@angular/language-service": "^17.3.11", "@cypress/schematic": "^1.5.0", "@fortawesome/fontawesome-free": "^6.4.0", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "^4.11.3", "@ngrx/store-devtools": "^17.1.1", - "@ngtools/webpack": "^16.2.12", - "@types/deep-freeze": "0.1.2", + "@ngtools/webpack": "^16.2.16", + "@types/deep-freeze": "0.1.5", "@types/ejs": "^3.1.2", "@types/express": "^4.17.17", + "@types/grecaptcha": "^3.0.9", "@types/jasmine": "~3.6.0", "@types/js-cookie": "2.2.6", - "@types/lodash": "^4.14.194", + "@types/lodash": "^4.17.13", "@types/node": "^14.14.9", - "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@typescript-eslint/rule-tester": "^7.2.0", "@typescript-eslint/utils": "^7.2.0", - "axe-core": "^4.7.2", - "browser-sync": "^3.0.0", + "axe-core": "^4.10.2", "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", "cross-env": "^7.0.3", - "cypress": "^13.15.0", + "csstype": "^3.1.3", + "cypress": "^13.15.1", "cypress-axe": "^1.5.0", "deep-freeze": "0.0.1", "eslint": "^8.39.0", "eslint-plugin-deprecation": "^1.4.1", "eslint-plugin-dspace-angular-html": "link:./lint/dist/src/rules/html", "eslint-plugin-dspace-angular-ts": "link:./lint/dist/src/rules/ts", - "eslint-plugin-import": "^2.27.5", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-import-newlines": "^1.3.1", "eslint-plugin-jsdoc": "^45.0.0", "eslint-plugin-jsonc": "^2.6.0", @@ -187,7 +173,7 @@ "eslint-plugin-rxjs": "^5.0.3", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unused-imports": "^3.2.0", - "express-static-gzip": "^2.1.7", + "express-static-gzip": "^2.1.8", "jasmine": "^3.8.0", "jasmine-core": "^3.8.0", "jasmine-marbles": "0.9.2", @@ -197,25 +183,24 @@ "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-mocha-reporter": "2.2.5", + "ng-mocks": "^14.13.1", "ngx-mask": "14.2.4", "nodemon": "^2.0.22", "postcss": "^8.4", - "postcss-apply": "0.12.0", "postcss-import": "^14.0.0", "postcss-loader": "^4.0.3", "postcss-preset-env": "^7.4.2", - "postcss-responsive-type": "1.0.0", + "prop-types": "^15.8.1", "react": "^16.14.0", + "react-copy-to-clipboard": "^5.1.0", "react-dom": "^16.14.0", "rimraf": "^3.0.2", - "rxjs-spy": "^8.0.2", - "sass": "~1.62.0", + "sass": "~1.80.6", "sass-loader": "^12.6.0", "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", - "typescript": "~5.3.3", - "webpack": "5.94.0", - "webpack-bundle-analyzer": "^4.8.0", + "typescript": "~5.4.5", + "webpack": "5.96.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" } diff --git a/postcss.config.js b/postcss.config.js index df092d1d39f..f8b9666b312 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,8 +1,6 @@ module.exports = { plugins: [ require('postcss-import')(), - require('postcss-preset-env')(), - require('postcss-apply')(), - require('postcss-responsive-type')() + require('postcss-preset-env')() ] }; diff --git a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts index 4bb89a85f4d..d6133f2a976 100644 --- a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts +++ b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts @@ -19,7 +19,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { ResourcePoliciesComponent } from '../../shared/resource-policies/resource-policies.component'; @Component({ - selector: 'ds-collection-authorizations', + selector: 'ds-bitstream-authorizations', templateUrl: './bitstream-authorizations.component.html', imports: [ ResourcePoliciesComponent, @@ -30,7 +30,7 @@ import { ResourcePoliciesComponent } from '../../shared/resource-policies/resour standalone: true, }) /** - * Component that handles the Collection Authorizations + * Component that handles the Bitstream Authorizations */ export class BitstreamAuthorizationsComponent implements OnInit { diff --git a/src/app/collection-page/create-collection-page/create-collection-page.component.html b/src/app/collection-page/create-collection-page/create-collection-page.component.html index 6ca55339247..5c1b7b32a58 100644 --- a/src/app/collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/collection-page/create-collection-page/create-collection-page.component.html @@ -1,8 +1,8 @@
-

{{ 'collection.create.sub-head' | translate:{ parent: dsoNameService.getName((parentRD$| async)?.payload) } }}

+

{{ 'collection.create.sub-head' | translate:{ parent: dsoNameService.getName((parentRD$| async)?.payload) } }}

- -

{{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}

+

{{ 'community.create.head' | translate }}

+

{{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}

diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index cb16cedb422..ef021123d42 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -7,7 +7,7 @@ import { import { Observable } from 'rxjs'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver'; +import { getItemPageLinksToFollow } from '../../item-page/item.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { ItemDataService } from '../data/item-data.service'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -24,7 +24,7 @@ export const itemBreadcrumbResolver: ResolveFn> = ( breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), dataService: ItemDataService = inject(ItemDataService), ): Observable> => { - const linksToFollow: FollowLinkConfig[] = ITEM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + const linksToFollow: FollowLinkConfig[] = getItemPageLinksToFollow() as FollowLinkConfig[]; return DSOBreadcrumbResolver( route, state, diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index f9999439e1e..f3328c2bedd 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -7,7 +7,6 @@ import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { createSuccessfulRemoteDataObject, @@ -18,7 +17,6 @@ import { createPaginatedList, getFirstUsedArgumentOfSpyMethod, } from '../../shared/testing/utils.test'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; import { RequestEntry } from '../data/request-entry.model'; import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; @@ -31,7 +29,6 @@ describe('BrowseService', () => { let scheduler: TestScheduler; let service: BrowseService; let requestService: RequestService; - let rdbService: RemoteDataBuildService; const browsesEndpointURL = 'https://rest.api/browses'; const halService: any = new HALEndpointServiceStub(browsesEndpointURL); @@ -129,7 +126,6 @@ describe('BrowseService', () => { halService, browseDefinitionDataService, hrefOnlyDataService, - rdbService, ); } @@ -141,11 +137,9 @@ describe('BrowseService', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(halService, 'getEndpoint').and .returnValue(hot('--a-', { a: browsesEndpointURL })); - spyOn(rdbService, 'buildList').and.callThrough(); }); it('should call BrowseDefinitionDataService to create the RemoteData Observable', () => { @@ -162,9 +156,7 @@ describe('BrowseService', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { @@ -215,7 +207,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions fires', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('--a-', { @@ -270,7 +261,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions doesn\'t fire', () => { it('should return undefined', () => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('----')); @@ -288,9 +278,7 @@ describe('BrowseService', () => { describe('getFirstItemFor', () => { beforeEach(() => { requestService = getMockRequestService(); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getFirstItemFor is called with a valid browse definition id', () => { diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 43f33f26e49..5fe06a700e5 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -6,6 +6,7 @@ import { startWith, } from 'rxjs/operators'; +import { environment } from '../../../environments/environment'; import { hasValue, hasValueOperator, @@ -16,7 +17,6 @@ import { followLink, FollowLinkConfig, } from '../../shared/utils/follow-link-config.model'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { SortDirection } from '../cache/models/sort-options.model'; import { HrefOnlyDataService } from '../data/href-only-data.service'; import { PaginatedList } from '../data/paginated-list.model'; @@ -38,9 +38,15 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { BrowseDefinitionDataService } from './browse-definition-data.service'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; -export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('thumbnail'), -]; +export function getBrowseLinksToFollow(): FollowLinkConfig[] { + const followLinks = [ + followLink('thumbnail'), + ]; + if (environment.item.showAccessStatuses) { + followLinks.push(followLink('accessStatus')); + } + return followLinks; +} /** * The service handling all browse requests @@ -67,7 +73,6 @@ export class BrowseService { protected halService: HALEndpointService, private browseDefinitionDataService: BrowseDefinitionDataService, private hrefOnlyDataService: HrefOnlyDataService, - private rdb: RemoteDataBuildService, ) { } @@ -117,7 +122,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail ) { - return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } @@ -165,7 +170,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail) { - return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 631ecd22097..553fd9b10af 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -174,20 +174,25 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { - const existing = state[action.payload.objectToCache._links.self.href] || {} as any; + const cacheLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink; + const existing = state[cacheLink] || {} as any; const newAltLinks = hasValue(action.payload.alternativeLink) ? [action.payload.alternativeLink] : []; - return Object.assign({}, state, { - [action.payload.objectToCache._links.self.href]: { - data: action.payload.objectToCache, - timeCompleted: action.payload.timeCompleted, - msToLive: action.payload.msToLive, - requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], - dependentRequestUUIDs: existing.dependentRequestUUIDs || [], - isDirty: isNotEmpty(existing.patches), - patches: existing.patches || [], - alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks], - } as ObjectCacheEntry, - }); + if (hasValue(cacheLink)) { + return Object.assign({}, state, { + [cacheLink]: { + data: action.payload.objectToCache, + timeCompleted: action.payload.timeCompleted, + msToLive: action.payload.msToLive, + requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], + dependentRequestUUIDs: existing.dependentRequestUUIDs || [], + isDirty: isNotEmpty(existing.patches), + patches: existing.patches || [], + alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks], + } as ObjectCacheEntry, + }); + } else { + return state; + } } /** diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 69242732a50..f645b5a878d 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -99,7 +99,9 @@ export class ObjectCacheService { * An optional alternative link to this object */ add(object: CacheableObject, msToLive: number, requestUUID: string, alternativeLink?: string): void { - object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + if (hasValue(object)) { + object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + } this.store.dispatch(new AddToObjectCacheAction(object, new Date().getTime(), msToLive, requestUUID, alternativeLink)); } @@ -175,11 +177,15 @@ export class ObjectCacheService { }, ), map((entry: ObjectCacheEntry) => { - const type: GenericConstructor = getClassForType((entry.data as any).type); - if (typeof type !== 'function') { - throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + if (hasValue(entry.data)) { + const type: GenericConstructor = getClassForType((entry.data as any).type); + if (typeof type !== 'function') { + throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + } + return Object.assign(new type(), entry.data) as T; + } else { + return null; } - return Object.assign(new type(), entry.data) as T; }), ); } diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts index 0177a9813aa..1cd286427f3 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -120,6 +120,13 @@ export class DspaceRestResponseParsingService implements ResponseParsingService if (hasValue(match)) { embedAltUrl = new URLCombiner(embedAltUrl, `?size=${match.size}`).toString(); } + if (data._embedded[property] == null) { + // Embedded object is null, meaning it exists (not undefined), but had an empty response (204) -> cache it as null + this.addToObjectCache(null, request, data, embedAltUrl); + } else if (!isCacheableObject(data._embedded[property])) { + // Embedded object exists, but doesn't contain a self link -> cache it using the alternative link instead + this.objectCache.add(data._embedded[property], hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid, embedAltUrl); + } this.process(data._embedded[property], request, embedAltUrl); }); } @@ -237,7 +244,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService * @param alternativeURL an alternative url that can be used to retrieve the object */ addToObjectCache(co: CacheableObject, request: RestRequest, data: any, alternativeURL?: string): void { - if (!isCacheableObject(co)) { + if (hasValue(co) && !isCacheableObject(co)) { const type = hasValue(data) && hasValue(data.type) ? data.type : 'object'; let dataJSON: string; if (hasValue(data._embedded)) { @@ -251,7 +258,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService return; } - if (alternativeURL === co._links.self.href) { + if (hasValue(co) && alternativeURL === co._links.self.href) { alternativeURL = undefined; } diff --git a/src/app/core/data/relationship-data.service.spec.ts b/src/app/core/data/relationship-data.service.spec.ts index 1fa30ae7e4d..d4a6658f477 100644 --- a/src/app/core/data/relationship-data.service.spec.ts +++ b/src/app/core/data/relationship-data.service.spec.ts @@ -3,6 +3,8 @@ import { Store } from '@ngrx/store'; import { provideMockStore } from '@ngrx/store/testing'; import { of as observableOf } from 'rxjs'; +import { APP_CONFIG } from '../../../config/app-config.interface'; +import { environment } from '../../../environments/environment.test'; import { PAGINATED_RELATIONS_TO_ITEMS_OPERATOR } from '../../item-page/simple/item-types/shared/item-relationships-utils'; import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; @@ -150,6 +152,7 @@ describe('RelationshipDataService', () => { { provide: RequestService, useValue: requestService }, { provide: PAGINATED_RELATIONS_TO_ITEMS_OPERATOR, useValue: jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v) }, { provide: Store, useValue: provideMockStore() }, + { provide: APP_CONFIG, useValue: environment }, RelationshipDataService, ], }); @@ -157,7 +160,7 @@ describe('RelationshipDataService', () => { }); describe('composition', () => { - const initService = () => new RelationshipDataService(null, null, null, null, null, null, null, null); + const initService = () => new RelationshipDataService(null, null, null, null, null, null, null, null, environment); testSearchDataImplementation(initService); }); diff --git a/src/app/core/data/relationship-data.service.ts b/src/app/core/data/relationship-data.service.ts index 0ab9a0a3a2e..d52c14f0a76 100644 --- a/src/app/core/data/relationship-data.service.ts +++ b/src/app/core/data/relationship-data.service.ts @@ -24,6 +24,10 @@ import { tap, } from 'rxjs/operators'; +import { + APP_CONFIG, + AppConfig, +} from '../../../config/app-config.interface'; import { AppState, keySelector, @@ -133,6 +137,7 @@ export class RelationshipDataService extends IdentifiableDataService, @Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable>>) => Observable>>, + @Inject(APP_CONFIG) private appConfig: AppConfig, ) { super('relationships', requestService, rdbService, objectCache, halService, 15 * 60 * 1000); @@ -319,7 +324,7 @@ export class RelationshipDataService extends IdentifiableDataService>> { - const linksToFollow: FollowLinkConfig[] = itemLinksToFollow(options.fetchThumbnail); + const linksToFollow: FollowLinkConfig[] = itemLinksToFollow(options.fetchThumbnail, this.appConfig.item.showAccessStatuses); linksToFollow.push(followLink('relationshipType')); return this.getItemRelationshipsByLabel(item, label, options, true, true, ...linksToFollow).pipe(this.paginatedRelationsToItems(item.uuid)); diff --git a/src/app/core/data/version-history-data.service.ts b/src/app/core/data/version-history-data.service.ts index cc93e275ba5..e15fcd39077 100644 --- a/src/app/core/data/version-history-data.service.ts +++ b/src/app/core/data/version-history-data.service.ts @@ -190,7 +190,7 @@ export class VersionHistoryDataService extends IdentifiableDataService) => { - if (versionRD.hasSucceeded && !versionRD.hasNoContent) { + if (versionRD.hasSucceeded && !versionRD.hasNoContent && hasValue(versionRD.payload)) { return versionRD.payload.versionhistory.pipe( getFirstCompletedRemoteData(), map((versionHistoryRD: RemoteData) => { diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 2c3b9ad89b2..8fc33145ff7 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -45,7 +45,7 @@ export class UUIDIndexEffects { addObject$ = createEffect(() => this.actions$ .pipe( ofType(ObjectCacheActionTypes.ADD), - filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)), + filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache) && hasValue(action.payload.objectToCache.uuid)), map((action: AddToObjectCacheAction) => { return new AddToIndexAction( IndexName.OBJECT, @@ -64,7 +64,7 @@ export class UUIDIndexEffects { ofType(ObjectCacheActionTypes.ADD), map((action: AddToObjectCacheAction) => { const alternativeLink = action.payload.alternativeLink; - const selfLink = action.payload.objectToCache._links.self.href; + const selfLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : alternativeLink; if (hasValue(alternativeLink) && alternativeLink !== selfLink) { return new AddToIndexAction( IndexName.ALTERNATIVE_OBJECT_LINK, diff --git a/src/app/core/provide-core.ts b/src/app/core/provide-core.ts index 37f0d616568..78629f9d95a 100644 --- a/src/app/core/provide-core.ts +++ b/src/app/core/provide-core.ts @@ -16,6 +16,7 @@ import { AccessStatusObject } from '../shared/object-collection/shared/badges/ac import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model'; import { Subscription } from '../shared/subscriptions/models/subscription.model'; import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; +import { SystemWideAlert } from '../system-wide-alert/system-wide-alert.model'; import { AuthStatus } from './auth/models/auth-status.model'; import { ShortLivedToken } from './auth/models/short-lived-token.model'; import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; @@ -186,4 +187,5 @@ export const models = Itemfilter, SubmissionCoarNotifyConfig, NotifyRequestsStatus, + SystemWideAlert, ]; diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts index 1ad10356fb5..f0290eac398 100644 --- a/src/app/core/shared/metadata.utils.ts +++ b/src/app/core/shared/metadata.utils.ts @@ -163,7 +163,7 @@ export class Metadata { const outputKeys: string[] = []; for (const inputKey of inputKeys) { if (inputKey.includes('*')) { - const inputKeyRegex = new RegExp('^' + inputKey.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); + const inputKeyRegex = new RegExp('^' + inputKey.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); for (const mapKey of Object.keys(mdMap)) { if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) { outputKeys.push(mapKey); diff --git a/src/app/home-page/recent-item-list/recent-item-list.component.ts b/src/app/home-page/recent-item-list/recent-item-list.component.ts index 65c1998b7a0..c7383dd883d 100644 --- a/src/app/home-page/recent-item-list/recent-item-list.component.ts +++ b/src/app/home-page/recent-item-list/recent-item-list.component.ts @@ -98,6 +98,9 @@ export class RecentItemListComponent implements OnInit, OnDestroy { if (this.appConfig.browseBy.showThumbnails) { linksToFollow.push(followLink('thumbnail')); } + if (this.appConfig.item.showAccessStatuses) { + linksToFollow.push(followLink('accessStatus')); + } this.itemRD$ = this.searchService.search( new PaginatedSearchOptions({ diff --git a/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 3829b3286c0..1e963a5cca2 100644 --- a/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -33,7 +33,7 @@ import { getAllSucceededRemoteData } from '../../../core/shared/operators'; import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item.resolver'; +import { getItemPageLinksToFollow } from '../../item.resolver'; import { getItemPageRoute } from '../../item-page-routing-paths'; @Component({ @@ -92,7 +92,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl this.item = rd.payload; }), switchMap((rd: RemoteData) => { - return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW); + return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...getItemPageLinksToFollow()); }), getAllSucceededRemoteData(), ).subscribe((rd: RemoteData) => { diff --git a/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts index 1655856a3ed..ae791ccd8e7 100644 --- a/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts @@ -320,8 +320,8 @@ export class ItemDeleteComponent this.linkService.resolveLinks( relationship, followLink('relationshipType'), - followLink('leftItem'), - followLink('rightItem'), + followLink('leftItem', undefined, followLink('accessStatus')), + followLink('rightItem', undefined, followLink('accessStatus')), ); return relationship.relationshipType.pipe( getFirstSucceededRemoteData(), diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 656d6089353..2b3e35c2292 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -478,7 +478,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { ); // this adds thumbnail images when required by configuration - const linksToFollow: FollowLinkConfig[] = itemLinksToFollow(this.fetchThumbnail); + const linksToFollow: FollowLinkConfig[] = itemLinksToFollow(this.fetchThumbnail, this.appConfig.item.showAccessStatuses); this.subs.push( observableCombineLatest([ diff --git a/src/app/item-page/item-page.resolver.ts b/src/app/item-page/item-page.resolver.ts index 431d8522e74..ef59bf00b8e 100644 --- a/src/app/item-page/item-page.resolver.ts +++ b/src/app/item-page/item-page.resolver.ts @@ -18,7 +18,7 @@ import { redirectOn4xx } from '../core/shared/authorized.operators'; import { Item } from '../core/shared/item.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { hasValue } from '../shared/empty.util'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from './item.resolver'; +import { getItemPageLinksToFollow } from './item.resolver'; import { getItemPageRoute } from './item-page-routing-paths'; /** @@ -40,16 +40,22 @@ export const itemPageResolver: ResolveFn> = ( store: Store = inject(Store), authService: AuthService = inject(AuthService), ): Observable> => { - return itemService.findById( + const itemRD$ = itemService.findById( route.params.id, true, false, - ...ITEM_PAGE_LINKS_TO_FOLLOW, + ...getItemPageLinksToFollow(), ).pipe( getFirstCompletedRemoteData(), redirectOn4xx(router, authService), + ); + + itemRD$.subscribe((itemRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, itemRD.payload)); + }); + + return itemRD$.pipe( map((rd: RemoteData) => { - store.dispatch(new ResolvedAction(state.url, rd.payload)); if (rd.hasSucceeded && hasValue(rd.payload)) { const thisRoute = state.url; diff --git a/src/app/item-page/item.resolver.ts b/src/app/item-page/item.resolver.ts index 343e0d19834..1fb00d91659 100644 --- a/src/app/item-page/item.resolver.ts +++ b/src/app/item-page/item.resolver.ts @@ -7,6 +7,7 @@ import { import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; import { AppState } from '../app.reducer'; import { ItemDataService } from '../core/data/item-data.service'; import { RemoteData } from '../core/data/remote-data'; @@ -22,15 +23,21 @@ import { * The self links defined in this list are expected to be requested somewhere in the near future * Requesting them as embeds will limit the number of requests */ -export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('owningCollection', {}, - followLink('parentCommunity', {}, - followLink('parentCommunity')), - ), - followLink('relationships'), - followLink('version', {}, followLink('versionhistory')), - followLink('thumbnail'), -]; +export function getItemPageLinksToFollow(): FollowLinkConfig[] { + const followLinks: FollowLinkConfig[] = [ + followLink('owningCollection', {}, + followLink('parentCommunity', {}, + followLink('parentCommunity')), + ), + followLink('relationships'), + followLink('version', {}, followLink('versionhistory')), + followLink('thumbnail'), + ]; + if (environment.item.showAccessStatuses) { + followLinks.push(followLink('accessStatus')); + } + return followLinks; +} export const itemResolver: ResolveFn> = ( route: ActivatedRouteSnapshot, @@ -42,7 +49,7 @@ export const itemResolver: ResolveFn> = ( route.params.id, true, false, - ...ITEM_PAGE_LINKS_TO_FOLLOW, + ...getItemPageLinksToFollow(), ).pipe( getFirstCompletedRemoteData(), ); diff --git a/src/app/item-page/versions/item-versions.component.html b/src/app/item-page/versions/item-versions.component.html index e9ea3bb0f95..6184b42aa8b 100644 --- a/src/app/item-page/versions/item-versions.component.html +++ b/src/app/item-page/versions/item-versions.component.html @@ -1,91 +1,90 @@ -
-
-
-

{{"item.version.history.head" | translate}}

- - {{ "item.version.history.selected.alert" | translate : {version: itemVersion.version} }} - - - - - - - - - - - - - - - - - + + +
{{"item.version.history.table.version" | translate}}{{"item.version.history.table.editor" | translate}}{{"item.version.history.table.date" | translate}}{{"item.version.history.table.summary" | translate}}
- - - {{version?.submitterName}} - - {{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}} - -
- - {{version?.summary}} - - - -
+
+
+

{{"item.version.history.head" | translate}}

+ + {{ "item.version.history.selected.alert" | translate : { version: itemVersion.version } }} + + + + + + + + + + + + + + + + + - - -
{{ "item.version.history.table.version" | translate }}{{ "item.version.history.table.editor" | translate }}{{ "item.version.history.table.date" | translate }}{{ "item.version.history.table.summary" | translate }}
+ + + {{ versionDTO.version.submitterName }} + + {{ versionDTO.version.created | date : 'yyyy-MM-dd HH:mm:ss' }} + +
+ + {{ versionDTO.version.summary }} + + + +
-
- - - - - - - - - -
- - -
-
* {{"item.version.history.selected" | translate}}
-
- -
+
+ + + + + + + + + + +
+
+
* {{"item.version.history.selected" | translate}}
+
+ + + + diff --git a/src/app/item-page/versions/item-versions.component.ts b/src/app/item-page/versions/item-versions.component.ts index a4f9d9328b8..8b820713a6a 100644 --- a/src/app/item-page/versions/item-versions.component.ts +++ b/src/app/item-page/versions/item-versions.component.ts @@ -18,7 +18,6 @@ import { TranslateService, } from '@ngx-translate/core'; import { - BehaviorSubject, combineLatest, Observable, Subscription, @@ -64,6 +63,16 @@ import { VarDirective } from '../../shared/utils/var.directive'; import { getItemPageRoute } from '../item-page-routing-paths'; import { ItemVersionsRowElementVersionComponent } from './item-versions-row-element-version/item-versions-row-element-version.component'; +interface VersionsDTO { + totalElements: number; + versionDTOs: VersionDTO[]; +} + +interface VersionDTO { + version: Version; + canEditVersion: Observable; +} + @Component({ selector: 'ds-item-versions', templateUrl: './item-versions.component.html', @@ -126,16 +135,15 @@ export class ItemVersionsComponent implements OnDestroy, OnInit { versionHistory$: Observable; /** - * The version history's list of versions + * The version history information that is used to render the HTML */ - versionsRD$: BehaviorSubject>> = new BehaviorSubject>>(null); + versionsDTO$: Observable; /** * Verify if the list of versions has at least one e-person to display * Used to hide the "Editor" column when no e-persons are present to display */ hasEpersons$: Observable; - /** * Verify if there is an inprogress submission in the version history * Used to disable the "Create version" button @@ -186,9 +194,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit { */ versionBeingEditedSummary: string; - canCreateVersion$: Observable; - createVersionTitle$: Observable; - constructor(private versionHistoryService: VersionHistoryDataService, private versionService: VersionDataService, private paginationService: PaginationService, @@ -305,16 +310,22 @@ export class ItemVersionsComponent implements OnDestroy, OnInit { */ getAllVersions(versionHistory$: Observable): void { const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options); - combineLatest([versionHistory$, currentPagination]).pipe( + this.versionsDTO$ = combineLatest([versionHistory$, currentPagination]).pipe( switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => { return this.versionHistoryService.getVersions(versionHistory.id, new PaginatedSearchOptions({ pagination: Object.assign({}, options, { currentPage: options.currentPage }) }), false, true, followLink('item'), followLink('eperson')); }), getFirstCompletedRemoteData(), - ).subscribe((res: RemoteData>) => { - this.versionsRD$.next(res); - }); + getRemoteDataPayload(), + map((versions: PaginatedList) => ({ + totalElements: versions.totalElements, + versionDTOs: (versions?.page ?? []).map((version: Version) => ({ + version: version, + canEditVersion: this.canEditVersion$(version), + })), + })), + ); } /** @@ -348,16 +359,12 @@ export class ItemVersionsComponent implements OnDestroy, OnInit { ); this.getAllVersions(this.versionHistory$); - this.hasEpersons$ = this.versionsRD$.pipe( - getAllSucceededRemoteData(), - getRemoteDataPayload(), - hasValueOperator(), - map((versions: PaginatedList) => versions.page.filter((version: Version) => version.eperson !== undefined).length > 0), + this.hasEpersons$ = this.versionsDTO$.pipe( + map((versionsDTO: VersionsDTO) => versionsDTO.versionDTOs.filter((versionDTO: VersionDTO) => versionDTO.version.eperson !== undefined).length > 0), startWith(false), ); - this.itemPageRoutes$ = this.versionsRD$.pipe( - getAllSucceededRemoteDataPayload(), - switchMap((versions) => combineLatest(versions.page.map((version) => version.item.pipe(getAllSucceededRemoteDataPayload())))), + this.itemPageRoutes$ = this.versionsDTO$.pipe( + switchMap((versionsDTO: VersionsDTO) => combineLatest(versionsDTO.versionDTOs.map((versionDTO: VersionDTO) => versionDTO.version.item.pipe(getAllSucceededRemoteDataPayload())))), map((versions) => { const itemPageRoutes = {}; versions.forEach((item) => itemPageRoutes[item.uuid] = getItemPageRoute(item)); diff --git a/src/app/notifications/qa/events/quality-assurance-events.component.html b/src/app/notifications/qa/events/quality-assurance-events.component.html index 4341764d1c8..8a9f40e2b2e 100644 --- a/src/app/notifications/qa/events/quality-assurance-events.component.html +++ b/src/app/notifications/qa/events/quality-assurance-events.component.html @@ -1,11 +1,11 @@
-

+

{{'notifications.events.title'| translate}}
-

+ @@ -17,9 +17,9 @@

-

+

{{'quality-assurance.events.topic' | translate}} {{this.showTopic}} -

+ @@ -247,7 +247,7 @@
diff --git a/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts index 0630a28a76b..f065fc9e190 100644 --- a/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts +++ b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts @@ -16,6 +16,7 @@ import { import { Metadata } from '../../../../../core/shared/metadata.utils'; import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model'; import { isNotEmpty } from '../../../../../shared/empty.util'; +import { FileSizePipe } from '../../../../../shared/utils/file-size-pipe'; import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; import { SubmissionSectionUploadAccessConditionsComponent } from '../../accessConditions/submission-section-upload-access-conditions.component'; @@ -31,6 +32,7 @@ import { SubmissionSectionUploadAccessConditionsComponent } from '../../accessCo TruncatePipe, NgIf, NgForOf, + FileSizePipe, ], standalone: true, }) diff --git a/src/app/submission/sections/upload/section-upload.component.html b/src/app/submission/sections/upload/section-upload.component.html index a9061e40f2a..9d916a4f982 100644 --- a/src/app/submission/sections/upload/section-upload.component.html +++ b/src/app/submission/sections/upload/section-upload.component.html @@ -51,7 +51,7 @@
-

{{'submission.sections.upload.no-file-uploaded' | translate}}

+
{{'submission.sections.upload.no-file-uploaded' | translate}}
diff --git a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html index f33a4a67069..75c265b4200 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html +++ b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html @@ -1,5 +1,5 @@
-

{{ 'workspace-item.delete.header' | translate }}

+

{{ 'workspace-item.delete.header' | translate }}

@@ -7,7 +7,7 @@

{{ 'workspace-item.delete.header' | translate }}