From 6e7fe85b4748472fb8b12b097424c9e4115b09ec Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 14 Apr 2021 16:42:11 +0200 Subject: [PATCH 1/6] use the chromedriver npm package to download the webdriver version compatible with the installed chrome version --- .github/workflows/build.yml | 7 ++++++- e2e/protractor-ci.conf.js | 4 ++++ package.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b5b3f9d8c..d2e8b9fe5e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,9 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: ${{ runner.os }}-yarn- + - name: Install the latest chromedriver compatible with the installed chrome version + run: yarn global add chromedriver --detect_chromedriver_version + - name: Install Yarn dependencies run: yarn install --frozen-lockfile @@ -94,7 +97,9 @@ jobs: run: curl http://localhost:8080/server/api - name: Run e2e tests (integration tests) - run: yarn run e2e:ci + run: | + chromedriver --url-base='/wd/hub' --port=4444 & + yarn run e2e:ci - name: Shutdown Docker containers run: docker-compose -f ./docker/docker-compose-ci.yml down diff --git a/e2e/protractor-ci.conf.js b/e2e/protractor-ci.conf.js index 63173e44e3..0cfc1f9eaf 100644 --- a/e2e/protractor-ci.conf.js +++ b/e2e/protractor-ci.conf.js @@ -7,4 +7,8 @@ config.capabilities = { } }; +// don't use protractor's webdriver, as it may be incompatible with the installed chrome version +config.directConnect = false; +config.seleniumAddress = 'http://localhost:4444/wd/hub'; + exports.config = config; diff --git a/package.json b/package.json index 4008bb0ac3..80af52e264 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "lint": "ng lint", "lint-fix": "ng lint --fix=true", "e2e": "ng e2e", - "e2e:ci": "ng e2e --protractor-config=./e2e/protractor-ci.conf.js", + "e2e:ci": "ng e2e --webdriver-update=false --protractor-config=./e2e/protractor-ci.conf.js", "compile:server": "webpack --config webpack.server.config.js --progress --color", "serve:ssr": "node dist/server", "clean:coverage": "rimraf coverage", From ef8e66546b31dcc55321b27113b86604f245cadd Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 13 Apr 2021 16:40:39 -0500 Subject: [PATCH 2/6] Minor updates to default and dspace theme for Testathon. Also updating DuraSpace to LYRASIS in several places. --- .../+home-page/home-news/home-news.component.html | 6 ++++-- src/app/footer/footer.component.html | 2 +- .../lang-switch/lang-switch.component.spec.ts | 2 +- src/assets/i18n/ar.json5 | 4 ++-- src/assets/i18n/cs.json5 | 4 ++-- src/assets/i18n/de.json5 | 4 ++-- src/assets/i18n/en.json5 | 2 +- src/assets/i18n/es.json5 | 4 ++-- src/assets/i18n/fi.json5 | 4 ++-- src/assets/i18n/fr.json5 | 4 ++-- src/assets/i18n/hu.json5 | 4 ++-- src/assets/i18n/ja.json5 | 4 ++-- src/assets/i18n/lv.json5 | 4 ++-- src/assets/i18n/nl.json5 | 4 ++-- src/assets/i18n/pl.json5 | 4 ++-- src/assets/i18n/pt-BR.json5 | 4 ++-- src/assets/i18n/pt-PT.json5 | 4 ++-- src/assets/i18n/sw.json5 | 4 ++-- src/assets/i18n/tr.json5 | 4 ++-- .../+home-page/home-news/home-news.component.html | 13 ++++++++++++- 20 files changed, 49 insertions(+), 36 deletions(-) diff --git a/src/app/+home-page/home-news/home-news.component.html b/src/app/+home-page/home-news/home-news.component.html index 812c38f798..6bee3cd76f 100644 --- a/src/app/+home-page/home-news/home-news.component.html +++ b/src/app/+home-page/home-news/home-news.component.html @@ -2,7 +2,7 @@
-

Welcome to the DSpace 7 Preview

+

