Merge pull request #221 from LotteHofstede/w2p-46063_truncation-implementation

Truncation
This commit is contained in:
Tim Donohue
2018-03-05 15:55:27 -06:00
committed by GitHub
62 changed files with 1686 additions and 295 deletions

View File

@@ -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"

View File

@@ -23,6 +23,7 @@ body {
display: flex;
min-height: 100vh;
flex-direction: column;
width: 100%;
}
.main-content {

View File

@@ -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
};

View 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))
]);

View 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))
]);

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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>

View File

@@ -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();
});
});
});

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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();
});
});
});

View File

@@ -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';

View File

@@ -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> {
}

View File

@@ -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();
});
});
});

View File

@@ -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',

View File

@@ -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> {
}

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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();
});
});
});

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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();
});
});
});

View File

@@ -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>

View File

@@ -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();
});
});
});

View File

@@ -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>

View File

@@ -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();
});
});
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
});
});
});

View File

@@ -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',

View File

@@ -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> {
}

View File

@@ -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>

View File

@@ -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();
});
});
});

View File

@@ -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';

View File

@@ -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> {
}

View File

@@ -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>

View File

@@ -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();
});
});
});

View File

@@ -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> {
}

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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,

View File

@@ -0,0 +1,5 @@
<div class="clamp-{{lines}} min-{{minLines}} {{type}} {{fixedHeight ? 'fixedHeight' : ''}}">
<div class="content">
<ng-content></ng-content>
</div>
</div>

View File

@@ -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);
}
}
}
}

View File

@@ -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');
});
});
});

View File

@@ -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();
}
}

View 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 */

View File

@@ -0,0 +1,3 @@
<div dsDragClick (actualClick)="toggle()" (mouseenter)="hoverExpand()" (mouseleave)="hoverCollapse">
<ng-content></ng-content>
</div>

View 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);
});
});
});

View 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);
}
}

View 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);
});
});

View 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;
}
}
}

View 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));
});
});
});

View 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;
}
});
}

View 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)
}
}
}

View File

@@ -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;
}

View File

@@ -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';