mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +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>
|
<p>{{'item.edit.move.description' | translate}}</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<ds-dso-input-suggestions #f id="search-form"
|
<div class="card mb-3">
|
||||||
[suggestions]="(collectionSearchResults | async)"
|
<div class="card-header">{{'dso-selector.placeholder' | translate: { type: 'collection' } }}</div>
|
||||||
[placeholder]="'item.edit.move.search.placeholder'| translate"
|
<div class="card-body">
|
||||||
[action]="getCurrentUrl()"
|
<ds-authorized-collection-selector [types]="COLLECTIONS"
|
||||||
[name]="'item-move'"
|
[currentDSOId]="selectedCollection ? selectedCollection.id : originalCollection.id"
|
||||||
[(ngModel)]="selectedCollectionName"
|
(onSelect)="selectDso($event)">
|
||||||
(clickSuggestion)="onClick($event)"
|
</ds-authorized-collection-selector>
|
||||||
(typeSuggestion)="resetCollection($event)"
|
</div>
|
||||||
(findSuggestions)="findSuggestions($event)"
|
<div></div>
|
||||||
(click)="f.open()"
|
</div>
|
||||||
ngDefaultControl>
|
|
||||||
</ds-dso-input-suggestions>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -33,16 +30,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button (click)="moveCollection()" class="btn btn-primary" [disabled]=!canSubmit>
|
<div class="button-row bottom">
|
||||||
<span *ngIf="!processing"> {{'item.edit.move.move' | translate}}</span>
|
<div class="float-right">
|
||||||
<span *ngIf="processing"><i class='fas fa-circle-notch fa-spin'></i>
|
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
|
||||||
{{'item.edit.move.processing' | translate}}
|
<i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}}
|
||||||
</span>
|
</button>
|
||||||
</button>
|
<button class="btn btn-primary mr-0" [disabled]="!canMove" (click)="moveToCollection()">
|
||||||
<button [routerLink]="[(itemPageRoute$ | async), 'edit']"
|
<span *ngIf="!processing">
|
||||||
class="btn btn-outline-secondary">
|
<i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}}
|
||||||
{{'item.edit.move.cancel' | translate}}
|
</span>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -21,6 +21,8 @@ import {
|
|||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$
|
||||||
} from '../../../shared/remote-data.utils';
|
} from '../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
|
||||||
describe('ItemMoveComponent', () => {
|
describe('ItemMoveComponent', () => {
|
||||||
let comp: ItemMoveComponent;
|
let comp: ItemMoveComponent;
|
||||||
@@ -47,18 +49,25 @@ describe('ItemMoveComponent', () => {
|
|||||||
name: 'Test collection 2'
|
name: 'Test collection 2'
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockItemDataService = jasmine.createSpyObj({
|
let itemDataService;
|
||||||
moveToCollection: createSuccessfulRemoteDataObject$(collection1)
|
|
||||||
|
const mockItemDataServiceSuccess = jasmine.createSpyObj({
|
||||||
|
moveToCollection: createSuccessfulRemoteDataObject$(collection1),
|
||||||
|
findById: createSuccessfulRemoteDataObject$(mockItem),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockItemDataServiceFail = jasmine.createSpyObj({
|
const mockItemDataServiceFail = jasmine.createSpyObj({
|
||||||
moveToCollection: createFailedRemoteDataObject$('Internal server error', 500)
|
moveToCollection: createFailedRemoteDataObject$('Internal server error', 500),
|
||||||
|
findById: createSuccessfulRemoteDataObject$(mockItem),
|
||||||
});
|
});
|
||||||
|
|
||||||
const routeStub = {
|
const routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
|
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 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', () => {
|
describe('ItemMoveComponent success', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
init(mockItemDataServiceSuccess);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
it('should load suggestions', () => {
|
|
||||||
const expected = [
|
|
||||||
collection1,
|
|
||||||
collection2
|
|
||||||
];
|
|
||||||
|
|
||||||
comp.collectionSearchResults.subscribe((value) => {
|
|
||||||
expect(value).toEqual(expected);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('should get current url ', () => {
|
it('should get current url ', () => {
|
||||||
expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
|
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;
|
const data = collection1;
|
||||||
|
|
||||||
comp.onClick(data);
|
comp.selectDso(data);
|
||||||
|
|
||||||
expect(comp.selectedCollectionName).toEqual('Test collection 1');
|
expect(comp.selectedCollectionName).toEqual('Test collection 1');
|
||||||
expect(comp.selectedCollection).toEqual(collection1);
|
expect(comp.selectedCollection).toEqual(collection1);
|
||||||
@@ -128,12 +134,12 @@ describe('ItemMoveComponent', () => {
|
|||||||
});
|
});
|
||||||
comp.selectedCollectionName = 'selected-collection-id';
|
comp.selectedCollectionName = 'selected-collection-id';
|
||||||
comp.selectedCollection = collection1;
|
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', () => {
|
it('should call notificationsService success message on success', () => {
|
||||||
comp.moveCollection();
|
comp.moveToCollection();
|
||||||
|
|
||||||
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -142,26 +148,11 @@ describe('ItemMoveComponent', () => {
|
|||||||
|
|
||||||
describe('ItemMoveComponent fail', () => {
|
describe('ItemMoveComponent fail', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
init(mockItemDataServiceFail);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call notificationsService error message on fail', () => {
|
it('should call notificationsService error message on fail', () => {
|
||||||
comp.moveCollection();
|
comp.moveToCollection();
|
||||||
|
|
||||||
expect(notificationsServiceStub.error).toHaveBeenCalled();
|
expect(notificationsServiceStub.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@@ -1,25 +1,21 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
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 { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
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 { Item } from '../../../core/shared/item.model';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteData,
|
getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload,
|
||||||
getFirstCompletedRemoteData, getAllSucceededRemoteDataPayload
|
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
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 { Collection } from '../../../core/shared/collection.model';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
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 { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-move',
|
selector: 'ds-item-move',
|
||||||
@@ -38,7 +34,8 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
|
|
||||||
inheritPolicies = false;
|
inheritPolicies = false;
|
||||||
itemRD$: Observable<RemoteData<Item>>;
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
collectionSearchResults: Observable<any[]> = observableOf([]);
|
originalCollection: Collection;
|
||||||
|
|
||||||
selectedCollectionName: string;
|
selectedCollectionName: string;
|
||||||
selectedCollection: Collection;
|
selectedCollection: Collection;
|
||||||
canSubmit = false;
|
canSubmit = false;
|
||||||
@@ -46,23 +43,26 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
item: Item;
|
item: Item;
|
||||||
processing = false;
|
processing = false;
|
||||||
|
|
||||||
pagination = new PaginationComponentOptions();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route to the item's page
|
* Route to the item's page
|
||||||
*/
|
*/
|
||||||
itemPageRoute$: Observable<string>;
|
itemPageRoute$: Observable<string>;
|
||||||
|
|
||||||
|
COLLECTIONS = [DSpaceObjectType.COLLECTION];
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute,
|
constructor(private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private itemDataService: ItemDataService,
|
private itemDataService: ItemDataService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private translateService: TranslateService) {
|
private translateService: TranslateService,
|
||||||
}
|
private requestService: RequestService,
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
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(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((item) => getItemPageRoute(item))
|
map((item) => getItemPageRoute(item))
|
||||||
@@ -71,43 +71,22 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
this.item = rd.payload;
|
this.item = rd.payload;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.pagination.pageSize = 5;
|
this.itemRD$.pipe(
|
||||||
this.loadSuggestions('');
|
getFirstSucceededRemoteData(),
|
||||||
}
|
getRemoteDataPayload(),
|
||||||
|
switchMap((item) => item.owningCollection),
|
||||||
/**
|
getFirstSucceededRemoteData(),
|
||||||
* Find suggestions based on entered query
|
getRemoteDataPayload(),
|
||||||
* @param query - Search query
|
).subscribe((collection) => {
|
||||||
*/
|
this.originalCollection = collection;
|
||||||
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;
|
|
||||||
});
|
|
||||||
}) ,
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the collection name and id based on the selected value
|
* Set the collection name and id based on the selected value
|
||||||
* @param data - obtained from the ds-input-suggestions component
|
* @param data - obtained from the ds-input-suggestions component
|
||||||
*/
|
*/
|
||||||
onClick(data: any): void {
|
selectDso(data: any): void {
|
||||||
this.selectedCollection = data;
|
this.selectedCollection = data;
|
||||||
this.selectedCollectionName = data.name;
|
this.selectedCollectionName = data.name;
|
||||||
this.canSubmit = true;
|
this.canSubmit = true;
|
||||||
@@ -123,26 +102,41 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Moves the item to a new collection based on the selected collection
|
* Moves the item to a new collection based on the selected collection
|
||||||
*/
|
*/
|
||||||
moveCollection() {
|
moveToCollection() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.itemDataService.moveToCollection(this.item.id, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe(
|
const move$ = this.itemDataService.moveToCollection(this.item.id, this.selectedCollection)
|
||||||
(response: RemoteData<Collection>) => {
|
.pipe(getFirstCompletedRemoteData());
|
||||||
this.router.navigate([getItemEditRoute(this.item)]);
|
|
||||||
if (response.hasSucceeded) {
|
move$.subscribe((response: RemoteData<any>) => {
|
||||||
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
|
if (response.hasSucceeded) {
|
||||||
} else {
|
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
|
||||||
this.notificationsService.error(this.translateService.get('item.edit.move.error'));
|
} else {
|
||||||
}
|
this.notificationsService.error(this.translateService.get('item.edit.move.error'));
|
||||||
this.processing = false;
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
|
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)]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
discard(): void {
|
||||||
* Resets the can submit when the user changes the content of the input field
|
this.selectedCollection = null;
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
resetCollection(data: any) {
|
|
||||||
this.canSubmit = false;
|
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 { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { PaginatedList } from './paginated-list.model';
|
import { PaginatedList } from './paginated-list.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import {
|
import { DeleteRequest, FindListOptions, GetRequest, PostRequest, PutRequest, RestRequest } from './request.models';
|
||||||
DeleteRequest,
|
|
||||||
FindListOptions,
|
|
||||||
GetRequest,
|
|
||||||
PostRequest,
|
|
||||||
PutRequest,
|
|
||||||
RestRequest
|
|
||||||
} from './request.models';
|
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||||
import { Bundle } from '../shared/bundle.model';
|
import { Bundle } from '../shared/bundle.model';
|
||||||
@@ -38,6 +31,9 @@ import { MetadataMap } from '../shared/metadata.models';
|
|||||||
import { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
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()
|
@Injectable()
|
||||||
@dataService(ITEM)
|
@dataService(ITEM)
|
||||||
@@ -229,7 +225,7 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
* @param itemId
|
* @param itemId
|
||||||
* @param collection
|
* @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({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
headers = headers.append('Content-Type', 'text/uri-list');
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
@@ -242,9 +238,17 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const request = new PutRequest(requestId, href, collection._links.self.href, options);
|
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);
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
}
|
}
|
||||||
|
@@ -14,12 +14,12 @@
|
|||||||
[infiniteScrollContainer]="'.scrollable-menu'"
|
[infiniteScrollContainer]="'.scrollable-menu'"
|
||||||
[fromRoot]="true"
|
[fromRoot]="true"
|
||||||
(scrolled)="onScrollDown()">
|
(scrolled)="onScrollDown()">
|
||||||
<ng-container *ngIf="listEntries">
|
<ng-container *ngIf="listEntries$ | async">
|
||||||
<button class="list-group-item list-group-item-action border-0 disabled"
|
<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 } }}
|
{{'dso-selector.no-results' | translate: { type: typesString } }}
|
||||||
</button>
|
</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"
|
class="list-group-item list-group-item-action border-0 list-entry"
|
||||||
[ngClass]="{'bg-primary': listEntry.indexableObject.id === currentDSOId}"
|
[ngClass]="{'bg-primary': listEntry.indexableObject.id === currentDSOId}"
|
||||||
title="{{ listEntry.indexableObject.name }}"
|
title="{{ listEntry.indexableObject.name }}"
|
||||||
|
@@ -92,12 +92,18 @@ describe('DSOSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('populating listEntries', () => {
|
describe('populating listEntries', () => {
|
||||||
it('should not be empty', () => {
|
it('should not be empty', (done) => {
|
||||||
expect(component.listEntries.length).toBeGreaterThan(0);
|
component.listEntries$.subscribe((listEntries) => {
|
||||||
|
expect(listEntries.length).toBeGreaterThan(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a combination of the current DSO and first page results', () => {
|
it('should contain a combination of the current DSO and first page results', (done) => {
|
||||||
expect(component.listEntries).toEqual([searchResult, ...firstPageResults]);
|
component.listEntries$.subscribe((listEntries) => {
|
||||||
|
expect(listEntries).toEqual([searchResult, ...firstPageResults]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when current page increases', () => {
|
describe('when current page increases', () => {
|
||||||
@@ -105,8 +111,11 @@ describe('DSOSelectorComponent', () => {
|
|||||||
component.currentPage$.next(2);
|
component.currentPage$.next(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a combination of the current DSO, as well as first and second page results', () => {
|
it('should contain a combination of the current DSO, as well as first and second page results', (done) => {
|
||||||
expect(component.listEntries).toEqual([searchResult, ...firstPageResults, ...nextPageResults]);
|
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
|
* 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
|
* The current page to load
|
||||||
@@ -160,7 +160,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
// The first page is loading, this means we should reset the list instead of adding to it
|
// 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(
|
return this.search(query, page).pipe(
|
||||||
map((rd) => {
|
map((rd) => {
|
||||||
@@ -181,15 +181,16 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
).subscribe((rd) => {
|
).subscribe((rd) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
if (hasNoValue(this.listEntries)) {
|
const currentEntries = this.listEntries$.getValue();
|
||||||
this.listEntries = rd.payload.page;
|
if (hasNoValue(currentEntries)) {
|
||||||
|
this.listEntries$.next(rd.payload.page);
|
||||||
} else {
|
} 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
|
// 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 {
|
} else {
|
||||||
this.listEntries = null;
|
this.listEntries$.next(null);
|
||||||
this.hasNextPage = false;
|
this.hasNextPage = false;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { ChangeDetectionStrategy, ComponentFactoryResolver, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ListableObjectComponentLoaderComponent } from './listable-object-component-loader.component';
|
import { ListableObjectComponentLoaderComponent } from './listable-object-component-loader.component';
|
||||||
import { ListableObject } from '../listable-object.model';
|
import { ListableObject } from '../listable-object.model';
|
||||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
@@ -117,17 +117,33 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('When a reloadedObject is emitted', () => {
|
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);
|
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
||||||
tick();
|
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,
|
ComponentFactoryResolver,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy, OnInit,
|
OnDestroy,
|
||||||
Output, ViewChild
|
OnInit,
|
||||||
,
|
Output,
|
||||||
EventEmitter
|
ViewChild,
|
||||||
|
EventEmitter,
|
||||||
|
SimpleChanges,
|
||||||
|
OnChanges,
|
||||||
|
ComponentRef
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ListableObject } from '../listable-object.model';
|
import { ListableObject } from '../listable-object.model';
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.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 { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
import { ListableObjectDirective } from './listable-object.directive';
|
import { ListableObjectDirective } from './listable-object.directive';
|
||||||
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
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 { Subscription } from 'rxjs/internal/Subscription';
|
||||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
import { take } from 'rxjs/operators';
|
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)
|
* 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
|
* The item or metadata to determine the component for
|
||||||
*/
|
*/
|
||||||
@@ -107,6 +111,25 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
|||||||
*/
|
*/
|
||||||
protected subs: Subscription[] = [];
|
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(
|
constructor(
|
||||||
private componentFactoryResolver: ComponentFactoryResolver,
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
private themeService: ThemeService
|
private themeService: ThemeService
|
||||||
@@ -120,6 +143,15 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
|||||||
this.instantiateComponent(this.object);
|
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() {
|
ngOnDestroy() {
|
||||||
this.subs
|
this.subs
|
||||||
.filter((subscription) => hasValue(subscription))
|
.filter((subscription) => hasValue(subscription))
|
||||||
@@ -137,28 +169,22 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
|||||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||||
viewContainerRef.clear();
|
viewContainerRef.clear();
|
||||||
|
|
||||||
const componentRef = viewContainerRef.createComponent(
|
this.compRef = viewContainerRef.createComponent(
|
||||||
componentFactory,
|
componentFactory,
|
||||||
0,
|
0,
|
||||||
undefined,
|
undefined,
|
||||||
[
|
[
|
||||||
[this.badges.nativeElement],
|
[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) {
|
this.connectInputsAndOutputs();
|
||||||
(componentRef.instance as any).reloadedObject.pipe(take(1)).subscribe((reloadedObject: DSpaceObject) => {
|
|
||||||
|
if ((this.compRef.instance as any).reloadedObject) {
|
||||||
|
(this.compRef.instance as any).reloadedObject.pipe(take(1)).subscribe((reloadedObject: DSpaceObject) => {
|
||||||
if (reloadedObject) {
|
if (reloadedObject) {
|
||||||
componentRef.destroy();
|
this.compRef.destroy();
|
||||||
this.object = reloadedObject;
|
this.object = reloadedObject;
|
||||||
this.instantiateComponent(reloadedObject);
|
this.connectInputsAndOutputs();
|
||||||
this.contentChange.emit(reloadedObject);
|
this.contentChange.emit(reloadedObject);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -187,4 +213,17 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
|
|||||||
context: Context): GenericConstructor<Component> {
|
context: Context): GenericConstructor<Component> {
|
||||||
return getListableObjectComponent(renderTypes, viewMode, context, this.themeService.getThemeName());
|
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.",
|
"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 '../../node_modules/nouislider/distribute/nouislider.min';
|
||||||
@import './_custom_variables.scss';
|
@import './_custom_variables.scss';
|
||||||
@import './bootstrap_variables_mapping.scss';
|
@import './bootstrap_variables_mapping.scss';
|
||||||
|
@import './_truncatable-part.component.scss';
|
||||||
@import './_global-styles.scss';
|
@import './_global-styles.scss';
|
||||||
|
@@ -9,4 +9,5 @@
|
|||||||
@import '../../../styles/_custom_variables.scss';
|
@import '../../../styles/_custom_variables.scss';
|
||||||
@import './_theme_css_variable_overrides.scss';
|
@import './_theme_css_variable_overrides.scss';
|
||||||
@import '../../../styles/bootstrap_variables_mapping.scss';
|
@import '../../../styles/bootstrap_variables_mapping.scss';
|
||||||
|
@import '../../../styles/_truncatable-part.component.scss';
|
||||||
@import './_global-styles.scss';
|
@import './_global-styles.scss';
|
||||||
|
@@ -9,4 +9,5 @@
|
|||||||
@import '../../../styles/_custom_variables.scss';
|
@import '../../../styles/_custom_variables.scss';
|
||||||
@import './_theme_css_variable_overrides.scss';
|
@import './_theme_css_variable_overrides.scss';
|
||||||
@import '../../../styles/bootstrap_variables_mapping.scss';
|
@import '../../../styles/bootstrap_variables_mapping.scss';
|
||||||
|
@import '../../../styles/_truncatable-part.component.scss';
|
||||||
@import './_global-styles.scss';
|
@import './_global-styles.scss';
|
||||||
|
Reference in New Issue
Block a user