Merge remote-tracking branch 'remotes/origin/master' into notification

# Conflicts:
#	src/app/app.reducer.ts
#	src/app/shared/shared.module.ts
This commit is contained in:
Giuseppe Digilio
2018-03-06 10:20:52 +01:00
69 changed files with 2179 additions and 610 deletions

View File

@@ -1,7 +1,11 @@
sudo: required
dist: trusty
addons:
- chrome: stable
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
language: node_js
@@ -21,6 +25,8 @@ install:
- travis_retry yarn install
script:
# Use Chromium instead of Chrome.
- export CHROME_BIN=chromium-browser
- yarn run build
- yarn run ci
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js

View File

@@ -137,17 +137,32 @@ yarn run clean:dist
Testing
-------
### Unit Test
### Test a Pull Request
Unit tests use Karma. You can find the configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test enviroment you need to edit the './karma.conf.js'. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run:`yarn run coverage`.
If you would like to contribute by testing a Pull Request (PR), here's how to do so. Keep in mind, you **do not need to have a DSpace backend / REST API installed locally to test a PR**. By default, the dspace-angular project points at our demo REST API
To correctly run the tests you need to run the build once with:`yarn run build`.
1. Pull down the branch that the Pull Request was built from. Easy instructions for doing so can be found on the Pull Request itself.
* Next to the "Merge" button, you'll see a link that says "command line instructions".
* Click it, and follow "Step 1" of those instructions to checkout the pull down the PR branch.
2. `yarn run clean` (This resets your local dependencies to ensure you are up-to-date with this PR)
3. `yarn install` (Updates your local dependencies to those in the PR)
4. `yarn start` (Rebuilds the project, and deploys to localhost:3000, by default)
5. At this point, the code from the PR will be deployed to http://localhost:3000. Test it out, and ensure that it does what is described in the PR (or fixes the bug described in the ticket linked to the PR).
Once you have tested the Pull Request, please add a comment and/or approval to the PR to let us know whether you found it to be successful (or not). Thanks!
### Unit Tests
Unit tests use Karma. You can find the configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test enviroment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `yarn run coverage`.
To correctly run the tests you need to run the build once with: `yarn run build`.
The default browser is Google Chrome.
Place your tests in the same location of the application source code files that they test.
and run:`yarn run test`
and run: `yarn run test`
### E2E test
@@ -161,15 +176,18 @@ Protractor needs a functional instance of the DSpace interface to run the E2E te
or any command that bring up the DSpace interface.
Place your tests at the following path:`./e2e`
Place your tests at the following path: `./e2e`
and run:`yarn run e2e`
and run: `yarn run e2e`
### Continuous Integration (CI) Test
To run all the tests (e.g.: to run tests with Continuous Integration software) you can execute:`yarn run ci` Keep in mind that this command prerequisites are the sum of unit test and E2E tests.
##Documentation To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts informations from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
Documentation
--------------
To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts informations from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder.
@@ -322,8 +340,8 @@ Install your library via `yarn add lib-name --save` and import it in your code.
If the library does not include typings, you can install them using yarn:
```bash
yarn add d3 --save
yarn add @types/d3 --save-dev
yarn add d3
yarn add @types/d3 --dev
```
If the library doesn't have typings available at `@types/`, you can still use it by manually adding typings for it:
@@ -349,14 +367,18 @@ If you're importing a module that uses CommonJS you need to import as
import * as _ from 'lodash';
```
yarn lockfile
Managing Dependencies (via yarn)
-------------
This project makes use of yarn to ensure that the exact same dependency versions are used every time you install it.
This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it.
yarn creates the file [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically every time you install a new dependency from the commandline (by using `yarn add some-lib --save` or `yarn add some-lib --save-dev`).
* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn.
* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`.
* If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev`
* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version`
* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it.
If you manually add a package or change a version in `package.json` you'll have to update yarn's lock file as well. You can do so by running `yarn upgrade`
As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.*
Frequently asked questions
--------------------------

View File

