diff --git a/config/config.example.yml b/config/config.example.yml index 4b9c1a27ace..0933619e013 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -285,8 +285,17 @@ item: # settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'. pageSize: 5 +# Community Page Config +community: + # Search tab config + searchSection: + showSidebar: true + # Collection Page Config collection: + # Search tab config + searchSection: + showSidebar: true edit: undoTimeout: 10000 # 10 seconds @@ -391,4 +400,3 @@ comcolSelectionSort: # suggestion: # - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af # source: "openaire" - diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss index 80adde4ecca..e69de29bb2d 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss @@ -1,12 +0,0 @@ -:host { - ::ng-deep { - .switch { - position: absolute; - top: calc(var(--bs-spacer) * 2.5); - } - } -} -:host ::ng-deep ds-dynamic-form-control-container > div > label { - margin-top: 1.75rem; -} - diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index b77d2151a9b..7aad6df2fb2 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -2,14 +2,35 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute, Router } from '@angular/router'; import { filter, map, switchMap, tap } from 'rxjs/operators'; -import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; -import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; +import { + combineLatest, + combineLatest as observableCombineLatest, + Observable, + of as observableOf, + Subscription +} from 'rxjs'; +import { + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicFormLayout, + DynamicFormService, + DynamicInputModel, + DynamicSelectModel +} from '@ng-dynamic-forms/core'; import { UntypedFormGroup } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; -import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; +import { + DynamicCustomSwitchModel +} from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; import cloneDeep from 'lodash/cloneDeep'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; -import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../../core/shared/operators'; +import { + getAllSucceededRemoteDataPayload, + getFirstCompletedRemoteData, + getFirstSucceededRemoteData, + getFirstSucceededRemoteDataPayload, + getRemoteDataPayload +} from '../../core/shared/operators'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; @@ -245,7 +266,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { /** * All input models in a simple array for easier iterations */ - inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, + inputModels = [this.primaryBitstreamModel, this.fileNameModel, this.descriptionModel, this.selectedFormatModel, this.newFormatModel]; /** @@ -256,8 +277,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { new DynamicFormGroupModel({ id: 'fileNamePrimaryContainer', group: [ - this.fileNameModel, - this.primaryBitstreamModel + this.primaryBitstreamModel, + this.fileNameModel ] }, { grid: { @@ -295,7 +316,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { }, primaryBitstream: { grid: { - host: 'col col-sm-4 d-inline-block switch border-0' + container: 'col-12' + }, + element: { + container: 'text-right' } }, description: { diff --git a/src/app/collection-page/collection-page-routing.module.ts b/src/app/collection-page/collection-page-routing.module.ts index 5ddef6ca68b..ed0c77c2f8a 100644 --- a/src/app/collection-page/collection-page-routing.module.ts +++ b/src/app/collection-page/collection-page-routing.module.ts @@ -25,7 +25,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { BrowseByGuard } from '../browse-by/browse-by-guard'; import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; -import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component'; +import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; @NgModule({ imports: [ @@ -73,7 +73,7 @@ import { CollectionRecentlyAddedComponent } from './sections/recently-added/coll { path: '', pathMatch: 'full', - component: CollectionRecentlyAddedComponent, + component: ComcolSearchSectionComponent, }, { path: 'browse/:id', diff --git a/src/app/collection-page/collection-page.module.ts b/src/app/collection-page/collection-page.module.ts index 8782be0a45f..5db85d48e98 100644 --- a/src/app/collection-page/collection-page.module.ts +++ b/src/app/collection-page/collection-page.module.ts @@ -19,7 +19,6 @@ import { ComcolModule } from '../shared/comcol/comcol.module'; import { DsoSharedModule } from '../dso-shared/dso-shared.module'; import { DsoPageModule } from '../shared/dso-page/dso-page.module'; import { BrowseByPageModule } from '../browse-by/browse-by-page.module'; -import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component'; const DECLARATIONS = [ CollectionPageComponent, @@ -29,7 +28,6 @@ const DECLARATIONS = [ EditItemTemplatePageComponent, ThemedEditItemTemplatePageComponent, CollectionItemMapperComponent, - CollectionRecentlyAddedComponent, ]; @NgModule({ diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.html b/src/app/collection-page/sections/recently-added/collection-recently-added.component.html deleted file mode 100644 index 002b8cceda7..00000000000 --- a/src/app/collection-page/sections/recently-added/collection-recently-added.component.html +++ /dev/null @@ -1,18 +0,0 @@ - -
-

{{'collection.page.browse.recent.head' | translate}}

