diff --git a/config/config.example.yml b/config/config.example.yml index f1e6be76aa..a62cce13eb 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -214,6 +214,9 @@ languages: - code: tr label: Türkçe active: true + - code: vi + label: Tiếng Việt + active: true - code: kk label: Қазақ active: true diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index ea772bb891..ddda4df9fa 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -8,10 +8,10 @@
- {{ 'communityList.showMore' | translate }} - +
diff --git a/src/app/community-list-page/community-list/community-list.component.spec.ts b/src/app/community-list-page/community-list/community-list.component.spec.ts index 575edf14e8..2120df62fa 100644 --- a/src/app/community-list-page/community-list/community-list.component.spec.ts +++ b/src/app/community-list-page/community-list/community-list.component.spec.ts @@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs'; import { By } from '@angular/platform-browser'; import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { FlatNode } from '../flat-node.model'; +import { RouterLinkWithHref } from '@angular/router'; describe('CommunityListComponent', () => { let component: CommunityListComponent; @@ -194,7 +195,7 @@ describe('CommunityListComponent', () => { }), CdkTreeModule, RouterTestingModule], - declarations: [CommunityListComponent], + declarations: [CommunityListComponent, RouterLinkWithHref], providers: [CommunityListComponent, { provide: CommunityListService, useValue: communityListServiceStub },], schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -230,9 +231,14 @@ describe('CommunityListComponent', () => { expect(showMoreEl).toBeTruthy(); }); + it('should not render the show more button as an empty link', () => { + const debugElements = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); + expect(debugElements).toBeTruthy(); + }); + describe('when show more of top communities is clicked', () => { beforeEach(fakeAsync(() => { - const showMoreLink = fixture.debugElement.query(By.css('.show-more-node a')); + const showMoreLink = fixture.debugElement.query(By.css('.show-more-node .btn-outline-primary')); showMoreLink.triggerEventHandler('click', { preventDefault: () => {/**/ } @@ -240,6 +246,7 @@ describe('CommunityListComponent', () => { tick(); fixture.detectChanges(); })); + it('tree contains maximum of currentPage (2) * (2) elementsPerPage of first top communities, or less if there are less communities (3)', () => { const expandableNodesFound = fixture.debugElement.queryAll(By.css('.expandable-node a')); const childlessNodesFound = fixture.debugElement.queryAll(By.css('.childless-node a')); diff --git a/src/app/core/data/base/base-data.service.spec.ts b/src/app/core/data/base/base-data.service.spec.ts index 17532f477a..098f075c10 100644 --- a/src/app/core/data/base/base-data.service.spec.ts +++ b/src/app/core/data/base/base-data.service.spec.ts @@ -641,6 +641,62 @@ describe('BaseDataService', () => { }); }); + describe('hasCachedResponse', () => { + it('should return false when the request will be dispatched', (done) => { + const result = service.hasCachedResponse('test-href'); + + result.subscribe((hasCachedResponse) => { + expect(hasCachedResponse).toBeFalse(); + done(); + }); + }); + + it('should return true when the request will not be dispatched', (done) => { + (requestService.shouldDispatchRequest as jasmine.Spy).and.returnValue(false); + const result = service.hasCachedResponse('test-href'); + + result.subscribe((hasCachedResponse) => { + expect(hasCachedResponse).toBeTrue(); + done(); + }); + }); + }); + + describe('hasCachedErrorResponse', () => { + it('should return false when no response is cached', (done) => { + spyOn(service,'hasCachedResponse').and.returnValue(observableOf(false)); + const result = service.hasCachedErrorResponse('test-href'); + + result.subscribe((hasCachedErrorResponse) => { + expect(hasCachedErrorResponse).toBeFalse(); + done(); + }); + }); + it('should return false when no error response is cached', (done) => { + spyOn(service,'hasCachedResponse').and.returnValue(observableOf(true)); + spyOn(rdbService,'buildSingle').and.returnValue(createSuccessfulRemoteDataObject$({})); + + const result = service.hasCachedErrorResponse('test-href'); + + result.subscribe((hasCachedErrorResponse) => { + expect(hasCachedErrorResponse).toBeFalse(); + done(); + }); + }); + + it('should return true when an error response is cached', (done) => { + spyOn(service,'hasCachedResponse').and.returnValue(observableOf(true)); + spyOn(rdbService,'buildSingle').and.returnValue(createFailedRemoteDataObject$()); + + const result = service.hasCachedErrorResponse('test-href'); + + result.subscribe((hasCachedErrorResponse) => { + expect(hasCachedErrorResponse).toBeTrue(); + done(); + }); + }); + }); + describe('addDependency', () => { let addDependencySpy; diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index 85603580a4..edd6d9e2a4 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -341,6 +341,48 @@ export class BaseDataService implements HALDataServic } } + /** + * Checks for the provided href whether a response is already cached + * @param href$ The url for which to check whether there is a cached response. + * Can be a string or an Observable + */ + hasCachedResponse(href$: string | Observable): Observable { + if (isNotEmpty(href$)) { + if (typeof href$ === 'string') { + href$ = observableOf(href$); + } + return href$.pipe( + isNotEmptyOperator(), + take(1), + map((href: string) => { + const requestId = this.requestService.generateRequestId(); + const request = new GetRequest(requestId, href); + return !this.requestService.shouldDispatchRequest(request, true); + }), + ); + } + throw new Error(`Can't check whether there is a cached response for an empty href$`); + } + + /** + * Checks for the provided href whether an ERROR response is currently cached + * @param href$ The url for which to check whether there is a cached ERROR response. + * Can be a string or an Observable + */ + hasCachedErrorResponse(href$: string | Observable): Observable { + return this.hasCachedResponse(href$).pipe( + switchMap((hasCachedResponse) => { + if (hasCachedResponse) { + return this.rdbService.buildSingle(href$).pipe( + getFirstCompletedRemoteData(), + map((rd => rd.hasFailed)) + ); + } + return observableOf(false); + }) + ); + } + /** * Return the links to traverse from the root of the api to the * endpoint this DataService represents diff --git a/src/app/core/data/external-source-data.service.spec.ts b/src/app/core/data/external-source-data.service.spec.ts index cdbdbaa006..723d7f9bed 100644 --- a/src/app/core/data/external-source-data.service.spec.ts +++ b/src/app/core/data/external-source-data.service.spec.ts @@ -5,6 +5,7 @@ import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { of as observableOf } from 'rxjs'; import { GetRequest } from './request.models'; import { testSearchDataImplementation } from './base/search-data.spec'; +import { take } from 'rxjs/operators'; describe('ExternalSourceService', () => { let service: ExternalSourceDataService; @@ -64,19 +65,42 @@ describe('ExternalSourceService', () => { }); describe('getExternalSourceEntries', () => { - let result; - beforeEach(() => { - result = service.getExternalSourceEntries('test'); + describe('when no error response is cached', () => { + let result; + beforeEach(() => { + spyOn(service, 'hasCachedErrorResponse').and.returnValue(observableOf(false)); + result = service.getExternalSourceEntries('test'); + }); + + it('should send a GetRequest', () => { + result.pipe(take(1)).subscribe(); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), true); + }); + + it('should return the entries', () => { + result.subscribe((resultRD) => { + expect(resultRD.payload.page).toBe(entries); + }); + }); }); - it('should send a GetRequest', () => { - expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), true); - }); + describe('when an error response is cached', () => { + let result; + beforeEach(() => { + spyOn(service, 'hasCachedErrorResponse').and.returnValue(observableOf(true)); + result = service.getExternalSourceEntries('test'); + }); - it('should return the entries', () => { - result.subscribe((resultRD) => { - expect(resultRD.payload.page).toBe(entries); + it('should send a GetRequest', () => { + result.pipe(take(1)).subscribe(); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), false); + }); + + it('should return the entries', () => { + result.subscribe((resultRD) => { + expect(resultRD.payload.page).toBe(entries); + }); }); }); }); diff --git a/src/app/core/data/external-source-data.service.ts b/src/app/core/data/external-source-data.service.ts index c0552aeaec..02c5e4a53c 100644 --- a/src/app/core/data/external-source-data.service.ts +++ b/src/app/core/data/external-source-data.service.ts @@ -74,7 +74,12 @@ export class ExternalSourceDataService extends IdentifiableDataService { + return this.findListByHref(href$, undefined, !hasCachedErrorResponse, reRequestOnStale, ...linksToFollow as any); + }) + ) as any; } /** diff --git a/src/app/curation-form/curation-form.component.ts b/src/app/curation-form/curation-form.component.ts index 4b67580e77..e8fc67c970 100644 --- a/src/app/curation-form/curation-form.component.ts +++ b/src/app/curation-form/curation-form.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ScriptDataService } from '../core/data/processes/script-data.service'; import { FormControl, FormGroup } from '@angular/forms'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; @@ -40,7 +40,8 @@ export class CurationFormComponent implements OnInit { private notificationsService: NotificationsService, private translateService: TranslateService, private handleService: HandleService, - private router: Router + private router: Router, + private cdr: ChangeDetectorRef ) { } @@ -59,6 +60,7 @@ export class CurationFormComponent implements OnInit { .filter((value) => isNotEmpty(value) && value.includes('=')) .map((value) => value.split('=')[1].trim()); this.form.get('task').patchValue(this.tasks[0]); + this.cdr.detectChanges(); }); } diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss index b297979fd0..b02b3c378c 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss @@ -1,7 +1,4 @@ -@media screen and (max-width: map-get($grid-breakpoints, md)) { - :host.open { - background-color: var(--bs-white); - top: 0; - position: sticky; - } +:host { + position: relative; + z-index: var(--ds-nav-z-index); } diff --git a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss deleted file mode 100644 index db392096aa..0000000000 --- a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - z-index: var(--ds-nav-z-index); -} diff --git a/src/app/header/header.component.scss b/src/app/header/header.component.scss index 546f6a06fa..cca3ed2abb 100644 --- a/src/app/header/header.component.scss +++ b/src/app/header/header.component.scss @@ -11,13 +11,12 @@ line-height: 1.5; } -.navbar ::ng-deep { - a { - color: var(--ds-header-icon-color); +.navbar-toggler { + border: none; + color: var(--ds-header-icon-color); - &:hover, &:focus { - color: var(--ds-header-icon-color-hover); - } + &:hover, &:focus { + color: var(--ds-header-icon-color-hover); } } diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index ef6abf4a84..9eb12bcd03 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -46,6 +46,7 @@ import { IdentifierDataService } from '../../core/data/identifier-data.service'; import { IdentifierDataComponent } from '../../shared/object-list/identifier-data/identifier-data.component'; import { ItemRegisterDoiComponent } from './item-register-doi/item-register-doi.component'; import { DsoSharedModule } from '../../dso-shared/dso-shared.module'; +import { ItemCurateComponent } from './item-curate/item-curate.component'; import { ItemAccessControlComponent } from './item-access-control/item-access-control.component'; import { ResultsBackButtonModule } from '../../shared/results-back-button/results-back-button.module'; import { @@ -96,6 +97,7 @@ import { ItemAuthorizationsComponent, IdentifierDataComponent, ItemRegisterDoiComponent, + ItemCurateComponent, ItemAccessControlComponent, ], providers: [ diff --git a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts index ce1d258b6b..818501a3f6 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts @@ -41,6 +41,7 @@ import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard'; import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard'; import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; import { ItemPageRegisterDoiGuard } from './item-page-register-doi.guard'; +import { ItemCurateComponent } from './item-curate/item-curate.component'; import { ItemAccessControlComponent } from './item-access-control/item-access-control.component'; /** @@ -83,6 +84,11 @@ import { ItemAccessControlComponent } from './item-access-control/item-access-co data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }, canActivate: [ItemPageMetadataGuard] }, + { + path: 'curate', + component: ItemCurateComponent, + data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true } + }, { path: 'relationships', component: ItemRelationshipsComponent, diff --git a/src/app/item-page/edit-item-page/item-curate/item-curate.component.html b/src/app/item-page/edit-item-page/item-curate/item-curate.component.html new file mode 100644 index 0000000000..7c7ed41bd9 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-curate/item-curate.component.html @@ -0,0 +1,7 @@ +
+

{{'item.edit.curate.title' |translate:{item: (itemName$ |async)} }}

+ +
diff --git a/src/app/item-page/edit-item-page/item-curate/item-curate.component.spec.ts b/src/app/item-page/edit-item-page/item-curate/item-curate.component.spec.ts new file mode 100644 index 0000000000..c104b4400b --- /dev/null +++ b/src/app/item-page/edit-item-page/item-curate/item-curate.component.spec.ts @@ -0,0 +1,75 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; +import { ItemCurateComponent } from './item-curate.component'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { ActivatedRoute } from '@angular/router'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { Item } from '../../../core/shared/item.model'; + +describe('ItemCurateComponent', () => { + let comp: ItemCurateComponent; + let fixture: ComponentFixture; + let debugEl: DebugElement; + + let routeStub; + let dsoNameService; + + const item = Object.assign(new Item(), { + handle: '123456789/1', + metadata: {'dc.title': ['Item Name']} + }); + + beforeEach(waitForAsync(() => { + routeStub = { + parent: { + data: observableOf({ + dso: createSuccessfulRemoteDataObject(item) + }) + } + }; + + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: 'Item Name' + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ItemCurateComponent], + providers: [ + {provide: ActivatedRoute, useValue: routeStub}, + {provide: DSONameService, useValue: dsoNameService} + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemCurateComponent); + comp = fixture.componentInstance; + debugEl = fixture.debugElement; + + fixture.detectChanges(); + }); + describe('init', () => { + it('should initialise the comp', () => { + expect(comp).toBeDefined(); + expect(debugEl.nativeElement.innerHTML).toContain('ds-curation-form'); + }); + + it('should contain the item information provided in the route', (done) => { + comp.dsoRD$.subscribe((value) => { + expect(value.payload.handle).toEqual('123456789/1'); + done(); + }); + }); + + it('should contain the item name', (done) => { + comp.itemName$.subscribe((value) => { + expect(value).toEqual('Item Name'); + done(); + }); + }); + }); +}); diff --git a/src/app/item-page/edit-item-page/item-curate/item-curate.component.ts b/src/app/item-page/edit-item-page/item-curate/item-curate.component.ts new file mode 100644 index 0000000000..fa1e0287fa --- /dev/null +++ b/src/app/item-page/edit-item-page/item-curate/item-curate.component.ts @@ -0,0 +1,39 @@ +import { Component, OnInit } from '@angular/core'; +import { filter, map, take } from 'rxjs/operators'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Observable } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { hasValue } from '../../../shared/empty.util'; +import { Item } from '../../../core/shared/item.model'; + +/** + * Component for managing a collection's curation tasks + */ +@Component({ + selector: 'ds-item-curate', + templateUrl: './item-curate.component.html', +}) +export class ItemCurateComponent implements OnInit { + dsoRD$: Observable>; + itemName$: Observable; + + constructor( + private route: ActivatedRoute, + private dsoNameService: DSONameService, + ) {} + + ngOnInit(): void { + this.dsoRD$ = this.route.parent.data.pipe( + take(1), + map((data) => data.dso), + ); + + this.itemName$ = this.dsoRD$.pipe( + filter((rd: RemoteData) => hasValue(rd)), + map((rd: RemoteData) => { + return this.dsoNameService.getName(rd.payload); + }) + ); + } +} diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.html b/src/app/item-page/simple/field-components/file-section/file-section.component.html index 8e4f4dfcb0..82d934fe7b 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.html +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.html @@ -8,10 +8,10 @@
- {{'item.page.bitstreams.view-more' | translate}} +
- {{'item.page.bitstreams.collapse' | translate}} +
diff --git a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.html b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.html index 65660eaa34..efbe9206d1 100644 --- a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.html +++ b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.html @@ -7,12 +7,12 @@
- {{'item.page.related-items.view-more' | - translate:{ amount: (total - (objects.length * incrementBy) < incrementBy) ? total - (objects.length * incrementBy) : incrementBy } }} +
- {{'item.page.related-items.view-less' | - translate:{ amount: representations?.length } }} +
diff --git a/src/app/item-page/simple/related-items/related-items.component.html b/src/app/item-page/simple/related-items/related-items.component.html index 0d1e14941d..bee1f345fd 100644 --- a/src/app/item-page/simple/related-items/related-items.component.html +++ b/src/app/item-page/simple/related-items/related-items.component.html @@ -7,12 +7,12 @@
- {{'item.page.related-items.view-more' | - translate:{ amount: (itemsRD?.payload?.totalElements - (incrementBy * objects.length) < incrementBy) ? itemsRD?.payload?.totalElements - (incrementBy * objects.length) : incrementBy } }} +
- {{'item.page.related-items.view-less' | - translate:{ amount: itemsRD?.payload?.page?.length } }} +
diff --git a/src/app/search-navbar/search-navbar.component.html b/src/app/search-navbar/search-navbar.component.html index e1de59ce51..2b30507f3e 100644 --- a/src/app/search-navbar/search-navbar.component.html +++ b/src/app/search-navbar/search-navbar.component.html @@ -4,9 +4,9 @@ - + diff --git a/src/app/search-navbar/search-navbar.component.scss b/src/app/search-navbar/search-navbar.component.scss index d5f3d8d615..cf46c25d91 100644 --- a/src/app/search-navbar/search-navbar.component.scss +++ b/src/app/search-navbar/search-navbar.component.scss @@ -1,13 +1,14 @@ input[type="text"] { margin-top: calc(-0.5 * var(--bs-font-size-base)); background-color: #fff !important; + border-color: var(--ds-header-icon-color); &.collapsed { opacity: 0; } } -a.submit-icon { +.submit-icon { cursor: pointer; position: sticky; top: 0; diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html index 94cbd4368a..05f502afa1 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html @@ -19,7 +19,7 @@