Skip to content

Commit

Permalink
Merge pull request DSpace#2631 from vNovski/CST-12044-visualize-the-p…
Browse files Browse the repository at this point in the history
…rimary-bitstream

CST-12044 visualize the primary bitstream & CST-12043 primary bitstream flag
  • Loading branch information
tdonohue authored Feb 20, 2024
2 parents daf6dfe + 02baf86 commit dcf5836
Show file tree
Hide file tree
Showing 31 changed files with 589 additions and 124 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}

Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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];

/**
Expand All @@ -256,8 +277,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
new DynamicFormGroupModel({
id: 'fileNamePrimaryContainer',
group: [
this.fileNameModel,
this.primaryBitstreamModel
this.primaryBitstreamModel,
this.fileNameModel
]
}, {
grid: {
Expand Down Expand Up @@ -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: {
Expand Down
34 changes: 34 additions & 0 deletions src/app/core/data/bitstream-data.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(), {
Expand Down Expand Up @@ -62,6 +68,7 @@ describe('BitstreamDataService', () => {
bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', {
getBrowseEndpoint: observableOf(bitstreamFormatHref)
});

rdbService = getMockRemoteDataBuildService();

TestBed.configureTestingModule({
Expand All @@ -76,6 +83,7 @@ describe('BitstreamDataService', () => {
],
});
service = TestBed.inject(BitstreamDataService);
bundleDataService = TestBed.inject(BundleDataService);
});

describe('composition', () => {
Expand Down Expand Up @@ -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<Bundle>()));
expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$);
});
});

it('should be able to delete multiple bitstreams', () => {
service.removeMultiple([bitstream1, bitstream2]);

Expand Down
36 changes: 34 additions & 2 deletions src/app/core/data/bitstream-data.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -201,6 +202,37 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> 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<Bitstream | null>}
* Return an observable that constains primary bitstream information or null
*/
public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<Bitstream | null> {
return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<Bundle>) => {
if (!rd.hasSucceeded) {
return EMPTY;
}
return rd.payload.primaryBitstream.pipe(
getFirstCompletedRemoteData(),
map((rdb: RemoteData<Bitstream>) => rdb.hasSucceeded ? rdb.payload : null)
);
})
);
}

/**
* Make a new FindListRequest with given search method
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
<div class="file-section">
<ds-themed-file-download-link *ngFor="let file of bitstreams; let last=last;" [bitstream]="file" [item]="item">
<span>{{ dsoNameService.getName(file) }}</span>
<span>
<span *ngIf="primaryBitsreamId === file.id" class="badge badge-primary">{{ 'item.page.bitstreams.primary' | translate }}</span>
{{ dsoNameService.getName(file) }}
</span>
<span> ({{(file?.sizeBytes) | dsFileSize }})</span>
<span *ngIf="!last" innerHTML="{{separator}}"></span>
</ds-themed-file-download-link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ describe('FileSectionComponent', () => {
let fixture: ComponentFixture<FileSectionComponent>;

const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([]))
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([])),
findPrimaryBitstreamByItemAndName: observableOf(null)
});

const mockBitstream: Bitstream = Object.assign(new Bitstream(),
Expand Down Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export class FileSectionComponent implements OnInit {

pageSize: number;

primaryBitsreamId: string;

constructor(
protected bitstreamDataService: BitstreamDataService,
protected notificationsService: NotificationsService,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div [formGroup]="group" class="form-check custom-control custom-switch" [class.disabled]="model.disabled">
<div [formGroup]="group" [ngClass]="getClass('element', 'container')" class="form-check custom-control custom-switch" [class.disabled]="model.disabled">
<input type="checkbox" class="form-check-input custom-control-input"
[checked]="model.checked"
[class.is-invalid]="showErrorMessages"
Expand All @@ -14,7 +14,7 @@
(change)="onChange($event)"
(focus)="onFocus($event)"/>
<label class="form-check-label custom-control-label" [for]="bindId && model.id">
<span [innerHTML]="model.label"
<span [innerHTML]="model.label | translate"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></span>
</label>
</div>
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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', () => {

Expand All @@ -20,9 +21,10 @@ describe('CustomSwitchComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
ReactiveFormsModule,
NoopAnimationsModule,
DynamicFormsCoreModule.forRoot()
DynamicFormsCoreModule.forRoot(),
],
declarations: [CustomSwitchComponent]

Expand Down
1 change: 1 addition & 0 deletions src/app/shared/form/builder/form-builder.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
3 changes: 3 additions & 0 deletions src/app/shared/mocks/section-upload.service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
6 changes: 6 additions & 0 deletions src/app/shared/mocks/submission.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': [
{
Expand Down
Loading

0 comments on commit dcf5836

Please sign in to comment.