- - -
- - - -
diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.spec.ts b/src/app/collection-page/sections/recently-added/collection-recently-added.component.spec.ts deleted file mode 100644 index 4acc24e3f59..00000000000 --- a/src/app/collection-page/sections/recently-added/collection-recently-added.component.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CollectionRecentlyAddedComponent } from './collection-recently-added.component'; -import { APP_CONFIG } from '../../../../config/app-config.interface'; -import { environment } from '../../../../environments/environment.test'; -import { ActivatedRoute } from '@angular/router'; -import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; -import { PaginationService } from '../../../core/pagination/pagination.service'; -import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; -import { SearchServiceStub } from '../../../shared/testing/search-service.stub'; -import { SearchService } from '../../../core/shared/search/search.service'; -import { VarDirective } from '../../../shared/utils/var.directive'; -import { TranslateModule } from '@ngx-translate/core'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; - -describe('CollectionRecentlyAddedComponent', () => { - let component: CollectionRecentlyAddedComponent; - let fixture: ComponentFixture; - - let activatedRoute: ActivatedRouteStub; - let paginationService: PaginationServiceStub; - let searchService: SearchServiceStub; - - beforeEach(async () => { - activatedRoute = new ActivatedRouteStub(); - paginationService = new PaginationServiceStub(); - searchService = new SearchServiceStub(); - - await TestBed.configureTestingModule({ - declarations: [ - CollectionRecentlyAddedComponent, - VarDirective, - ], - imports: [ - TranslateModule.forRoot(), - ], - providers: [ - { provide: ActivatedRoute, useValue: activatedRoute }, - { provide: APP_CONFIG, useValue: environment }, - { provide: PaginationService, useValue: paginationService }, - { provide: SearchService, useValue: SearchServiceStub }, - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(CollectionRecentlyAddedComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.ts b/src/app/collection-page/sections/recently-added/collection-recently-added.component.ts deleted file mode 100644 index 65af77a63b5..00000000000 --- a/src/app/collection-page/sections/recently-added/collection-recently-added.component.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Component, OnInit, Inject, OnDestroy } from '@angular/core'; -import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; -import { Item } from '../../../core/shared/item.model'; -import { switchMap, map, startWith, take } from 'rxjs/operators'; -import { getFirstSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators'; -import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; -import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; -import { BROWSE_LINKS_TO_FOLLOW } from '../../../core/browse/browse.service'; -import { PaginationService } from '../../../core/pagination/pagination.service'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { SortOptions, SortDirection } from '../../../core/cache/models/sort-options.model'; -import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; -import { SearchService } from '../../../core/shared/search/search.service'; -import { Collection } from '../../../core/shared/collection.model'; -import { ActivatedRoute, Data } from '@angular/router'; -import { fadeIn } from '../../../shared/animations/fade'; - -@Component({ - selector: 'ds-collection-recently-added', - templateUrl: './collection-recently-added.component.html', - styleUrls: ['./collection-recently-added.component.scss'], - animations: [fadeIn], -}) -export class CollectionRecentlyAddedComponent implements OnInit, OnDestroy { - - paginationConfig: PaginationComponentOptions; - - sortConfig: SortOptions; - - collectionRD$: Observable>; - - itemRD$: Observable>>; - - constructor( - @Inject(APP_CONFIG) protected appConfig: AppConfig, - protected paginationService: PaginationService, - protected route: ActivatedRoute, - protected searchService: SearchService, - ) { - this.paginationConfig = Object.assign(new PaginationComponentOptions(), { - id: 'cp', - currentPage: 1, - pageSize: this.appConfig.browseBy.pageSize, - }); - - this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC); - } - - ngOnInit(): void { - this.collectionRD$ = this.route.data.pipe( - map((data: Data) => data.dso as RemoteData), - take(1), - ); - - this.itemRD$ = observableCombineLatest([ - this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig), - this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig), - ]).pipe( - switchMap(([currentPagination, currentSort]: [PaginationComponentOptions, SortOptions]) => this.collectionRD$.pipe( - getFirstSucceededRemoteData(), - map((rd: RemoteData) => rd.payload.id), - switchMap((id: string) => this.searchService.search( - new PaginatedSearchOptions({ - scope: id, - pagination: currentPagination, - sort: currentSort, - dsoTypes: [DSpaceObjectType.ITEM] - }), null, true, true, ...BROWSE_LINKS_TO_FOLLOW).pipe( - toDSpaceObjectListRD() - ) as Observable>>), - startWith(undefined), // Make sure switching pages shows loading component - )), - ); - } - - ngOnDestroy(): void { - this.paginationService.clearPagination(this.paginationConfig.id); - } - -} diff --git a/src/app/community-page/community-page-routing.module.ts b/src/app/community-page/community-page-routing.module.ts index 5ca544bb541..f38e670546b 100644 --- a/src/app/community-page/community-page-routing.module.ts +++ b/src/app/community-page/community-page-routing.module.ts @@ -19,6 +19,8 @@ import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-co import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; import { BrowseByGuard } from '../browse-by/browse-by-guard'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; +import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; @NgModule({ imports: [ @@ -56,7 +58,16 @@ import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse { path: '', pathMatch: 'full', + component: ComcolSearchSectionComponent, + }, + { + path: 'subcoms-cols', + pathMatch: 'full', component: SubComColSectionComponent, + resolve: { + breadcrumb: I18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'community.subcoms-cols' }, }, { path: 'browse/:id', diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts index 804299d3d98..cb3c41aa97d 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts @@ -11,6 +11,7 @@ describe('SubComColSectionComponent', () => { beforeEach(async () => { activatedRoute = new ActivatedRouteStub(); + activatedRoute.parent = new ActivatedRouteStub(); await TestBed.configureTestingModule({ declarations: [ diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts index ff30e51607b..a72674adec3 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts @@ -20,7 +20,7 @@ export class SubComColSectionComponent implements OnInit { } ngOnInit(): void { - this.community$ = this.route.data.pipe( + this.community$ = this.route.parent.data.pipe( map((data: Data) => (data.dso as RemoteData).payload), ); } diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index 89178f8dd21..ccdff75fdb9 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -21,6 +21,11 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import objectContaining = jasmine.objectContaining; import { RemoteData } from './remote-data'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { BundleDataService } from './bundle-data.service'; +import { ItemMock } from 'src/app/shared/mocks/item.mock'; +import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils'; +import { Bundle } from '../shared/bundle.model'; +import { cold } from 'jasmine-marbles'; describe('BitstreamDataService', () => { let service: BitstreamDataService; @@ -29,6 +34,7 @@ describe('BitstreamDataService', () => { let halService: HALEndpointService; let bitstreamFormatService: BitstreamFormatDataService; let rdbService: RemoteDataBuildService; + let bundleDataService: BundleDataService; const bitstreamFormatHref = 'rest-api/bitstreamformats'; const bitstream1 = Object.assign(new Bitstream(), { @@ -62,6 +68,7 @@ describe('BitstreamDataService', () => { bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', { getBrowseEndpoint: observableOf(bitstreamFormatHref) }); + rdbService = getMockRemoteDataBuildService(); TestBed.configureTestingModule({ @@ -76,6 +83,7 @@ describe('BitstreamDataService', () => { ], }); service = TestBed.inject(BitstreamDataService); + bundleDataService = TestBed.inject(BundleDataService); }); describe('composition', () => { @@ -118,6 +126,32 @@ describe('BitstreamDataService', () => { expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self'); }); + describe('findPrimaryBitstreamByItemAndName', () => { + it('should return primary bitstream', () => { + const exprected$ = cold('(a|)', { a: bitstream1} ); + const bundle = Object.assign(new Bundle(), { + primaryBitstream: observableOf(createSuccessfulRemoteDataObject(bitstream1)), + }); + spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle))); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$); + }); + + it('should return null if primary bitstream has not be succeeded ', () => { + const exprected$ = cold('(a|)', { a: null} ); + const bundle = Object.assign(new Bundle(), { + primaryBitstream: observableOf(createFailedRemoteDataObject()), + }); + spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle))); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$); + }); + + it('should return EMPTY if nothing where found', () => { + const exprected$ = cold('(|)', {} ); + spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createFailedRemoteDataObject())); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$); + }); + }); + it('should be able to delete multiple bitstreams', () => { service.removeMultiple([bitstream1, bitstream2]); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index bb4ec281665..97949ffa25c 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -1,9 +1,9 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, EMPTY } from 'rxjs'; import { find, map, switchMap, take } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { FollowLinkConfig, followLink } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Bitstream } from '../shared/bitstream.model'; @@ -34,6 +34,7 @@ import { NoContent } from '../shared/NoContent.model'; import { IdentifiableDataService } from './base/identifiable-data.service'; import { dataService } from './base/data-service.decorator'; import { Operation, RemoveOperation } from 'fast-json-patch'; +import { getFirstCompletedRemoteData } from '../shared/operators'; /** * A service to retrieve {@link Bitstream}s from the REST API @@ -201,6 +202,37 @@ export class BitstreamDataService extends IdentifiableDataService imp return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow); } + + /** + * + * Make a request to get primary bitstream + * in all current use cases, and having it simplifies this method + * + * @param item the {@link Item} the {@link Bundle} is a part of + * @param bundleName the name of the {@link Bundle} we want to find + * {@link Bitstream}s for + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @return {Observable} + * Return an observable that constains primary bitstream information or null + */ + public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable { + return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe( + getFirstCompletedRemoteData(), + switchMap((rd: RemoteData) => { + if (!rd.hasSucceeded) { + return EMPTY; + } + return rd.payload.primaryBitstream.pipe( + getFirstCompletedRemoteData(), + map((rdb: RemoteData) => rdb.hasSucceeded ? rdb.payload : null) + ); + }) + ); + } + /** * Make a new FindListRequest with given search method * diff --git a/src/app/core/submission/models/workspaceitem-section-upload.model.ts b/src/app/core/submission/models/workspaceitem-section-upload.model.ts index f98e0584ebc..d992567df4c 100644 --- a/src/app/core/submission/models/workspaceitem-section-upload.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-upload.model.ts @@ -4,7 +4,10 @@ import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-up * An interface to represent submission's upload section data. */ export interface WorkspaceitemSectionUploadObject { - + /** + * Primary bitstream flag + */ + primary: string | null; /** * A list of [[WorkspaceitemSectionUploadFileObject]] */ 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 cd708510e8c..eee29603a54 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 @@ -2,7 +2,10 @@
- {{ dsoNameService.getName(file) }} + + {{ 'item.page.bitstreams.primary' | translate }} + {{ dsoNameService.getName(file) }} + ({{(file?.sizeBytes) | dsFileSize }}) diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts index 8acf405b55f..4a825e50c91 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts @@ -25,7 +25,8 @@ describe('FileSectionComponent', () => { let fixture: ComponentFixture; const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { - findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([])) + findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([])), + findPrimaryBitstreamByItemAndName: observableOf(null) }); const mockBitstream: Bitstream = Object.assign(new Bitstream(), @@ -81,6 +82,20 @@ describe('FileSectionComponent', () => { fixture.detectChanges(); })); + it('should set the id of primary bitstream', () => { + comp.primaryBitsreamId = undefined; + bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(observableOf(mockBitstream)); + comp.ngOnInit(); + expect(comp.primaryBitsreamId).toBe(mockBitstream.id); + }); + + it('should not set the id of primary bitstream', () => { + comp.primaryBitsreamId = undefined; + bitstreamDataService.findPrimaryBitstreamByItemAndName.and.returnValue(observableOf(null)); + comp.ngOnInit(); + expect(comp.primaryBitsreamId).toBeUndefined(); + }); + describe('when the bitstreams are loading', () => { beforeEach(() => { comp.bitstreams$.next([mockBitstream]); diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.ts index 3c41731c5f4..76f33de9063 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.ts +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.ts @@ -39,6 +39,8 @@ export class FileSectionComponent implements OnInit { pageSize: number; + primaryBitsreamId: string; + constructor( protected bitstreamDataService: BitstreamDataService, protected notificationsService: NotificationsService, @@ -50,9 +52,19 @@ export class FileSectionComponent implements OnInit { } ngOnInit(): void { + this.getPrimaryBitstreamId(); this.getNextPage(); } + private getPrimaryBitstreamId() { + this.bitstreamDataService.findPrimaryBitstreamByItemAndName(this.item, 'ORIGINAL', true, true).subscribe((primaryBitstream: Bitstream | null) => { + if (!primaryBitstream) { + return; + } + this.primaryBitsreamId = primaryBitstream?.id; + }); + } + /** * This method will retrieve the next page of Bitstreams from the external BitstreamDataService call. * It'll retrieve the currentPage from the class variables and it'll add the next page of bitstreams with the diff --git a/src/app/search-navbar/search-navbar.component.spec.ts b/src/app/search-navbar/search-navbar.component.spec.ts index 7edae293e12..ece2391dcb0 100644 --- a/src/app/search-navbar/search-navbar.component.spec.ts +++ b/src/app/search-navbar/search-navbar.component.spec.ts @@ -88,7 +88,7 @@ describe('SearchNavbarComponent', () => { fixture.detectChanges(); })); it('to search page with empty query', () => { - const extras: NavigationExtras = { queryParams: { query: '' }, queryParamsHandling: 'merge' }; + const extras: NavigationExtras = { queryParams: { query: '' } }; expect(component.onSubmit).toHaveBeenCalledWith({ query: '' }); expect(router.navigate).toHaveBeenCalledWith(['search'], extras); }); @@ -113,7 +113,7 @@ describe('SearchNavbarComponent', () => { fixture.detectChanges(); })); it('to search page with query', async () => { - const extras: NavigationExtras = { queryParams: { query: 'test' }, queryParamsHandling: 'merge' }; + const extras: NavigationExtras = { queryParams: { query: 'test' } }; expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' }); expect(router.navigate).toHaveBeenCalledWith(['search'], extras); diff --git a/src/app/search-navbar/search-navbar.component.ts b/src/app/search-navbar/search-navbar.component.ts index 98e64f6e10f..7f8f951073f 100644 --- a/src/app/search-navbar/search-navbar.component.ts +++ b/src/app/search-navbar/search-navbar.component.ts @@ -66,8 +66,7 @@ export class SearchNavbarComponent { this.searchForm.reset(); this.router.navigate(linkToNavigateTo, { - queryParams: queryParams, - queryParamsHandling: 'merge' + queryParams: queryParams }); } } diff --git a/src/app/search-page/configuration-search-page.component.ts b/src/app/search-page/configuration-search-page.component.ts index 768149de6a8..9196dda0251 100644 --- a/src/app/search-page/configuration-search-page.component.ts +++ b/src/app/search-page/configuration-search-page.component.ts @@ -8,6 +8,7 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu import { RouteService } from '../core/services/route.service'; import { SearchService } from '../core/shared/search/search.service'; import { Router } from '@angular/router'; +import { APP_CONFIG, AppConfig } from '../../config/app-config.interface'; /** * This component renders a search page using a configuration as input. @@ -32,7 +33,9 @@ export class ConfigurationSearchPageComponent extends SearchComponent { protected windowService: HostWindowService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, protected routeService: RouteService, - protected router: Router) { - super(service, sidebarService, windowService, searchConfigService, routeService, router); + protected router: Router, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + ) { + super(service, sidebarService, windowService, searchConfigService, routeService, router, appConfig); } } diff --git a/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts index 2a3ec220d6a..c6e77d49e25 100644 --- a/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts @@ -55,16 +55,21 @@ export class ComcolPageBrowseByComponent implements OnDestroy, OnInit { if (this.contentType === 'collection') { comColRoute = getCollectionPageRoute(this.id); allOptions.push({ - id: 'recent_submissions', - label: 'collection.page.browse.recent.head', + id: 'search', + label: 'collection.page.browse.search.head', routerLink: comColRoute, }); } else if (this.contentType === 'community') { comColRoute = getCommunityPageRoute(this.id); + allOptions.push({ + id: 'search', + label: 'collection.page.browse.search.head', + routerLink: comColRoute, + }); allOptions.push({ id: 'comcols', label: 'community.all-lists.head', - routerLink: comColRoute, + routerLink: `${comColRoute}/subcoms-cols`, }); } diff --git a/src/app/shared/comcol/comcol.module.ts b/src/app/shared/comcol/comcol.module.ts index 7ce48a3a55d..21c6e368916 100644 --- a/src/app/shared/comcol/comcol.module.ts +++ b/src/app/shared/comcol/comcol.module.ts @@ -18,6 +18,8 @@ import { FormModule } from '../form/form.module'; import { UploadModule } from '../upload/upload.module'; import { ComcolBrowseByComponent } from './sections/comcol-browse-by/comcol-browse-by.component'; import { BrowseByModule } from '../../browse-by/browse-by.module'; +import { SearchModule } from '../search/search.module'; +import { ComcolSearchSectionComponent } from './sections/comcol-search-section/comcol-search-section.component'; const COMPONENTS = [ ComcolPageContentComponent, @@ -33,6 +35,7 @@ const COMPONENTS = [ ComcolRoleComponent, ThemedComcolPageHandleComponent, ComcolBrowseByComponent, + ComcolSearchSectionComponent, ]; @NgModule({ @@ -45,6 +48,7 @@ const COMPONENTS = [ SharedModule, UploadModule, BrowseByModule, + SearchModule, ], exports: [ ...COMPONENTS, diff --git a/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.html b/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.html new file mode 100644 index 00000000000..7c97dabf43a --- /dev/null +++ b/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.html @@ -0,0 +1,7 @@ + + diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.scss b/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.scss similarity index 100% rename from src/app/collection-page/sections/recently-added/collection-recently-added.component.scss rename to src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.scss diff --git a/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.spec.ts b/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.spec.ts new file mode 100644 index 00000000000..6b1f9236b1f --- /dev/null +++ b/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComcolSearchSectionComponent } from './comcol-search-section.component'; +import { ActivatedRoute } from '@angular/router'; +import { ActivatedRouteStub } from '../../../testing/active-router.stub'; +import { APP_CONFIG } from '../../../../../config/app-config.interface'; +import { environment } from '../../../../../environments/environment.test'; + +describe('ComcolSearchSectionComponent', () => { + let component: ComcolSearchSectionComponent; + let fixture: ComponentFixture; + + let route: ActivatedRouteStub; + + beforeEach(async () => { + route = new ActivatedRouteStub(); + + await TestBed.configureTestingModule({ + declarations: [ + ComcolSearchSectionComponent, + ], + providers: [ + { provide: APP_CONFIG, useValue: environment }, + { provide: ActivatedRoute, useValue: route }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ComcolSearchSectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.ts b/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.ts new file mode 100644 index 00000000000..fe50147395a --- /dev/null +++ b/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ActivatedRoute, Data } from '@angular/router'; +import { map } from 'rxjs/operators'; +import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { Community } from '../../../../core/shared/community.model'; +import { Collection } from '../../../../core/shared/collection.model'; +import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface'; +import { hasValue } from '../../../empty.util'; + +/** + * The search tab on community & collection pages + */ +@Component({ + selector: 'ds-comcol-search-section', + templateUrl: './comcol-search-section.component.html', + styleUrls: ['./comcol-search-section.component.scss'], + providers: [ + { + provide: SEARCH_CONFIG_SERVICE, + useClass: SearchConfigurationService, + }, + ], +}) +export class ComcolSearchSectionComponent implements OnInit { + + comcol$: Observable; + + showSidebar$: Observable; + + constructor( + @Inject(APP_CONFIG) public appConfig: AppConfig, + protected route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.comcol$ = this.route.data.pipe( + map((data: Data) => (data.dso as RemoteData).payload), + ); + this.showSidebar$ = this.comcol$.pipe( + map((comcol: Community | Collection) => hasValue(comcol) && this.appConfig[comcol.type as any].searchSection.showSidebar), + ); + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html index ed117a50219..5beb8d52dab 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss index e69de29bb2d..206ad174929 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss @@ -0,0 +1,16 @@ +div.custom-switch { + &.custom-control-right { + margin-left: 0; + margin-right: 0; + + &::after { + right: -1.5rem; + left: auto; + } + + &::before { + right: -2.35rem; + left: auto; + } + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts index ceb498fe567..4d626e7cdd3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts @@ -1,11 +1,12 @@ import { DynamicFormsCoreModule, DynamicFormService } from '@ng-dynamic-forms/core'; import { UntypedFormGroup, ReactiveFormsModule } from '@angular/forms'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; -import { DebugElement } from '@angular/core'; +import { DebugElement} from '@angular/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { DynamicCustomSwitchModel } from './custom-switch.model'; import { CustomSwitchComponent } from './custom-switch.component'; +import { TranslateModule } from '@ngx-translate/core'; describe('CustomSwitchComponent', () => { @@ -20,9 +21,10 @@ describe('CustomSwitchComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ + TranslateModule.forRoot(), ReactiveFormsModule, NoopAnimationsModule, - DynamicFormsCoreModule.forRoot() + DynamicFormsCoreModule.forRoot(), ], declarations: [CustomSwitchComponent] diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts index 5e0538a1715..c902807cd43 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts @@ -11,7 +11,7 @@ import { import { UntypedFormGroup } from '@angular/forms'; import { Observable, of as observableOf } from 'rxjs'; -import { catchError, map, tap } from 'rxjs/operators'; +import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; @@ -68,10 +68,14 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom */ ngOnInit() { this.updatePageInfo(this.model.maxOptions, 1); - this.loadOptions(); + this.loadOptions(true); + this.group.get(this.model.id).valueChanges.pipe(distinctUntilChanged()) + .subscribe((value) => { + this.setCurrentValue(value); + }); } - loadOptions() { + loadOptions(fromInit: boolean) { this.loading = true; this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe( getFirstSucceededRemoteDataPayload(), @@ -79,6 +83,10 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom tap(() => this.loading = false) ).subscribe((list: PaginatedList) => { this.optionsList = list.page; + if (fromInit && this.model.value) { + this.setCurrentValue(this.model.value, true); + } + this.updatePageInfo( list.pageInfo.elementsPerPage, list.pageInfo.currentPage, @@ -104,7 +112,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom this.group.markAsUntouched(); this.inputText = null; this.updatePageInfo(this.model.maxOptions, 1); - this.loadOptions(); + this.loadOptions(false); sdRef.open(); } } @@ -161,7 +169,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom this.inputText += keyName; // When a new key is added, we need to reset the page info this.updatePageInfo(this.model.maxOptions, 1); - this.loadOptions(); + this.loadOptions(false); } removeKeyFromInput() { @@ -170,7 +178,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom if (this.inputText === '') { this.inputText = null; } - this.loadOptions(); + this.loadOptions(false); } } diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index cf6f38bf7b8..8bd449a07d1 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -196,6 +196,7 @@ export class FormBuilderService extends DynamicFormService { return new FormFieldMetadataValueObject((controlValue as any).value, controlLanguage, authority, (controlValue as any).display, place, (controlValue as any).confidence); } } + return controlValue; }; const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): void => { diff --git a/src/app/shared/mocks/section-upload.service.mock.ts b/src/app/shared/mocks/section-upload.service.mock.ts index ae3515105d9..4e872522c33 100644 --- a/src/app/shared/mocks/section-upload.service.mock.ts +++ b/src/app/shared/mocks/section-upload.service.mock.ts @@ -5,6 +5,9 @@ import { SubmissionFormsConfigDataService } from '../../core/config/submission-f */ export function getMockSectionUploadService(): SubmissionFormsConfigDataService { return jasmine.createSpyObj('SectionUploadService', { + updatePrimaryBitstreamOperation: jasmine.createSpy('updatePrimaryBitstreamOperation'), + updateFilePrimaryBitstream: jasmine.createSpy('updateFilePrimaryBitstream'), + getUploadedFilesData: jasmine.createSpy('getUploadedFilesData'), getUploadedFileList: jasmine.createSpy('getUploadedFileList'), getFileData: jasmine.createSpy('getFileData'), getDefaultPolicies: jasmine.createSpy('getDefaultPolicies'), diff --git a/src/app/shared/mocks/submission.mock.ts b/src/app/shared/mocks/submission.mock.ts index 268ae33ab39..bec08013c35 100644 --- a/src/app/shared/mocks/submission.mock.ts +++ b/src/app/shared/mocks/submission.mock.ts @@ -1612,7 +1612,13 @@ export const mockUploadFiles = [ } ]; +export const mockUploadFilesData = { + primary: null, + files: JSON.parse(JSON.stringify(mockUploadFiles)) +}; + export const mockFileFormData = { + primary: [true], metadata: { 'dc.title': [ { diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index 95a063bdd67..e6718ede58f 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Input, Output, OnChanges } from '@angular/core'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Router } from '@angular/router'; -import { isNotEmpty } from '../empty.util'; +import { isNotEmpty, hasValue } from '../empty.util'; import { SearchService } from '../../core/shared/search/search.service'; import { currentPath } from '../utils/route.utils'; import { PaginationService } from '../../core/pagination/pagination.service'; @@ -39,6 +39,11 @@ export class SearchFormComponent implements OnChanges { @Input() scope = ''; + /** + * Hides the scope in the url, this can be useful when you hardcode the scope in another way + */ + @Input() hideScopeInUrl = false; + selectedScope: BehaviorSubject = new BehaviorSubject(undefined); @Input() currentUrl: string; @@ -122,6 +127,9 @@ export class SearchFormComponent implements OnChanges { }, data ); + if (hasValue(data.scope) && this.hideScopeInUrl) { + delete queryParams.scope; + } void this.router.navigate(this.getSearchLinkParts(), { queryParams: queryParams, diff --git a/src/app/shared/search-form/themed-search-form.component.ts b/src/app/shared/search-form/themed-search-form.component.ts index 50b3751b06d..83c396b2f9a 100644 --- a/src/app/shared/search-form/themed-search-form.component.ts +++ b/src/app/shared/search-form/themed-search-form.component.ts @@ -18,6 +18,8 @@ export class ThemedSearchFormComponent extends ThemedComponent = new EventEmitter(); protected inAndOutputNames: (keyof SearchFormComponent & keyof this)[] = [ - 'query', 'inPlaceSearch', 'scope', 'currentUrl', 'large', 'brandColor', 'searchPlaceholder', 'showScopeSelector', + 'query', + 'inPlaceSearch', + 'scope', + 'hideScopeInUrl', + 'currentUrl', + 'large', + 'brandColor', + 'searchPlaceholder', + 'showScopeSelector', 'submitSearch', ]; diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts index 4469a124ce6..a6212ca9c5e 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts @@ -27,6 +27,8 @@ import { SearchConfigurationServiceStub } from '../../../../testing/search-confi import { VocabularyEntryDetail } from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { FacetValue} from '../../../models/facet-value.model'; import { SearchFilterConfig } from '../../../models/search-filter-config.model'; +import { APP_CONFIG } from '../../../../../../config/app-config.interface'; +import { environment } from '../../../../../../environments/environment.test'; describe('SearchHierarchyFilterComponent', () => { @@ -34,7 +36,7 @@ describe('SearchHierarchyFilterComponent', () => { let showVocabularyTreeLink: DebugElement; const testSearchLink = 'test-search'; - const testSearchFilter = 'test-search-filter'; + const testSearchFilter = 'subject'; const VocabularyTreeViewComponent = { select: new EventEmitter(), }; @@ -73,6 +75,7 @@ describe('SearchHierarchyFilterComponent', () => { { provide: Router, useValue: router }, { provide: NgbModal, useValue: ngbModal }, { provide: VocabularyService, useValue: vocabularyService }, + { provide: APP_CONFIG, useValue: environment }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: IN_PLACE_SEARCH, useValue: false }, { provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) }, @@ -86,7 +89,7 @@ describe('SearchHierarchyFilterComponent', () => { function init() { fixture = TestBed.createComponent(SearchHierarchyFilterComponent); fixture.detectChanges(); - showVocabularyTreeLink = fixture.debugElement.query(By.css('a#show-test-search-filter-tree')); + showVocabularyTreeLink = fixture.debugElement.query(By.css(`a#show-${testSearchFilter}-tree`)); } describe('if the vocabulary doesn\'t exist', () => { diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts index d53fa37cf4b..18ddbbdf97e 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts @@ -24,9 +24,11 @@ import { filter, map, take } from 'rxjs/operators'; import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service'; import { Observable, BehaviorSubject } from 'rxjs'; import { PageInfo } from '../../../../../core/shared/page-info.model'; -import { environment } from '../../../../../../environments/environment'; import { addOperatorToFilterValue } from '../../../search.utils'; import { VocabularyTreeviewModalComponent } from '../../../../form/vocabulary-treeview-modal/vocabulary-treeview-modal.component'; +import { hasValue } from '../../../../empty.util'; +import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface'; +import { FilterVocabularyConfig } from '../../../../../../config/filter-vocabulary-config'; @Component({ selector: 'ds-search-hierarchy-filter', @@ -47,6 +49,7 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i protected router: Router, protected modalService: NgbModal, protected vocabularyService: VocabularyService, + @Inject(APP_CONFIG) protected appConfig: AppConfig, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean, @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, @@ -67,17 +70,20 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i super.onSubmit(addOperatorToFilterValue(data, 'query')); } - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); - this.vocabularyExists$ = this.vocabularyService.searchTopEntries( - this.getVocabularyEntry(), new PageInfo(), true, false, - ).pipe( - filter(rd => rd.hasCompleted), - take(1), - map(rd => { - return rd.hasSucceeded; - }), - ); + const vocabularyName: string = this.getVocabularyEntry(); + if (hasValue(vocabularyName)) { + this.vocabularyExists$ = this.vocabularyService.searchTopEntries( + vocabularyName, new PageInfo(), true, false, + ).pipe( + filter(rd => rd.hasCompleted), + take(1), + map(rd => { + return rd.hasSucceeded; + }), + ); + } } /** @@ -93,11 +99,11 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i name: this.getVocabularyEntry(), closed: true }; - modalRef.result.then((detail: VocabularyEntryDetail) => { - this.selectedValues$ + void modalRef.result.then((detail: VocabularyEntryDetail) => { + this.subs.push(this.selectedValues$ .pipe(take(1)) .subscribe((selectedValues) => { - this.router.navigate( + void this.router.navigate( [this.searchService.getSearchLink()], { queryParams: { @@ -107,16 +113,16 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i queryParamsHandling: 'merge', }, ); - }); - }).catch(); + })); + }); } /** * Returns the matching vocabulary entry for the given search filter. * These are configurable in the config file. */ - getVocabularyEntry() { - const foundVocabularyConfig = environment.vocabularies.filter((v) => v.filter === this.filterConfig.name); + getVocabularyEntry(): string { + const foundVocabularyConfig: FilterVocabularyConfig[] = this.appConfig.vocabularies.filter((v: FilterVocabularyConfig) => v.filter === this.filterConfig.name); if (foundVocabularyConfig.length > 0 && foundVocabularyConfig[0].enabled === true) { return foundVocabularyConfig[0].vocabulary; } diff --git a/src/app/shared/search/search.component.html b/src/app/shared/search/search.component.html index e7dd02e2863..acd60f2616b 100644 --- a/src/app/shared/search/search.component.html +++ b/src/app/shared/search/search.component.html @@ -85,6 +85,7 @@ ; @@ -209,7 +211,8 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar { provide: SEARCH_CONFIG_SERVICE, useValue: searchConfigurationServiceStub - } + }, + { provide: APP_CONFIG, useValue: environment }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(compType, { diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts index fc07893d721..d2343234cc5 100644 --- a/src/app/shared/search/search.component.ts +++ b/src/app/shared/search/search.component.ts @@ -31,13 +31,13 @@ import { ViewMode } from '../../core/shared/view-mode.model'; import { SelectionConfig } from './search-results/search-results.component'; import { ListableObject } from '../object-collection/shared/listable-object.model'; import { CollectionElementLinkType } from '../object-collection/collection-element-link.type'; -import { environment } from 'src/environments/environment'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SearchFilterConfig } from './models/search-filter-config.model'; import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; import { ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths'; import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths'; +import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface'; @Component({ selector: 'ds-search', @@ -171,6 +171,11 @@ export class SearchComponent implements OnDestroy, OnInit { */ @Input() scope: string; + /** + * Hides the scope in the url, this can be useful when you hardcode the scope in another way + */ + @Input() hideScopeInUrl: boolean; + /** * The current configuration used during the search */ @@ -278,7 +283,9 @@ export class SearchComponent implements OnDestroy, OnInit { protected windowService: HostWindowService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, protected routeService: RouteService, - protected router: Router) { + protected router: Router, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + ) { this.isXsOrSm$ = this.windowService.isXsOrSm(); } @@ -445,8 +452,10 @@ export class SearchComponent implements OnDestroy, OnInit { let followLinks = [ followLink('thumbnail', { isOptional: true }), followLink('item', { isOptional: true }, followLink('thumbnail', { isOptional: true })) as any, - followLink('accessStatus', { isOptional: true, shouldEmbed: environment.item.showAccessStatuses }), ]; + if (this.appConfig.item.showAccessStatuses) { + followLinks.push(followLink('accessStatus', { isOptional: true })); + } if (this.configuration === 'supervision') { followLinks.push(followLink('supervisionOrders', { isOptional: true }) as any); } @@ -476,18 +485,20 @@ export class SearchComponent implements OnDestroy, OnInit { * This method should only be called once and is essentially what SearchTrackingComponent used to do (now removed) * @private */ - private subscribeToRoutingEvents() { - this.subs.push( - this.router.events.pipe( - filter((event) => event instanceof NavigationStart), - map((event: NavigationStart) => this.getDsoUUIDFromUrl(event.url)), - hasValueOperator(), - ).subscribe((uuid) => { - if (this.resultsRD$.value.hasSucceeded) { - this.service.trackSearch(this.searchOptions$.value, this.resultsRD$.value.payload as SearchObjects, uuid); - } - }), - ); + private subscribeToRoutingEvents(): void { + if (this.trackStatistics) { + this.subs.push( + this.router.events.pipe( + filter((event) => event instanceof NavigationStart), + map((event: NavigationStart) => this.getDsoUUIDFromUrl(event.url)), + hasValueOperator(), + ).subscribe((uuid) => { + if (this.resultsRD$.value.hasSucceeded) { + this.service.trackSearch(this.searchOptions$.value, this.resultsRD$.value.payload as SearchObjects, uuid); + } + }), + ); + } } /** diff --git a/src/app/shared/search/themed-search.component.ts b/src/app/shared/search/themed-search.component.ts index ef0afad7b3c..d2f16d80b27 100644 --- a/src/app/shared/search/themed-search.component.ts +++ b/src/app/shared/search/themed-search.component.ts @@ -43,6 +43,7 @@ export class ThemedSearchComponent extends ThemedComponent { 'trackStatistics', 'query', 'scope', + 'hideScopeInUrl', 'resultFound', 'deselectObject', 'selectObject', @@ -94,6 +95,8 @@ export class ThemedSearchComponent extends ThemedComponent { @Input() scope: string; + @Input() hideScopeInUrl: boolean; + @Output() resultFound: EventEmitter> = new EventEmitter(); @Output() deselectObject: EventEmitter = new EventEmitter(); diff --git a/src/app/shared/sidebar/page-with-sidebar.component.html b/src/app/shared/sidebar/page-with-sidebar.component.html index 9feb6c792ef..75f40ef8d63 100644 --- a/src/app/shared/sidebar/page-with-sidebar.component.html +++ b/src/app/shared/sidebar/page-with-sidebar.component.html @@ -3,10 +3,12 @@
-
+
diff --git a/src/app/shared/testing/active-router.stub.ts b/src/app/shared/testing/active-router.stub.ts index 495920555b3..4c6dd456d3e 100644 --- a/src/app/shared/testing/active-router.stub.ts +++ b/src/app/shared/testing/active-router.stub.ts @@ -1,19 +1,19 @@ import { map } from 'rxjs/operators'; -import { convertToParamMap, Params } from '@angular/router'; - +import { ActivatedRoute, convertToParamMap, Data, Params } from '@angular/router'; import { BehaviorSubject } from 'rxjs'; export class ActivatedRouteStub { - private _testParams?: any; - private _testData?: any; + private _testParams?: Params; + private _testData?: Data; // ActivatedRoute.params is Observable - private subject?: BehaviorSubject = new BehaviorSubject(this.testParams); - private dataSubject?: BehaviorSubject = new BehaviorSubject(this.testData); + private subject?: BehaviorSubject = new BehaviorSubject(this.testParams); + private dataSubject?: BehaviorSubject = new BehaviorSubject(this.testData); params = this.subject.asObservable(); queryParams = this.subject.asObservable(); paramMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params))); + parent: ActivatedRoute | ActivatedRouteStub; queryParamMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params))); data = this.dataSubject.asObservable(); @@ -35,17 +35,17 @@ export class ActivatedRouteStub { return this._testParams; } - set testParams(params: {}) { + set testParams(params: Params) { this._testParams = params; this.subject.next(params); } // Test data get testData() { - return this._testParams; + return this._testData; } - set testData(data: {}) { + set testData(data: Data) { this._testData = data; this.dataSubject.next(data); } diff --git a/src/app/statistics/angulartics/dspace-provider.ts b/src/app/statistics/angulartics/dspace-provider.ts index 86c16b5c011..957b09a0c8a 100644 --- a/src/app/statistics/angulartics/dspace-provider.ts +++ b/src/app/statistics/angulartics/dspace-provider.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; +import { Angulartics2, EventTrack } from 'angulartics2'; import { StatisticsService } from '../statistics.service'; /** @@ -23,7 +23,7 @@ export class Angulartics2DSpace { .subscribe((event) => this.eventTrack(event)); } - private eventTrack(event) { + private eventTrack(event: Partial): void { if (event.action === 'page_view') { this.statisticsService.trackViewEvent(event.properties.object, event.properties.referrer); } else if (event.action === 'search') { @@ -32,7 +32,7 @@ export class Angulartics2DSpace { event.properties.page, event.properties.sort, event.properties.filters, - event.properties.clickedObject, + event.properties.clickedObject?.split('?')[0], ); } } diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 9182611e479..86d90f05f30 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -59,6 +59,7 @@ export const SubmissionObjectActionTypes = { // Upload file types NEW_FILE: type('dspace/submission/NEW_FILE'), EDIT_FILE_DATA: type('dspace/submission/EDIT_FILE_DATA'), + EDIT_FILE_PRIMARY_BITSTREAM_DATA: type('dspace/submission/EDIT_FILE_PRIMARY_BITSTREAM_DATA'), DELETE_FILE: type('dspace/submission/DELETE_FILE'), // Errors @@ -760,6 +761,29 @@ export class NewUploadedFileAction implements Action { } } +export class EditFilePrimaryBitstreamAction implements Action { + type = SubmissionObjectActionTypes.EDIT_FILE_PRIMARY_BITSTREAM_DATA; + payload: { + submissionId: string; + sectionId: string; + fileId: string | null; + }; + + /** + * Edit a file data + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID + * @param fileId + * the file's ID + */ + constructor(submissionId: string, sectionId: string, fileId: string | null) { + this.payload = { submissionId, sectionId, fileId: fileId }; + } +} + export class EditFileDataAction implements Action { type = SubmissionObjectActionTypes.EDIT_FILE_DATA; payload: { @@ -833,6 +857,7 @@ export type SubmissionObjectAction = DisableSectionAction | SectionStatusChangeAction | NewUploadedFileAction | EditFileDataAction + | EditFilePrimaryBitstreamAction | DeleteUploadedFileAction | InertSectionErrorsAction | DeleteSectionErrorsAction diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index a05bf05f52c..4970e25d325 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -1,4 +1,4 @@ -import { hasValue, isEmpty, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, isNotNull, isNull, isUndefined } from '../../shared/empty.util'; import differenceWith from 'lodash/differenceWith'; import findKey from 'lodash/findKey'; import isEqual from 'lodash/isEqual'; @@ -14,6 +14,7 @@ import { DepositSubmissionSuccessAction, DisableSectionAction, EditFileDataAction, + EditFilePrimaryBitstreamAction, EnableSectionAction, InertSectionErrorsAction, InitSectionAction, @@ -203,6 +204,10 @@ export function submissionObjectReducer(state = initialState, action: Submission return newFile(state, action as NewUploadedFileAction); } + case SubmissionObjectActionTypes.EDIT_FILE_PRIMARY_BITSTREAM_DATA: { + return editPrimaryBitstream(state, action as EditFilePrimaryBitstreamAction); + } + case SubmissionObjectActionTypes.EDIT_FILE_DATA: { return editFileData(state, action as EditFileDataAction); } @@ -735,6 +740,46 @@ function newFile(state: SubmissionObjectState, action: NewUploadedFileAction): S }); } +/** + * Edit primary bitstream. + * + * @param state + * the current state + * @param action + * an EditFilePrimaryBitstreamAction action + * @return SubmissionObjectState + * the new state, with the edited file. + */ +function editPrimaryBitstream(state: SubmissionObjectState, action: EditFilePrimaryBitstreamAction): SubmissionObjectState { + const filesData = state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data as WorkspaceitemSectionUploadObject; + const { submissionId, sectionId, fileId } = action.payload; + + const fileIndex = findKey(filesData.files, { uuid: fileId }); + if (isNull(fileIndex)) { + return state; + } + + const submission = state[submissionId]; + return { + ...state, + [submissionId]: { + ...submission, + sections: { + ...submission.sections, + [sectionId]: { + ...submission.sections[sectionId], + data: { + ...submission.sections[sectionId].data as WorkspaceitemSectionUploadObject, + primary: fileId + } + } + }, + isLoading: submission.isLoading, + savePending: submission.savePending, + } + }; +} + /** * Edit a file. * diff --git a/src/app/submission/sections/license/section-license.component.ts b/src/app/submission/sections/license/section-license.component.ts index e9a0cf15668..4b2e6761cc6 100644 --- a/src/app/submission/sections/license/section-license.component.ts +++ b/src/app/submission/sections/license/section-license.component.ts @@ -213,6 +213,7 @@ export class SubmissionSectionLicenseComponent extends SectionModelComponent { } else { this.operationsBuilder.remove(this.pathCombiner.getPath(path)); } + this.submissionService.dispatchSaveSection(this.submissionId, this.sectionData.id); } /** diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html index 2baa6c1555a..0cb79e51cb0 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html @@ -6,7 +6,6 @@