Unverified Commit 6bfc11aa authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Remove AssemblySelector from Species Selector (#471)

Plus, remove code responsible for selecting assemblies in SpeciesSelector
parent 0bf38aae
Pipeline #137418 passed with stages
in 6 minutes and 24 seconds
@import 'src/styles/common';
.assemblySelectorLabel {
color: $grey;
margin-right: 0.5em;
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { mount } from 'enzyme';
import faker from 'faker';
import times from 'lodash/times';
import random from 'lodash/random';
import sample from 'lodash/sample';
import { AssemblySelector } from './AssemblySelector';
import Select from 'src/shared/components/select/Select';
import { Assembly } from 'src/content/app/species-selector/types/species-search';
const createAssembly = () => ({
genome_id: faker.random.uuid(),
assembly_name: faker.lorem.words()
});
const assemblies = times(5, () => createAssembly());
const onSelect = jest.fn();
const defaultProps = {
genomeId: assemblies[0].genome_id,
assemblies,
onSelect
};
describe('<AssemblySelector />', () => {
it('does not render anything if genomeId is null', () => {
const props = { ...defaultProps, genomeId: null };
const wrapper = mount(<AssemblySelector {...props} />);
expect(wrapper.isEmptyRender()).toBe(true);
});
it('does not render anything if assemblies array is empty', () => {
const props = { ...defaultProps, assemblies: [] };
const wrapper = mount(<AssemblySelector {...props} />);
expect(wrapper.isEmptyRender()).toBe(true);
});
it('renders only the name of the selected assembly if it is the only one', () => {
const assemblies = defaultProps.assemblies.slice(0, 1);
const props = { ...defaultProps, assemblies };
const wrapper = mount(<AssemblySelector {...props} />);
expect(wrapper.find(Select).length).toBe(0);
expect(wrapper.text()).toMatch(assemblies[0].assembly_name);
});
it('renders Select component if multiple assemblies are available', () => {
const wrapper = mount(<AssemblySelector {...defaultProps} />);
const selectElement = wrapper.find(Select);
expect(selectElement.length).toBe(1);
const options: any = selectElement.prop('options');
expect(options.length).toEqual(defaultProps.assemblies.length);
});
it('correctly identifies selected assembly when rendering Select component', () => {
// Selected assembly is the one whose genome id corresponds
// to the genomeId prop passed to the AssemblySelector.
const selectedAssembly = sample(defaultProps.assemblies) as Assembly;
const selectedAssemblyIndex = defaultProps.assemblies.findIndex(
({ genome_id }) => genome_id === selectedAssembly.genome_id
);
const props = { ...defaultProps, genomeId: selectedAssembly.genome_id };
const wrapper = mount(<AssemblySelector {...props} />);
const selectElement = wrapper.find(Select);
const options: any = selectElement.prop('options');
const selectedOption = options.find(
({ isSelected }: { isSelected: boolean }) => isSelected
);
const selectedOptionIndex = options.findIndex(
({ isSelected }: { isSelected: boolean }) => isSelected
);
// the option presented as selected to the Select element
// is indeed the selected assembly
expect(selectedOptionIndex).toBe(selectedAssemblyIndex);
// the value of the selected option is the index of the selected assembly
// in the assemblies array passed to AssemblySelector
expect(selectedOption.value).toBe(selectedOptionIndex);
});
it('calls the onSelect prop passing to it the selected assembly', () => {
const wrapper = mount(<AssemblySelector {...defaultProps} />);
const selectElement = wrapper.find(Select);
const selectHandler = selectElement.prop('onSelect');
const assemblyIndex = random(0, assemblies.length - 1);
const randomAssembly = assemblies[assemblyIndex];
selectHandler(assemblyIndex);
expect(onSelect.mock.calls[0][0]).toEqual(randomAssembly);
});
});
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { connect } from 'react-redux';
import find from 'lodash/find';
import { changeAssembly } from 'src/content/app/species-selector/state/speciesSelectorActions';
import {
getCurrentSpeciesGenomeId,
getCurrentSpeciesAssemblies
} from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import Select from 'src/shared/components/select/Select';
import { RootState } from 'src/store';
import { Assembly } from 'src/content/app/species-selector/types/species-search';
import styles from './AssemblySelector.scss';
type Props = {
genomeId: string | null; // id of selected species; will correspond to genome_id field of an Assembly
assemblies: Assembly[];
onSelect: (assembly: Assembly) => void;
};
const label = <span className={styles.assemblySelectorLabel}>Assembly</span>;
export const AssemblySelector = (props: Props) => {
const selectedAssembly = find(
props.assemblies,
(assembly) => assembly.genome_id === props.genomeId
);
const handleSelect = (index: number) => {
props.onSelect(props.assemblies[index]);
};
if (!props.genomeId || !selectedAssembly) {
// this branch covers the case when there are no items in props.assemblies
return null;
} else if (props.assemblies.length === 1) {
// if there's just one assembly, there is no point in showing the select element
return (
<>
{label}
<span>{selectedAssembly.assembly_name}</span>
</>
);
} else {
// more than one item in props.assemblies
const options = props.assemblies.map((assembly, index) => ({
value: index,
label: assembly.assembly_name,
isSelected: props.genomeId === assembly.genome_id
}));
return (
<>
{label}
<Select options={options} onSelect={handleSelect} />
</>
);
}
};
const mapStateToProps = (state: RootState) => ({
genomeId: getCurrentSpeciesGenomeId(state),
assemblies: getCurrentSpeciesAssemblies(state)
});
const mapDispatchToProps = {
onSelect: changeAssembly
};
export default connect(mapStateToProps, mapDispatchToProps)(AssemblySelector);
......@@ -15,6 +15,11 @@
margin-left: -22px;
}
.assemblyWrapper {
.selectedAssembly {
margin-left: 1em;
}
.selectedAssemblyLabel {
color: $grey;
margin-right: 0.5em;
}
......@@ -15,10 +15,12 @@
*/
import React from 'react';
import { useSelector } from 'react-redux';
import { getSelectedItem } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import SpeciesSearchField from 'src/content/app/species-selector/components/species-search-field/SpeciesSearchField';
import SpeciesCommitButton from 'src/content/app/species-selector/components/species-commit-button/SpeciesCommitButton';
import AssemblySelector from 'src/content/app/species-selector/components/assembly-selector/AssemblySelector';
import styles from './SpeciesSearchPanel.scss';
......@@ -29,11 +31,25 @@ const SearchPanel = () => {
<SpeciesSearchField />
<SpeciesCommitButton />
</div>
<div className={styles.assemblyWrapper}>
<AssemblySelector />
</div>
<SelectedAssembly />
</section>
);
};
const SelectedAssembly = () => {
const selectedSpecies = useSelector(getSelectedItem);
const selectedAssembly = selectedSpecies?.assembly_name;
if (!selectedAssembly) {
return null;
}
return (
<div className={styles.selectedAssembly}>
<span className={styles.selectedAssemblyLabel}>Assembly</span>
<span>{selectedAssembly}</span>
</div>
);
};
export default SearchPanel;
......@@ -18,13 +18,11 @@ import { createAsyncAction, createAction } from 'typesafe-actions';
import { ActionCreator, Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
import find from 'lodash/find';
import get from 'lodash/get';
import pickBy from 'lodash/pickBy';
import apiService from 'src/services/api-service';
import speciesSelectorStorageService from 'src/content/app/species-selector/services/species-selector-storage-service';
import analyticsTracking from 'src/services/analytics-service';
import buildAnalyticsObject from 'src/analyticsHelper';
import { deleteSpeciesInGenomeBrowser } from 'src/content/app/browser/browserActions';
import { deleteSpeciesInEntityViewer } from 'src/content/app/entity-viewer/state/general/entityViewerGeneralActions';
......@@ -40,7 +38,6 @@ import {
SearchMatch,
SearchMatches,
// Strain,
Assembly,
PopularSpecies,
CommittedItem
} from 'src/content/app/species-selector/types/species-search';
......@@ -54,18 +51,14 @@ const buildCommittedItem = (data: CurrentItem): CommittedItem => ({
reference_genome_id: data.reference_genome_id,
common_name: data.common_name,
scientific_name: data.scientific_name,
assembly_name: get(
find(data.assemblies, ({ genome_id }) => genome_id === data.genome_id),
'assembly_name'
) as string,
assembly_name: data.assembly_name as string,
isEnabled: true
});
enum categories {
POPULAR_SPECIES = 'popular_species',
ADD_SPECIES = 'add_species',
SELECTED_SPECIES = 'selected_Species',
ASSEMBLY_SELECTOR = 'assembly_selector'
SELECTED_SPECIES = 'selected_Species'
}
import { MINIMUM_SEARCH_LENGTH } from 'src/content/app/species-selector/constants/speciesSelectorConstants';
......@@ -121,12 +114,6 @@ export const fetchPopularSpeciesAsyncActions = createAsyncAction(
'species_selector/popular_species_failure'
)<undefined, { popularSpecies: PopularSpecies[] }, Error>();
export const fetchAssembliesAsyncActions = createAsyncAction(
'species_selector/assemblies_request',
'species_selector/assemblies_success',
'species_selector/assemblies_failure'
)<undefined, { assemblies: Assembly[] }, Error>();
export const setSelectedSpecies = createAction(
'species_selector/species_selected'
)<SearchMatch | PopularSpecies>();
......@@ -200,28 +187,6 @@ export const ensureSpeciesIsEnabled: ActionCreator<ThunkAction<
dispatch(toggleSpeciesUseAndSave(genomeId));
};
export const fetchAssemblies: ActionCreator<ThunkAction<
void,
any,
null,
Action<string>
>> = (genomeId: string) => async (dispatch) => {
try {
dispatch(fetchAssembliesAsyncActions.request());
const url = `/api/genomesearch/alternative_assemblies?genome_id=${genomeId}`;
const response = await apiService.fetch(url, { preserveEndpoint: true });
dispatch(
fetchAssembliesAsyncActions.success({
assemblies: response.alternative_assemblies
})
);
} catch (error) {
dispatch(fetchAssembliesAsyncActions.failure(error));
}
};
export const fetchPopularSpecies: ActionCreator<ThunkAction<
void,
any,
......@@ -251,11 +216,9 @@ export const handleSelectedSpecies: ActionCreator<ThunkAction<
Action<string>
>> = (item: SearchMatch | PopularSpecies) => (dispatch) => {
dispatch(setSelectedSpecies(item));
const { genome_id } = item;
// TODO: fetch strains when they are ready
// dispatch(fetchStrains(genome_id));
dispatch(fetchAssemblies(genome_id));
};
export const updateCommittedSpecies = createAction(
......@@ -356,14 +319,3 @@ export const deleteSpeciesAndSave = (
dispatch(deleteSpeciesInEntityViewer(genomeId));
speciesSelectorStorageService.saveSelectedSpecies(updatedCommittedSpecies);
};
export const changeAssembly = createAction(
'species_selector/change_assembly',
(assembly: Assembly) => assembly,
(assembly: Assembly) =>
buildAnalyticsObject({
category: categories.ASSEMBLY_SELECTOR,
label: assembly.assembly_name,
action: 'select'
})
)();
......@@ -25,8 +25,7 @@ import initialState, {
import {
SearchMatch,
PopularSpecies,
Assembly
PopularSpecies
} from 'src/content/app/species-selector/types/species-search';
// NOTE: CurrentItem can be built from a search match or from a popular species
......@@ -38,16 +37,10 @@ const buildCurrentItem = (data: SearchMatch | PopularSpecies): CurrentItem => {
scientific_name: data.scientific_name,
assembly_name: data.assembly_name,
selectedStrainId: null,
strains: [],
assemblies: [buildAssembly(data)]
strains: []
};
};
const buildAssembly = (data: SearchMatch | PopularSpecies): Assembly => ({
genome_id: data.genome_id,
assembly_name: data.assembly_name
});
export default function speciesSelectorReducer(
state: SpeciesSelectorState = initialState,
action: ActionType<typeof speciesSelectorActions>
......@@ -87,25 +80,6 @@ export default function speciesSelectorReducer(
// : null
// }
// };
case getType(speciesSelectorActions.fetchAssembliesAsyncActions.success):
return {
...state,
currentItem: {
...(state.currentItem as CurrentItem),
assemblies: [
...(state.currentItem as CurrentItem).assemblies,
...action.payload.assemblies
]
}
};
case getType(speciesSelectorActions.changeAssembly):
return {
...state,
currentItem: {
...(state.currentItem as CurrentItem),
...action.payload
}
};
case getType(
speciesSelectorActions.fetchPopularSpeciesAsyncActions.success
):
......
......@@ -47,32 +47,12 @@ export const getCurrentSpeciesGenomeId = (state: RootState) => {
return get(state, 'speciesSelector.currentItem.genome_id', null);
};
export const getCurrentSpeciesStrains = (state: RootState) => {
return get(state, 'speciesSelector.currentItem.strains', []);
};
export const getCurrentSpeciesAssemblies = (state: RootState) => {
return get(state, 'speciesSelector.currentItem.assemblies', []);
};
export const isSelectingStrain = (state: RootState) => {
return state.speciesSelector.ui.isSelectingStrain;
};
export const isSelectingAssembly = (state: RootState) => {
return state.speciesSelector.ui.isSelectingAssembly;
};
export const hasCurrentSpecies = (state: RootState) => {
return Boolean(state.speciesSelector.currentItem);
};
export const canCommitSpecies = (state: RootState) => {
return (
hasCurrentSpecies(state) &&
!isSelectingStrain(state) &&
!isSelectingAssembly(state)
);
return hasCurrentSpecies(state);
};
export const getCommittedSpecies = (state: RootState): CommittedItem[] => {
......
......@@ -20,7 +20,6 @@ import { LoadingState } from 'src/shared/types/loading-state';
import {
SearchMatches,
Strain,
Assembly,
CommittedItem,
PopularSpecies
} from 'src/content/app/species-selector/types/species-search';
......@@ -33,7 +32,6 @@ export type CurrentItem = {
assembly_name: string | null; // name of the selected assembly
selectedStrainId: string | null; // genome_id of selected strain
strains: Strain[];
assemblies: Assembly[];
};
export type SpeciesSelectorState = {
......@@ -42,7 +40,6 @@ export type SpeciesSelectorState = {
};
ui: {
isSelectingStrain: boolean;
isSelectingAssembly: boolean;
};
search: {
text: string;
......@@ -60,8 +57,7 @@ const initialState: SpeciesSelectorState = {
search: LoadingState.NOT_REQUESTED
},
ui: {
isSelectingStrain: false,
isSelectingAssembly: false
isSelectingStrain: false
},
search: {
text: '',
......
......@@ -49,11 +49,6 @@ export type Strain = {
display_name: string;
};
export type Assembly = {
genome_id: string;
assembly_name: string;
};
export type CommittedItem = {
genome_id: string;
reference_genome_id: string | null; // because in Species Selector we need to keep track of how many strains of the same species are selected
......
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