DSpace 7

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

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

    Join an international community of leading institutions using DSpace.

    +

    Join an international community of leading institutions using DSpace. +

    diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index 3756bce188..bc407c2a97 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -56,7 +56,7 @@

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

    Join an international community of leading institutions using DSpace.

    +

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

    + Photo by @inspiredimages From a4a747e922b23c23b46aa931c202ac18b7b44ce8 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 13 Apr 2021 12:28:27 +0200 Subject: [PATCH 3/6] fix issue where the submission form wouldn't update after a relationship was added --- src/app/core/shared/operators.ts | 64 +++++++-------- .../edit/submission-edit.component.ts | 82 +++++++++++++------ .../submit/submission-submit.component.ts | 40 ++++++++- 3 files changed, 124 insertions(+), 62 deletions(-) diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index dd610b6ca7..3128538ea9 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -45,32 +45,32 @@ export const sendRequest = (requestService: RequestService) => (source: Observable): Observable => source.pipe(tap((request: RestRequest) => requestService.send(request))); -export const getRemoteDataPayload = () => - (source: Observable>): Observable => +export const getRemoteDataPayload = () => + (source: Observable>): Observable => source.pipe(map((remoteData: RemoteData) => remoteData.payload)); -export const getPaginatedListPayload = () => - (source: Observable>): Observable => +export const getPaginatedListPayload = () => + (source: Observable>): Observable => source.pipe(map((list: PaginatedList) => list.page)); -export const getAllCompletedRemoteData = () => - (source: Observable>): Observable> => +export const getAllCompletedRemoteData = () => + (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => hasValue(rd) && rd.hasCompleted)); -export const getFirstCompletedRemoteData = () => - (source: Observable>): Observable> => +export const getFirstCompletedRemoteData = () => + (source: Observable>): Observable> => source.pipe(getAllCompletedRemoteData(), take(1)); -export const takeUntilCompletedRemoteData = () => - (source: Observable>): Observable> => +export const takeUntilCompletedRemoteData = () => + (source: Observable>): Observable> => source.pipe(takeWhile((rd: RemoteData) => hasNoValue(rd) || rd.isLoading, true)); -export const getFirstSucceededRemoteData = () => - (source: Observable>): Observable> => +export const getFirstSucceededRemoteData = () => + (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => rd.hasSucceeded), take(1)); -export const getFirstSucceededRemoteWithNotEmptyData = () => - (source: Observable>): Observable> => +export const getFirstSucceededRemoteWithNotEmptyData = () => + (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded && isNotEmpty(rd.payload))); /** @@ -83,8 +83,8 @@ export const getFirstSucceededRemoteWithNotEmptyData = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getFirstSucceededRemoteDataPayload = () => - (source: Observable>): Observable => +export const getFirstSucceededRemoteDataPayload = () => + (source: Observable>): Observable => source.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload() @@ -100,8 +100,8 @@ export const getFirstSucceededRemoteDataPayload = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getFirstSucceededRemoteDataWithNotEmptyPayload = () => - (source: Observable>): Observable => +export const getFirstSucceededRemoteDataWithNotEmptyPayload = () => + (source: Observable>): Observable => source.pipe( getFirstSucceededRemoteWithNotEmptyData(), getRemoteDataPayload() @@ -117,8 +117,8 @@ export const getFirstSucceededRemoteDataWithNotEmptyPayload = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getAllSucceededRemoteDataPayload = () => - (source: Observable>): Observable => +export const getAllSucceededRemoteDataPayload = () => + (source: Observable>): Observable => source.pipe( getAllSucceededRemoteData(), getRemoteDataPayload() @@ -138,8 +138,8 @@ export const getAllSucceededRemoteDataPayload = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getFirstSucceededRemoteListPayload = () => - (source: Observable>>): Observable => +export const getFirstSucceededRemoteListPayload = () => + (source: Observable>>): Observable => source.pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), @@ -160,8 +160,8 @@ export const getFirstSucceededRemoteListPayload = () => * These operators were created as a first step in refactoring * out all the instances where this is used incorrectly. */ -export const getAllSucceededRemoteListPayload = () => - (source: Observable>>): Observable => +export const getAllSucceededRemoteListPayload = () => + (source: Observable>>): Observable => source.pipe( getAllSucceededRemoteData(), getRemoteDataPayload(), @@ -174,8 +174,8 @@ export const getAllSucceededRemoteListPayload = () => * @param router The router used to navigate to a new page * @param authService Service to check if the user is authenticated */ -export const redirectOn4xx = (router: Router, authService: AuthService) => - (source: Observable>): Observable> => +export const redirectOn4xx = (router: Router, authService: AuthService) => + (source: Observable>): Observable> => observableCombineLatest(source, authService.isAuthenticated()).pipe( map(([rd, isAuthenticated]: [RemoteData, boolean]) => { if (rd.hasFailed) { @@ -229,16 +229,16 @@ export const returnEndUserAgreementUrlTreeOnFalse = (router: Router, redirect: s return hasAgreed ? hasAgreed : router.createUrlTree([getEndUserAgreementPath()], { queryParams }); })); -export const getFinishedRemoteData = () => - (source: Observable>): Observable> => +export const getFinishedRemoteData = () => + (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => !rd.isLoading)); -export const getAllSucceededRemoteData = () => - (source: Observable>): Observable> => +export const getAllSucceededRemoteData = () => + (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => rd.hasSucceeded)); -export const toDSpaceObjectListRD = () => - (source: Observable>>>): Observable>> => +export const toDSpaceObjectListRD = () => + (source: Observable>>>): Observable>> => source.pipe( filter((rd: RemoteData>>) => rd.hasSucceeded), map((rd: RemoteData>>) => { diff --git a/src/app/submission/edit/submission-edit.component.ts b/src/app/submission/edit/submission-edit.component.ts index 7908a052b7..34fdcba104 100644 --- a/src/app/submission/edit/submission-edit.component.ts +++ b/src/app/submission/edit/submission-edit.component.ts @@ -2,11 +2,11 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { filter, switchMap } from 'rxjs/operators'; +import { filter, switchMap, debounceTime } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; -import { hasValue, isEmpty, isNotNull } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotNull, isNotEmptyOperator } from '../../shared/empty.util'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; import { SubmissionService } from '../submission.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -14,6 +14,9 @@ import { SubmissionObject } from '../../core/submission/models/submission-object import { Collection } from '../../core/shared/collection.model'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; +import { getAllSucceededRemoteData } from '../../core/shared/operators'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; /** * This component allows to edit an existing workspaceitem/workflowitem. @@ -60,6 +63,16 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { * @type {Array} */ private subs: Subscription[] = []; + + /** + * BehaviorSubject containing the self link to the item for this submission + * @private + */ + private itemLink$: BehaviorSubject = new BehaviorSubject(undefined); + + /** + * The item for this submission. + */ public item: Item; /** @@ -69,6 +82,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { * @param {NotificationsService} notificationsService * @param {ActivatedRoute} route * @param {Router} router + * @param {ItemDataService} itemDataService * @param {SubmissionService} submissionService * @param {TranslateService} translate */ @@ -76,6 +90,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { private notificationsService: NotificationsService, private route: ActivatedRoute, private router: Router, + private itemDataService: ItemDataService, private submissionService: SubmissionService, private translate: TranslateService) { } @@ -84,32 +99,47 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { * Retrieve workspaceitem/workflowitem from server and initialize all instance variables */ ngOnInit() { - this.subs.push(this.route.paramMap.pipe( - switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))), - // NOTE new submission is retrieved on the browser side only, so get null on server side rendering - filter((submissionObjectRD: RemoteData) => isNotNull(submissionObjectRD)) - ).subscribe((submissionObjectRD: RemoteData) => { - if (submissionObjectRD.hasSucceeded) { - if (isEmpty(submissionObjectRD.payload)) { - this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); - this.router.navigate(['/mydspace']); + this.subs.push( + this.route.paramMap.pipe( + switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))), + // NOTE new submission is retrieved on the browser side only, so get null on server side rendering + filter((submissionObjectRD: RemoteData) => isNotNull(submissionObjectRD)) + ).subscribe((submissionObjectRD: RemoteData) => { + if (submissionObjectRD.hasSucceeded) { + if (isEmpty(submissionObjectRD.payload)) { + this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); + this.router.navigate(['/mydspace']); + } else { + this.submissionId = submissionObjectRD.payload.id.toString(); + this.collectionId = (submissionObjectRD.payload.collection as Collection).id; + this.selfUrl = submissionObjectRD.payload._links.self.href; + this.sections = submissionObjectRD.payload.sections; + this.itemLink$.next(submissionObjectRD.payload._links.item.href); + this.item = submissionObjectRD.payload.item; + this.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel); + } } else { - this.submissionId = submissionObjectRD.payload.id.toString(); - this.collectionId = (submissionObjectRD.payload.collection as Collection).id; - this.selfUrl = submissionObjectRD.payload._links.self.href; - this.sections = submissionObjectRD.payload.sections; - this.item = submissionObjectRD.payload.item as Item; - this.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel); - this.changeDetectorRef.detectChanges(); + if (submissionObjectRD.statusCode === 404) { + // redirect to not found page + this.router.navigate(['/404'], { skipLocationChange: true }); + } + // TODO handle generic error } - } else { - if (submissionObjectRD.statusCode === 404) { - // redirect to not found page - this.router.navigate(['/404'], { skipLocationChange: true }); - } - // TODO handle generic error - } - })); + }), + this.itemLink$.pipe( + isNotEmptyOperator(), + switchMap((itemLink: string) => + this.itemDataService.findByHref(itemLink) + ), + getAllSucceededRemoteData(), + // Multiple sources can update the item in quick succession. + // We only want to rerender the form if the item is unchanged for some time + debounceTime(300), + ).subscribe((itemRd: RemoteData) => { + this.item = itemRd.payload; + this.changeDetectorRef.detectChanges(); + }), + ); } /** diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index 003f5280a8..0c2172368a 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { hasValue, isEmpty, isNotNull } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotNull, isNotEmptyOperator } from '../../shared/empty.util'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -12,6 +12,11 @@ import { SubmissionObject } from '../../core/submission/models/submission-object import { Collection } from '../../core/shared/collection.model'; import { Item } from '../../core/shared/item.model'; import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { switchMap, debounceTime } from 'rxjs/operators'; +import { getAllSucceededRemoteData } from '../../core/shared/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { ItemDataService } from '../../core/data/item-data.service'; /** * This component allows to submit a new workspaceitem. @@ -28,6 +33,16 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { * @type {string} */ public collectionId: string; + + /** + * BehaviorSubject containing the self link to the item for this submission + * @private + */ + private itemLink$: BehaviorSubject = new BehaviorSubject(undefined); + + /** + * The item for this submission. + */ public item: Item; /** @@ -71,6 +86,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { * * @param {ChangeDetectorRef} changeDetectorRef * @param {NotificationsService} notificationsService + * @param {ItemDataService} itemDataService * @param {SubmissionService} submissionService * @param {Router} router * @param {TranslateService} translate @@ -80,13 +96,16 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { constructor(private changeDetectorRef: ChangeDetectorRef, private notificationsService: NotificationsService, private router: Router, + private itemDataService: ItemDataService, private submissionService: SubmissionService, private translate: TranslateService, private viewContainerRef: ViewContainerRef, private route: ActivatedRoute) { this.route .queryParams - .subscribe((params) => { this.collectionParam = (params.collection); }); + .subscribe((params) => { + this.collectionParam = (params.collection); + }); } /** @@ -108,11 +127,24 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { this.selfUrl = submissionObject._links.self.href; this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel); this.submissionId = submissionObject.id; + this.itemLink$.next(submissionObject._links.item.href); this.item = submissionObject.item as Item; - this.changeDetectorRef.detectChanges(); } } - }) + }), + this.itemLink$.pipe( + isNotEmptyOperator(), + switchMap((itemLink: string) => + this.itemDataService.findByHref(itemLink) + ), + getAllSucceededRemoteData(), + // Multiple sources can update the item in quick succession. + // We only want to rerender the form if the item is unchanged for some time + debounceTime(300), + ).subscribe((itemRd: RemoteData) => { + this.item = itemRd.payload; + this.changeDetectorRef.detectChanges(); + }) ); } From 56a54ae1c99b787ca3e9f1c4168a4ce90db2f365 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 13 Apr 2021 13:54:16 +0200 Subject: [PATCH 4/6] don't hide the lookup button for virtual relationship fields --- .../ds-dynamic-form-control-container.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index bfa9c214e9..085ccb6248 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -35,7 +35,7 @@ -
    +