mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #221 from LotteHofstede/w2p-46063_truncation-implementation
Truncation
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<div *ngIf="searchResults?.hasSucceeded" @fadeIn>
|
||||
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading" @fadeIn>
|
||||
<h2 *ngIf="searchResults?.payload ?.length > 0">{{ 'search.results.head' | translate }}</h2>
|
||||
<ds-viewable-collection
|
||||
[config]="searchConfig.pagination"
|
||||
|
@@ -23,6 +23,7 @@ body {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
filterReducer,
|
||||
SearchFiltersState
|
||||
} from './+search-page/search-filters/search-filter/search-filter.reducer';
|
||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||
|
||||
export interface AppState {
|
||||
router: fromRouter.RouterReducerState;
|
||||
@@ -18,6 +19,7 @@ export interface AppState {
|
||||
header: HeaderState;
|
||||
searchSidebar: SearchSidebarState;
|
||||
searchFilter: SearchFiltersState;
|
||||
truncatable: TruncatablesState;
|
||||
}
|
||||
|
||||
export const appReducers: ActionReducerMap<AppState> = {
|
||||
@@ -25,5 +27,6 @@ export const appReducers: ActionReducerMap<AppState> = {
|
||||
hostWindow: hostWindowReducer,
|
||||
header: headerReducer,
|
||||
searchSidebar: sidebarReducer,
|
||||
searchFilter: filterReducer
|
||||
searchFilter: filterReducer,
|
||||
truncatable: truncatableReducer
|
||||
};
|
||||
|
19
src/app/shared/animations/focus.ts
Normal file
19
src/app/shared/animations/focus.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { animate, state, transition, trigger, style } from '@angular/animations';
|
||||
|
||||
export const focusShadow = trigger('focusShadow', [
|
||||
|
||||
state('focus', style({ 'box-shadow': 'rgba(119, 119, 119, 0.6) 0px 0px 6px' })),
|
||||
|
||||
state('blur', style({ 'box-shadow': 'none' })),
|
||||
|
||||
transition('focus <=> blur', animate(250))
|
||||
]);
|
||||
|
||||
export const focusBackground = trigger('focusBackground', [
|
||||
|
||||
state('focus', style({ 'background-color': 'rgba(119, 119, 119, 0.1)' })),
|
||||
|
||||
state('blur', style({ 'background-color': 'transparent' })),
|
||||
|
||||
transition('focus <=> blur', animate(250))
|
||||
]);
|
10
src/app/shared/animations/overlay.ts
Normal file
10
src/app/shared/animations/overlay.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { animate, state, transition, trigger, style } from '@angular/animations';
|
||||
|
||||
export const overlay = trigger('overlay', [
|
||||
|
||||
state('show', style({ opacity: 0.5 })),
|
||||
|
||||
state('hide', style({ opacity: 0 })),
|
||||
|
||||
transition('show <=> hide', animate(250))
|
||||
]);
|
@@ -0,0 +1,5 @@
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { SearchResult } from '../../../+search-page/search-result.model';
|
||||
|
||||
export class CollectionSearchResult extends SearchResult<Collection> {
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
import { SearchResult } from '../../../+search-page/search-result.model';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
|
||||
export class CommunitySearchResult extends SearchResult<Community> {
|
||||
}
|
@@ -1,21 +1,13 @@
|
||||
import { CollectionGridElementComponent } from './collection-grid-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterStub } from '../../testing/router-stub';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
|
||||
let collectionGridElementComponent: CollectionGridElementComponent;
|
||||
let fixture: ComponentFixture<CollectionGridElementComponent>;
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const activatedRouteStub = {
|
||||
queryParams: Observable.of({
|
||||
query: queryParam,
|
||||
scope: scopeParam
|
||||
})
|
||||
};
|
||||
const mockCollection: Collection = Object.assign(new Collection(), {
|
||||
|
||||
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
@@ -23,37 +15,56 @@ const mockCollection: Collection = Object.assign(new Collection(), {
|
||||
value: 'Short description'
|
||||
}]
|
||||
});
|
||||
const createdGridElementComponent:CollectionGridElementComponent= new CollectionGridElementComponent(mockCollection);
|
||||
|
||||
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}]
|
||||
});
|
||||
|
||||
describe('CollectionGridElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CollectionGridElementComponent ],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: Router, useClass: RouterStub },
|
||||
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent)}
|
||||
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract)}
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).compileComponents(); // compile template and css
|
||||
}).overrideComponent(CollectionGridElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(CollectionGridElementComponent);
|
||||
collectionGridElementComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
it('should show the collection cards in the grid element',() => {
|
||||
expect(fixture.debugElement.query(By.css('ds-collection-grid-element'))).toBeDefined();
|
||||
describe('When the collection has an abstract', () => {
|
||||
beforeEach(() => {
|
||||
collectionGridElementComponent.object = mockCollectionWithAbstract;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should only show the description if "short description" metadata is present',() => {
|
||||
const descriptionText = expect(fixture.debugElement.query(By.css('p.card-text')));
|
||||
|
||||
if (mockCollection.shortDescription.length > 0) {
|
||||
expect(descriptionText).toBeDefined();
|
||||
} else {
|
||||
expect(descriptionText).not.toBeDefined();
|
||||
}
|
||||
it('should show the description paragraph', () => {
|
||||
const collectionAbstractField = fixture.debugElement.query(By.css('p.card-text'));
|
||||
expect(collectionAbstractField).not.toBeNull();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('When the collection has no abstract', () => {
|
||||
beforeEach(() => {
|
||||
collectionGridElementComponent.object = mockCollectionWithoutAbstract;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the description paragraph', () => {
|
||||
const collectionAbstractField = fixture.debugElement.query(By.css('p.card-text'));
|
||||
expect(collectionAbstractField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,25 +1,13 @@
|
||||
import { CommunityGridElementComponent } from './community-grid-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterStub } from '../../testing/router-stub';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
|
||||
let communityGridElementComponent: CommunityGridElementComponent;
|
||||
let fixture: ComponentFixture<CommunityGridElementComponent>;
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const activatedRouteStub = {
|
||||
queryParams: Observable.of({
|
||||
query: queryParam,
|
||||
scope: scopeParam
|
||||
})
|
||||
};
|
||||
|
||||
const mockCommunity: Community = Object.assign(new Community(), {
|
||||
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
@@ -28,39 +16,55 @@ const mockCommunity: Community = Object.assign(new Community(), {
|
||||
}]
|
||||
});
|
||||
|
||||
const createdGridElementComponent:CommunityGridElementComponent= new CommunityGridElementComponent(mockCommunity);
|
||||
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}]
|
||||
});
|
||||
|
||||
describe('CommunityGridElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CommunityGridElementComponent ],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: Router, useClass: RouterStub },
|
||||
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent)}
|
||||
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract)}
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).compileComponents(); // compile template and css
|
||||
}).overrideComponent(CommunityGridElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(CommunityGridElementComponent);
|
||||
communityGridElementComponent = fixture.componentInstance;
|
||||
|
||||
}));
|
||||
|
||||
it('should show the community cards in the grid element',() => {
|
||||
expect(fixture.debugElement.query(By.css('ds-community-grid-element'))).toBeDefined();
|
||||
})
|
||||
describe('When the community has an abstract', () => {
|
||||
beforeEach(() => {
|
||||
communityGridElementComponent.object = mockCommunityWithAbstract;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should only show the description if "short description" metadata is present',() => {
|
||||
const descriptionText = expect(fixture.debugElement.query(By.css('p.card-text')));
|
||||
it('should show the description paragraph', () => {
|
||||
const communityAbstractField = fixture.debugElement.query(By.css('p.card-text'));
|
||||
expect(communityAbstractField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
if (mockCommunity.shortDescription.length > 0) {
|
||||
expect(descriptionText).toBeDefined();
|
||||
} else {
|
||||
expect(descriptionText).not.toBeDefined();
|
||||
}
|
||||
describe('When the community has no abstract', () => {
|
||||
beforeEach(() => {
|
||||
communityGridElementComponent.object = mockCommunityWithoutAbstract;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the description paragraph', () => {
|
||||
const communityAbstractField = fixture.debugElement.query(By.css('p.card-text'));
|
||||
expect(communityAbstractField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -6,12 +6,11 @@
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
|
||||
|
||||
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-authors card-text text-muted">
|
||||
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
|
||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||
<span *ngIf="!last">; </span>
|
||||
</span>
|
||||
<span *ngIf="object.findMetadata('dc.date.issued')">{{object.findMetadata("dc.date.issued")}}</span>
|
||||
<span *ngIf="object.findMetadata('dc.date.issued')" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
|
||||
</p>
|
||||
|
||||
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</p>
|
||||
|
@@ -1,47 +1,55 @@
|
||||
import { ItemGridElementComponent } from './item-grid-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterStub } from '../../testing/router-stub';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TruncatePipe } from '../../utils/truncate.pipe';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
let itemGridElementComponent: ItemGridElementComponent;
|
||||
let fixture: ComponentFixture<ItemGridElementComponent>;
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const activatedRouteStub = {
|
||||
queryParams: Observable.of({
|
||||
query: queryParam,
|
||||
scope: scopeParam
|
||||
})
|
||||
};
|
||||
/* tslint:disable:no-shadowed-variable */
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
|
||||
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
|
||||
bitstreams: Observable.of({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.contributor.author',
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
},
|
||||
{
|
||||
key: 'dc.date.issued',
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}]
|
||||
});
|
||||
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
|
||||
bitstreams: Observable.of({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
},
|
||||
{
|
||||
key: 'dc.type',
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}]
|
||||
});
|
||||
|
||||
const createdGridElementComponent:ItemGridElementComponent= new ItemGridElementComponent(mockItem);
|
||||
|
||||
describe('ItemGridElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ItemGridElementComponent , TruncatePipe],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: Router, useClass: RouterStub },
|
||||
{ provide: 'objectElementProvider', useValue: {createdGridElementComponent}}
|
||||
{ provide: 'objectElementProvider', useValue: {mockItemWithAuthorAndDate}}
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).compileComponents(); // compile template and css
|
||||
}).overrideComponent(ItemGridElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
@@ -50,18 +58,51 @@ describe('ItemGridElementComponent', () => {
|
||||
|
||||
}));
|
||||
|
||||
it('should show the item cards in the grid element',() => {
|
||||
expect(fixture.debugElement.query(By.css('ds-item-grid-element'))).toBeDefined()
|
||||
describe('When the item has an author', () => {
|
||||
beforeEach(() => {
|
||||
itemGridElementComponent.object = mockItemWithAuthorAndDate;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should only show the author span if the author metadata is present',() => {
|
||||
const itemAuthorField = expect(fixture.debugElement.query(By.css('p.item-authors')));
|
||||
|
||||
if (mockItem.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0) {
|
||||
expect(itemAuthorField).toBeDefined();
|
||||
} else {
|
||||
expect(itemAuthorField).toBeDefined();
|
||||
}
|
||||
it('should show the author paragraph', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors'));
|
||||
expect(itemAuthorField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
describe('When the item has no author', () => {
|
||||
beforeEach(() => {
|
||||
itemGridElementComponent.object = mockItemWithoutAuthorAndDate;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the author paragraph', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors'));
|
||||
expect(itemAuthorField).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has an issuedate', () => {
|
||||
beforeEach(() => {
|
||||
itemGridElementComponent.object = mockItemWithAuthorAndDate;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the issuedate span', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('span.item-date'));
|
||||
expect(itemAuthorField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has no issuedate', () => {
|
||||
beforeEach(() => {
|
||||
itemGridElementComponent.object = mockItemWithoutAuthorAndDate;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the issuedate span', () => {
|
||||
const dateField = fixture.debugElement.query(By.css('span.item-date'));
|
||||
expect(dateField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -10,8 +10,8 @@
|
||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||
(sortFieldChange)="onSortFieldChange($event)"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
<div class="row mt-2" *ngIf="objects?.hasSucceeded" @fadeIn>
|
||||
<div class="col-lg-4 col-sm-6 col-xs-12 "
|
||||
<div class="card-columns" *ngIf="objects?.hasSucceeded" @fadeIn>
|
||||
<div
|
||||
*ngFor="let object of objects?.payload?.page">
|
||||
<ds-wrapper-grid-element [object]="object"></ds-wrapper-grid-element>
|
||||
</div>
|
||||
|
@@ -1,27 +1,24 @@
|
||||
@import '../../../styles/variables';
|
||||
@import '../../../styles/mixins';
|
||||
|
||||
ds-wrapper-grid-element ::ng-deep {
|
||||
div.thumbnail > img {
|
||||
height: $card-thumbnail-height;
|
||||
width: 100%;
|
||||
}
|
||||
.card-title {
|
||||
line-height: $headings-line-height;
|
||||
height: ($headings-line-height*3) +em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.item-abstract {
|
||||
line-height: $line-height-base;
|
||||
height: ($line-height-base*5)+em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.item-authors{
|
||||
line-height: $line-height-base;
|
||||
height: ($line-height-base*1.5)+em;
|
||||
}
|
||||
div.card {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: $spacer;
|
||||
}
|
||||
}
|
||||
|
||||
.card-columns {
|
||||
@include media-breakpoint-only(lg) {
|
||||
column-count: 3;
|
||||
}
|
||||
@include media-breakpoint-only(sm) {
|
||||
column-count: 2;
|
||||
}
|
||||
@include media-breakpoint-only(xs) {
|
||||
column-count: 1;
|
||||
}
|
||||
}
|
@@ -1,64 +1,83 @@
|
||||
import {CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component';
|
||||
import { CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../testing/router-stub';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TruncatePipe } from '../../../utils/truncate.pipe';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
||||
|
||||
let collectionSearchResultGridElementComponent: CollectionSearchResultGridElementComponent;
|
||||
let fixture: ComponentFixture<CollectionSearchResultGridElementComponent>;
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const activatedRouteStub = {
|
||||
queryParams: Observable.of({
|
||||
query: queryParam,
|
||||
scope: scopeParam
|
||||
})
|
||||
|
||||
const truncatableServiceStub: any = {
|
||||
isCollapsed: (id: number) => Observable.of(true),
|
||||
};
|
||||
const mockCollection: Collection = Object.assign(new Collection(), {
|
||||
|
||||
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||
mockCollectionWithAbstract.hitHighlights = [];
|
||||
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
} ]
|
||||
|
||||
});
|
||||
|
||||
const createdGridElementComponent: CollectionSearchResultGridElementComponent = new CollectionSearchResultGridElementComponent(mockCollection);
|
||||
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||
mockCollectionWithoutAbstract.hitHighlights = [];
|
||||
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
} ]
|
||||
});
|
||||
|
||||
describe('CollectionSearchResultGridElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CollectionSearchResultGridElementComponent, TruncatePipe ],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: Router, useClass: RouterStub },
|
||||
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent) }
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract) }
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).compileComponents(); // compile template and css
|
||||
}).overrideComponent(CollectionSearchResultGridElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(CollectionSearchResultGridElementComponent);
|
||||
collectionSearchResultGridElementComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
it('should show the item result cards in the grid element', () => {
|
||||
expect(fixture.debugElement.query(By.css('ds-collection-search-result-grid-element'))).toBeDefined();
|
||||
describe('When the collection has an abstract', () => {
|
||||
beforeEach(() => {
|
||||
collectionSearchResultGridElementComponent.dso = mockCollectionWithAbstract.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should only show the description if "short description" metadata is present',() => {
|
||||
const descriptionText = expect(fixture.debugElement.query(By.css('p.card-text')));
|
||||
it('should show the description paragraph', () => {
|
||||
const collectionAbstractField = fixture.debugElement.query(By.css('p.card-text'));
|
||||
expect(collectionAbstractField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
if (mockCollection.shortDescription.length > 0) {
|
||||
expect(descriptionText).toBeDefined();
|
||||
} else {
|
||||
expect(descriptionText).not.toBeDefined();
|
||||
}
|
||||
describe('When the collection has no abstract', () => {
|
||||
beforeEach(() => {
|
||||
collectionSearchResultGridElementComponent.dso = mockCollectionWithoutAbstract.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the description paragraph', () => {
|
||||
const collectionAbstractField = fixture.debugElement.query(By.css('p.card-text'));
|
||||
expect(collectionAbstractField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -2,7 +2,7 @@ import { Component } from '@angular/core';
|
||||
|
||||
import { renderElementsFor} from '../../../object-collection/shared/dso-element-decorator';
|
||||
|
||||
import { CollectionSearchResult } from './collection-search-result.model';
|
||||
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
||||
import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
|
@@ -1,5 +0,0 @@
|
||||
import { SearchResult } from '../../../../+search-page/search-result.model';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
|
||||
export class CollectionSearchResult extends SearchResult<Collection> {
|
||||
}
|
@@ -1,63 +1,83 @@
|
||||
import { CommunitySearchResultGridElementComponent } from './community-search-result-grid-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../testing/router-stub';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TruncatePipe } from '../../../utils/truncate.pipe';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
|
||||
|
||||
let communitySearchResultGridElementComponent: CommunitySearchResultGridElementComponent;
|
||||
let fixture: ComponentFixture<CommunitySearchResultGridElementComponent>;
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const activatedRouteStub = {
|
||||
queryParams: Observable.of({
|
||||
query: queryParam,
|
||||
scope: scopeParam
|
||||
})
|
||||
|
||||
const truncatableServiceStub: any = {
|
||||
isCollapsed: (id: number) => Observable.of(true),
|
||||
};
|
||||
const mockCommunity: Community = Object.assign(new Community(), {
|
||||
|
||||
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||
mockCommunityWithAbstract.hitHighlights = [];
|
||||
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
} ]
|
||||
|
||||
});
|
||||
|
||||
const createdGridElementComponent: CommunitySearchResultGridElementComponent = new CommunitySearchResultGridElementComponent(mockCommunity);
|
||||
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||
mockCommunityWithoutAbstract.hitHighlights = [];
|
||||
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
} ]
|
||||
});
|
||||
|
||||
describe('CommunitySearchResultGridElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CommunitySearchResultGridElementComponent, TruncatePipe ],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: Router, useClass: RouterStub },
|
||||
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent) }
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract) }
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).compileComponents(); // compile template and css
|
||||
}).overrideComponent(CommunitySearchResultGridElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(CommunitySearchResultGridElementComponent);
|
||||
communitySearchResultGridElementComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
it('should show the item result cards in the grid element', () => {
|
||||
expect(fixture.debugElement.query(By.css('ds-community-search-result-grid-element'))).toBeDefined();
|
||||
describe('When the community has an abstract', () => {
|
||||
beforeEach(() => {
|
||||
communitySearchResultGridElementComponent.dso = mockCommunityWithAbstract.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should only show the description if "short description" metadata is present',() => {
|
||||
const descriptionText = expect(fixture.debugElement.query(By.css('p.card-text')));
|
||||
it('should show the description paragraph', () => {
|
||||
const communityAbstractField = fixture.debugElement.query(By.css('p.card-text'));
|
||||
expect(communityAbstractField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
if (mockCommunity.shortDescription.length > 0) {
|
||||
expect(descriptionText).toBeDefined();
|
||||
} else {
|
||||
expect(descriptionText).not.toBeDefined();
|
||||
}
|
||||
describe('When the community has no abstract', () => {
|
||||
beforeEach(() => {
|
||||
communitySearchResultGridElementComponent.dso = mockCommunityWithoutAbstract.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the description paragraph', () => {
|
||||
const communityAbstractField = fixture.debugElement.query(By.css('p.card-text'));
|
||||
expect(communityAbstractField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { CommunitySearchResult } from './community-search-result.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
|
||||
import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-search-result-grid-element',
|
||||
|
@@ -1,5 +0,0 @@
|
||||
import { SearchResult } from '../../../../+search-page/search-result.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
|
||||
export class CommunitySearchResult extends SearchResult<Community> {
|
||||
}
|
@@ -1,34 +1,33 @@
|
||||
<div class="card">
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<div class="card mt-1" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'">
|
||||
<a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width">
|
||||
<div>
|
||||
<ds-grid-thumbnail [thumbnail]="dso.getThumbnail()">
|
||||
</ds-grid-thumbnail>
|
||||
</div>
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3" type="h4">
|
||||
<h4 class="card-title" [innerHTML]="dso.findMetadata('dc.title')"></h4>
|
||||
|
||||
<p *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
|
||||
</ds-truncatable-part>
|
||||
<p *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
||||
class="item-authors card-text text-muted">
|
||||
<span
|
||||
*ngFor="let authorMd of dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let first=first;">
|
||||
<span *ngIf="first" [innerHTML]="authorMd.value">
|
||||
<span
|
||||
*ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length>1">, ...</span>
|
||||
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="1">
|
||||
<span *ngIf="dso.findMetadata('dc.date.issued').length > 0" class="item-date">{{dso.findMetadata("dc.date.issued")}}</span>
|
||||
<span *ngFor="let authorMd of dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">,
|
||||
<span [innerHTML]="authorMd.value"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span *ngIf="dso.findMetadata('dc.date.issued')"
|
||||
class="item-list-date">
|
||||
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length>1">,</span>
|
||||
{{dso.findMetadata("dc.date.issued")}}</span>
|
||||
|
||||
|
||||
</ds-truncatable-part>
|
||||
</p>
|
||||
<p class="item-abstract card-text" [innerHTML]="getFirstValue('dc.description.abstract') | dsTruncate:[200]">
|
||||
<p class="item-abstract card-text">
|
||||
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3">
|
||||
<span [innerHTML]="getFirstValue('dc.description.abstract')"></span>
|
||||
</ds-truncatable-part>
|
||||
</p>
|
||||
|
||||
<div class="text-center">
|
||||
<a [routerLink]="['/items/' + dso.id]" class="lead btn btn-primary viewButton">View</a>
|
||||
<a [routerLink]="['/items/' + dso.id]"
|
||||
class="lead btn btn-primary viewButton">View</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ds-truncatable>
|
@@ -1,2 +1,14 @@
|
||||
@import '../../../../../styles/variables';
|
||||
|
||||
.card {
|
||||
a > div {
|
||||
position: relative;
|
||||
.thumbnail-overlay {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background-color: map-get($theme-colors, primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,24 +1,25 @@
|
||||
import { ItemSearchResultGridElementComponent } from './item-search-result-grid-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../testing/router-stub';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NO_ERRORS_SCHEMA, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TruncatePipe } from '../../../utils/truncate.pipe';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
|
||||
|
||||
let itemSearchResultGridElementComponent: ItemSearchResultGridElementComponent;
|
||||
let fixture: ComponentFixture<ItemSearchResultGridElementComponent>;
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const activatedRouteStub = {
|
||||
queryParams: Observable.of({
|
||||
query: queryParam,
|
||||
scope: scopeParam
|
||||
})
|
||||
|
||||
const truncatableServiceStub: any = {
|
||||
isCollapsed: (id: number) => Observable.of(true),
|
||||
};
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
|
||||
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithAuthorAndDate.hitHighlights = [];
|
||||
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
||||
bitstreams: Observable.of({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.contributor.author',
|
||||
@@ -28,53 +29,92 @@ const mockItem: Item = Object.assign(new Item(), {
|
||||
{
|
||||
key: 'dc.date.issued',
|
||||
language: null,
|
||||
value: '1650-06-26'
|
||||
value: '2015-06-26'
|
||||
}]
|
||||
});
|
||||
|
||||
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithoutAuthorAndDate.hitHighlights = [];
|
||||
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
||||
bitstreams: Observable.of({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
},
|
||||
{
|
||||
key: 'dc.type',
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}]
|
||||
});
|
||||
const createdGridElementComponent:ItemSearchResultGridElementComponent= new ItemSearchResultGridElementComponent(mockItem);
|
||||
|
||||
describe('ItemSearchResultGridElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ItemSearchResultGridElementComponent, TruncatePipe ],
|
||||
imports: [NoopAnimationsModule],
|
||||
declarations: [ItemSearchResultGridElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: Router, useClass: RouterStub },
|
||||
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent) }
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
{ provide: 'objectElementProvider', useValue: (mockItemWithoutAuthorAndDate) }
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).compileComponents(); // compile template and css
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ItemSearchResultGridElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(ItemSearchResultGridElementComponent);
|
||||
itemSearchResultGridElementComponent = fixture.componentInstance;
|
||||
|
||||
}));
|
||||
|
||||
it('should show the item result cards in the grid element',() => {
|
||||
expect(fixture.debugElement.query(By.css('ds-item-search-result-grid-element'))).toBeDefined();
|
||||
describe('When the item has an author', () => {
|
||||
beforeEach(() => {
|
||||
itemSearchResultGridElementComponent.dso = mockItemWithAuthorAndDate.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should only show the author span if the author metadata is present',() => {
|
||||
const itemAuthorField = expect(fixture.debugElement.query(By.css('p.item-authors')));
|
||||
|
||||
if (mockItem.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0) {
|
||||
expect(itemAuthorField).toBeDefined();
|
||||
} else {
|
||||
expect(itemAuthorField).not.toBeDefined();
|
||||
}
|
||||
it('should show the author paragraph', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors'));
|
||||
expect(itemAuthorField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should only show the date span if the issuedate is present',() => {
|
||||
const dateField = expect(fixture.debugElement.query(By.css('span.item-list-date')));
|
||||
|
||||
if (mockItem.findMetadata('dc.date.issued').length > 0) {
|
||||
expect(dateField).toBeDefined();
|
||||
} else {
|
||||
expect(dateField).not.toBeDefined();
|
||||
}
|
||||
describe('When the item has no author', () => {
|
||||
beforeEach(() => {
|
||||
itemSearchResultGridElementComponent.dso = mockItemWithoutAuthorAndDate.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the author paragraph', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors'));
|
||||
expect(itemAuthorField).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has an issuedate', () => {
|
||||
beforeEach(() => {
|
||||
itemSearchResultGridElementComponent.dso = mockItemWithAuthorAndDate.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the issuedate span', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('span.item-date'));
|
||||
expect(itemAuthorField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has no issuedate', () => {
|
||||
beforeEach(() => {
|
||||
itemSearchResultGridElementComponent.dso = mockItemWithoutAuthorAndDate.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the issuedate span', () => {
|
||||
const dateField = fixture.debugElement.query(By.css('span.item-date'));
|
||||
expect(dateField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -5,11 +5,13 @@ import { SearchResultGridElementComponent } from '../search-result-grid-element.
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { focusShadow } from '../../../../shared/animations/focus';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-search-result-grid-element',
|
||||
styleUrls: ['../search-result-grid-element.component.scss', 'item-search-result-grid-element.component.scss'],
|
||||
templateUrl: 'item-search-result-grid-element.component.html'
|
||||
templateUrl: 'item-search-result-grid-element.component.html',
|
||||
animations: [focusShadow],
|
||||
})
|
||||
|
||||
@renderElementsFor(ItemSearchResult, ViewMode.Grid)
|
||||
|
@@ -6,6 +6,8 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||
import { isEmpty, hasNoValue } from '../../empty.util';
|
||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||
import { TruncatableService } from '../../truncatable/truncatable.service';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-result-grid-element',
|
||||
@@ -15,8 +17,8 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m
|
||||
export class SearchResultGridElementComponent<T extends SearchResult<K>, K extends DSpaceObject> extends AbstractListableElementComponent<T> {
|
||||
dso: K;
|
||||
|
||||
public constructor(@Inject('objectElementProvider') public gridable: ListableObject) {
|
||||
super(gridable);
|
||||
public constructor(@Inject('objectElementProvider') public listableObject: ListableObject, private truncatableService: TruncatableService) {
|
||||
super(listableObject);
|
||||
this.dso = this.object.dspaceObject;
|
||||
}
|
||||
|
||||
@@ -54,4 +56,8 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isCollapsed(): Observable<boolean> {
|
||||
return this.truncatableService.isCollapsed(this.dso.id);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<a [routerLink]="['/collections/' + object.id]" class="lead">
|
||||
{{object.name}}
|
||||
</a>
|
||||
<div *ngIf="object.shortDescription" class="text-muted">
|
||||
<div *ngIf="object.shortDescription" class="text-muted abstract-text">
|
||||
{{object.shortDescription}}
|
||||
</div>
|
||||
|
@@ -0,0 +1,70 @@
|
||||
import { CollectionListElementComponent } from './collection-list-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
|
||||
let collectionListElementComponent: CollectionListElementComponent;
|
||||
let fixture: ComponentFixture<CollectionListElementComponent>;
|
||||
|
||||
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}]
|
||||
});
|
||||
|
||||
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}]
|
||||
});
|
||||
|
||||
describe('CollectionListElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CollectionListElementComponent ],
|
||||
providers: [
|
||||
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract)}
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).overrideComponent(CollectionListElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(CollectionListElementComponent);
|
||||
collectionListElementComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
describe('When the collection has an abstract', () => {
|
||||
beforeEach(() => {
|
||||
collectionListElementComponent.object = mockCollectionWithAbstract;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the description paragraph', () => {
|
||||
const collectionAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
|
||||
expect(collectionAbstractField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the collection has no abstract', () => {
|
||||
beforeEach(() => {
|
||||
collectionListElementComponent.object = mockCollectionWithoutAbstract;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the description paragraph', () => {
|
||||
const collectionAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
|
||||
expect(collectionAbstractField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,6 +1,6 @@
|
||||
<a [routerLink]="['/communities/' + object.id]" class="lead">
|
||||
{{object.name}}
|
||||
</a>
|
||||
<div *ngIf="object.shortDescription" class="text-muted">
|
||||
<div *ngIf="object.shortDescription" class="text-muted abstract-text">
|
||||
{{object.shortDescription}}
|
||||
</div>
|
||||
|
@@ -0,0 +1,70 @@
|
||||
import { CommunityListElementComponent } from './community-list-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
|
||||
let communityListElementComponent: CommunityListElementComponent;
|
||||
let fixture: ComponentFixture<CommunityListElementComponent>;
|
||||
|
||||
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}]
|
||||
});
|
||||
|
||||
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}]
|
||||
});
|
||||
|
||||
describe('CommunityListElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CommunityListElementComponent ],
|
||||
providers: [
|
||||
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract)}
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).overrideComponent(CommunityListElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(CommunityListElementComponent);
|
||||
communityListElementComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
describe('When the community has an abstract', () => {
|
||||
beforeEach(() => {
|
||||
communityListElementComponent.object = mockCommunityWithAbstract;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the description paragraph', () => {
|
||||
const communityAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
|
||||
expect(communityAbstractField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the community has no abstract', () => {
|
||||
beforeEach(() => {
|
||||
communityListElementComponent.object = mockCommunityWithoutAbstract;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the description paragraph', () => {
|
||||
const communityAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
|
||||
expect(communityAbstractField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -3,12 +3,16 @@
|
||||
</a>
|
||||
<div>
|
||||
<span class="text-muted">
|
||||
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-list-authors">
|
||||
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
||||
class="item-list-authors">
|
||||
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||
<span *ngIf="!last">; </span>
|
||||
</span>
|
||||
</span>
|
||||
(<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span *ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
|
||||
(<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
|
||||
*ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
|
||||
</span>
|
||||
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</div>
|
||||
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
|
||||
{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,108 @@
|
||||
import { ItemListElementComponent } from './item-list-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TruncatePipe } from '../../utils/truncate.pipe';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
let itemListElementComponent: ItemListElementComponent;
|
||||
let fixture: ComponentFixture<ItemListElementComponent>;
|
||||
|
||||
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
|
||||
bitstreams: Observable.of({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.contributor.author',
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
},
|
||||
{
|
||||
key: 'dc.date.issued',
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}]
|
||||
});
|
||||
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
|
||||
bitstreams: Observable.of({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
},
|
||||
{
|
||||
key: 'dc.type',
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}]
|
||||
});
|
||||
|
||||
describe('ItemListElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ItemListElementComponent , TruncatePipe],
|
||||
providers: [
|
||||
{ provide: 'objectElementProvider', useValue: {mockItemWithAuthorAndDate}}
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).overrideComponent(ItemListElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(ItemListElementComponent);
|
||||
itemListElementComponent = fixture.componentInstance;
|
||||
|
||||
}));
|
||||
|
||||
describe('When the item has an author', () => {
|
||||
beforeEach(() => {
|
||||
itemListElementComponent.object = mockItemWithAuthorAndDate;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the author paragraph', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-authors'));
|
||||
expect(itemAuthorField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has no author', () => {
|
||||
beforeEach(() => {
|
||||
itemListElementComponent.object = mockItemWithoutAuthorAndDate;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the author paragraph', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-authors'));
|
||||
expect(itemAuthorField).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has an issuedate', () => {
|
||||
beforeEach(() => {
|
||||
itemListElementComponent.object = mockItemWithAuthorAndDate;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the issuedate span', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-date'));
|
||||
expect(itemAuthorField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has no issuedate', () => {
|
||||
beforeEach(() => {
|
||||
itemListElementComponent.object = mockItemWithoutAuthorAndDate;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the issuedate span', () => {
|
||||
const dateField = fixture.debugElement.query(By.css('span.item-list-date'));
|
||||
expect(dateField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -10,8 +10,8 @@
|
||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||
(sortFieldChange)="onSortFieldChange($event)"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
<ul *ngIf="objects?.hasSucceeded"> <!--class="list-unstyled"-->
|
||||
<li *ngFor="let object of objects?.payload?.page">
|
||||
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled">
|
||||
<li *ngFor="let object of objects?.payload?.page" class="mt-4 mb-4">
|
||||
<ds-wrapper-list-element [object]="object"></ds-wrapper-list-element>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -1,2 +1,2 @@
|
||||
<a [routerLink]="['/collections/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a>
|
||||
<div *ngIf="dso.shortDescription" class="text-muted" [innerHTML]="getFirstValue('dc.description.abstract')"></div>
|
||||
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="getFirstValue('dc.description.abstract')"></div>
|
||||
|
@@ -0,0 +1,83 @@
|
||||
import { CollectionSearchResultListElementComponent } from './collection-search-result-list-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TruncatePipe } from '../../../utils/truncate.pipe';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
||||
|
||||
let collectionSearchResultListElementComponent: CollectionSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<CollectionSearchResultListElementComponent>;
|
||||
|
||||
const truncatableServiceStub: any = {
|
||||
isCollapsed: (id: number) => Observable.of(true),
|
||||
};
|
||||
|
||||
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||
mockCollectionWithAbstract.hitHighlights = [];
|
||||
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
} ]
|
||||
});
|
||||
|
||||
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
|
||||
mockCollectionWithoutAbstract.hitHighlights = [];
|
||||
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
} ]
|
||||
});
|
||||
|
||||
describe('CollectionSearchResultListElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CollectionSearchResultListElementComponent, TruncatePipe ],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract) }
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).overrideComponent(CollectionSearchResultListElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(CollectionSearchResultListElementComponent);
|
||||
collectionSearchResultListElementComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
describe('When the collection has an abstract', () => {
|
||||
beforeEach(() => {
|
||||
collectionSearchResultListElementComponent.dso = mockCollectionWithAbstract.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the description paragraph', () => {
|
||||
const collectionAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
|
||||
expect(collectionAbstractField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the collection has no abstract', () => {
|
||||
beforeEach(() => {
|
||||
collectionSearchResultListElementComponent.dso = mockCollectionWithoutAbstract.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the description paragraph', () => {
|
||||
const collectionAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
|
||||
expect(collectionAbstractField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,10 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
|
||||
import { CollectionSearchResult } from './collection-search-result.model';
|
||||
import { SearchResultListElementComponent } from '../search-result-list-element.component';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-search-result-list-element',
|
||||
|
@@ -1,5 +0,0 @@
|
||||
import { SearchResult } from '../../../../+search-page/search-result.model';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
|
||||
export class CollectionSearchResult extends SearchResult<Collection> {
|
||||
}
|
@@ -1,2 +1,2 @@
|
||||
<a [routerLink]="['/communities/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a>
|
||||
<div *ngIf="dso.shortDescription" class="text-muted" [innerHTML]="getFirstValue('dc.description.abstract')"></div>
|
||||
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="getFirstValue('dc.description.abstract')"></div>
|
||||
|
@@ -0,0 +1,83 @@
|
||||
import { CommunitySearchResultListElementComponent } from './community-search-result-list-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TruncatePipe } from '../../../utils/truncate.pipe';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
|
||||
|
||||
let communitySearchResultListElementComponent: CommunitySearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<CommunitySearchResultListElementComponent>;
|
||||
|
||||
const truncatableServiceStub: any = {
|
||||
isCollapsed: (id: number) => Observable.of(true),
|
||||
};
|
||||
|
||||
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||
mockCommunityWithAbstract.hitHighlights = [];
|
||||
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.description.abstract',
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
} ]
|
||||
});
|
||||
|
||||
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
|
||||
mockCommunityWithoutAbstract.hitHighlights = [];
|
||||
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
} ]
|
||||
});
|
||||
|
||||
describe('CommunitySearchResultListElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CommunitySearchResultListElementComponent, TruncatePipe ],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract) }
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).overrideComponent(CommunitySearchResultListElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(CommunitySearchResultListElementComponent);
|
||||
communitySearchResultListElementComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
describe('When the community has an abstract', () => {
|
||||
beforeEach(() => {
|
||||
communitySearchResultListElementComponent.dso = mockCommunityWithAbstract.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the description paragraph', () => {
|
||||
const communityAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
|
||||
expect(communityAbstractField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the community has no abstract', () => {
|
||||
beforeEach(() => {
|
||||
communitySearchResultListElementComponent.dso = mockCommunityWithoutAbstract.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the description paragraph', () => {
|
||||
const communityAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
|
||||
expect(communityAbstractField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
|
||||
import { CommunitySearchResult } from './community-search-result.model';
|
||||
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
|
||||
import { SearchResultListElementComponent } from '../search-result-list-element.component';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
|
@@ -1,5 +0,0 @@
|
||||
import { SearchResult } from '../../../../+search-page/search-result.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
|
||||
export class CommunitySearchResult extends SearchResult<Community> {
|
||||
}
|
@@ -1,12 +1,24 @@
|
||||
<a [routerLink]="['/items/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a>
|
||||
<div>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<a
|
||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||
[innerHTML]="getFirstValue('dc.title')"></a>
|
||||
<span class="text-muted">
|
||||
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-list-authors">
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||
(<span *ngIf="dso.findMetadata('dc.publisher')" class="item-list-publisher"
|
||||
[innerHTML]="getFirstValue('dc.publisher')">, </span><span
|
||||
*ngIf="dso.findMetadata('dc.date.issued')" class="item-list-date"
|
||||
[innerHTML]="getFirstValue('dc.date.issued')"></span>)
|
||||
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
|
||||
class="item-list-authors">
|
||||
<span *ngFor="let author of getValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">
|
||||
<span [innerHTML]="author"><span [innerHTML]="author"></span></span>
|
||||
</span>
|
||||
</span>
|
||||
(<span *ngIf="dso.findMetadata('dc.publisher')" class="item-list-publisher" [innerHTML]="getFirstValue('dc.publisher')">, </span><span *ngIf="dso.findMetadata('dc.date.issued')" class="item-list-date" [innerHTML]="getFirstValue('dc.date.issued')"></span>)
|
||||
</ds-truncatable-part>
|
||||
</span>
|
||||
<div *ngIf="dso.findMetadata('dc.description.abstract')" class="item-list-abstract" [innerHTML]="getFirstValue('dc.description.abstract') | dsTruncate:[200]"></div>
|
||||
</div>
|
||||
<div *ngIf="dso.findMetadata('dc.description.abstract')" class="item-list-abstract">
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
|
||||
[innerHTML]="getFirstValue('dc.description.abstract')"></span>
|
||||
</ds-truncatable-part>
|
||||
</div>
|
||||
</ds-truncatable>
|
@@ -0,0 +1,120 @@
|
||||
import { ItemSearchResultListElementComponent } from './item-search-result-list-element.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { NO_ERRORS_SCHEMA, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TruncatePipe } from '../../../utils/truncate.pipe';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
|
||||
|
||||
let itemSearchResultListElementComponent: ItemSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<ItemSearchResultListElementComponent>;
|
||||
|
||||
const truncatableServiceStub: any = {
|
||||
isCollapsed: (id: number) => Observable.of(true),
|
||||
};
|
||||
|
||||
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithAuthorAndDate.hitHighlights = [];
|
||||
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
||||
bitstreams: Observable.of({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.contributor.author',
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
},
|
||||
{
|
||||
key: 'dc.date.issued',
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}]
|
||||
});
|
||||
|
||||
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
|
||||
mockItemWithoutAuthorAndDate.hitHighlights = [];
|
||||
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
|
||||
bitstreams: Observable.of({}),
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
},
|
||||
{
|
||||
key: 'dc.type',
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}]
|
||||
});
|
||||
|
||||
describe('ItemSearchResultListElementComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
declarations: [ItemSearchResultListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
{ provide: 'objectElementProvider', useValue: (mockItemWithoutAuthorAndDate) }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ItemSearchResultListElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(ItemSearchResultListElementComponent);
|
||||
itemSearchResultListElementComponent = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
describe('When the item has an author', () => {
|
||||
beforeEach(() => {
|
||||
itemSearchResultListElementComponent.dso = mockItemWithAuthorAndDate.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the author paragraph', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-authors'));
|
||||
expect(itemAuthorField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has no author', () => {
|
||||
beforeEach(() => {
|
||||
itemSearchResultListElementComponent.dso = mockItemWithoutAuthorAndDate.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the author paragraph', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-authors'));
|
||||
expect(itemAuthorField).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has an issuedate', () => {
|
||||
beforeEach(() => {
|
||||
itemSearchResultListElementComponent.dso = mockItemWithAuthorAndDate.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the issuedate span', () => {
|
||||
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-date'));
|
||||
expect(itemAuthorField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item has no issuedate', () => {
|
||||
beforeEach(() => {
|
||||
itemSearchResultListElementComponent.dso = mockItemWithoutAuthorAndDate.dspaceObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the issuedate span', () => {
|
||||
const dateField = fixture.debugElement.query(By.css('span.item-list-date'));
|
||||
expect(dateField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,16 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core';
|
||||
|
||||
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
|
||||
import { SearchResultListElementComponent } from '../search-result-list-element.component';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { ListableObject } from '../../../object-collection/shared/listable-object.model';
|
||||
import { focusBackground } from '../../../animations/focus';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-search-result-list-element',
|
||||
styleUrls: ['../search-result-list-element.component.scss', 'item-search-result-list-element.component.scss'],
|
||||
templateUrl: 'item-search-result-list-element.component.html'
|
||||
templateUrl: 'item-search-result-list-element.component.html',
|
||||
animations: [focusBackground],
|
||||
|
||||
})
|
||||
|
||||
@renderElementsFor(ItemSearchResult, ViewMode.List)
|
||||
export class ItemSearchResultListElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> {}
|
||||
export class ItemSearchResultListElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> {
|
||||
}
|
||||
|
@@ -6,6 +6,8 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||
import { isEmpty, hasNoValue } from '../../empty.util';
|
||||
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { TruncatableService } from '../../truncatable/truncatable.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-result-list-element',
|
||||
@@ -15,7 +17,7 @@ import { AbstractListableElementComponent } from '../../object-collection/shared
|
||||
export class SearchResultListElementComponent<T extends SearchResult<K>, K extends DSpaceObject> extends AbstractListableElementComponent<T> {
|
||||
dso: K;
|
||||
|
||||
public constructor(@Inject('objectElementProvider') public listable: ListableObject) {
|
||||
public constructor(@Inject('objectElementProvider') public listable: ListableObject, private truncatableService: TruncatableService) {
|
||||
super(listable);
|
||||
this.dso = this.object.dspaceObject;
|
||||
}
|
||||
@@ -54,4 +56,8 @@ export class SearchResultListElementComponent<T extends SearchResult<K>, K exten
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isCollapsed(): Observable<boolean> {
|
||||
return this.truncatableService.isCollapsed(this.dso.id);
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ import { Component, Input } from '@angular/core';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { Router } from '@angular/router';
|
||||
import { isNotEmpty, hasValue, isEmpty } from '../empty.util';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
|
@@ -12,7 +12,6 @@ import { NgxPaginationModule } from 'ngx-pagination';
|
||||
import { EnumKeysPipe } from './utils/enum-keys-pipe';
|
||||
import { FileSizePipe } from './utils/file-size-pipe';
|
||||
import { SafeUrlPipe } from './utils/safe-url-pipe';
|
||||
import { TruncatePipe } from './utils/truncate.pipe';
|
||||
|
||||
import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component';
|
||||
import { CommunityListElementComponent } from './object-list/community-list-element/community-list-element.component';
|
||||
@@ -41,6 +40,11 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
|
||||
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
||||
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
|
||||
import { VarDirective } from './utils/var.directive';
|
||||
import { DragClickDirective } from './utils/drag-click.directive';
|
||||
import { TruncatePipe } from './utils/truncate.pipe';
|
||||
import { TruncatableComponent } from './truncatable/truncatable.component';
|
||||
import { TruncatableService } from './truncatable/truncatable.service';
|
||||
import { TruncatablePartComponent } from './truncatable/truncatable-part/truncatable-part.component';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -79,7 +83,9 @@ const COMPONENTS = [
|
||||
ThumbnailComponent,
|
||||
GridThumbnailComponent,
|
||||
WrapperListElementComponent,
|
||||
ViewModeSwitchComponent
|
||||
ViewModeSwitchComponent,
|
||||
TruncatableComponent,
|
||||
TruncatablePartComponent,
|
||||
];
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
@@ -94,8 +100,13 @@ const ENTRY_COMPONENTS = [
|
||||
SearchResultGridElementComponent
|
||||
];
|
||||
|
||||
const PROVIDERS = [
|
||||
TruncatableService
|
||||
];
|
||||
|
||||
const DIRECTIVES = [
|
||||
VarDirective
|
||||
VarDirective,
|
||||
DragClickDirective
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -109,6 +120,9 @@ const DIRECTIVES = [
|
||||
...ENTRY_COMPONENTS,
|
||||
...DIRECTIVES
|
||||
],
|
||||
providers: [
|
||||
...PROVIDERS
|
||||
],
|
||||
exports: [
|
||||
...MODULES,
|
||||
...PIPES,
|
||||
|
@@ -0,0 +1,5 @@
|
||||
<div class="clamp-{{lines}} min-{{minLines}} {{type}} {{fixedHeight ? 'fixedHeight' : ''}}">
|
||||
<div class="content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,63 @@
|
||||
@import '../../../../styles/variables';
|
||||
@import '../../../../styles/mixins';
|
||||
|
||||
@mixin clamp($lines, $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), $body-bg 70%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
@for $i from 1 through 15 {
|
||||
.clamp-#{$i} {
|
||||
transition: height 1s;
|
||||
@include clamp($i);
|
||||
&.title {
|
||||
@include clamp($i, 1.25);
|
||||
}
|
||||
&.h4 {
|
||||
@include clamp($i, $h4-factor, $headings-line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { TruncatablePartComponent } from './truncatable-part.component';
|
||||
import { TruncatableService } from '../truncatable.service';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('TruncatablePartComponent', () => {
|
||||
let comp: TruncatablePartComponent;
|
||||
let fixture: ComponentFixture<TruncatablePartComponent>;
|
||||
const id1 = '123';
|
||||
const id2 = '456';
|
||||
|
||||
let truncatableService;
|
||||
const truncatableServiceStub: any = {
|
||||
isCollapsed: (id: string) => {
|
||||
if (id === id1) {
|
||||
return Observable.of(true)
|
||||
} else {
|
||||
return Observable.of(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
declarations: [TruncatablePartComponent],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(TruncatablePartComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TruncatablePartComponent);
|
||||
comp = fixture.componentInstance; // TruncatablePartComponent test instance
|
||||
fixture.detectChanges();
|
||||
truncatableService = (comp as any).filterService;
|
||||
});
|
||||
|
||||
describe('When the item is collapsed', () => {
|
||||
beforeEach(() => {
|
||||
comp.id = id1;
|
||||
comp.minLines = 5;
|
||||
(comp as any).setLines();
|
||||
fixture.detectChanges();
|
||||
})
|
||||
;
|
||||
|
||||
it('lines should equal minlines', () => {
|
||||
expect((comp as any).lines).toEqual(comp.minLines.toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item is expanded', () => {
|
||||
beforeEach(() => {
|
||||
comp.id = id2;
|
||||
})
|
||||
;
|
||||
|
||||
it('lines should equal maxlines when maxlines has a value', () => {
|
||||
comp.maxLines = 5;
|
||||
(comp as any).setLines();
|
||||
fixture.detectChanges();
|
||||
expect((comp as any).lines).toEqual(comp.maxLines.toString());
|
||||
});
|
||||
|
||||
it('lines should equal \'none\' when maxlines has no value', () => {
|
||||
(comp as any).setLines();
|
||||
fixture.detectChanges();
|
||||
expect((comp as any).lines).toEqual('none');
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,39 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { TruncatableService } from '../truncatable.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-truncatable-part',
|
||||
templateUrl: './truncatable-part.component.html',
|
||||
styleUrls: ['./truncatable-part.component.scss']
|
||||
})
|
||||
|
||||
export class TruncatablePartComponent implements OnInit, OnDestroy {
|
||||
@Input() minLines: number;
|
||||
@Input() maxLines = -1;
|
||||
@Input() id: string;
|
||||
@Input() type: string;
|
||||
@Input() fixedHeight = false;
|
||||
lines: string;
|
||||
private sub;
|
||||
|
||||
public constructor(private service: TruncatableService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.setLines();
|
||||
}
|
||||
|
||||
private setLines() {
|
||||
this.sub = this.service.isCollapsed(this.id).subscribe((collapsed: boolean) => {
|
||||
if (collapsed) {
|
||||
this.lines = this.minLines.toString();
|
||||
} else {
|
||||
this.lines = this.maxLines < 0 ? 'none' : this.maxLines.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
39
src/app/shared/truncatable/truncatable.actions.ts
Normal file
39
src/app/shared/truncatable/truncatable.actions.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../ngrx/type';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
* enum object for all of this group's action types.
|
||||
*
|
||||
* The 'type' utility function coerces strings into string
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const TruncatableActionTypes = {
|
||||
TOGGLE: type('dspace/truncatable/TOGGLE'),
|
||||
COLLAPSE: type('dspace/truncatable/COLLAPSE'),
|
||||
EXPAND: type('dspace/truncatable/EXPAND'),
|
||||
};
|
||||
|
||||
export class TruncatableAction implements Action {
|
||||
id: string;
|
||||
type;
|
||||
constructor(name: string) {
|
||||
this.id = name;
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class TruncatableToggleAction extends TruncatableAction {
|
||||
type = TruncatableActionTypes.TOGGLE;
|
||||
}
|
||||
|
||||
export class TruncatableCollapseAction extends TruncatableAction {
|
||||
type = TruncatableActionTypes.COLLAPSE;
|
||||
}
|
||||
|
||||
export class TruncatableExpandAction extends TruncatableAction {
|
||||
type = TruncatableActionTypes.EXPAND;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
3
src/app/shared/truncatable/truncatable.component.html
Normal file
3
src/app/shared/truncatable/truncatable.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div dsDragClick (actualClick)="toggle()" (mouseenter)="hoverExpand()" (mouseleave)="hoverCollapse">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
101
src/app/shared/truncatable/truncatable.component.spec.ts
Normal file
101
src/app/shared/truncatable/truncatable.component.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { TruncatableComponent } from './truncatable.component';
|
||||
import { TruncatableService } from './truncatable.service';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('TruncatableComponent', () => {
|
||||
let comp: TruncatableComponent;
|
||||
let fixture: ComponentFixture<TruncatableComponent>;
|
||||
const identifier = '1234567890';
|
||||
let truncatableService;
|
||||
const truncatableServiceStub: any = {
|
||||
/* tslint:disable:no-empty */
|
||||
isCollapsed: (id: string) => {
|
||||
if (id === '1') {
|
||||
return Observable.of(true)
|
||||
} else {
|
||||
return Observable.of(false);
|
||||
}
|
||||
},
|
||||
expand: (id: string) => {
|
||||
},
|
||||
collapse: (id: string) => {
|
||||
},
|
||||
toggle: (id: string) => {
|
||||
}
|
||||
/* tslint:enable:no-empty */
|
||||
};
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
declarations: [TruncatableComponent],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(TruncatableComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TruncatableComponent);
|
||||
comp = fixture.componentInstance; // TruncatableComponent test instance
|
||||
comp.id = identifier;
|
||||
fixture.detectChanges();
|
||||
truncatableService = (comp as any).service;
|
||||
});
|
||||
|
||||
describe('When the item is hoverable', () => {
|
||||
beforeEach(() => {
|
||||
comp.onHover = true;
|
||||
fixture.detectChanges();
|
||||
})
|
||||
;
|
||||
|
||||
it('should call collapse on the TruncatableService', () => {
|
||||
spyOn(truncatableService, 'collapse');
|
||||
comp.hoverCollapse();
|
||||
expect(truncatableService.collapse).toHaveBeenCalledWith(identifier);
|
||||
});
|
||||
|
||||
it('should call expand on the TruncatableService', () => {
|
||||
spyOn(truncatableService, 'expand');
|
||||
comp.hoverExpand();
|
||||
expect(truncatableService.expand).toHaveBeenCalledWith(identifier);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the item is not hoverable', () => {
|
||||
beforeEach(() => {
|
||||
comp.onHover = false;
|
||||
fixture.detectChanges();
|
||||
})
|
||||
;
|
||||
|
||||
it('should not call collapse on the TruncatableService', () => {
|
||||
spyOn(truncatableService, 'collapse');
|
||||
comp.hoverCollapse();
|
||||
expect(truncatableService.collapse).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call expand on the TruncatableService', () => {
|
||||
spyOn(truncatableService, 'expand');
|
||||
comp.hoverExpand();
|
||||
expect(truncatableService.expand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When toggle is called', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(truncatableService, 'toggle');
|
||||
comp.toggle();
|
||||
});
|
||||
|
||||
it('should call toggle on the TruncatableService', () => {
|
||||
expect(truncatableService.toggle).toHaveBeenCalledWith(identifier);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
44
src/app/shared/truncatable/truncatable.component.ts
Normal file
44
src/app/shared/truncatable/truncatable.component.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Component, Input
|
||||
} from '@angular/core';
|
||||
import { TruncatableService } from './truncatable.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-truncatable',
|
||||
templateUrl: './truncatable.component.html',
|
||||
styleUrls: ['./truncatable.component.scss'],
|
||||
|
||||
})
|
||||
export class TruncatableComponent {
|
||||
@Input() initialExpand = false;
|
||||
@Input() id: string;
|
||||
@Input() onHover = false;
|
||||
|
||||
public constructor(private service: TruncatableService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.initialExpand) {
|
||||
this.service.expand(this.id);
|
||||
} else {
|
||||
this.service.collapse(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
public hoverCollapse() {
|
||||
if (this.onHover) {
|
||||
this.service.collapse(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
public hoverExpand() {
|
||||
if (this.onHover) {
|
||||
this.service.expand(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
public toggle() {
|
||||
this.service.toggle(this.id);
|
||||
}
|
||||
|
||||
}
|
96
src/app/shared/truncatable/truncatable.reducer.spec.ts
Normal file
96
src/app/shared/truncatable/truncatable.reducer.spec.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import { truncatableReducer } from './truncatable.reducer';
|
||||
import {
|
||||
TruncatableCollapseAction, TruncatableExpandAction,
|
||||
TruncatableToggleAction
|
||||
} from './truncatable.actions';
|
||||
|
||||
const id1 = '123';
|
||||
const id2 = '456';
|
||||
class NullAction extends TruncatableCollapseAction {
|
||||
type = null;
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
describe('truncatableReducer', () => {
|
||||
|
||||
it('should return the current state when no valid actions have been made', () => {
|
||||
const state = { 123: { collapsed: true, page: 1 } };
|
||||
const action = new NullAction();
|
||||
const newState = truncatableReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should start with an empty object', () => {
|
||||
const state = Object.create({});
|
||||
const action = new NullAction();
|
||||
const initialState = truncatableReducer(undefined, action);
|
||||
|
||||
// The search filter starts collapsed
|
||||
expect(initialState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should set collapsed to true in response to the COLLAPSE action', () => {
|
||||
const state = {};
|
||||
state[id1] = { collapsed: false};
|
||||
const action = new TruncatableCollapseAction(id1);
|
||||
const newState = truncatableReducer(state, action);
|
||||
|
||||
expect(newState[id1].collapsed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should perform the COLLAPSE action without affecting the previous state', () => {
|
||||
const state = {};
|
||||
state[id1] = { collapsed: false};
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new TruncatableCollapseAction(id1);
|
||||
truncatableReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
});
|
||||
|
||||
it('should set filterCollapsed to false in response to the EXPAND action', () => {
|
||||
const state = {};
|
||||
state[id1] = { collapsed: true };
|
||||
const action = new TruncatableExpandAction(id1);
|
||||
const newState = truncatableReducer(state, action);
|
||||
|
||||
expect(newState[id1].collapsed).toEqual(false);
|
||||
});
|
||||
|
||||
it('should perform the EXPAND action without affecting the previous state', () => {
|
||||
const state = {};
|
||||
state[id1] = { collapsed: true };
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new TruncatableExpandAction(id1);
|
||||
truncatableReducer(state, action);
|
||||
});
|
||||
|
||||
it('should flip the value of filterCollapsed in response to the TOGGLE action', () => {
|
||||
const state1 = {};
|
||||
state1[id1] = { collapsed: true };
|
||||
const action = new TruncatableToggleAction(id1);
|
||||
|
||||
const state2 = truncatableReducer(state1, action);
|
||||
const state3 = truncatableReducer(state2, action);
|
||||
|
||||
expect(state2[id1].collapsed).toEqual(false);
|
||||
expect(state3[id1].collapsed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should perform the TOGGLE action without affecting the previous state', () => {
|
||||
const state = {};
|
||||
state[id2] = { collapsed: true };
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new TruncatableToggleAction(id2);
|
||||
truncatableReducer(state, action);
|
||||
});
|
||||
});
|
43
src/app/shared/truncatable/truncatable.reducer.ts
Normal file
43
src/app/shared/truncatable/truncatable.reducer.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TruncatableAction, TruncatableActionTypes } from './truncatable.actions';
|
||||
|
||||
export interface TruncatableState {
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
export interface TruncatablesState {
|
||||
[id: string]: TruncatableState
|
||||
}
|
||||
|
||||
const initialState: TruncatablesState = Object.create(null);
|
||||
|
||||
export function truncatableReducer(state = initialState, action: TruncatableAction): TruncatablesState {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case TruncatableActionTypes.COLLAPSE: {
|
||||
return Object.assign({}, state, {
|
||||
[action.id]: {
|
||||
collapsed: true,
|
||||
}
|
||||
});
|
||||
} case TruncatableActionTypes.EXPAND: {
|
||||
return Object.assign({}, state, {
|
||||
[action.id]: {
|
||||
collapsed: false,
|
||||
}
|
||||
});
|
||||
} case TruncatableActionTypes.TOGGLE: {
|
||||
if (!state[action.id]) {
|
||||
state[action.id] = {collapsed: false};
|
||||
}
|
||||
return Object.assign({}, state, {
|
||||
[action.id]: {
|
||||
collapsed: !state[action.id].collapsed,
|
||||
}
|
||||
});
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
54
src/app/shared/truncatable/truncatable.service.spec.ts
Normal file
54
src/app/shared/truncatable/truncatable.service.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Store } from '@ngrx/store';
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { TruncatableService } from './truncatable.service';
|
||||
import { TruncatableCollapseAction, TruncatableExpandAction } from './truncatable.actions';
|
||||
import { TruncatablesState } from './truncatable.reducer';
|
||||
|
||||
describe('TruncatableService', () => {
|
||||
const id1 = '123';
|
||||
const id2 = '456';
|
||||
let service: TruncatableService;
|
||||
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
dispatch: {},
|
||||
/* tslint:enable:no-empty */
|
||||
select: Observable.of(true)
|
||||
});
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
providers: [
|
||||
{
|
||||
provide: Store, useValue: store
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
service = new TruncatableService(store);
|
||||
});
|
||||
|
||||
describe('when the collapse method is triggered', () => {
|
||||
beforeEach(() => {
|
||||
service.collapse(id1);
|
||||
});
|
||||
|
||||
it('TruncatableCollapseAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new TruncatableCollapseAction(id1));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when the expand method is triggered', () => {
|
||||
beforeEach(() => {
|
||||
service.expand(id2);
|
||||
});
|
||||
|
||||
it('TruncatableExpandAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new TruncatableExpandAction(id2));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
52
src/app/shared/truncatable/truncatable.service.ts
Normal file
52
src/app/shared/truncatable/truncatable.service.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { createSelector, MemoizedSelector, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { TruncatablesState, TruncatableState } from './truncatable.reducer';
|
||||
import { TruncatableExpandAction, TruncatableToggleAction, TruncatableCollapseAction } from './truncatable.actions';
|
||||
import { hasValue } from '../empty.util';
|
||||
|
||||
const truncatableStateSelector = (state: TruncatablesState) => state.truncatable;
|
||||
|
||||
@Injectable()
|
||||
export class TruncatableService {
|
||||
|
||||
constructor(private store: Store<TruncatablesState>) {
|
||||
}
|
||||
|
||||
isCollapsed(id: string): Observable<boolean> {
|
||||
return this.store.select(truncatableByIdSelector(id))
|
||||
.map((object: TruncatableState) => {
|
||||
if (object) {
|
||||
return object.collapsed;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public toggle(id: string): void {
|
||||
this.store.dispatch(new TruncatableToggleAction(id));
|
||||
}
|
||||
|
||||
public collapse(id: string): void {
|
||||
this.store.dispatch(new TruncatableCollapseAction(id));
|
||||
}
|
||||
|
||||
public expand(id: string): void {
|
||||
this.store.dispatch(new TruncatableExpandAction(id));
|
||||
}
|
||||
}
|
||||
|
||||
function truncatableByIdSelector(id: string): MemoizedSelector<TruncatablesState, TruncatableState> {
|
||||
return keySelector<TruncatableState>(id);
|
||||
}
|
||||
|
||||
export function keySelector<T>(key: string): MemoizedSelector<TruncatablesState, T> {
|
||||
return createSelector(truncatableStateSelector, (state: TruncatableState) => {
|
||||
if (hasValue(state)) {
|
||||
return state[key];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
23
src/app/shared/utils/drag-click.directive.ts
Normal file
23
src/app/shared/utils/drag-click.directive.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[dsDragClick]'
|
||||
})
|
||||
export class DragClickDirective {
|
||||
private start;
|
||||
@Output() actualClick = new EventEmitter();
|
||||
|
||||
@HostListener('mousedown', ['$event'])
|
||||
mousedownEvent(event) {
|
||||
this.start = new Date();
|
||||
}
|
||||
|
||||
@HostListener('mouseup', ['$event'])
|
||||
mouseupEvent(event) {
|
||||
const end: any = new Date();
|
||||
const clickTime = end - this.start;
|
||||
if (clickTime < 250) {
|
||||
this.actualClick.emit(event)
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,3 +2,11 @@
|
||||
$remSize: $size / 16px;
|
||||
@return $remSize;
|
||||
}
|
||||
|
||||
@function strip-unit($number) {
|
||||
@if type-of($number) == 'number' and not unitless($number) {
|
||||
@return $number / ($number * 0 + 1);
|
||||
}
|
||||
|
||||
@return $number;
|
||||
}
|
@@ -1,5 +1,3 @@
|
||||
@import '../../node_modules/bootstrap/scss/functions.scss';
|
||||
@import '../../node_modules/bootstrap/scss/mixins.scss';
|
||||
|
||||
/* Custom mixins go here */
|
||||
|
||||
@import '../../node_modules/bootstrap/scss/variables.scss';
|
Reference in New Issue
Block a user