diff --git a/config/config.example.yml b/config/config.example.yml index 771c7b1653..d098d18ceb 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -231,6 +231,10 @@ themes: rel: manifest href: assets/dspace/images/favicons/manifest.webmanifest +# The default bundles that should always be displayed as suggestions when you upload a new bundle +bundle: + - standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ] + # Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video'). # For images, this enables a gallery viewer where you can zoom or page through images. # For videos, this enables embedded video streaming diff --git a/src/app/item-page/bitstreams/upload/upload-bitstream.component.html b/src/app/item-page/bitstreams/upload/upload-bitstream.component.html index 289ede209a..7a193451f7 100644 --- a/src/app/item-page/bitstreams/upload/upload-bitstream.component.html +++ b/src/app/item-page/bitstreams/upload/upload-bitstream.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/app/item-page/bitstreams/upload/upload-bitstream.component.spec.ts b/src/app/item-page/bitstreams/upload/upload-bitstream.component.spec.ts index 8fa475a2ce..e85dca2a98 100644 --- a/src/app/item-page/bitstreams/upload/upload-bitstream.component.spec.ts +++ b/src/app/item-page/bitstreams/upload/upload-bitstream.component.spec.ts @@ -21,19 +21,23 @@ import { createPaginatedList } from '../../../shared/testing/utils.test'; import { RouterStub } from '../../../shared/testing/router.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { AuthServiceStub } from '../../../shared/testing/auth-service.stub'; -import { getTestScheduler } from 'jasmine-marbles'; +import { environment } from '../../../../environments/environment'; +import { buildPaginatedList } from '../../../core/data/paginated-list.model'; +import { PageInfo } from '../../../core/shared/page-info.model'; -describe('UploadBistreamComponent', () => { +describe('UploadBitstreamComponent', () => { let comp: UploadBitstreamComponent; let fixture: ComponentFixture; + const customName = 'customBundleName'; const bundle = Object.assign(new Bundle(), { id: 'bundle', uuid: 'bundle', + name: customName, metadata: { 'dc.title': [ { - value: 'bundleName', + value: customName, language: null } ] @@ -42,14 +46,15 @@ describe('UploadBistreamComponent', () => { self: { href: 'bundle-selflink' } } }); - const customName = 'Custom Name'; + const customCreatedName = 'customCreatedBundleName'; const createdBundle = Object.assign(new Bundle(), { id: 'created-bundle', uuid: 'created-bundle', + name: customCreatedName, metadata: { 'dc.title': [ { - value: customName, + value: customCreatedName, language: null } ] @@ -72,13 +77,14 @@ describe('UploadBistreamComponent', () => { }, bundles: createSuccessfulRemoteDataObject$(createPaginatedList([bundle])) }); + const standardBundleSuggestions = environment.bundle.standardBundles; let routeStub; const routerStub = new RouterStub(); const restEndpoint = 'fake-rest-endpoint'; const mockItemDataService = jasmine.createSpyObj('mockItemDataService', { getBitstreamsEndpoint: observableOf(restEndpoint), createBundle: createSuccessfulRemoteDataObject$(createdBundle), - getBundles: createSuccessfulRemoteDataObject$([bundle]) + getBundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [bundle])), }); const bundleService = jasmine.createSpyObj('bundleService', { getBitstreamsEndpoint: observableOf(restEndpoint), @@ -94,22 +100,6 @@ describe('UploadBistreamComponent', () => { removeByHrefSubstring: {} }); - - describe('on init', () => { - beforeEach(waitForAsync(() => { - createUploadBitstreamTestingModule({ - bundle: bundle.id - }); - })); - beforeEach(() => { - loadFixtureAndComp(); - }); - it('should initialize the bundles', () => { - expect(comp.bundlesRD$).toBeDefined(); - getTestScheduler().expectObservable(comp.bundlesRD$).toBe('(a|)', {a: createSuccessfulRemoteDataObject([bundle])}); - }); - }); - describe('when a file is uploaded', () => { beforeEach(waitForAsync(() => { createUploadBitstreamTestingModule({}); @@ -164,12 +154,24 @@ describe('UploadBistreamComponent', () => { }); describe('and bundle name changed', () => { - beforeEach(() => { + beforeEach(waitForAsync(() => { + jasmine.getEnv().allowRespy(true); + mockItemDataService.getBundles.and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [bundle]))); + loadFixtureAndComp(); + })); + + it('should clear out the selected id if the name doesn\'t exist', () => { + comp.selectedBundleName = ''; comp.bundleNameChange(); + + expect(comp.selectedBundleId).toBeUndefined(); }); - it('should clear out the selected id', () => { - expect(comp.selectedBundleId).toBeUndefined(); + it('should select the correct id if the name exist', () => { + comp.selectedBundleName = customName; + comp.bundleNameChange(); + + expect(comp.selectedBundleId).toEqual(bundle.id); }); }); }); @@ -203,6 +205,69 @@ describe('UploadBistreamComponent', () => { }); }); + describe('when item has no bundles yet', () => { + beforeEach(waitForAsync(() => { + createUploadBitstreamTestingModule({ + bundle: bundle.id + }); + jasmine.getEnv().allowRespy(true); + mockItemDataService.getBundles.and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []))); + loadFixtureAndComp(); + })); + + it('should display only the standard bundles in dropdown', () => { + expect(comp.bundles.length).toEqual(standardBundleSuggestions.length); + for (let i = 0; i < standardBundleSuggestions.length; i++) { + // noinspection JSDeprecatedSymbols + expect(comp.bundles[i].name).toEqual(standardBundleSuggestions[i]); + } + }); + }); + + describe('when item has a custom bundle', () => { + beforeEach(waitForAsync(() => { + createUploadBitstreamTestingModule({ + bundle: bundle.id + }); + jasmine.getEnv().allowRespy(true); + mockItemDataService.getBundles.and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [bundle]))); + loadFixtureAndComp(); + })); + + it('should still display existing bitstream bundles if the bitstream has bundles', () => { + const expectedSuggestions = [bundle.name, ...standardBundleSuggestions]; + expect(comp.bundles.length).toEqual(expectedSuggestions.length); + for (let i = 0; i < expectedSuggestions.length; i++) { + // noinspection JSDeprecatedSymbols + expect(comp.bundles[i].name).toEqual(expectedSuggestions[i]); + } + }); + }); + + describe('when item has a standard bundle', () => { + let clonedBundle; + beforeEach(waitForAsync(() => { + clonedBundle = { ...bundle }; + expect(standardBundleSuggestions.length).toBeGreaterThan(0); + clonedBundle.name = standardBundleSuggestions[0]; + createUploadBitstreamTestingModule({ + bundle: clonedBundle.id + }); + jasmine.getEnv().allowRespy(true); + mockItemDataService.getBundles.and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [clonedBundle]))); + loadFixtureAndComp(); + })); + + it('should not show duplicate bundle names', () => { + const expectedSuggestions = [clonedBundle.name, ...standardBundleSuggestions.filter((standardBundleSuggestion: string) => standardBundleSuggestion !== clonedBundle.name)]; + expect(comp.bundles.length).toEqual(expectedSuggestions.length); + for (let i = 0; i < expectedSuggestions.length; i++) { + // noinspection JSDeprecatedSymbols + expect(comp.bundles[i].name).toEqual(expectedSuggestions[i]); + } + }); + }); + /** * Setup an UploadBitstreamComponent testing module with custom queryParams for the route * @param queryParams diff --git a/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts b/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts index f68aca2252..1e5295a347 100644 --- a/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts +++ b/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts @@ -1,8 +1,8 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { Observable, Subscription } from 'rxjs'; +import { Observable, Subscription, of as observableOf } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; -import { map, take } from 'rxjs/operators'; +import { map, take, switchMap } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { UploaderOptions } from '../../../shared/uploader/uploader-options.model'; import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; @@ -13,11 +13,12 @@ import { TranslateService } from '@ngx-translate/core'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Bundle } from '../../../core/shared/bundle.model'; import { BundleDataService } from '../../../core/data/bundle-data.service'; -import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { getFirstSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { UploaderComponent } from '../../../shared/uploader/uploader.component'; import { RequestService } from '../../../core/data/request.service'; import { getBitstreamModuleRoute } from '../../../app-routing-paths'; import { getEntityEditRoute } from '../../item-page-routing-paths'; +import { environment } from '../../../../environments/environment'; @Component({ selector: 'ds-upload-bitstream', @@ -49,9 +50,9 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { itemRD$: Observable>; /** - * The item's bundles + * The item's bundles and default the default bundles that should be suggested (defined in environment) */ - bundlesRD$: Observable>>; + bundles: Bundle[] = []; /** * The ID of the currently selected bundle to upload a bitstream to @@ -99,7 +100,6 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { /** * Initialize component properties: * itemRD$ Fetched from the current route data (populated by BitstreamPageResolver) - * bundlesRD$ List of bundles on the item * selectedBundleId Starts off by checking if the route's queryParams contain a "bundle" parameter. If none is found, * the ID of the first bundle in the list is selected. * Calls setUploadUrl after setting the selected bundle @@ -108,16 +108,47 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { this.itemId = this.route.snapshot.params.id; this.entityType = this.route.snapshot.params['entity-type']; this.itemRD$ = this.route.data.pipe(map((data) => data.dso)); - this.bundlesRD$ = this.itemService.getBundles(this.itemId); + const bundlesRD$ = this.itemService.getBundles(this.itemId).pipe( + getFirstCompletedRemoteData(), + switchMap((remoteData: RemoteData>) => { + if (remoteData.hasSucceeded) { + if (remoteData.payload.page) { + this.bundles = remoteData.payload.page; + for (const defaultBundle of environment.bundle.standardBundles) { + let check = true; + remoteData.payload.page.forEach((bundle: Bundle) => { + // noinspection JSDeprecatedSymbols + if (defaultBundle === bundle.name) { + check = false; + } + }); + if (check) { + this.bundles.push(Object.assign(new Bundle(), { + _name: defaultBundle, + type: 'bundle' + })); + } + } + } else { + this.bundles = environment.bundle.standardBundles.map((defaultBundle: string) => Object.assign(new Bundle(), { + _name: defaultBundle, + type: 'bundle' + }) + ); + } + return observableOf(remoteData.payload.page); + } + })); this.selectedBundleId = this.route.snapshot.queryParams.bundle; if (isNotEmpty(this.selectedBundleId)) { - this.bundleService.findById(this.selectedBundleId).pipe( + this.subs.push(this.bundleService.findById(this.selectedBundleId).pipe( getFirstSucceededRemoteDataPayload() ).subscribe((bundle: Bundle) => { this.selectedBundleName = bundle.name; - }); + })); this.setUploadUrl(); } + this.subs.push(bundlesRD$.subscribe()); } /** @@ -142,6 +173,13 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { */ bundleNameChange() { this.selectedBundleId = undefined; + for (const bundle of this.bundles) { + // noinspection JSDeprecatedSymbols + if (this.selectedBundleName === bundle.name) { + this.selectedBundleId = bundle.id; + break; + } + } } /** @@ -191,7 +229,9 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { onClick(bundle: Bundle) { this.selectedBundleId = bundle.id; this.selectedBundleName = bundle.name; - this.setUploadUrl(); + if (bundle.id) { + this.setUploadUrl(); + } } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index a7ce942e7d..f1b5d01c86 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1643,7 +1643,7 @@ "item.bitstreams.upload.bundle": "Bundle", - "item.bitstreams.upload.bundle.placeholder": "Select a bundle", + "item.bitstreams.upload.bundle.placeholder": "Select a bundle or input new bundle name", "item.bitstreams.upload.bundle.new": "Create bundle", diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 121e80cd74..a41ec05b82 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -14,6 +14,7 @@ import { AuthConfig } from './auth-config.interfaces'; import { UIServerConfig } from './ui-server-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface'; import { BrowseByConfig } from './browse-by-config.interface'; +import { BundleConfig } from './bundle-config.interface'; interface AppConfig extends Config { ui: UIServerConfig; @@ -32,6 +33,7 @@ interface AppConfig extends Config { collection: CollectionPageConfig; themes: ThemeConfig[]; mediaViewer: MediaViewerConfig; + bundle: BundleConfig; } const APP_CONFIG = new InjectionToken('APP_CONFIG'); diff --git a/src/config/bundle-config.interface.ts b/src/config/bundle-config.interface.ts new file mode 100644 index 0000000000..5ffef2156a --- /dev/null +++ b/src/config/bundle-config.interface.ts @@ -0,0 +1,11 @@ +import { Config } from './config.interface'; + +export interface BundleConfig extends Config { + + /** + * List of standard bundles to select in adding bitstreams to items + * Used by {@link UploadBitstreamComponent}. + */ + standardBundles: string[]; + +} diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index dc54c2fcb0..3b6dd67b91 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -14,6 +14,7 @@ import { ServerConfig } from './server-config.interface'; import { SubmissionConfig } from './submission-config.interface'; import { ThemeConfig } from './theme.model'; import { UIServerConfig } from './ui-server-config.interface'; +import { BundleConfig } from './bundle-config.interface'; export class DefaultAppConfig implements AppConfig { production = false; @@ -300,6 +301,11 @@ export class DefaultAppConfig implements AppConfig { ] }, ]; + // The default bundles that should always be displayed when you edit or add a bundle even when no bundle has been + // added to the item yet. + bundle: BundleConfig = { + standardBundles: ['ORIGINAL', 'THUMBNAIL', 'LICENSE'] + }; // Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with "image" or "video"). // For images, this enables a gallery viewer where you can zoom or page through images. // For videos, this enables embedded video streaming diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 7c24ef8f05..bc1622f5f2 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -228,6 +228,9 @@ export const environment: BuildConfig = { name: 'base', }, ], + bundle: { + standardBundles: ['ORIGINAL', 'THUMBNAIL', 'LICENSE'], + }, mediaViewer: { image: true, video: true