@@ -69,26 +69,26 @@
"coverage": "http-server -c-1 -o -p 9875 ./coverage"
},
"dependencies": {
"@angular/animations": "5.2.1",
"@angular/common": "5.2.1",
"@angular/core": "5.2.1",
"@angular/forms": "5.2.1",
"@angular/http": "5.2.1",
"@angular/platform-browser": "5.2.1",
"@angular/platform-browser-dynamic": "5.2.1",
"@angular/platform-server": "5.2.1",
"@angular/router": "5.2.1",
"@angular/animations": "^5.2.5",
"@angular/common": "^5.2.5",
"@angular/core": "^5.2.5",
"@angular/forms": "^5.2.5",
"@angular/http": "^5.2.5",
"@angular/platform-browser": "^5.2.5",
"@angular/platform-browser-dynamic": "^5.2.5",
"@angular/platform-server": "^5.2.5",
"@angular/router": "^5.2.5",
"@angularclass/bootloader": "1.0.1",
"@ng-bootstrap/ng-bootstrap": "1.0.0-beta.9",
"@ngrx/effects": "4.1.1",
"@ngrx/router-store": "4.1.1",
"@ngrx/store": "4.1.1",
"@ng-bootstrap/ng-bootstrap": "^1.0.0",
"@ngrx/effects": "^5.1.0",
"@ngrx/router-store": "^5.0.1",
"@ngrx/store": "^5.1.0",
"@nguniversal/express-engine": "5.0.0-beta.5",
"@ngx-translate/core": "9.1.1",
"@ngx-translate/http-loader": "2.0.1",
"angular-idle-preload": "2.0.4",
"body-parser": "1.18.2",
"bootstrap": "4.0.0-beta",
"bootstrap": "^4.0.0",
"cerialize": "0.1.18",
"compression": "1.7.1",
"cookie-parser": "1.4.3",
@@ -106,45 +106,45 @@
"pem": "1.12.3",
"reflect-metadata": "0.1.12",
"rxjs": "5.5.6",
"ts-md5": "1.2.3",
"ts-md5": "^1.2.4",
"uuid": "^3.2.1",
"webfontloader": "1.6.28",
"zone.js": "0.8.20"
},
"devDependencies": {
"@angular/compiler": "^5.2.1",
"@angular/compiler-cli": "^5.2.1",
"@ngrx/store-devtools": "4.1.1",
"@ngtools/webpack": "1.9.5",
"@angular/compiler": "^5.2.5",
"@angular/compiler-cli": "^5.2.5",
"@ngrx/store-devtools": "^5.1.0",
"@ngtools/webpack": "^1.10.0",
"@types/cookie-parser": "1.4.1",
"@types/deep-freeze": "0.1.1",
"@types/express": "4.11.0",
"@types/express": "^4.11.1",
"@types/express-serve-static-core": "4.11.1",
"@types/hammerjs": "2.0.35",
"@types/jasmine": "2.8.4",
"@types/jasmine": "^2.8.6",
"@types/memory-cache": "0.2.0",
"@types/mime": "2.0.0",
"@types/node": "^9.3.0",
"@types/node": "^9.4.6",
"@types/serve-static": "1.13.1",
"@types/uuid": "^3.4.3",
"@types/webfontloader": "1.6.29",
"ajv": "6.0.1",
"ajv-keywords": "3.0.0",
"ajv": "^6.1.1",
"ajv-keywords": "^3.1.0",
"angular2-template-loader": "0.6.2",
"autoprefixer": "7.2.5",
"autoprefixer": "^8.0.0",
"awesome-typescript-loader": "3.4.1",
"caniuse-lite": "1.0.30000792",
"caniuse-lite": "^1.0.30000697",
"codelyzer": "^4.1.0",
"compression-webpack-plugin": "1.1.3",
"copy-webpack-plugin": "4.3.1",
"compression-webpack-plugin": "^1.1.6",
"copy-webpack-plugin": "^4.4.1",
"coveralls": "3.0.0",
"css-loader": "0.28.9",
"deep-freeze": "0.0.1",
"exports-loader": "0.6.4",
"exports-loader": "^0.7.0",
"html-webpack-plugin": "2.30.1",
"imports-loader": "0.7.1",
"istanbul-instrumenter-loader": "3.0.0",
"jasmine-core": "2.9.1",
"jasmine-core": "^2.99.1",
"jasmine-marbles": "0.2.0",
"jasmine-spec-reporter": "4.2.1",
"json-loader": "0.5.7",
@@ -156,31 +156,31 @@
"karma-jasmine": "1.1.1",
"karma-mocha-reporter": "2.2.5",
"karma-phantomjs-launcher": "1.0.4",
"karma-remap-coverage": "0.1.4",
"karma-remap-coverage": "^0.1.5",
"karma-remap-istanbul": "0.6.0",
"karma-sourcemap-loader": "0.3.7",
"karma-webdriver-launcher": "1.0.5",
"karma-webpack": "2.0.9",
"ngrx-store-freeze": "0.2.0",
"node-sass": "4.7.2",
"nodemon": "1.14.11",
"ngrx-store-freeze": "^0.2.1",
"node-sass": "^4.7.2",
"nodemon": "^1.15.0",
"npm-run-all": "4.1.2",
"postcss": "6.0.16",
"postcss": "^6.0.18",
"postcss-apply": "0.8.0",
"postcss-cli": "4.1.1",
"postcss-cli": "^5.0.0",
"postcss-cssnext": "3.1.0",
"postcss-loader": "2.0.10",
"postcss-loader": "^2.1.0",
"postcss-responsive-type": "1.0.0",
"postcss-smart-import": "0.7.6",
"protractor": "5.2.2",
"protractor": "^5.3.0",
"protractor-istanbul-plugin": "2.0.0",
"raw-loader": "0.5.1",
"resolve-url-loader": "2.2.1",
"rimraf": "2.6.2",
"rollup": "0.54.1",
"rollup-plugin-commonjs": "8.2.6",
"rollup": "^0.56.0",
"rollup-plugin-commonjs": "^8.3.0",
"rollup-plugin-node-globals": "1.1.0",
"rollup-plugin-node-resolve": "3.0.2",
"rollup-plugin-node-resolve": "^3.0.3",
"rollup-plugin-uglify": "3.0.0",
"sass-loader": "6.0.6",
"script-ext-html-webpack-plugin": "1.8.8",
@@ -191,11 +191,11 @@
"ts-helpers": "1.1.2",
"ts-node": "4.1.0",
"tslint": "5.9.1",
"typedoc": "0.9.0",
"typedoc": "^0.9.0",
"typescript": "2.6.2",
"webpack": "^3.10.0",
"webpack-bundle-analyzer": "2.9.2",
"webpack-dev-middleware": "2.0.4",
"webpack": "^3.11.0",
"webpack-bundle-analyzer": "^2.10.0",
"webpack-dev-middleware": "^2.0.5",
"webpack-dev-server": "2.11.1",
"webpack-merge": "4.1.1",
"webpack-node-externals": "1.6.0"

View File

@@ -1,4 +1,4 @@
<div *ngIf="searchResults?.hasSucceeded" @fadeIn>
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading" @fadeIn>
<h2 *ngIf="searchResults?.payload ?.length > 0">{{ 'search.results.head' | translate }}</h2>
<ds-viewable-collection
[config]="searchConfig.pagination"

View File

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

View File

@@ -12,6 +12,7 @@ import {
SearchFiltersState
} from './+search-page/search-filters/search-filter/search-filter.reducer';
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
export interface AppState {
router: fromRouter.RouterReducerState;
@@ -20,6 +21,7 @@ export interface AppState {
notifications: NotificationsState;
searchSidebar: SearchSidebarState;
searchFilter: SearchFiltersState;
truncatable: TruncatablesState;
}
export const appReducers: ActionReducerMap<AppState> = {
@@ -28,5 +30,6 @@ export const appReducers: ActionReducerMap<AppState> = {
header: headerReducer,
notifications: notificationsReducer,
searchSidebar: sidebarReducer,
searchFilter: filterReducer
searchFilter: filterReducer,
truncatable: truncatableReducer
};

View File

@@ -1,5 +1,5 @@
<header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<nav class="navbar navbar-dark bg-primary navbar-expand-md">
<div [ngClass]="{'clearfix': !(isNavBarCollapsed | async)}">
<a class="navbar-brand" routerLink="/home">{{ 'title' | translate }}</a>
</div>

View File

@@ -0,0 +1,19 @@
import { animate, state, transition, trigger, style } from '@angular/animations';
export const focusShadow = trigger('focusShadow', [
state('focus', style({ 'box-shadow': 'rgba(119, 119, 119, 0.6) 0px 0px 6px' })),
state('blur', style({ 'box-shadow': 'none' })),
transition('focus <=> blur', animate(250))
]);
export const focusBackground = trigger('focusBackground', [
state('focus', style({ 'background-color': 'rgba(119, 119, 119, 0.1)' })),
state('blur', style({ 'background-color': 'transparent' })),
transition('focus <=> blur', animate(250))
]);

View File

@@ -0,0 +1,10 @@
import { animate, state, transition, trigger, style } from '@angular/animations';
export const overlay = trigger('overlay', [
state('show', style({ opacity: 0.5 })),
state('hide', style({ opacity: 0 })),
transition('show <=> hide', animate(250))
]);

View File

@@ -0,0 +1,5 @@
import { Collection } from '../../../core/shared/collection.model';
import { SearchResult } from '../../../+search-page/search-result.model';
export class CollectionSearchResult extends SearchResult<Collection> {
}

View File

@@ -0,0 +1,5 @@
import { SearchResult } from '../../../+search-page/search-result.model';
import { Community } from '../../../core/shared/community.model';
export class CommunitySearchResult extends SearchResult<Community> {
}

View File

@@ -1,21 +1,13 @@
import { CollectionGridElementComponent } from './collection-grid-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterStub } from '../../testing/router-stub';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { Collection } from '../../../core/shared/collection.model';
let collectionGridElementComponent: CollectionGridElementComponent;
let fixture: ComponentFixture<CollectionGridElementComponent>;
const queryParam = 'test query';
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
const activatedRouteStub = {
queryParams: Observable.of({
query: queryParam,
scope: scopeParam
})
};
const mockCollection: Collection = Object.assign(new Collection(), {
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
metadata: [
{
key: 'dc.description.abstract',
@@ -23,37 +15,56 @@ const mockCollection: Collection = Object.assign(new Collection(), {
value: 'Short description'
}]
});
const createdGridElementComponent:CollectionGridElementComponent= new CollectionGridElementComponent(mockCollection);
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}]
});
describe('CollectionGridElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CollectionGridElementComponent ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: Router, useClass: RouterStub },
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent)}
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract)}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents(); // compile template and css
}).overrideComponent(CollectionGridElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(CollectionGridElementComponent);
collectionGridElementComponent = fixture.componentInstance;
}));
it('should show the collection cards in the grid element',() => {
expect(fixture.debugElement.query(By.css('ds-collection-grid-element'))).toBeDefined();
describe('When the collection has an abstract', () => {
beforeEach(() => {
collectionGridElementComponent.object = mockCollectionWithAbstract;
fixture.detectChanges();
});
it('should 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();
});
});
})
});

View File

@@ -1,25 +1,13 @@
import { CommunityGridElementComponent } from './community-grid-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterStub } from '../../testing/router-stub';
import { Observable } from 'rxjs/Observable';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ListableObject } from '../../object-collection/shared/listable-object.model';
import { Community } from '../../../core/shared/community.model';
let communityGridElementComponent: CommunityGridElementComponent;
let fixture: ComponentFixture<CommunityGridElementComponent>;
const queryParam = 'test query';
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
const activatedRouteStub = {
queryParams: Observable.of({
query: queryParam,
scope: scopeParam
})
};
const mockCommunity: Community = Object.assign(new Community(), {
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
metadata: [
{
key: 'dc.description.abstract',
@@ -28,39 +16,55 @@ const mockCommunity: Community = Object.assign(new Community(), {
}]
});
const createdGridElementComponent:CommunityGridElementComponent= new CommunityGridElementComponent(mockCommunity);
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}]
});
describe('CommunityGridElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CommunityGridElementComponent ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: Router, useClass: RouterStub },
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent)}
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract)}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents(); // compile template and css
}).overrideComponent(CommunityGridElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(CommunityGridElementComponent);
communityGridElementComponent = fixture.componentInstance;
}));
it('should show the community cards in the grid element',() => {
expect(fixture.debugElement.query(By.css('ds-community-grid-element'))).toBeDefined();
})
describe('When the community has an abstract', () => {
beforeEach(() => {
communityGridElementComponent.object = mockCommunityWithAbstract;
fixture.detectChanges();
});
it('should only show the description if "short description" metadata is present',() => {
const descriptionText = expect(fixture.debugElement.query(By.css('p.card-text')));
it('should show the description paragraph', () => {
const communityAbstractField = fixture.debugElement.query(By.css('p.card-text'));
expect(communityAbstractField).not.toBeNull();
});
});
if (mockCommunity.shortDescription.length > 0) {
expect(descriptionText).toBeDefined();
} else {
expect(descriptionText).not.toBeDefined();
}
describe('When the community has no abstract', () => {
beforeEach(() => {
communityGridElementComponent.object = mockCommunityWithoutAbstract;
fixture.detectChanges();
});
it('should not show the description paragraph', () => {
const communityAbstractField = fixture.debugElement.query(By.css('p.card-text'));
expect(communityAbstractField).toBeNull();
});
});
});

