Commit dd3f7c06 authored by Dan Sheppard's avatar Dan Sheppard
Browse files

Experimental merge of master onto vizbi branch prior to other direction.

Merge branch 'master' into vizbi
parents 1e66bcf5 6b75a0f6
image: docker:latest
services:
- docker:dind
before_script:
- docker info
test_job:
script:
- docker build -t my-docker-image src/ensembl
node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
FROM node:10.15.1
ARG SOURCE_DIR="./"
RUN mkdir -p /srv/ensembl-client
COPY ${SOURCE_DIR} /srv/ensembl-client/
# NOTE: NODE_ENV=development before npm install ensures that dev dependencies won't get skipped
RUN cd /srv/ensembl-client/ && \
NODE_ENV=development npm install && \
npm run test
......@@ -2,7 +2,6 @@ export default {
// environment
isDevelopment: process.env.NODE_ENV === 'development',
isProduction: process.env.NODE_ENV === 'production',
apiHost: process.env.API_HOST,
// keys for services
......
......@@ -48,6 +48,7 @@
"classnames": "2.2.6",
"dotenv": "6.2.0",
"foundation-sites": "6.5.3",
"lodash": "4.17.11",
"react": "16.8.1",
"react-cookie": "^3.0.8",
"react-dom": "16.8.1",
......@@ -80,6 +81,7 @@
"@types/enzyme": "3.1.17",
"@types/enzyme-adapter-react-16": "1.0.3",
"@types/jest": "23.3.14",
"@types/lodash": "4.14.122",
"@types/node": "10.12.24",
"@types/prettier": "1.16.0",
"@types/react": "16.8.2",
......
......@@ -44,6 +44,8 @@ import styles from './Browser.scss';
import 'static/browser/browser.js';
import { getChrLocationFromStr, getChrLocationStr } from './browserHelper';
import { replace } from 'connected-react-router';
type StateProps = {
browserActivated: boolean;
browserNavOpened: boolean;
......@@ -65,6 +67,7 @@ type DispatchProps = {
toggleDrawer: (drawerOpened: boolean) => void;
updateBrowserNavStates: (browserNavStates: BrowserNavStates) => void;
updateChrLocation: (chrLocation: ChrLocation) => void;
replace: (path: string) => void;
};
type OwnProps = {};
......@@ -171,7 +174,8 @@ const mapDispatchToProps: DispatchProps = {
replace,
toggleDrawer,
updateBrowserNavStates,
updateChrLocation
updateChrLocation,
replace
};
export default withRouter(
......
......@@ -4,15 +4,24 @@ import SpeciesSearchMatch from '../species-search-match/SpeciesSearchMatch';
import styles from './SpeciesAutosuggestionPanel.scss';
import { SearchMatches } from 'src/content/app/species-selector/types/species-search';
import { SearchMatch } from 'src/content/app/species-selector/types/species-search';
type Props = {
matches: SearchMatches;
matches: SearchMatch[];
onMatchSelected: (match: SearchMatch) => void;
};
const SpeciesAutosuggestionPanel = (props: Props) => {
const onMatchSelected = (match: SearchMatch) => () => {
props.onMatchSelected(match);
};
const matches = props.matches.map((match) => (
<SpeciesSearchMatch match={match} key={match.description} />
<SpeciesSearchMatch
match={match}
onClick={onMatchSelected(match)}
key={match.description}
/>
));
return <div className={styles.speciesAutosuggestionPanel}>{matches}</div>;
......
@import 'src/styles/common';
.speciesSearchMatch {
font-weight: bold;
color: $ens-black;
padding: 2px 24px;
margin: 0 -23px;
cursor: pointer;
&:hover {
background-color: $ens-light-blue;
}
}
.speciesSearchMatchMatched {
font-weight: $dark;
}
.speciesSearchMatchScientificName {
font-style: italic;
font-weight: $light;
margin-left: 1rem;
}
import React from 'react';
import { mount, render } from 'enzyme';
import SpeciesSearchMatch from './SpeciesSearchMatch';
import styles from './SpeciesSearchMatch.scss';
const onClick = jest.fn();
const matchTemplate = {
description: 'Human GRCh38.p12',
scientific_name: 'Homo sapiens',
matched_substrings: [
{
length: 3,
offset: 0,
match: 'description' as 'description'
}
],
genome: 'GRCh38_demo'
};
describe('<SpeciesSearchMatch />', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('highlights a single match in the description field', () => {
const renderedMatch = render(
<SpeciesSearchMatch match={matchTemplate} onClick={onClick} />
);
const highlightedFragments = renderedMatch.find(
`.${styles.speciesSearchMatchMatched}`
);
expect(highlightedFragments.length).toBe(1);
// highlighting Hum in Human
expect(highlightedFragments.first().text()).toBe('Hum');
});
test('highlights a single match in the scientific_name field', () => {
const match = {
...matchTemplate,
matched_substrings: [
{
length: 3,
offset: 0,
match: 'scientific_name' as 'scientific_name'
}
]
};
const renderedMatch = render(
<SpeciesSearchMatch match={match} onClick={onClick} />
);
const highlightedFragments = renderedMatch.find(
`.${styles.speciesSearchMatchScientificName}
.${styles.speciesSearchMatchMatched}`
);
expect(highlightedFragments.length).toBe(1);
// highlighting Hom in Homo sapiens
expect(highlightedFragments.first().text()).toBe('Hom');
});
test('highlights multiple matches in the description field', () => {
const match = {
...matchTemplate,
description: 'Bacillus subtilis',
matched_substrings: [
{
length: 3,
offset: 0,
match: 'description' as 'description'
},
{
length: 3,
offset: 9,
match: 'description' as 'description'
}
]
};
delete match.scientific_name;
const renderedMatch = render(
<SpeciesSearchMatch match={match} onClick={onClick} />
);
const highlightedFragments = renderedMatch.find(
`.${styles.speciesSearchMatchMatched}`
);
expect(highlightedFragments.length).toBe(2);
// highlighting Bac in Bacillus and sub in subtilis
expect(highlightedFragments.first().text()).toBe('Bac');
expect(highlightedFragments.last().text()).toBe('sub');
});
test('calls click handler when clicked', () => {
const renderedMatch = mount(
<SpeciesSearchMatch match={matchTemplate} onClick={onClick} />
);
renderedMatch.simulate('click');
expect(onClick).toHaveBeenCalled();
});
});
import React from 'react';
import classNames from 'classnames';
import zip from 'lodash/zip';
import sortBy from 'lodash/sortBy';
import { SearchMatch } from 'src/content/app/species-selector/types/species-search';
import {
SearchMatch,
MatchedSubstring
} from 'src/content/app/species-selector/types/species-search';
import styles from './SpeciesSearchMatch.scss';
type Props = {
match: SearchMatch;
onClick: () => void;
};
const SpeciesSearchMatch = ({ match }: Props) => {
return <div className={styles.speciesSearchMatch}>{match.description}</div>;
type SplitterProps = {
string: string;
matchedSubsctrings: MatchedSubstring[];
};
type FormatStringProps = {
string: string;
substrings: SplitSubstring[];
};
type SplitSubstring = {
start: number;
end: number;
isMatch: boolean;
};
type NumberTuple = [number, number];
const SpeciesSearchMatch = ({ match, onClick }: Props) => {
return (
<div className={styles.speciesSearchMatch} onClick={onClick}>
<CommonName match={match} />
<ScientificName match={match} />
</div>
);
};
const CommonName = ({ match }: { match: SearchMatch }) => {
const { description, matched_substrings } = match;
const descriptionMatches = matched_substrings.filter(
({ match }) => match === 'description'
);
const substrings = sortBy(
splitMatch({ string: description, matchedSubsctrings: descriptionMatches }),
({ start }) => start
);
return <span>{formatString({ string: description, substrings })}</span>;
};
const ScientificName = ({ match }: { match: SearchMatch }) => {
const { scientific_name, matched_substrings } = match;
if (!scientific_name) return null;
const scientificNameMatches = matched_substrings.filter(
({ match }) => match === 'scientific_name'
);
const substrings = sortBy(
splitMatch({
string: scientific_name,
matchedSubsctrings: scientificNameMatches
}),
({ start }) => start
);
return (
<span className={styles.speciesSearchMatchScientificName}>
{formatString({ string: scientific_name, substrings })}
</span>
);
};
const formatString = ({ string, substrings }: FormatStringProps) =>
substrings.length
? substrings.map(({ start, end, isMatch }) => (
<span
className={classNames({
[styles.speciesSearchMatchMatched]: isMatch
})}
key={`${start}-${end}`}
>
{string.substring(start, end)}
</span>
))
: string;
const splitMatch = ({ string, matchedSubsctrings }: SplitterProps) => {
const matchStartIndices = matchedSubsctrings.map(({ offset }) => offset);
const matchEndIndices = matchedSubsctrings.map(
({ offset, length }) => offset + length
);
const matchIndices = zip(matchStartIndices, matchEndIndices) as NumberTuple[];
const accumulator: SplitSubstring[] = [];
return matchIndices.reduce((result, current, index, array) => {
const [currentStartIndex, currentEndIndex] = current; // notice, currentEndIndex is exclusive
const nextStartIndex =
index < array.length - 1 ? array[index + 1][0] : null;
if (index === 0 && current[0] > 0) {
// if there is an unmatched part of the string before the first match,
// add it as the first item in the list of substrings
result = [
{
start: 0,
end: currentStartIndex,
isMatch: false
}
];
}
// add the matched substring
result = [
...result,
{
start: currentStartIndex,
end: currentEndIndex,
isMatch: true
}
];
if (nextStartIndex) {
// if there is another match in the same string, add the unmatched portion between
// the current and the next match
result = [
...result,
{
start: currentEndIndex,
end: nextStartIndex,
isMatch: false
}
];
} else if (
index === array.length - 1 &&
currentEndIndex < string.length - 1
) {
// if there is unmatched trailing portion of the string, add it to the list of substrings
result = [
...result,
{
start: currentEndIndex,
end: string.length,
isMatch: false
}
];
}
return result;
}, accumulator);
};
export default SpeciesSearchMatch;
......@@ -111,6 +111,7 @@ $ens-dark-grey: #6f8190;
$ens-white: #fff;
/* stylelint-disable */
:export {
ens-black: $ens-black;
ens-blue: $ens-blue;
......@@ -121,6 +122,7 @@ $ens-white: #fff;
ens-light-grey: $ens-light-grey;
ens-white: $ens-white;
}
/* stylelint-enable */
$img-base-url: '/static/img';
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment