mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #1247 from atmire/w2p-80195_Fix-move-item-page
Fix move item page
This commit is contained in:
@@ -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}}
|
||||
<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 [routerLink]="[(itemPageRoute$ | async), 'edit']"
|
||||
class="btn btn-outline-secondary">
|
||||
{{'item.edit.move.cancel' | translate}}
|
||||
<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>
|
||||
|
@@ -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,8 +88,9 @@ describe('ItemMoveComponent', () => {
|
||||
|
||||
const notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
describe('ItemMoveComponent success', () => {
|
||||
beforeEach(() => {
|
||||
const init = (mockItemDataService) => {
|
||||
itemDataService = mockItemDataService;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [ItemMoveComponent],
|
||||
@@ -90,6 +100,7 @@ describe('ItemMoveComponent', () => {
|
||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
{ provide: SearchService, useValue: mockSearchService },
|
||||
{ provide: RequestService, useValue: getMockRequestService() },
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
@@ -97,25 +108,20 @@ describe('ItemMoveComponent', () => {
|
||||
fixture = TestBed.createComponent(ItemMoveComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should load suggestions', () => {
|
||||
const expected = [
|
||||
collection1,
|
||||
collection2
|
||||
];
|
||||
};
|
||||
|
||||
comp.collectionSearchResults.subscribe((value) => {
|
||||
expect(value).toEqual(expected);
|
||||
}
|
||||
);
|
||||
describe('ItemMoveComponent success', () => {
|
||||
beforeEach(() => {
|
||||
init(mockItemDataServiceSuccess);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
@@ -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)]);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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 }}"
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
}));
|
||||
|
@@ -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);
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -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];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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.",
|
||||
|
||||
|
80
src/styles/_truncatable-part.component.scss
Normal file
80
src/styles/_truncatable-part.component.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
Reference in New Issue
Block a user