diff --git a/src/app/+search-page/search-results/search-results.component.html b/src/app/+search-page/search-results/search-results.component.html index 1b9bd7d52a..7d7c169380 100644 --- a/src/app/+search-page/search-results/search-results.component.html +++ b/src/app/+search-page/search-results/search-results.component.html @@ -1,4 +1,4 @@ -
+

{{ 'search.results.head' | translate }}

= { @@ -25,5 +27,6 @@ export const appReducers: ActionReducerMap = { hostWindow: hostWindowReducer, header: headerReducer, searchSidebar: sidebarReducer, - searchFilter: filterReducer + searchFilter: filterReducer, + truncatable: truncatableReducer }; diff --git a/src/app/shared/animations/focus.ts b/src/app/shared/animations/focus.ts new file mode 100644 index 0000000000..33a5010629 --- /dev/null +++ b/src/app/shared/animations/focus.ts @@ -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)) +]); diff --git a/src/app/shared/animations/overlay.ts b/src/app/shared/animations/overlay.ts new file mode 100644 index 0000000000..15d6bf79de --- /dev/null +++ b/src/app/shared/animations/overlay.ts @@ -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)) +]); diff --git a/src/app/shared/object-collection/shared/collection-search-result.model.ts b/src/app/shared/object-collection/shared/collection-search-result.model.ts new file mode 100644 index 0000000000..63b6a0d37a --- /dev/null +++ b/src/app/shared/object-collection/shared/collection-search-result.model.ts @@ -0,0 +1,5 @@ +import { Collection } from '../../../core/shared/collection.model'; +import { SearchResult } from '../../../+search-page/search-result.model'; + +export class CollectionSearchResult extends SearchResult { +} diff --git a/src/app/shared/object-collection/shared/community-search-result.model.ts b/src/app/shared/object-collection/shared/community-search-result.model.ts new file mode 100644 index 0000000000..79ea34b6cd --- /dev/null +++ b/src/app/shared/object-collection/shared/community-search-result.model.ts @@ -0,0 +1,5 @@ +import { SearchResult } from '../../../+search-page/search-result.model'; +import { Community } from '../../../core/shared/community.model'; + +export class CommunitySearchResult extends SearchResult { +} diff --git a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.spec.ts b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.spec.ts index 217f357bcf..cc72ff3043 100644 --- a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.spec.ts @@ -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; -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 show the description paragraph', () => { + const collectionAbstractField = fixture.debugElement.query(By.css('p.card-text')); + expect(collectionAbstractField).not.toBeNull(); + }); }); - it('should only show the description if "short description" metadata is present',() => { - const descriptionText = expect(fixture.debugElement.query(By.css('p.card-text'))); + describe('When the collection has no abstract', () => { + beforeEach(() => { + collectionGridElementComponent.object = mockCollectionWithoutAbstract; + fixture.detectChanges(); + }); - if (mockCollection.shortDescription.length > 0) { - expect(descriptionText).toBeDefined(); - } else { - expect(descriptionText).not.toBeDefined(); - } + it('should not show the description paragraph', () => { + const collectionAbstractField = fixture.debugElement.query(By.css('p.card-text')); + expect(collectionAbstractField).toBeNull(); + }); }); -}) +}); diff --git a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.spec.ts b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.spec.ts index cc07beec62..dabb137ea7 100644 --- a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.spec.ts @@ -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; -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(); + }); }); }); diff --git a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html index b8bacfaf2e..328bfc3bc9 100644 --- a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html @@ -6,12 +6,11 @@

{{object.findMetadata('dc.title')}}

- -

+

{{authorMd.value}} ; - {{object.findMetadata("dc.date.issued")}} + {{object.findMetadata("dc.date.issued")}}

{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}