View File

@@ -6,12 +6,11 @@
</a>
<div class="card-body">
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-authors card-text text-muted">
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="!last">; </span>
</span>
<span *ngIf="object.findMetadata('dc.date.issued')">{{object.findMetadata("dc.date.issued")}}</span>
<span *ngIf="object.findMetadata('dc.date.issued')" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
</p>
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</p>

View File

@@ -1,47 +1,55 @@
import { ItemGridElementComponent } from './item-grid-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterStub } from '../../testing/router-stub';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../utils/truncate.pipe';
import { Item } from '../../../core/shared/item.model';
import { Observable } from 'rxjs/Observable';
let itemGridElementComponent: ItemGridElementComponent;
let fixture: ComponentFixture<ItemGridElementComponent>;
const queryParam = 'test query';
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
const activatedRouteStub = {
queryParams: Observable.of({
query: queryParam,
scope: scopeParam
})
};
/* tslint:disable:no-shadowed-variable */
const mockItem: Item = Object.assign(new Item(), {
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
bitstreams: Observable.of({}),
metadata: [
{
key: 'dc.contributor.author',
language: 'en_US',
value: 'Smith, Donald'
},
{
key: 'dc.date.issued',
language: null,
value: '2015-06-26'
}]
});
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
bitstreams: Observable.of({}),
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'This is just another title'
},
{
key: 'dc.type',
language: null,
value: 'Article'
}]
});
const createdGridElementComponent:ItemGridElementComponent= new ItemGridElementComponent(mockItem);
describe('ItemGridElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ItemGridElementComponent , TruncatePipe],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: Router, useClass: RouterStub },
{ provide: 'objectElementProvider', useValue: {createdGridElementComponent}}
{ provide: 'objectElementProvider', useValue: {mockItemWithAuthorAndDate}}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents(); // compile template and css
}).overrideComponent(ItemGridElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
@@ -50,18 +58,51 @@ describe('ItemGridElementComponent', () => {
}));
it('should show the item cards in the grid element',() => {
expect(fixture.debugElement.query(By.css('ds-item-grid-element'))).toBeDefined()
describe('When the item has an author', () => {
beforeEach(() => {
itemGridElementComponent.object = mockItemWithAuthorAndDate;
fixture.detectChanges();
});
it('should 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();
});
});
});

View File

@@ -10,8 +10,8 @@
(sortDirectionChange)="onSortDirectionChange($event)"
(sortFieldChange)="onSortFieldChange($event)"
(paginationChange)="onPaginationChange($event)">
<div class="row mt-2" *ngIf="objects?.hasSucceeded" @fadeIn>
<div class="col-lg-4 col-sm-6 col-xs-12 "
<div class="card-columns" *ngIf="objects?.hasSucceeded" @fadeIn>
<div
*ngFor="let object of objects?.payload?.page">
<ds-wrapper-grid-element [object]="object"></ds-wrapper-grid-element>
</div>

View File

@@ -1,27 +1,24 @@
@import '../../../styles/variables';
@import '../../../styles/mixins';
ds-wrapper-grid-element ::ng-deep {
div.thumbnail > img {
height: $card-thumbnail-height;
width: 100%;
}
.card-title {
line-height: $headings-line-height;
height: ($headings-line-height*3) +em;
overflow: hidden;
text-overflow: ellipsis;
}
.item-abstract {
line-height: $line-height-base;
height: ($line-height-base*5)+em;
overflow: hidden;
text-overflow: ellipsis;
}
.item-authors{
line-height: $line-height-base;
height: ($line-height-base*1.5)+em;
}
div.card {
margin-bottom: 20px;
margin-bottom: $spacer;
}
}
.card-columns {
@include media-breakpoint-only(lg) {
column-count: 3;
}
@include media-breakpoint-only(sm) {
column-count: 2;
}
@include media-breakpoint-only(xs) {
column-count: 1;
}
}

View File

@@ -1,64 +1,83 @@
import {CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component';
import { CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterStub } from '../../../testing/router-stub';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../../utils/truncate.pipe';
import { Community } from '../../../../core/shared/community.model';
import { Collection } from '../../../../core/shared/collection.model';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
let collectionSearchResultGridElementComponent: CollectionSearchResultGridElementComponent;
let fixture: ComponentFixture<CollectionSearchResultGridElementComponent>;
const queryParam = 'test query';
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
const activatedRouteStub = {
queryParams: Observable.of({
query: queryParam,
scope: scopeParam
})
const truncatableServiceStub: any = {
isCollapsed: (id: number) => Observable.of(true),
};
const mockCollection: Collection = Object.assign(new Collection(), {
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
mockCollectionWithAbstract.hitHighlights = [];
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
metadata: [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
} ]
});
const createdGridElementComponent: CollectionSearchResultGridElementComponent = new CollectionSearchResultGridElementComponent(mockCollection);
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
mockCollectionWithoutAbstract.hitHighlights = [];
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
} ]
});
describe('CollectionSearchResultGridElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CollectionSearchResultGridElementComponent, TruncatePipe ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: Router, useClass: RouterStub },
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent) }
{ provide: TruncatableService, useValue: truncatableServiceStub },
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract) }
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents(); // compile template and css
}).overrideComponent(CollectionSearchResultGridElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(CollectionSearchResultGridElementComponent);
collectionSearchResultGridElementComponent = fixture.componentInstance;
}));
it('should show the item result cards in the grid element', () => {
expect(fixture.debugElement.query(By.css('ds-collection-search-result-grid-element'))).toBeDefined();
describe('When the collection has an abstract', () => {
beforeEach(() => {
collectionSearchResultGridElementComponent.dso = mockCollectionWithAbstract.dspaceObject;
fixture.detectChanges();
});
it('should 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();
});
});
});

View File

@@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { renderElementsFor} from '../../../object-collection/shared/dso-element-decorator';
import { CollectionSearchResult } from './collection-search-result.model';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
import { Collection } from '../../../../core/shared/collection.model';
import { ViewMode } from '../../../../+search-page/search-options.model';

View File

@@ -1,5 +0,0 @@
import { SearchResult } from '../../../../+search-page/search-result.model';
import { Collection } from '../../../../core/shared/collection.model';
export class CollectionSearchResult extends SearchResult<Collection> {
}

View File

@@ -1,63 +1,83 @@
import { CommunitySearchResultGridElementComponent } from './community-search-result-grid-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterStub } from '../../../testing/router-stub';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../../utils/truncate.pipe';
import { Community } from '../../../../core/shared/community.model';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
let communitySearchResultGridElementComponent: CommunitySearchResultGridElementComponent;
let fixture: ComponentFixture<CommunitySearchResultGridElementComponent>;
const queryParam = 'test query';
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
const activatedRouteStub = {
queryParams: Observable.of({
query: queryParam,
scope: scopeParam
})
const truncatableServiceStub: any = {
isCollapsed: (id: number) => Observable.of(true),
};
const mockCommunity: Community = Object.assign(new Community(), {
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
mockCommunityWithAbstract.hitHighlights = [];
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
metadata: [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
} ]
});
const createdGridElementComponent: CommunitySearchResultGridElementComponent = new CommunitySearchResultGridElementComponent(mockCommunity);
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
mockCommunityWithoutAbstract.hitHighlights = [];
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
} ]
});
describe('CommunitySearchResultGridElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CommunitySearchResultGridElementComponent, TruncatePipe ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: Router, useClass: RouterStub },
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent) }
{ provide: TruncatableService, useValue: truncatableServiceStub },
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract) }
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents(); // compile template and css
}).overrideComponent(CommunitySearchResultGridElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(CommunitySearchResultGridElementComponent);
communitySearchResultGridElementComponent = fixture.componentInstance;
}));
it('should show the item result cards in the grid element', () => {
expect(fixture.debugElement.query(By.css('ds-community-search-result-grid-element'))).toBeDefined();
describe('When the community has an abstract', () => {
beforeEach(() => {
communitySearchResultGridElementComponent.dso = mockCommunityWithAbstract.dspaceObject;
fixture.detectChanges();
});
it('should 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();
});
});
});

View File

