Merge pull request #1247 from atmire/w2p-80195_Fix-move-item-page

Fix move item page
This commit is contained in:
Tim Donohue
2021-07-01 10:13:00 -05:00
committed by GitHub
15 changed files with 337 additions and 270 deletions

View File

@@ -5,19 +5,16 @@
<p>{{'item.edit.move.description' | translate}}</p>
<div class="row">
<div class="col-12">
<ds-dso-input-suggestions #f id="search-form"
[suggestions]="(collectionSearchResults | async)"
[placeholder]="'item.edit.move.search.placeholder'| translate"
[action]="getCurrentUrl()"
[name]="'item-move'"
[(ngModel)]="selectedCollectionName"
(clickSuggestion)="onClick($event)"
(typeSuggestion)="resetCollection($event)"
(findSuggestions)="findSuggestions($event)"
(click)="f.open()"
ngDefaultControl>
</ds-dso-input-suggestions>
<div class="card mb-3">
<div class="card-header">{{'dso-selector.placeholder' | translate: { type: 'collection' } }}</div>
<div class="card-body">
<ds-authorized-collection-selector [types]="COLLECTIONS"
[currentDSOId]="selectedCollection ? selectedCollection.id : originalCollection.id"
(onSelect)="selectDso($event)">
</ds-authorized-collection-selector>
</div>
<div></div>
</div>
</div>
</div>
<div class="row">
@@ -33,16 +30,24 @@
</div>
</div>
<button (click)="moveCollection()" class="btn btn-primary" [disabled]=!canSubmit>
<span *ngIf="!processing"> {{'item.edit.move.move' | translate}}</span>
<span *ngIf="processing"><i class='fas fa-circle-notch fa-spin'></i>
{{'item.edit.move.processing' | translate}}
</span>
</button>
<button [routerLink]="[(itemPageRoute$ | async), 'edit']"
class="btn btn-outline-secondary">
{{'item.edit.move.cancel' | translate}}
</button>
<div class="button-row bottom">
<div class="float-right">
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}}
</button>
<button class="btn btn-primary mr-0" [disabled]="!canMove" (click)="moveToCollection()">
<span *ngIf="!processing">
<i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}}
</span>
<span *ngIf="processing">
<i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}}
</span>
</button>
<button class="btn btn-danger" [disabled]="!canSubmit" (click)="discard()">
<i class="fas fa-times"></i> {{"item.edit.move.discard-button" | translate}}
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -21,6 +21,8 @@ import {
createSuccessfulRemoteDataObject$
} from '../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { RequestService } from '../../../core/data/request.service';
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
describe('ItemMoveComponent', () => {
let comp: ItemMoveComponent;
@@ -47,18 +49,25 @@ describe('ItemMoveComponent', () => {
name: 'Test collection 2'
});
const mockItemDataService = jasmine.createSpyObj({
moveToCollection: createSuccessfulRemoteDataObject$(collection1)
let itemDataService;
const mockItemDataServiceSuccess = jasmine.createSpyObj({
moveToCollection: createSuccessfulRemoteDataObject$(collection1),
findById: createSuccessfulRemoteDataObject$(mockItem),
});
const mockItemDataServiceFail = jasmine.createSpyObj({
moveToCollection: createFailedRemoteDataObject$('Internal server error', 500)
moveToCollection: createFailedRemoteDataObject$('Internal server error', 500),
findById: createSuccessfulRemoteDataObject$(mockItem),
});
const routeStub = {
data: observableOf({
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
id: 'item1'
id: 'item1',
owningCollection: createSuccessfulRemoteDataObject$(Object.assign(new Collection(), {
id: 'originalOwningCollection',
}))
}))
})
};
@@ -79,43 +88,40 @@ describe('ItemMoveComponent', () => {
const notificationsServiceStub = new NotificationsServiceStub();
const init = (mockItemDataService) => {
itemDataService = mockItemDataService;
TestBed.configureTestingModule({
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [ItemMoveComponent],
providers: [
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: Router, useValue: routerStub },
{ provide: ItemDataService, useValue: mockItemDataService },
{ provide: NotificationsService, useValue: notificationsServiceStub },
{ provide: SearchService, useValue: mockSearchService },
{ provide: RequestService, useValue: getMockRequestService() },
], schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
fixture = TestBed.createComponent(ItemMoveComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
};
describe('ItemMoveComponent success', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [ItemMoveComponent],
providers: [
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: Router, useValue: routerStub },
{ provide: ItemDataService, useValue: mockItemDataService },
{ provide: NotificationsService, useValue: notificationsServiceStub },
{ provide: SearchService, useValue: mockSearchService },
], schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
fixture = TestBed.createComponent(ItemMoveComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
init(mockItemDataServiceSuccess);
});
it('should load suggestions', () => {
const expected = [
collection1,
collection2
];
comp.collectionSearchResults.subscribe((value) => {
expect(value).toEqual(expected);
}
);
});
it('should get current url ', () => {
expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
});
it('should on click select the correct collection name and id', () => {
it('should select the correct collection name and id on click', () => {
const data = collection1;
comp.onClick(data);
comp.selectDso(data);
expect(comp.selectedCollectionName).toEqual('Test collection 1');
expect(comp.selectedCollection).toEqual(collection1);
@@ -128,12 +134,12 @@ describe('ItemMoveComponent', () => {
});
comp.selectedCollectionName = 'selected-collection-id';
comp.selectedCollection = collection1;
comp.moveCollection();
comp.moveToCollection();
expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
expect(itemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
});
it('should call notificationsService success message on success', () => {
comp.moveCollection();
comp.moveToCollection();
expect(notificationsServiceStub.success).toHaveBeenCalled();
});
@@ -142,26 +148,11 @@ describe('ItemMoveComponent', () => {
describe('ItemMoveComponent fail', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [ItemMoveComponent],
providers: [
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: Router, useValue: routerStub },
{ provide: ItemDataService, useValue: mockItemDataServiceFail },
{ provide: NotificationsService, useValue: notificationsServiceStub },
{ provide: SearchService, useValue: mockSearchService },
], schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
fixture = TestBed.createComponent(ItemMoveComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
init(mockItemDataServiceFail);
});
it('should call notificationsService error message on fail', () => {
comp.moveCollection();
comp.moveToCollection();
expect(notificationsServiceStub.error).toHaveBeenCalled();
});

View File

@@ -1,25 +1,21 @@
import { Component, OnInit } from '@angular/core';
import { first, map } from 'rxjs/operators';
import { map, switchMap } from 'rxjs/operators';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { RemoteData } from '../../../core/data/remote-data';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { Item } from '../../../core/shared/item.model';
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import {
getFirstSucceededRemoteData,
getFirstCompletedRemoteData, getAllSucceededRemoteDataPayload
getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload,
} from '../../../core/shared/operators';
import { ItemDataService } from '../../../core/data/item-data.service';
import { Observable, of as observableOf } from 'rxjs';
import { Observable } from 'rxjs';
import { Collection } from '../../../core/shared/collection.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SearchService } from '../../../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
import { SearchResult } from '../../../shared/search/search-result.model';
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { RequestService } from '../../../core/data/request.service';
@Component({
selector: 'ds-item-move',
@@ -38,7 +34,8 @@ export class ItemMoveComponent implements OnInit {
inheritPolicies = false;
itemRD$: Observable<RemoteData<Item>>;
collectionSearchResults: Observable<any[]> = observableOf([]);
originalCollection: Collection;
selectedCollectionName: string;
selectedCollection: Collection;
canSubmit = false;
@@ -46,23 +43,26 @@ export class ItemMoveComponent implements OnInit {
item: Item;
processing = false;
pagination = new PaginationComponentOptions();
/**
* Route to the item's page
*/
itemPageRoute$: Observable<string>;
COLLECTIONS = [DSpaceObjectType.COLLECTION];
constructor(private route: ActivatedRoute,
private router: Router,
private notificationsService: NotificationsService,
private itemDataService: ItemDataService,
private searchService: SearchService,
private translateService: TranslateService) {
}
private translateService: TranslateService,
private requestService: RequestService,
) {}
ngOnInit(): void {
this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getFirstSucceededRemoteData()) as Observable<RemoteData<Item>>;
this.itemRD$ = this.route.data.pipe(
map((data) => data.dso), getFirstSucceededRemoteData()
) as Observable<RemoteData<Item>>;
this.itemPageRoute$ = this.itemRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((item) => getItemPageRoute(item))
@@ -71,43 +71,22 @@ export class ItemMoveComponent implements OnInit {
this.item = rd.payload;
}
);
this.pagination.pageSize = 5;
this.loadSuggestions('');
}
/**
* Find suggestions based on entered query
* @param query - Search query
*/
findSuggestions(query): void {
this.loadSuggestions(query);
}
/**
* Load all available collections to move the item to.
* TODO: When the API support it, only fetch collections where user has ADD rights to.
*/
loadSuggestions(query): void {
this.collectionSearchResults = this.searchService.search(new PaginatedSearchOptions({
pagination: this.pagination,
dsoTypes: [DSpaceObjectType.COLLECTION],
query: query
})).pipe(
first(),
map((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => {
return rd.payload.page.map((searchResult) => {
return searchResult.indexableObject;
});
}) ,
);
this.itemRD$.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
switchMap((item) => item.owningCollection),
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
).subscribe((collection) => {
this.originalCollection = collection;
});
}
/**
* Set the collection name and id based on the selected value
* @param data - obtained from the ds-input-suggestions component
*/
onClick(data: any): void {
selectDso(data: any): void {
this.selectedCollection = data;
this.selectedCollectionName = data.name;
this.canSubmit = true;
@@ -123,26 +102,41 @@ export class ItemMoveComponent implements OnInit {
/**
* Moves the item to a new collection based on the selected collection
*/
moveCollection() {
moveToCollection() {
this.processing = true;
this.itemDataService.moveToCollection(this.item.id, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe(
(response: RemoteData<Collection>) => {
this.router.navigate([getItemEditRoute(this.item)]);
if (response.hasSucceeded) {
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
} else {
this.notificationsService.error(this.translateService.get('item.edit.move.error'));
}
this.processing = false;
const move$ = this.itemDataService.moveToCollection(this.item.id, this.selectedCollection)
.pipe(getFirstCompletedRemoteData());
move$.subscribe((response: RemoteData<any>) => {
if (response.hasSucceeded) {
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
} else {
this.notificationsService.error(this.translateService.get('item.edit.move.error'));
}
);
});
move$.pipe(
switchMap(() => this.requestService.setStaleByHrefSubstring(this.item.id)),
switchMap(() =>
this.itemDataService.findById(
this.item.id,
false,
true,
followLink('owningCollection')
)),
getFirstCompletedRemoteData()
).subscribe(() => {
this.processing = false;
this.router.navigate([getItemEditRoute(this.item)]);
});
}
/**
* Resets the can submit when the user changes the content of the input field
* @param data
*/
resetCollection(data: any) {
discard(): void {
this.selectedCollection = null;
this.canSubmit = false;
}
get canMove(): boolean {
return this.canSubmit && this.selectedCollection?.id !== this.originalCollection.id;
}
}

View File

@@ -23,14 +23,7 @@ import { DataService } from './data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
import {
DeleteRequest,
FindListOptions,
GetRequest,
PostRequest,
PutRequest,
RestRequest
} from './request.models';
import { DeleteRequest, FindListOptions, GetRequest, PostRequest, PutRequest, RestRequest } from './request.models';
import { RequestService } from './request.service';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { Bundle } from '../shared/bundle.model';
@@ -38,6 +31,9 @@ import { MetadataMap } from '../shared/metadata.models';
import { BundleDataService } from './bundle-data.service';
import { Operation } from 'fast-json-patch';
import { NoContent } from '../shared/NoContent.model';
import { GenericConstructor } from '../shared/generic-constructor';
import { ResponseParsingService } from './parsing.service';
import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service';
@Injectable()
@dataService(ITEM)
@@ -229,7 +225,7 @@ export class ItemDataService extends DataService<Item> {
* @param itemId
* @param collection
*/
public moveToCollection(itemId: string, collection: Collection): Observable<RemoteData<Collection>> {
public moveToCollection(itemId: string, collection: Collection): Observable<RemoteData<any>> {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
@@ -242,9 +238,17 @@ export class ItemDataService extends DataService<Item> {
find((href: string) => hasValue(href)),
map((href: string) => {
const request = new PutRequest(requestId, href, collection._links.self.href, options);
this.requestService.send(request);
Object.assign(request, {
// TODO: for now, the move Item endpoint returns a malformed collection -- only look at the status code
getResponseParser(): GenericConstructor<ResponseParsingService> {
return StatusCodeOnlyResponseParsingService;
}
});
return request;
})
).subscribe();
).subscribe((request) => {
this.requestService.send(request);
});
return this.rdbService.buildFromRequestUUID(requestId);
}

View File

@@ -14,12 +14,12 @@
[infiniteScrollContainer]="'.scrollable-menu'"
[fromRoot]="true"
(scrolled)="onScrollDown()">
<ng-container *ngIf="listEntries">
<ng-container *ngIf="listEntries$ | async">
<button class="list-group-item list-group-item-action border-0 disabled"
*ngIf="listEntries.length == 0">
*ngIf="(listEntries$ | async).length == 0">
{{'dso-selector.no-results' | translate: { type: typesString } }}
</button>
<button *ngFor="let listEntry of listEntries"
<button *ngFor="let listEntry of (listEntries$ | async)"
class="list-group-item list-group-item-action border-0 list-entry"
[ngClass]="{'bg-primary': listEntry.indexableObject.id === currentDSOId}"
title="{{ listEntry.indexableObject.name }}"

View File

@@ -92,12 +92,18 @@ describe('DSOSelectorComponent', () => {
});
describe('populating listEntries', () => {
it('should not be empty', () => {
expect(component.listEntries.length).toBeGreaterThan(0);
it('should not be empty', (done) => {
component.listEntries$.subscribe((listEntries) => {
expect(listEntries.length).toBeGreaterThan(0);
done();
});
});
it('should contain a combination of the current DSO and first page results', () => {
expect(component.listEntries).toEqual([searchResult, ...firstPageResults]);
it('should contain a combination of the current DSO and first page results', (done) => {
component.listEntries$.subscribe((listEntries) => {
expect(listEntries).toEqual([searchResult, ...firstPageResults]);
done();
});
});
describe('when current page increases', () => {
@@ -105,8 +111,11 @@ describe('DSOSelectorComponent', () => {
component.currentPage$.next(2);
});
it('should contain a combination of the current DSO, as well as first and second page results', () => {
expect(component.listEntries).toEqual([searchResult, ...firstPageResults, ...nextPageResults]);
it('should contain a combination of the current DSO, as well as first and second page results', (done) => {
component.listEntries$.subscribe((listEntries) => {
expect(listEntries).toEqual([searchResult, ...firstPageResults, ...nextPageResults]);
done();
});
});
});
});

View File

@@ -81,7 +81,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
/**
* List with search results of DSpace objects for the current query
*/
listEntries: SearchResult<DSpaceObject>[] = null;
listEntries$: BehaviorSubject<SearchResult<DSpaceObject>[]> = new BehaviorSubject(null);
/**
* The current page to load
@@ -160,7 +160,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
this.loading = true;
if (page === 1) {
// The first page is loading, this means we should reset the list instead of adding to it
this.listEntries = null;
this.listEntries$.next(null);
}
return this.search(query, page).pipe(
map((rd) => {
@@ -181,15 +181,16 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
).subscribe((rd) => {
this.loading = false;
if (rd.hasSucceeded) {
if (hasNoValue(this.listEntries)) {
this.listEntries = rd.payload.page;
const currentEntries = this.listEntries$.getValue();
if (hasNoValue(currentEntries)) {
this.listEntries$.next(rd.payload.page);
} else {
this.listEntries.push(...rd.payload.page);
this.listEntries$.next([...currentEntries, ...rd.payload.page]);
}
// Check if there are more pages available after the current one
this.hasNextPage = rd.payload.totalElements > this.listEntries.length;
this.hasNextPage = rd.payload.totalElements > this.listEntries$.getValue().length;
} else {
this.listEntries = null;
this.listEntries$.next(null);
this.hasNextPage = false;
}
}));

View File

@@ -1,5 +1,5 @@
import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { ChangeDetectionStrategy, ComponentFactoryResolver, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { ListableObjectComponentLoaderComponent } from './listable-object-component-loader.component';
import { ListableObject } from '../listable-object.model';
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
@@ -117,17 +117,33 @@ describe('ListableObjectComponentLoaderComponent', () => {
});
describe('When a reloadedObject is emitted', () => {
let listableComponent;
let reloadedObject: any;
it('should re-instantiate the listable component ', fakeAsync(() => {
beforeEach(() => {
spyOn((comp as any), 'connectInputsAndOutputs').and.returnValue(null);
spyOn((comp as any).contentChange, 'emit').and.returnValue(null);
spyOn((comp as any), 'instantiateComponent').and.returnValue(null);
listableComponent = fixture.debugElement.query(By.css('ds-item-list-element')).componentInstance;
reloadedObject = 'object';
});
it('should pass it on connectInputsAndOutputs', fakeAsync(() => {
expect((comp as any).connectInputsAndOutputs).not.toHaveBeenCalled();
const listableComponent = fixture.debugElement.query(By.css('ds-item-list-element')).componentInstance;
const reloadedObject: any = 'object';
(listableComponent as any).reloadedObject.emit(reloadedObject);
tick();
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject);
expect((comp as any).connectInputsAndOutputs).toHaveBeenCalled();
}));
it('should re-emit it as a contentChange', fakeAsync(() => {
expect((comp as any).contentChange.emit).not.toHaveBeenCalled();
(listableComponent as any).reloadedObject.emit(reloadedObject);
tick();
expect((comp as any).contentChange.emit).toHaveBeenCalledWith(reloadedObject);
}));
});

View File

@@ -3,10 +3,14 @@ import {
ComponentFactoryResolver,
ElementRef,
Input,
OnDestroy, OnInit,
Output, ViewChild
,
EventEmitter
OnDestroy,
OnInit,
Output,
ViewChild,
EventEmitter,
SimpleChanges,
OnChanges,
ComponentRef
} from '@angular/core';
import { ListableObject } from '../listable-object.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
@@ -15,7 +19,7 @@ import { getListableObjectComponent } from './listable-object.decorator';
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from './listable-object.directive';
import { CollectionElementLinkType } from '../../collection-element-link.type';
import { hasValue } from '../../../empty.util';
import { hasValue, isNotEmpty } from '../../../empty.util';
import { Subscription } from 'rxjs/internal/Subscription';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { take } from 'rxjs/operators';
@@ -29,7 +33,7 @@ import { ThemeService } from '../../../theme-support/theme.service';
/**
* Component for determining what component to use depending on the item's entity type (dspace.entity.type)
*/
export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy {
export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges, OnDestroy {
/**
* The item or metadata to determine the component for
*/
@@ -107,6 +111,25 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
*/
protected subs: Subscription[] = [];
/**
* The reference to the dynamic component
*/
protected compRef: ComponentRef<Component>;
/**
* The list of input and output names for the dynamic component
*/
protected inAndOutputNames: string[] = [
'object',
'index',
'linkType',
'listID',
'showLabel',
'context',
'viewMode',
'value',
];
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private themeService: ThemeService
@@ -120,6 +143,15 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
this.instantiateComponent(this.object);
}
/**
* Whenever the inputs change, update the inputs of the dynamic component
*/
ngOnChanges(changes: SimpleChanges): void {
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
this.connectInputsAndOutputs();
}
}
ngOnDestroy() {
this.subs
.filter((subscription) => hasValue(subscription))
@@ -137,28 +169,22 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(
this.compRef = viewContainerRef.createComponent(
componentFactory,
0,
undefined,
[
[this.badges.nativeElement],
]);
(componentRef.instance as any).object = object;
(componentRef.instance as any).index = this.index;
(componentRef.instance as any).linkType = this.linkType;
(componentRef.instance as any).listID = this.listID;
(componentRef.instance as any).showLabel = this.showLabel;
(componentRef.instance as any).context = this.context;
(componentRef.instance as any).viewMode = this.viewMode;
(componentRef.instance as any).value = this.value;
if ((componentRef.instance as any).reloadedObject) {
(componentRef.instance as any).reloadedObject.pipe(take(1)).subscribe((reloadedObject: DSpaceObject) => {
this.connectInputsAndOutputs();
if ((this.compRef.instance as any).reloadedObject) {
(this.compRef.instance as any).reloadedObject.pipe(take(1)).subscribe((reloadedObject: DSpaceObject) => {
if (reloadedObject) {
componentRef.destroy();
this.compRef.destroy();
this.object = reloadedObject;
this.instantiateComponent(reloadedObject);
this.connectInputsAndOutputs();
this.contentChange.emit(reloadedObject);
}
});
@@ -187,4 +213,17 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
context: Context): GenericConstructor<Component> {
return getListableObjectComponent(renderTypes, viewMode, context, this.themeService.getThemeName());
}
/**
* Connect the in and outputs of this component to the dynamic component,
* to ensure they're in sync
*/
protected connectInputsAndOutputs(): void {
if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
this.inAndOutputNames.forEach((name: any) => {
this.compRef.instance[name] = this[name];
});
}
}
}

View File

@@ -1,79 +0,0 @@
//TODO switch to css variables
@mixin clamp($lines, $bg, $size-factor: 1, $line-height: $line-height-base) {
$height: $line-height * $font-size-base * $size-factor;
&.fixedHeight {
height: $lines * $height;
}
.content {
max-height: $lines * $height;
position: relative;
overflow: hidden;
line-height: $line-height;
overflow-wrap: break-word;
&:after {
content: "";
position: absolute;
padding-right: 15px;
top: ($lines - 1) * $height;
right: 0;
width: 30%;
min-width: 75px;
max-width: 150px;
height: $height;
background: linear-gradient(to right, rgba(255, 255, 255, 0), $bg 70%);
pointer-events: none;
}
}
}
@mixin min($lines, $size-factor: 1, $line-height: $line-height-base) {
$height: $line-height * $font-size-base * $size-factor;
min-height: $lines * $height;
}
$h4-factor: strip-unit($h4-font-size);
@mixin clamp-with-titles($i, $bg) {
transition: height 1s;
@include clamp($i, $bg);
&.title {
@include clamp($i, $bg, 1.25);
}
&.h4 {
@include clamp($i, $bg, $h4-factor, $headings-line-height);
}
}
@for $i from 1 through 15 {
.clamp-default-#{$i} {
@include clamp-with-titles($i, $body-bg);
}
:host-context(.ds-hover) .clamp-default-#{$i} {
@include clamp-with-titles($i, $list-group-hover-bg);
}
.clamp-primary-#{$i} {
@include clamp-with-titles($i, $primary);
}
:host-context(.ds-hover) .clamp-primary-#{$i} {
@include clamp-with-titles($i, darken($primary, 10%));
}
}
.clamp-none {
overflow: hidden;
@for $i from 1 through 15 {
&.fixedHeight.min-#{$i} {
transition: height 1s;
@include min($i);
&.title {
@include min($i, 1.25);
}
&.h4 {
@include min($i, $h4-factor, $headings-line-height);
}
}
}
}

View File

@@ -1635,7 +1635,11 @@
"item.edit.move.cancel": "Cancel",
"item.edit.move.cancel": "Back",
"item.edit.move.save-button": "Save",
"item.edit.move.discard-button": "Discard",
"item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.",

View File

@@ -0,0 +1,80 @@
@mixin clamp($lines, $bg, $size-factor: 1, $line-height: $line-height-base) {
$height: $line-height * $font-size-base * $size-factor;
&.fixedHeight {
height: $lines * $height;
}
.content {
max-height: $lines * $height;
position: relative;
overflow: hidden;
line-height: $line-height;
overflow-wrap: break-word;
&:after {
content: "";
position: absolute;
padding-right: 15px;
top: ($lines - 1) * $height;
right: 0;
width: 30%;
min-width: 75px;
max-width: 150px;
height: $height;
background: linear-gradient(to right, rgba(255, 255, 255, 0), $bg 70%);
pointer-events: none;
}
}
}
@mixin min($lines, $size-factor: 1, $line-height: $line-height-base) {
$height: $line-height * $font-size-base * $size-factor;
min-height: $lines * $height;
}
$h4-factor: strip-unit($h4-font-size);
@mixin clamp-with-titles($i, $bg) {
transition: height 1s;
@include clamp($i, $bg);
&.title {
@include clamp($i, $bg, 1.25);
}
&.h4 {
@include clamp($i, $bg, $h4-factor, $headings-line-height);
}
}
@for $i from 1 through 15 {
.clamp-default-#{$i} {
@include clamp-with-titles($i, $body-bg);
}
:focus .clamp-default-#{$i},
.ds-hover .clamp-default-#{$i} {
@include clamp-with-titles($i, $list-group-hover-bg);
}
.clamp-primary-#{$i} {
@include clamp-with-titles($i, $primary);
}
:focus .clamp-primary-#{$i},
.ds-hover .clamp-primary-#{$i} {
@include clamp-with-titles($i, darken($primary, 10%));
}
}
.clamp-none {
overflow: hidden;
@for $i from 1 through 15 {
&.fixedHeight.min-#{$i} {
transition: height 1s;
@include min($i);
&.title {
@include min($i, 1.25);
}
&.h4 {
@include min($i, $h4-factor, $headings-line-height);
}
}
}
}

View File

@@ -3,4 +3,5 @@
@import '../../node_modules/nouislider/distribute/nouislider.min';
@import './_custom_variables.scss';
@import './bootstrap_variables_mapping.scss';
@import './_truncatable-part.component.scss';
@import './_global-styles.scss';

View File

@@ -9,4 +9,5 @@
@import '../../../styles/_custom_variables.scss';
@import './_theme_css_variable_overrides.scss';
@import '../../../styles/bootstrap_variables_mapping.scss';
@import '../../../styles/_truncatable-part.component.scss';
@import './_global-styles.scss';

View File

@@ -9,4 +9,5 @@
@import '../../../styles/_custom_variables.scss';
@import './_theme_css_variable_overrides.scss';
@import '../../../styles/bootstrap_variables_mapping.scss';
@import '../../../styles/_truncatable-part.component.scss';
@import './_global-styles.scss';