diff --git a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.spec.ts b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.spec.ts index 06b3a04f9f..0dd7f0be0a 100644 --- a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.spec.ts @@ -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; -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 show the author paragraph', () => { + const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors')); + expect(itemAuthorField).not.toBeNull(); + }); }); - it('should only show the author span if the author metadata is present',() => { - const itemAuthorField = expect(fixture.debugElement.query(By.css('p.item-authors'))); + describe('When the item has no author', () => { + beforeEach(() => { + itemGridElementComponent.object = mockItemWithoutAuthorAndDate; + fixture.detectChanges(); + }); - if (mockItem.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0) { - expect(itemAuthorField).toBeDefined(); - } else { - expect(itemAuthorField).toBeDefined(); - } + 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(); + }); + }); +}); diff --git a/src/app/shared/object-grid/object-grid.component.html b/src/app/shared/object-grid/object-grid.component.html index 8040a99552..fcf3a42662 100644 --- a/src/app/shared/object-grid/object-grid.component.html +++ b/src/app/shared/object-grid/object-grid.component.html @@ -10,8 +10,8 @@ (sortDirectionChange)="onSortDirectionChange($event)" (sortFieldChange)="onSortFieldChange($event)" (paginationChange)="onPaginationChange($event)"> -
-
+
diff --git a/src/app/shared/object-grid/object-grid.component.scss b/src/app/shared/object-grid/object-grid.component.scss index a85e38d26f..1b9418be48 100644 --- a/src/app/shared/object-grid/object-grid.component.scss +++ b/src/app/shared/object-grid/object-grid.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts index 9dcfe784a7..f3220baaa3 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts @@ -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; -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 show the description paragraph', () => { + const collectionAbstractField = fixture.debugElement.query(By.css('p.card-text')); + expect(collectionAbstractField).not.toBeNull(); + }); }); - it('should only show the description if "short description" metadata is present',() => { - const descriptionText = expect(fixture.debugElement.query(By.css('p.card-text'))); + describe('When the collection has no abstract', () => { + beforeEach(() => { + collectionSearchResultGridElementComponent.dso = mockCollectionWithoutAbstract.dspaceObject; + fixture.detectChanges(); + }); - if (mockCollection.shortDescription.length > 0) { - expect(descriptionText).toBeDefined(); - } else { - expect(descriptionText).not.toBeDefined(); - } + it('should not show the description paragraph', () => { + const collectionAbstractField = fixture.debugElement.query(By.css('p.card-text')); + expect(collectionAbstractField).toBeNull(); + }); }); }); diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts index 0228107a57..e5747a1243 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts @@ -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'; diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result.model.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result.model.ts deleted file mode 100644 index ad48247e70..0000000000 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SearchResult } from '../../../../+search-page/search-result.model'; -import { Collection } from '../../../../core/shared/collection.model'; - -export class CollectionSearchResult extends SearchResult { -} diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts index 6c5bace810..aa62baadc9 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts @@ -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; -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 show the description paragraph', () => { + const communityAbstractField = fixture.debugElement.query(By.css('p.card-text')); + expect(communityAbstractField).not.toBeNull(); + }); }); - it('should only show the description if "short description" metadata is present',() => { - const descriptionText = expect(fixture.debugElement.query(By.css('p.card-text'))); + describe('When the community has no abstract', () => { + beforeEach(() => { + communitySearchResultGridElementComponent.dso = mockCommunityWithoutAbstract.dspaceObject; + fixture.detectChanges(); + }); - if (mockCommunity.shortDescription.length > 0) { - expect(descriptionText).toBeDefined(); - } else { - expect(descriptionText).not.toBeDefined(); - } + it('should not show the description paragraph', () => { + const communityAbstractField = fixture.debugElement.query(By.css('p.card-text')); + expect(communityAbstractField).toBeNull(); + }); }); }); diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts index 4876a784fc..d08286ff2e 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts @@ -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', diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result.model.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result.model.ts deleted file mode 100644 index efeb328e11..0000000000 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SearchResult } from '../../../../+search-page/search-result.model'; -import { Community } from '../../../../core/shared/community.model'; - -export class CommunitySearchResult extends SearchResult { -} diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html index ce9324477f..b185caa18f 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html @@ -1,34 +1,33 @@ -
- - - - -
-

- -

- - - , ... - - - - , - {{dso.findMetadata("dc.date.issued")}} - - -

-

-

- -
- View + +
+ +
+ + +
+
+
+ +

+
+

+ + {{dso.findMetadata("dc.date.issued")}} + , + + + +

+

+ + + +