@@ -1,10 +1,10 @@
import { Component } from '@angular/core';
import { CommunitySearchResult } from './community-search-result.model';
import { Community } from '../../../../core/shared/community.model';
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
@Component({
selector: 'ds-community-search-result-grid-element',

View File

@@ -1,5 +0,0 @@
import { SearchResult } from '../../../../+search-page/search-result.model';
import { Community } from '../../../../core/shared/community.model';
export class CommunitySearchResult extends SearchResult<Community> {
}

View File

@@ -1,34 +1,33 @@
<div class="card">
<a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width">
<ds-grid-thumbnail [thumbnail]="dso.getThumbnail()">
</ds-grid-thumbnail>
</a>
<div class="card-body">
<h4 class="card-title" [innerHTML]="dso.findMetadata('dc.title')"></h4>
<p *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
class="item-authors card-text text-muted">
<span
*ngFor="let authorMd of dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let first=first;">
<span *ngIf="first" [innerHTML]="authorMd.value">
<span
*ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length>1">, ...</span>
</span>
</span>
<span *ngIf="dso.findMetadata('dc.date.issued')"
class="item-list-date">
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length>1">,</span>
{{dso.findMetadata("dc.date.issued")}}</span>
</p>
<p class="item-abstract card-text" [innerHTML]="getFirstValue('dc.description.abstract') | dsTruncate:[200]">
</p>
<div class="text-center">
<a [routerLink]="['/items/' + dso.id]" class="lead btn btn-primary viewButton">View</a>
<ds-truncatable [id]="dso.id">
<div class="card mt-1" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'">
<a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="dso.getThumbnail()">
</ds-grid-thumbnail>
</div>
</a>
<div class="card-body">
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3" type="h4">
<h4 class="card-title" [innerHTML]="dso.findMetadata('dc.title')"></h4>
</ds-truncatable-part>
<p *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
class="item-authors card-text text-muted">
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="1">
<span *ngIf="dso.findMetadata('dc.date.issued').length > 0" class="item-date">{{dso.findMetadata("dc.date.issued")}}</span>
<span *ngFor="let authorMd of dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">,
<span [innerHTML]="authorMd.value"></span>
</span>
</ds-truncatable-part>
</p>
<p class="item-abstract card-text">
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3">
<span [innerHTML]="getFirstValue('dc.description.abstract')"></span>
</ds-truncatable-part>
</p>
<div class="text-center">
<a [routerLink]="['/items/' + dso.id]"
class="lead btn btn-primary viewButton">View</a>
</div>
</div>
</div>
</div>
</div>
</ds-truncatable>

View File

@@ -1,2 +1,14 @@
@import '../../../../../styles/variables';
.card {
a > div {
position: relative;
.thumbnail-overlay {
height: 100%;
position: absolute;
top: 0;
width: 100%;
background-color: map-get($theme-colors, primary);
}
}
}

View File

@@ -1,24 +1,25 @@
import { ItemSearchResultGridElementComponent } from './item-search-result-grid-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterStub } from '../../../testing/router-stub';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NO_ERRORS_SCHEMA, ChangeDetectionStrategy } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../../utils/truncate.pipe';
import { Item } from '../../../../core/shared/item.model';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
let itemSearchResultGridElementComponent: ItemSearchResultGridElementComponent;
let fixture: ComponentFixture<ItemSearchResultGridElementComponent>;
const queryParam = 'test query';
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
const activatedRouteStub = {
queryParams: Observable.of({
query: queryParam,
scope: scopeParam
})
const truncatableServiceStub: any = {
isCollapsed: (id: number) => Observable.of(true),
};
const mockItem: Item = Object.assign(new Item(), {
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
mockItemWithAuthorAndDate.hitHighlights = [];
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
bitstreams: Observable.of({}),
metadata: [
{
key: 'dc.contributor.author',
@@ -28,53 +29,92 @@ const mockItem: Item = Object.assign(new Item(), {
{
key: 'dc.date.issued',
language: null,
value: '1650-06-26'
value: '2015-06-26'
}]
});
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
mockItemWithoutAuthorAndDate.hitHighlights = [];
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
bitstreams: Observable.of({}),
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'This is just another title'
},
{
key: 'dc.type',
language: null,
value: 'Article'
}]
});
const createdGridElementComponent:ItemSearchResultGridElementComponent= new ItemSearchResultGridElementComponent(mockItem);
describe('ItemSearchResultGridElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ItemSearchResultGridElementComponent, TruncatePipe ],
imports: [NoopAnimationsModule],
declarations: [ItemSearchResultGridElementComponent, TruncatePipe],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: Router, useClass: RouterStub },
{ provide: 'objectElementProvider', useValue: (createdGridElementComponent) }
{ provide: TruncatableService, useValue: truncatableServiceStub },
{ provide: 'objectElementProvider', useValue: (mockItemWithoutAuthorAndDate) }
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents(); // compile template and css
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemSearchResultGridElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(ItemSearchResultGridElementComponent);
itemSearchResultGridElementComponent = fixture.componentInstance;
}));
it('should show the item result cards in the grid element',() => {
expect(fixture.debugElement.query(By.css('ds-item-search-result-grid-element'))).toBeDefined();
describe('When the item has an author', () => {
beforeEach(() => {
itemSearchResultGridElementComponent.dso = mockItemWithAuthorAndDate.dspaceObject;
fixture.detectChanges();
});
it('should 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();
});
});
});

View File

@@ -5,11 +5,13 @@ import { SearchResultGridElementComponent } from '../search-result-grid-element.
import { Item } from '../../../../core/shared/item.model';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { focusShadow } from '../../../../shared/animations/focus';
@Component({
selector: 'ds-item-search-result-grid-element',
styleUrls: ['../search-result-grid-element.component.scss', 'item-search-result-grid-element.component.scss'],
templateUrl: 'item-search-result-grid-element.component.html'
templateUrl: 'item-search-result-grid-element.component.html',
animations: [focusShadow],
})
@renderElementsFor(ItemSearchResult, ViewMode.Grid)

View File

@@ -6,6 +6,8 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
import { isEmpty, hasNoValue } from '../../empty.util';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { ListableObject } from '../../object-collection/shared/listable-object.model';
import { TruncatableService } from '../../truncatable/truncatable.service';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'ds-search-result-grid-element',
@@ -15,8 +17,8 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m
export class SearchResultGridElementComponent<T extends SearchResult<K>, K extends DSpaceObject> extends AbstractListableElementComponent<T> {
dso: K;
public constructor(@Inject('objectElementProvider') public gridable: ListableObject) {
super(gridable);
public constructor(@Inject('objectElementProvider') public listableObject: ListableObject, private truncatableService: TruncatableService) {
super(listableObject);
this.dso = this.object.dspaceObject;
}
@@ -44,7 +46,7 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, 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<T extends SearchResult<K>, K exten
}
return result;
}
isCollapsed(): Observable<boolean> {
return this.truncatableService.isCollapsed(this.dso.id);
}
}

View File

@@ -1,6 +1,6 @@
<a [routerLink]="['/collections/' + object.id]" class="lead">
{{object.name}}
</a>
<div *ngIf="object.shortDescription" class="text-muted">
<div *ngIf="object.shortDescription" class="text-muted abstract-text">
{{object.shortDescription}}
</div>

View File

@@ -0,0 +1,70 @@
import { CollectionListElementComponent } from './collection-list-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { Collection } from '../../../core/shared/collection.model';
let collectionListElementComponent: CollectionListElementComponent;
let fixture: ComponentFixture<CollectionListElementComponent>;
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
metadata: [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
}]
});
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}]
});
describe('CollectionListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CollectionListElementComponent ],
providers: [
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract)}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).overrideComponent(CollectionListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(CollectionListElementComponent);
collectionListElementComponent = fixture.componentInstance;
}));
describe('When the collection has an abstract', () => {
beforeEach(() => {
collectionListElementComponent.object = mockCollectionWithAbstract;
fixture.detectChanges();
});
it('should show the description paragraph', () => {
const collectionAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
expect(collectionAbstractField).not.toBeNull();
});
});
describe('When the collection has no abstract', () => {
beforeEach(() => {
collectionListElementComponent.object = mockCollectionWithoutAbstract;
fixture.detectChanges();
});
it('should not show the description paragraph', () => {
const collectionAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
expect(collectionAbstractField).toBeNull();
});
});
});

View File

@@ -1,6 +1,6 @@
<a [routerLink]="['/communities/' + object.id]" class="lead">
{{object.name}}
</a>
<div *ngIf="object.shortDescription" class="text-muted">
<div *ngIf="object.shortDescription" class="text-muted abstract-text">
{{object.shortDescription}}
</div>

View File

@@ -0,0 +1,70 @@
import { CommunityListElementComponent } from './community-list-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { Community } from '../../../core/shared/community.model';
let communityListElementComponent: CommunityListElementComponent;
let fixture: ComponentFixture<CommunityListElementComponent>;
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
metadata: [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
}]
});
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}]
});
describe('CommunityListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CommunityListElementComponent ],
providers: [
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract)}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).overrideComponent(CommunityListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(CommunityListElementComponent);
communityListElementComponent = fixture.componentInstance;
}));
describe('When the community has an abstract', () => {
beforeEach(() => {
communityListElementComponent.object = mockCommunityWithAbstract;
fixture.detectChanges();
});
it('should show the description paragraph', () => {
const communityAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
expect(communityAbstractField).not.toBeNull();
});
});
describe('When the community has no abstract', () => {
beforeEach(() => {
communityListElementComponent.object = mockCommunityWithoutAbstract;
fixture.detectChanges();
});
it('should not show the description paragraph', () => {
const communityAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
expect(communityAbstractField).toBeNull();
});
});
});

View File

@@ -3,12 +3,16 @@
</a>
<div>
<span class="text-muted">
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-list-authors">
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
class="item-list-authors">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="!last">; </span>
</span>
</span>
(<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span *ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
(<span *ngIf="object.findMetadata('dc.publisher')" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
*ngIf="object.findMetadata('dc.date.issued')" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
</span>
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</div>
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}
</div>
</div>

View File

@@ -0,0 +1,108 @@
import { ItemListElementComponent } from './item-list-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../utils/truncate.pipe';
import { Item } from '../../../core/shared/item.model';
import { Observable } from 'rxjs/Observable';
let itemListElementComponent: ItemListElementComponent;
let fixture: ComponentFixture<ItemListElementComponent>;
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
bitstreams: Observable.of({}),
metadata: [
{
key: 'dc.contributor.author',
language: 'en_US',
value: 'Smith, Donald'
},
{
key: 'dc.date.issued',
language: null,
value: '2015-06-26'
}]
});
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
bitstreams: Observable.of({}),
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'This is just another title'
},
{
key: 'dc.type',
language: null,
value: 'Article'
}]
});
describe('ItemListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ItemListElementComponent , TruncatePipe],
providers: [
{ provide: 'objectElementProvider', useValue: {mockItemWithAuthorAndDate}}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).overrideComponent(ItemListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(ItemListElementComponent);
itemListElementComponent = fixture.componentInstance;
}));
describe('When the item has an author', () => {
beforeEach(() => {
itemListElementComponent.object = mockItemWithAuthorAndDate;
fixture.detectChanges();
});
it('should show the author paragraph', () => {
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-authors'));
expect(itemAuthorField).not.toBeNull();
});
});
describe('When the item has no author', () => {
beforeEach(() => {
itemListElementComponent.object = mockItemWithoutAuthorAndDate;
fixture.detectChanges();
});
it('should not show the author paragraph', () => {
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-authors'));
expect(itemAuthorField).toBeNull();
});
});
describe('When the item has an issuedate', () => {
beforeEach(() => {
itemListElementComponent.object = mockItemWithAuthorAndDate;
fixture.detectChanges();
});
it('should show the issuedate span', () => {
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-date'));
expect(itemAuthorField).not.toBeNull();
});
});
describe('When the item has no issuedate', () => {
beforeEach(() => {
itemListElementComponent.object = mockItemWithoutAuthorAndDate;
fixture.detectChanges();
});
it('should not show the issuedate span', () => {
const dateField = fixture.debugElement.query(By.css('span.item-list-date'));
expect(dateField).toBeNull();
});
});
});

View File

@@ -10,8 +10,8 @@
(sortDirectionChange)="onSortDirectionChange($event)"
(sortFieldChange)="onSortFieldChange($event)"
(paginationChange)="onPaginationChange($event)">
<ul *ngIf="objects?.hasSucceeded"> <!--class="list-unstyled"-->
<li *ngFor="let object of objects?.payload?.page">
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled">
<li *ngFor="let object of objects?.payload?.page" class="mt-4 mb-4">
<ds-wrapper-list-element [object]="object"></ds-wrapper-list-element>
</li>
</ul>

View File

@@ -1 +1 @@
@import '../../../styles/variables';
@import '../../../styles/variables';

View File

@@ -1,2 +1,2 @@
<a [routerLink]="['/collections/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a>
<div *ngIf="dso.shortDescription" class="text-muted" [innerHTML]="getFirstValue('dc.description.abstract')"></div>
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="getFirstValue('dc.description.abstract')"></div>

View File

@@ -0,0 +1,83 @@
import { CollectionSearchResultListElementComponent } from './collection-search-result-list-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../../utils/truncate.pipe';
import { Collection } from '../../../../core/shared/collection.model';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
let collectionSearchResultListElementComponent: CollectionSearchResultListElementComponent;
let fixture: ComponentFixture<CollectionSearchResultListElementComponent>;
const truncatableServiceStub: any = {
isCollapsed: (id: number) => Observable.of(true),
};
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
mockCollectionWithAbstract.hitHighlights = [];
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
metadata: [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
} ]
});
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
mockCollectionWithoutAbstract.hitHighlights = [];
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
} ]
});
describe('CollectionSearchResultListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CollectionSearchResultListElementComponent, TruncatePipe ],
providers: [
{ provide: TruncatableService, useValue: truncatableServiceStub },
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract) }
],
schemas: [ NO_ERRORS_SCHEMA ]
}).overrideComponent(CollectionSearchResultListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(CollectionSearchResultListElementComponent);
collectionSearchResultListElementComponent = fixture.componentInstance;
}));
describe('When the collection has an abstract', () => {
beforeEach(() => {
collectionSearchResultListElementComponent.dso = mockCollectionWithAbstract.dspaceObject;
fixture.detectChanges();
});
it('should show the description paragraph', () => {
const collectionAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
expect(collectionAbstractField).not.toBeNull();
});
});
describe('When the collection has no abstract', () => {
beforeEach(() => {
collectionSearchResultListElementComponent.dso = mockCollectionWithoutAbstract.dspaceObject;
fixture.detectChanges();
});
it('should not show the description paragraph', () => {
const collectionAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
expect(collectionAbstractField).toBeNull();
});
});
});

View File

@@ -1,10 +1,10 @@
import { Component } from '@angular/core';
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
import { CollectionSearchResult } from './collection-search-result.model';
import { SearchResultListElementComponent } from '../search-result-list-element.component';
import { Collection } from '../../../../core/shared/collection.model';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
@Component({
selector: 'ds-collection-search-result-list-element',

View File

@@ -1,5 +0,0 @@
import { SearchResult } from '../../../../+search-page/search-result.model';
import { Collection } from '../../../../core/shared/collection.model';
export class CollectionSearchResult extends SearchResult<Collection> {
}

View File

@@ -1,2 +1,2 @@
<a [routerLink]="['/communities/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a>
<div *ngIf="dso.shortDescription" class="text-muted" [innerHTML]="getFirstValue('dc.description.abstract')"></div>
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="getFirstValue('dc.description.abstract')"></div>

View File

@@ -0,0 +1,83 @@
import { CommunitySearchResultListElementComponent } from './community-search-result-list-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../../utils/truncate.pipe';
import { Community } from '../../../../core/shared/community.model';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
let communitySearchResultListElementComponent: CommunitySearchResultListElementComponent;
let fixture: ComponentFixture<CommunitySearchResultListElementComponent>;
const truncatableServiceStub: any = {
isCollapsed: (id: number) => Observable.of(true),
};
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
mockCommunityWithAbstract.hitHighlights = [];
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
metadata: [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
} ]
});
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
mockCommunityWithoutAbstract.hitHighlights = [];
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
} ]
});
describe('CommunitySearchResultListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CommunitySearchResultListElementComponent, TruncatePipe ],
providers: [
{ provide: TruncatableService, useValue: truncatableServiceStub },
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract) }
],
schemas: [ NO_ERRORS_SCHEMA ]
}).overrideComponent(CommunitySearchResultListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(CommunitySearchResultListElementComponent);
communitySearchResultListElementComponent = fixture.componentInstance;
}));
describe('When the community has an abstract', () => {
beforeEach(() => {
communitySearchResultListElementComponent.dso = mockCommunityWithAbstract.dspaceObject;
fixture.detectChanges();
});
it('should show the description paragraph', () => {
const communityAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
expect(communityAbstractField).not.toBeNull();
});
});
describe('When the community has no abstract', () => {
beforeEach(() => {
communitySearchResultListElementComponent.dso = mockCommunityWithoutAbstract.dspaceObject;
fixture.detectChanges();
});
it('should not show the description paragraph', () => {
const communityAbstractField = fixture.debugElement.query(By.css('div.abstract-text'));
expect(communityAbstractField).toBeNull();
});
});
});

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
import { CommunitySearchResult } from './community-search-result.model';
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
import { SearchResultListElementComponent } from '../search-result-list-element.component';
import { Community } from '../../../../core/shared/community.model';
import { ViewMode } from '../../../../+search-page/search-options.model';