+
+ View +
+
-
- -
- + \ No newline at end of file diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.scss b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.scss index bd63aa6a3a..e2751279b6 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.scss +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.scss @@ -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); + } + } +} diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts index 7e460439ac..cf8a097ddb 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts @@ -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; -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 show the author paragraph', () => { + const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors')); + expect(itemAuthorField).not.toBeNull(); + }); }); - it('should only show the author span if the author metadata is present',() => { - const itemAuthorField = expect(fixture.debugElement.query(By.css('p.item-authors'))); + describe('When the item has no author', () => { + beforeEach(() => { + itemSearchResultGridElementComponent.dso = mockItemWithoutAuthorAndDate.dspaceObject; + fixture.detectChanges(); + }); - if (mockItem.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0) { - expect(itemAuthorField).toBeDefined(); - } else { - expect(itemAuthorField).not.toBeDefined(); - } + it('should not show the author paragraph', () => { + const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors')); + expect(itemAuthorField).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'))); + describe('When the item has an issuedate', () => { + beforeEach(() => { + itemSearchResultGridElementComponent.dso = mockItemWithAuthorAndDate.dspaceObject; + fixture.detectChanges(); + }); - if (mockItem.findMetadata('dc.date.issued').length > 0) { - expect(dateField).toBeDefined(); - } else { - expect(dateField).not.toBeDefined(); - } + 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(); + }); + }); }); diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts index f9fe13cb88..518fc23a44 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts @@ -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) diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index 8e1d7e0647..e6217eb0bb 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -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, K extends DSpaceObject> extends AbstractListableElementComponent { 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; } @@ -44,7 +46,7 @@ export class SearchResultGridElementComponent, K exten this.object.hitHighlights.some( (md: Metadatum) => { if (key === md.key) { - result = md.value; + result = md.value; return true; } } @@ -54,4 +56,8 @@ export class SearchResultGridElementComponent, K exten } return result; } + + isCollapsed(): Observable { + return this.truncatableService.isCollapsed(this.dso.id); + } } diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html index 8fb498d474..dec2794dca 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html @@ -1,6 +1,6 @@ {{object.name}} -
+
{{object.shortDescription}}
diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts new file mode 100644 index 0000000000..a31af1e50c --- /dev/null +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts @@ -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; + +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(); + }); + }); +}); diff --git a/src/app/shared/object-list/community-list-element/community-list-element.component.html b/src/app/shared/object-list/community-list-element/community-list-element.component.html index d39995de40..7582680fb2 100644 --- a/src/app/shared/object-list/community-list-element/community-list-element.component.html +++ b/src/app/shared/object-list/community-list-element/community-list-element.component.html @@ -1,6 +1,6 @@ {{object.name}} -
+
{{object.shortDescription}}
diff --git a/src/app/shared/object-list/community-list-element/community-list-element.component.spec.ts b/src/app/shared/object-list/community-list-element/community-list-element.component.spec.ts new file mode 100644 index 0000000000..08147d8573 --- /dev/null +++ b/src/app/shared/object-list/community-list-element/community-list-element.component.spec.ts @@ -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; + +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(); + }); + }); +}); diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.html b/src/app/shared/object-list/item-list-element/item-list-element.component.html index cc24ba76b8..b4259c25c2 100644 --- a/src/app/shared/object-list/item-list-element/item-list-element.component.html +++ b/src/app/shared/object-list/item-list-element/item-list-element.component.html @@ -3,12 +3,16 @@
- + {{authorMd.value}} ; - ({{object.findMetadata("dc.publisher")}}, {{object.findMetadata("dc.date.issued")}}) + ({{object.findMetadata("dc.publisher")}}, {{object.findMetadata("dc.date.issued")}}) -
{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}
+
+ {{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }} +
diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.spec.ts b/src/app/shared/object-list/item-list-element/item-list-element.component.spec.ts new file mode 100644 index 0000000000..fc40527693 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/item-list-element.component.spec.ts @@ -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; + +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(); + }); + }); +}); diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index 8de695ae58..420886668a 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -10,8 +10,8 @@ (sortDirectionChange)="onSortDirectionChange($event)" (sortFieldChange)="onSortFieldChange($event)" (paginationChange)="onPaginationChange($event)"> -
    -
  • +
      +
    diff --git a/src/app/shared/object-list/object-list.component.scss b/src/app/shared/object-list/object-list.component.scss index 48e6526dff..3d2af4d023 100644 --- a/src/app/shared/object-list/object-list.component.scss +++ b/src/app/shared/object-list/object-list.component.scss @@ -1 +1 @@ -@import '../../../styles/variables'; +@import '../../../styles/variables'; \ No newline at end of file diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html index 914fb49487..be549b2b76 100644 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html @@ -1,2 +1,2 @@ -
    +
    diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.spec.ts b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.spec.ts new file mode 100644 index 0000000000..0395904070 --- /dev/null +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.spec.ts @@ -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; + +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(); + }); + }); +}); diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts index 5545ea17ec..9a462c124e 100644 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts @@ -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', diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result.model.ts b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result.model.ts deleted file mode 100644 index ad48247e70..0000000000 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SearchResult } from '../../../../+search-page/search-result.model'; -import { Collection } from '../../../../core/shared/collection.model'; - -export class CollectionSearchResult extends SearchResult { -} diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html index d09ef7d668..150ca503cc 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html @@ -1,2 +1,2 @@ -
    +
    diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.spec.ts b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.spec.ts new file mode 100644 index 0000000000..54dde5dee6 --- /dev/null +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.spec.ts @@ -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; + +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(); + }); + }); +}); diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts index 2d96f61833..5664e840e3 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts @@ -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'; diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result.model.ts b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result.model.ts deleted file mode 100644 index efeb328e11..0000000000 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SearchResult } from '../../../../+search-page/search-result.model'; -import { Community } from '../../../../core/shared/community.model'; - -export class CommunitySearchResult extends SearchResult { -} diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html index 27b40eeeeb..b8f3197a7c 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html @@ -1,12 +1,24 @@ - -
    + + - - - - - - (, ) + + (, ) + + + + + + -
    -
    +
    + + +
    + \ No newline at end of file diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts new file mode 100644 index 0000000000..f492c58483 --- /dev/null +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts @@ -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; + +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(); + }); + }); +}); diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts index d1011c8c45..b776abc214 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts @@ -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 {} +export class ItemSearchResultListElementComponent extends SearchResultListElementComponent { +} diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts index 6c79eaad53..9675a58a1e 100644 --- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts @@ -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, K extends DSpaceObject> extends AbstractListableElementComponent { 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, K exten } return result; } + + isCollapsed(): Observable { + return this.truncatableService.isCollapsed(this.dso.id); + } } diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index 76b33a8fd6..fb3c6ba5a2 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -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. diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index ca13067851..c78c218fa9 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -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, diff --git a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html new file mode 100644 index 0000000000..72f41cd770 --- /dev/null +++ b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html @@ -0,0 +1,5 @@ +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss new file mode 100644 index 0000000000..ac3641df39 --- /dev/null +++ b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.spec.ts b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.spec.ts new file mode 100644 index 0000000000..4c3af4cd7d --- /dev/null +++ b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.spec.ts @@ -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; + 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'); + }); + }); +}); diff --git a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.ts b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.ts new file mode 100644 index 0000000000..0f695625ec --- /dev/null +++ b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.ts @@ -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(); + } +} diff --git a/src/app/shared/truncatable/truncatable.actions.ts b/src/app/shared/truncatable/truncatable.actions.ts new file mode 100644 index 0000000000..9d3a51219b --- /dev/null +++ b/src/app/shared/truncatable/truncatable.actions.ts @@ -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 */ diff --git a/src/app/shared/truncatable/truncatable.component.html b/src/app/shared/truncatable/truncatable.component.html new file mode 100644 index 0000000000..c03e93c2ce --- /dev/null +++ b/src/app/shared/truncatable/truncatable.component.html @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/src/app/shared/truncatable/truncatable.component.scss b/src/app/shared/truncatable/truncatable.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/truncatable/truncatable.component.spec.ts b/src/app/shared/truncatable/truncatable.component.spec.ts new file mode 100644 index 0000000000..3c2ce7150a --- /dev/null +++ b/src/app/shared/truncatable/truncatable.component.spec.ts @@ -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; + 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); + }); + }); + +}); diff --git a/src/app/shared/truncatable/truncatable.component.ts b/src/app/shared/truncatable/truncatable.component.ts new file mode 100644 index 0000000000..81ad2b3cff --- /dev/null +++ b/src/app/shared/truncatable/truncatable.component.ts @@ -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); + } + +} diff --git a/src/app/shared/truncatable/truncatable.reducer.spec.ts b/src/app/shared/truncatable/truncatable.reducer.spec.ts new file mode 100644 index 0000000000..00949d1113 --- /dev/null +++ b/src/app/shared/truncatable/truncatable.reducer.spec.ts @@ -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); + }); +}); diff --git a/src/app/shared/truncatable/truncatable.reducer.ts b/src/app/shared/truncatable/truncatable.reducer.ts new file mode 100644 index 0000000000..d9a2111682 --- /dev/null +++ b/src/app/shared/truncatable/truncatable.reducer.ts @@ -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; + } + } +} diff --git a/src/app/shared/truncatable/truncatable.service.spec.ts b/src/app/shared/truncatable/truncatable.service.spec.ts new file mode 100644 index 0000000000..dafa889b87 --- /dev/null +++ b/src/app/shared/truncatable/truncatable.service.spec.ts @@ -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 = 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)); + }); + }); + +}); diff --git a/src/app/shared/truncatable/truncatable.service.ts b/src/app/shared/truncatable/truncatable.service.ts new file mode 100644 index 0000000000..5f36c6a60d --- /dev/null +++ b/src/app/shared/truncatable/truncatable.service.ts @@ -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) { + } + + isCollapsed(id: string): Observable { + 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 { + return keySelector(id); +} + +export function keySelector(key: string): MemoizedSelector { + return createSelector(truncatableStateSelector, (state: TruncatableState) => { + if (hasValue(state)) { + return state[key]; + } else { + return undefined; + } + }); +} diff --git a/src/app/shared/utils/drag-click.directive.ts b/src/app/shared/utils/drag-click.directive.ts new file mode 100644 index 0000000000..ec9b02dea5 --- /dev/null +++ b/src/app/shared/utils/drag-click.directive.ts @@ -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) + } + } +} diff --git a/src/styles/_functions.scss b/src/styles/_functions.scss index 16c5040e03..81de954285 100644 --- a/src/styles/_functions.scss +++ b/src/styles/_functions.scss @@ -1,4 +1,12 @@ @function calculateRem($size) { $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; } \ No newline at end of file diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 7d05343152..564ffc7755 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -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'; \ No newline at end of file