View File

@@ -1,5 +0,0 @@
import { SearchResult } from '../../../../+search-page/search-result.model';
import { Community } from '../../../../core/shared/community.model';
export class CommunitySearchResult extends SearchResult<Community> {
}

View File

@@ -1,12 +1,24 @@
<a [routerLink]="['/items/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a>
<div>
<ds-truncatable [id]="dso.id">
<a
[routerLink]="['/items/' + dso.id]" class="lead"
[innerHTML]="getFirstValue('dc.title')"></a>
<span class="text-muted">
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-list-authors">
<span *ngFor="let author of getValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">
<span [innerHTML]="author"><span [innerHTML]="author"></span></span>
</span>
</span>
(<span *ngIf="dso.findMetadata('dc.publisher')" class="item-list-publisher" [innerHTML]="getFirstValue('dc.publisher')">, </span><span *ngIf="dso.findMetadata('dc.date.issued')" class="item-list-date" [innerHTML]="getFirstValue('dc.date.issued')"></span>)
<ds-truncatable-part [id]="dso.id" [minLines]="1">
(<span *ngIf="dso.findMetadata('dc.publisher')" class="item-list-publisher"
[innerHTML]="getFirstValue('dc.publisher')">, </span><span
*ngIf="dso.findMetadata('dc.date.issued')" class="item-list-date"
[innerHTML]="getFirstValue('dc.date.issued')"></span>)
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
class="item-list-authors">
<span *ngFor="let author of getValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">
<span [innerHTML]="author"><span [innerHTML]="author"></span></span>
</span>
</span>
</ds-truncatable-part>
</span>
<div *ngIf="dso.findMetadata('dc.description.abstract')" class="item-list-abstract" [innerHTML]="getFirstValue('dc.description.abstract') | dsTruncate:[200]"></div>
</div>
<div *ngIf="dso.findMetadata('dc.description.abstract')" class="item-list-abstract">
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
[innerHTML]="getFirstValue('dc.description.abstract')"></span>
</ds-truncatable-part>
</div>
</ds-truncatable>

View File

@@ -0,0 +1,120 @@
import { ItemSearchResultListElementComponent } from './item-search-result-list-element.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { NO_ERRORS_SCHEMA, ChangeDetectionStrategy } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../../utils/truncate.pipe';
import { Item } from '../../../../core/shared/item.model';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
let itemSearchResultListElementComponent: ItemSearchResultListElementComponent;
let fixture: ComponentFixture<ItemSearchResultListElementComponent>;
const truncatableServiceStub: any = {
isCollapsed: (id: number) => Observable.of(true),
};
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
mockItemWithAuthorAndDate.hitHighlights = [];
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
bitstreams: Observable.of({}),
metadata: [
{
key: 'dc.contributor.author',
language: 'en_US',
value: 'Smith, Donald'
},
{
key: 'dc.date.issued',
language: null,
value: '2015-06-26'
}]
});
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
mockItemWithoutAuthorAndDate.hitHighlights = [];
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
bitstreams: Observable.of({}),
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'This is just another title'
},
{
key: 'dc.type',
language: null,
value: 'Article'
}]
});
describe('ItemSearchResultListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule],
declarations: [ItemSearchResultListElementComponent, TruncatePipe],
providers: [
{ provide: TruncatableService, useValue: truncatableServiceStub },
{ provide: 'objectElementProvider', useValue: (mockItemWithoutAuthorAndDate) }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemSearchResultListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(ItemSearchResultListElementComponent);
itemSearchResultListElementComponent = fixture.componentInstance;
}));
describe('When the item has an author', () => {
beforeEach(() => {
itemSearchResultListElementComponent.dso = mockItemWithAuthorAndDate.dspaceObject;
fixture.detectChanges();
});
it('should show the author paragraph', () => {
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-authors'));
expect(itemAuthorField).not.toBeNull();
});
});
describe('When the item has no author', () => {
beforeEach(() => {
itemSearchResultListElementComponent.dso = mockItemWithoutAuthorAndDate.dspaceObject;
fixture.detectChanges();
});
it('should not show the author paragraph', () => {
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-authors'));
expect(itemAuthorField).toBeNull();
});
});
describe('When the item has an issuedate', () => {
beforeEach(() => {
itemSearchResultListElementComponent.dso = mockItemWithAuthorAndDate.dspaceObject;
fixture.detectChanges();
});
it('should show the issuedate span', () => {
const itemAuthorField = fixture.debugElement.query(By.css('span.item-list-date'));
expect(itemAuthorField).not.toBeNull();
});
});
describe('When the item has no issuedate', () => {
beforeEach(() => {
itemSearchResultListElementComponent.dso = mockItemWithoutAuthorAndDate.dspaceObject;
fixture.detectChanges();
});
it('should not show the issuedate span', () => {
const dateField = fixture.debugElement.query(By.css('span.item-list-date'));
expect(dateField).toBeNull();
});
});
});

View File

@@ -1,16 +1,21 @@
import { Component } from '@angular/core';
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core';
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
import { SearchResultListElementComponent } from '../search-result-list-element.component';
import { Item } from '../../../../core/shared/item.model';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
import { ViewMode } from '../../../../+search-page/search-options.model';
import { ListableObject } from '../../../object-collection/shared/listable-object.model';
import { focusBackground } from '../../../animations/focus';
@Component({
selector: 'ds-item-search-result-list-element',
styleUrls: ['../search-result-list-element.component.scss', 'item-search-result-list-element.component.scss'],
templateUrl: 'item-search-result-list-element.component.html'
templateUrl: 'item-search-result-list-element.component.html',
animations: [focusBackground],
})
@renderElementsFor(ItemSearchResult, ViewMode.List)
export class ItemSearchResultListElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> {}
export class ItemSearchResultListElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> {
}

View File

@@ -6,6 +6,8 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
import { isEmpty, hasNoValue } from '../../empty.util';
import { ListableObject } from '../../object-collection/shared/listable-object.model';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { Observable } from 'rxjs/Observable';
import { TruncatableService } from '../../truncatable/truncatable.service';
@Component({
selector: 'ds-search-result-list-element',
@@ -15,7 +17,7 @@ import { AbstractListableElementComponent } from '../../object-collection/shared
export class SearchResultListElementComponent<T extends SearchResult<K>, K extends DSpaceObject> extends AbstractListableElementComponent<T> {
dso: K;
public constructor(@Inject('objectElementProvider') public listable: ListableObject) {
public constructor(@Inject('objectElementProvider') public listable: ListableObject, private truncatableService: TruncatableService) {
super(listable);
this.dso = this.object.dspaceObject;
}
@@ -54,4 +56,8 @@ export class SearchResultListElementComponent<T extends SearchResult<K>, K exten
}
return result;
}
isCollapsed(): Observable<boolean> {
return this.truncatableService.isCollapsed(this.dso.id);
}
}

View File

@@ -1,4 +1,4 @@
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row">
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row" action="/search">
<div *ngIf="isNotEmpty(scopes)" class="col-12 col-sm-3">
<select [(ngModel)]="selectedId" name="scope" class="form-control" aria-label="Search scope" [compareWith]="byId">
<option value>{{'search.form.search_dspace' | translate}}</option>
@@ -8,7 +8,7 @@
<div [ngClass]="{'col-sm-9': isNotEmpty(scopes)}" class="col-12">
<div class="form-group input-group">
<input type="text" [(ngModel)]="query" name="query" class="form-control" aria-label="Search input">
<span class="input-group-btn">
<span class="input-group-append">
<button type="submit" class="search-button btn btn-secondary">{{ ('search.form.search' | translate) }}</button>
</span>
</div>

View File

@@ -2,7 +2,6 @@ import { Component, Input } from '@angular/core';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Router } from '@angular/router';
import { isNotEmpty, hasValue, isEmpty } from '../empty.util';
import { Observable } from 'rxjs/Observable';
/**
* This component renders a simple item page.

View File

@@ -12,7 +12,6 @@ import { NgxPaginationModule } from 'ngx-pagination';
import { EnumKeysPipe } from './utils/enum-keys-pipe';
import { FileSizePipe } from './utils/file-size-pipe';
import { SafeUrlPipe } from './utils/safe-url-pipe';
import { TruncatePipe } from './utils/truncate.pipe';
import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component';
import { CommunityListElementComponent } from './object-list/community-list-element/community-list-element.component';
@@ -43,6 +42,11 @@ import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbn
import { VarDirective } from './utils/var.directive';
import { NotificationComponent } from './notifications/notification/notification.component';
import { NotificationsBoardComponent } from './notifications/notifications-board/notifications-board.component';
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
@@ -82,8 +86,8 @@ const COMPONENTS = [
GridThumbnailComponent,
WrapperListElementComponent,
ViewModeSwitchComponent,
// NotificationComponent,
// NotificationsBoardComponent
TruncatableComponent,
TruncatablePartComponent,
];
const ENTRY_COMPONENTS = [
@@ -96,11 +100,15 @@ const ENTRY_COMPONENTS = [
CollectionGridElementComponent,
CommunityGridElementComponent,
SearchResultGridElementComponent,
// NotificationComponent,
];
const PROVIDERS = [
TruncatableService
];
const DIRECTIVES = [
VarDirective
VarDirective,
DragClickDirective
];
@NgModule({
@@ -114,6 +122,9 @@ const DIRECTIVES = [
...ENTRY_COMPONENTS,
...DIRECTIVES
],
providers: [
...PROVIDERS
],
exports: [
...MODULES,
...PIPES,

View File

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

View File

@@ -0,0 +1,63 @@
@import '../../../../styles/variables';
@import '../../../../styles/mixins';
@mixin clamp($lines, $size-factor: 1, $line-height: $line-height-base) {
$height: $line-height * $font-size-base * $size-factor;
&.fixedHeight {
height: $lines * $height;
}
.content {
max-height: $lines * $height;
position: relative;
overflow: hidden;
line-height: $line-height;
overflow-wrap: break-word;
&:after {
content: "";
position: absolute;
padding-right: 15px;
top: ($lines - 1) * $height;
right: 0;
width: 30%;
min-width: 75px;
max-width: 150px;
height: $height;
background: linear-gradient(to right, rgba(255, 255, 255, 0), $body-bg 70%);
}
}
}
@mixin min($lines, $size-factor: 1, $line-height: $line-height-base) {
$height: $line-height * $font-size-base * $size-factor;
min-height: $lines * $height;
}
$h4-factor: strip-unit($h4-font-size);
@for $i from 1 through 15 {
.clamp-#{$i} {
transition: height 1s;
@include clamp($i);
&.title {
@include clamp($i, 1.25);
}
&.h4 {
@include clamp($i, $h4-factor, $headings-line-height);
}
}
}
.clamp-none {
overflow: hidden;
@for $i from 1 through 15 {
&.fixedHeight.min-#{$i} {
transition: height 1s;
@include min($i);
&.title {
@include min($i, 1.25);
}
&.h4 {
@include min($i, $h4-factor, $headings-line-height);
}
}
}
}

View File

@@ -0,0 +1,76 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { TruncatablePartComponent } from './truncatable-part.component';
import { TruncatableService } from '../truncatable.service';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('TruncatablePartComponent', () => {
let comp: TruncatablePartComponent;
let fixture: ComponentFixture<TruncatablePartComponent>;
const id1 = '123';
const id2 = '456';
let truncatableService;
const truncatableServiceStub: any = {
isCollapsed: (id: string) => {
if (id === id1) {
return Observable.of(true)
} else {
return Observable.of(false);
}
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule],
declarations: [TruncatablePartComponent],
providers: [
{ provide: TruncatableService, useValue: truncatableServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(TruncatablePartComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TruncatablePartComponent);
comp = fixture.componentInstance; // TruncatablePartComponent test instance
fixture.detectChanges();
truncatableService = (comp as any).filterService;
});
describe('When the item is collapsed', () => {
beforeEach(() => {
comp.id = id1;
comp.minLines = 5;
(comp as any).setLines();
fixture.detectChanges();
})
;
it('lines should equal minlines', () => {
expect((comp as any).lines).toEqual(comp.minLines.toString());
});
});
describe('When the item is expanded', () => {
beforeEach(() => {
comp.id = id2;
})
;
it('lines should equal maxlines when maxlines has a value', () => {
comp.maxLines = 5;
(comp as any).setLines();
fixture.detectChanges();
expect((comp as any).lines).toEqual(comp.maxLines.toString());
});
it('lines should equal \'none\' when maxlines has no value', () => {
(comp as any).setLines();
fixture.detectChanges();
expect((comp as any).lines).toEqual('none');
});
});
});

View File

@@ -0,0 +1,39 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { TruncatableService } from '../truncatable.service';
@Component({
selector: 'ds-truncatable-part',
templateUrl: './truncatable-part.component.html',
styleUrls: ['./truncatable-part.component.scss']
})
export class TruncatablePartComponent implements OnInit, OnDestroy {
@Input() minLines: number;
@Input() maxLines = -1;
@Input() id: string;
@Input() type: string;
@Input() fixedHeight = false;
lines: string;
private sub;
public constructor(private service: TruncatableService) {
}
ngOnInit() {
this.setLines();
}
private setLines() {
this.sub = this.service.isCollapsed(this.id).subscribe((collapsed: boolean) => {
if (collapsed) {
this.lines = this.minLines.toString();
} else {
this.lines = this.maxLines < 0 ? 'none' : this.maxLines.toString();
}
});
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
}

View File

@@ -0,0 +1,39 @@
import { Action } from '@ngrx/store';
import { type } from '../ngrx/type';
/**
* For each action type in an action group, make a simple
* enum object for all of this group's action types.
*
* The 'type' utility function coerces strings into string
* literal types and runs a simple check to guarantee all
* action types in the application are unique.
*/
export const TruncatableActionTypes = {
TOGGLE: type('dspace/truncatable/TOGGLE'),
COLLAPSE: type('dspace/truncatable/COLLAPSE'),
EXPAND: type('dspace/truncatable/EXPAND'),
};
export class TruncatableAction implements Action {
id: string;
type;
constructor(name: string) {
this.id = name;
}
}
/* tslint:disable:max-classes-per-file */
export class TruncatableToggleAction extends TruncatableAction {
type = TruncatableActionTypes.TOGGLE;
}
export class TruncatableCollapseAction extends TruncatableAction {
type = TruncatableActionTypes.COLLAPSE;
}
export class TruncatableExpandAction extends TruncatableAction {
type = TruncatableActionTypes.EXPAND;
}
/* tslint:enable:max-classes-per-file */

View File

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

View File

@@ -0,0 +1,101 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { TruncatableComponent } from './truncatable.component';
import { TruncatableService } from './truncatable.service';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('TruncatableComponent', () => {
let comp: TruncatableComponent;
let fixture: ComponentFixture<TruncatableComponent>;
const identifier = '1234567890';
let truncatableService;
const truncatableServiceStub: any = {
/* tslint:disable:no-empty */
isCollapsed: (id: string) => {
if (id === '1') {
return Observable.of(true)
} else {
return Observable.of(false);
}
},
expand: (id: string) => {
},
collapse: (id: string) => {
},
toggle: (id: string) => {
}
/* tslint:enable:no-empty */
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule],
declarations: [TruncatableComponent],
providers: [
{ provide: TruncatableService, useValue: truncatableServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(TruncatableComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TruncatableComponent);
comp = fixture.componentInstance; // TruncatableComponent test instance
comp.id = identifier;
fixture.detectChanges();
truncatableService = (comp as any).service;
});
describe('When the item is hoverable', () => {
beforeEach(() => {
comp.onHover = true;
fixture.detectChanges();
})
;
it('should call collapse on the TruncatableService', () => {
spyOn(truncatableService, 'collapse');
comp.hoverCollapse();
expect(truncatableService.collapse).toHaveBeenCalledWith(identifier);
});
it('should call expand on the TruncatableService', () => {
spyOn(truncatableService, 'expand');
comp.hoverExpand();
expect(truncatableService.expand).toHaveBeenCalledWith(identifier);
});
});
describe('When the item is not hoverable', () => {
beforeEach(() => {
comp.onHover = false;
fixture.detectChanges();
})
;
it('should not call collapse on the TruncatableService', () => {
spyOn(truncatableService, 'collapse');
comp.hoverCollapse();
expect(truncatableService.collapse).not.toHaveBeenCalled();
});
it('should not call expand on the TruncatableService', () => {
spyOn(truncatableService, 'expand');
comp.hoverExpand();
expect(truncatableService.expand).not.toHaveBeenCalled();
});
});
describe('When toggle is called', () => {
beforeEach(() => {
spyOn(truncatableService, 'toggle');
comp.toggle();
});
it('should call toggle on the TruncatableService', () => {
expect(truncatableService.toggle).toHaveBeenCalledWith(identifier);
});
});
});

View File

@@ -0,0 +1,44 @@
import {
Component, Input
} from '@angular/core';
import { TruncatableService } from './truncatable.service';
@Component({
selector: 'ds-truncatable',
templateUrl: './truncatable.component.html',
styleUrls: ['./truncatable.component.scss'],
})
export class TruncatableComponent {
@Input() initialExpand = false;
@Input() id: string;
@Input() onHover = false;
public constructor(private service: TruncatableService) {
}
ngOnInit() {
if (this.initialExpand) {
this.service.expand(this.id);
} else {
this.service.collapse(this.id);
}
}
public hoverCollapse() {
if (this.onHover) {
this.service.collapse(this.id);
}
}
public hoverExpand() {
if (this.onHover) {
this.service.expand(this.id);
}
}
public toggle() {
this.service.toggle(this.id);
}
}

View File

@@ -0,0 +1,96 @@
import * as deepFreeze from 'deep-freeze';
import { truncatableReducer } from './truncatable.reducer';
import {
TruncatableCollapseAction, TruncatableExpandAction,
TruncatableToggleAction
} from './truncatable.actions';
const id1 = '123';
const id2 = '456';
class NullAction extends TruncatableCollapseAction {
type = null;
constructor() {
super(undefined);
}
}
describe('truncatableReducer', () => {
it('should return the current state when no valid actions have been made', () => {
const state = { 123: { collapsed: true, page: 1 } };
const action = new NullAction();
const newState = truncatableReducer(state, action);
expect(newState).toEqual(state);
});
it('should start with an empty object', () => {
const state = Object.create({});
const action = new NullAction();
const initialState = truncatableReducer(undefined, action);
// The search filter starts collapsed
expect(initialState).toEqual(state);
});
it('should set collapsed to true in response to the COLLAPSE action', () => {
const state = {};
state[id1] = { collapsed: false};
const action = new TruncatableCollapseAction(id1);
const newState = truncatableReducer(state, action);
expect(newState[id1].collapsed).toEqual(true);
});
it('should perform the COLLAPSE action without affecting the previous state', () => {
const state = {};
state[id1] = { collapsed: false};
deepFreeze([state]);
const action = new TruncatableCollapseAction(id1);
truncatableReducer(state, action);
// no expect required, deepFreeze will ensure an exception is thrown if the state
// is mutated, and any uncaught exception will cause the test to fail
});
it('should set filterCollapsed to false in response to the EXPAND action', () => {
const state = {};
state[id1] = { collapsed: true };
const action = new TruncatableExpandAction(id1);
const newState = truncatableReducer(state, action);
expect(newState[id1].collapsed).toEqual(false);
});
it('should perform the EXPAND action without affecting the previous state', () => {
const state = {};
state[id1] = { collapsed: true };
deepFreeze([state]);
const action = new TruncatableExpandAction(id1);
truncatableReducer(state, action);
});
it('should flip the value of filterCollapsed in response to the TOGGLE action', () => {
const state1 = {};
state1[id1] = { collapsed: true };
const action = new TruncatableToggleAction(id1);
const state2 = truncatableReducer(state1, action);
const state3 = truncatableReducer(state2, action);
expect(state2[id1].collapsed).toEqual(false);
expect(state3[id1].collapsed).toEqual(true);
});
it('should perform the TOGGLE action without affecting the previous state', () => {
const state = {};
state[id2] = { collapsed: true };
deepFreeze([state]);
const action = new TruncatableToggleAction(id2);
truncatableReducer(state, action);
});
});

View File

@@ -0,0 +1,43 @@
import { TruncatableAction, TruncatableActionTypes } from './truncatable.actions';
export interface TruncatableState {
collapsed: boolean;
}
export interface TruncatablesState {
[id: string]: TruncatableState
}
const initialState: TruncatablesState = Object.create(null);
export function truncatableReducer(state = initialState, action: TruncatableAction): TruncatablesState {
switch (action.type) {
case TruncatableActionTypes.COLLAPSE: {
return Object.assign({}, state, {
[action.id]: {
collapsed: true,
}
});
} case TruncatableActionTypes.EXPAND: {
return Object.assign({}, state, {
[action.id]: {
collapsed: false,
}
});
} case TruncatableActionTypes.TOGGLE: {
if (!state[action.id]) {
state[action.id] = {collapsed: false};
}
return Object.assign({}, state, {
[action.id]: {
collapsed: !state[action.id].collapsed,
}
});
}
default: {
return state;
}
}
}

View File

@@ -0,0 +1,54 @@
import { Store } from '@ngrx/store';
import { async, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { TruncatableService } from './truncatable.service';
import { TruncatableCollapseAction, TruncatableExpandAction } from './truncatable.actions';
import { TruncatablesState } from './truncatable.reducer';
describe('TruncatableService', () => {
const id1 = '123';
const id2 = '456';
let service: TruncatableService;
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */
dispatch: {},
/* tslint:enable:no-empty */
select: Observable.of(true)
});
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
{
provide: Store, useValue: store
}
]
}).compileComponents();
}));
beforeEach(() => {
service = new TruncatableService(store);
});
describe('when the collapse method is triggered', () => {
beforeEach(() => {
service.collapse(id1);
});
it('TruncatableCollapseAction should be dispatched to the store', () => {
expect(store.dispatch).toHaveBeenCalledWith(new TruncatableCollapseAction(id1));
});
});
describe('when the expand method is triggered', () => {
beforeEach(() => {
service.expand(id2);
});
it('TruncatableExpandAction should be dispatched to the store', () => {
expect(store.dispatch).toHaveBeenCalledWith(new TruncatableExpandAction(id2));
});
});
});

View File

@@ -0,0 +1,52 @@
import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { TruncatablesState, TruncatableState } from './truncatable.reducer';
import { TruncatableExpandAction, TruncatableToggleAction, TruncatableCollapseAction } from './truncatable.actions';
import { hasValue } from '../empty.util';
const truncatableStateSelector = (state: TruncatablesState) => state.truncatable;
@Injectable()
export class TruncatableService {
constructor(private store: Store<TruncatablesState>) {
}
isCollapsed(id: string): Observable<boolean> {
return this.store.select(truncatableByIdSelector(id))
.map((object: TruncatableState) => {
if (object) {
return object.collapsed;
} else {
return false;
}
});
}
public toggle(id: string): void {
this.store.dispatch(new TruncatableToggleAction(id));
}
public collapse(id: string): void {
this.store.dispatch(new TruncatableCollapseAction(id));
}
public expand(id: string): void {
this.store.dispatch(new TruncatableExpandAction(id));
}
}
function truncatableByIdSelector(id: string): MemoizedSelector<TruncatablesState, TruncatableState> {
return keySelector<TruncatableState>(id);
}
export function keySelector<T>(key: string): MemoizedSelector<TruncatablesState, T> {
return createSelector(truncatableStateSelector, (state: TruncatableState) => {
if (hasValue(state)) {
return state[key];
} else {
return undefined;
}
});
}

View File

@@ -0,0 +1,23 @@
import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
@Directive({
selector: '[dsDragClick]'
})
export class DragClickDirective {
private start;
@Output() actualClick = new EventEmitter();
@HostListener('mousedown', ['$event'])
mousedownEvent(event) {
this.start = new Date();
}
@HostListener('mouseup', ['$event'])
mouseupEvent(event) {
const end: any = new Date();
const clickTime = end - this.start;
if (clickTime < 250) {
this.actualClick.emit(event)
}
}
}

View File

@@ -29,3 +29,8 @@ $theme-colors: (
/* Fonts */
$link-color: map-get($theme-colors, info) !default;
$navbar-dark-color: rgba(white, .5) !default;
$navbar-light-color: rgba(black, .5) !default;
$navbar-dark-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-dark-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E");
$navbar-light-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-light-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E");

View File

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

View File

@@ -1,5 +1,3 @@
@import '../../node_modules/bootstrap/scss/functions.scss';
@import '../../node_modules/bootstrap/scss/mixins.scss';
/* Custom mixins go here */
@import '../../node_modules/bootstrap/scss/variables.scss';

650
yarn.lock

File diff suppressed because it is too large Load Diff