Unverified Commit 3bf073a6 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub

Add use / don't use / remove functionality to species page (#347)

parent 4bc19889
Pipeline #99084 passed with stages
in 8 minutes and 29 seconds
......@@ -279,12 +279,9 @@ export const commitSelectedSpeciesAndSave: ActionCreator<ThunkAction<
speciesSelectorStorageService.saveSelectedSpecies(newCommittedSpecies);
};
export const toggleSpeciesUseAndSave: ActionCreator<ThunkAction<
void,
any,
null,
Action<string>
>> = (genomeId: string) => (dispatch, getState) => {
export const toggleSpeciesUseAndSave = (
genomeId: string
): ThunkAction<void, any, null, Action<string>> => (dispatch, getState) => {
const state = getState();
const committedSpecies = getCommittedSpecies(state);
const currentSpecies = getCommittedSpeciesById(state, genomeId);
......@@ -313,12 +310,9 @@ export const toggleSpeciesUseAndSave: ActionCreator<ThunkAction<
speciesSelectorStorageService.saveSelectedSpecies(updatedCommittedSpecies);
};
export const deleteSpeciesAndSave: ActionCreator<ThunkAction<
void,
any,
null,
Action<string>
>> = (genomeId: string) => (dispatch, getState) => {
export const deleteSpeciesAndSave = (
genomeId: string
): ThunkAction<void, any, null, Action<string>> => (dispatch, getState) => {
const committedSpecies = getCommittedSpecies(getState());
const deletedSpecies = find(
committedSpecies,
......
......@@ -21,6 +21,8 @@ import { useSelector, useDispatch } from 'react-redux';
import { BreakpointWidth } from 'src/global/globalConfig';
import { fetchGenomeData } from 'src/shared/state/genome/genomeActions';
import { setActiveGenomeId } from 'src/content/app/species/state/general/speciesGeneralSlice';
import { getCommittedSpeciesById } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import { isSidebarOpen } from 'src/content/app/species/state/sidebar/speciesSidebarSelectors';
......@@ -31,6 +33,7 @@ import {
StandardAppLayout,
SidebarBehaviourType
} from 'src/shared/components/layout';
import SpeciesMainView from 'src/content/app/species/components/species-main-view/SpeciesMainView';
import { RootState } from 'src/store';
......@@ -46,13 +49,16 @@ const SpeciesPage = () => {
const sidebarStatus = useSelector(isSidebarOpen);
const dispatch = useDispatch();
useEffect(() => {
dispatch(setActiveGenomeId(genomeId));
}, [genomeId]);
useEffect(() => {
if (!currentSpecies) {
dispatch(fetchGenomeData(genomeId));
}
}, [genomeId, currentSpecies]);
const mainContent = 'I am main content';
const sidebarContent = 'I am sidebar';
const sidebarNavigationContent = 'I am sidebar navigation';
const topbarContent = 'I am topbar content';
......@@ -61,7 +67,7 @@ const SpeciesPage = () => {
<>
<SpeciesAppBar />
<StandardAppLayout
mainContent={mainContent}
mainContent={<SpeciesMainView />}
sidebarContent={sidebarContent}
sidebarNavigation={sidebarNavigationContent}
topbarContent={topbarContent}
......
.speciesMainViewTop {
display: grid;
grid-template-columns: max-content auto;
align-items: center;
padding-top: 20px;
}
.speciesLabelBlock {
padding: 0 75px 0 60px;
}
/**
* 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 SpeciesMainViewTop from './SpeciesMainViewTop';
const SpeciesMainView = () => {
return (
<div>
<SpeciesMainViewTop />
</div>
);
};
export default SpeciesMainView;
/**
* 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 SpeciesSelectionControls from 'src/content/app/species/components/species-selection-controls/SpeciesSelectionControls';
import styles from './SpeciesMainView.scss';
const SpeciesMainViewTop = () => {
const mockSpeciesIcon = (
<div
style={{
height: '57px',
width: '57px',
background: '#d4d9de',
display: 'inline-block',
verticalAlign: 'middle',
marginRight: '18px'
}}
/>
);
return (
<div className={styles.speciesMainViewTop}>
<div className={styles.speciesLabelBlock}>
{mockSpeciesIcon}
Species name
</div>
<SpeciesSelectionControls />
</div>
);
};
export default SpeciesMainViewTop;
@import 'src/styles/common';
.speciesSelectionControls {
display: flex;
align-items: center;
font-size: 12px;
line-height: 1;
}
.speciesUseToggle {
display: flex;
align-items: center;
span {
white-space: nowrap;
}
span:last-of-type {
margin-right: 15px;
}
}
.toggle {
margin: 0 12px;
}
.removalContainer {
margin-left: 60px;
}
.clickable {
color: $blue;
cursor: pointer;
}
.speciesRemovalConfirmation {
button {
margin: 0 25px 0 35px;
}
}
.speciesRemovalWarning {
color: $red;
}
/**
* 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 { Provider } from 'react-redux';
import { push } from 'connected-react-router';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import * as urlFor from 'src/shared/helpers/urlHelper';
import {
toggleSpeciesUseAndSave,
deleteSpeciesAndSave
} from 'src/content/app/species-selector/state/speciesSelectorActions';
import { createSelectedSpecies } from 'tests/fixtures/selected-species';
import SpeciesSelectionControls, {
speciesRemovalConfirmationMessage
} from './SpeciesSelectionControls';
import SlideToggle from 'src/shared/components/slide-toggle/SlideToggle';
jest.mock('connected-react-router', () => ({
push: jest.fn(() => ({ type: 'push' }))
}));
jest.mock(
'src/content/app/species-selector/state/speciesSelectorActions',
() => ({
deleteSpeciesAndSave: jest.fn(() => ({ type: 'deleteSpeciesAndSave' })),
toggleSpeciesUseAndSave: jest.fn(() => ({
type: 'toggleSpeciesUseAndSave'
}))
})
);
const selectedSpecies = createSelectedSpecies();
const disabledSpecies = {
...selectedSpecies,
isEnabled: false
};
const stateWithEnabledSpecies = {
speciesPage: {
general: {
activeGenomeId: selectedSpecies.genome_id
}
},
speciesSelector: {
committedItems: [selectedSpecies]
}
};
const stateWithDisabledSpecies = {
...stateWithEnabledSpecies,
speciesSelector: {
committedItems: [disabledSpecies]
}
};
const mockStore = configureMockStore([thunk]);
const wrapInRedux = (
state: typeof stateWithEnabledSpecies = stateWithEnabledSpecies
) => {
return mount(
<Provider store={mockStore(state)}>
<SpeciesSelectionControls />
</Provider>
);
};
describe('SpeciesSelectionControls', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('shows correct controls for enabled species', () => {
const wrapper = wrapInRedux();
const slideToggle = wrapper.find(SlideToggle);
const useLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === 'Use')
.first();
const doNotUseLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === "Don't use")
.first();
expect(slideToggle.prop('isOn')).toBe(true);
expect(useLabel.hasClass('clickable')).toBe(false);
expect(doNotUseLabel.hasClass('clickable')).toBe(true);
});
it('shows correct controls for disabled species', () => {
const wrapper = wrapInRedux(stateWithDisabledSpecies);
const slideToggle = wrapper.find(SlideToggle);
const useLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === 'Use')
.first();
const doNotUseLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === "Don't use")
.first();
expect(slideToggle.prop('isOn')).toBe(false);
expect(useLabel.hasClass('clickable')).toBe(true);
expect(doNotUseLabel.hasClass('clickable')).toBe(false);
});
it('changes species status via the toggle', () => {
const wrapper = wrapInRedux(stateWithDisabledSpecies);
const slideToggle = wrapper.find(SlideToggle);
slideToggle.prop('onChange')(true);
expect(toggleSpeciesUseAndSave).toHaveBeenCalledWith(
disabledSpecies.genome_id
);
jest.clearAllMocks();
slideToggle.prop('onChange')(false);
expect(toggleSpeciesUseAndSave).toHaveBeenCalledWith(
disabledSpecies.genome_id
);
});
it('disables species by clicking on label', () => {
const wrapper = wrapInRedux();
const doNotUseLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === "Don't use")
.first();
doNotUseLabel.simulate('click');
expect(toggleSpeciesUseAndSave).toHaveBeenCalledWith(
selectedSpecies.genome_id
);
});
it('enables species by clicking on label', () => {
const wrapper = wrapInRedux(stateWithDisabledSpecies);
const useLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === 'Use')
.first();
useLabel.simulate('click');
expect(toggleSpeciesUseAndSave).toHaveBeenCalledWith(
selectedSpecies.genome_id
);
});
it('correctly toggles removal dialog', () => {
const wrapper = wrapInRedux();
const removeLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === 'Remove')
.first();
removeLabel.simulate('click');
expect(wrapper.text()).toContain(speciesRemovalConfirmationMessage);
const doNotRemoveLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === 'Do not remove')
.first();
doNotRemoveLabel.simulate('click');
expect(wrapper.text()).not.toContain(speciesRemovalConfirmationMessage);
});
it('removes species and redirects to species selector after removal', () => {
const wrapper = wrapInRedux();
// open removal confitmation dialog
const removeLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === 'Remove')
.first();
removeLabel.simulate('click');
const removeButton = wrapper.find('button.primaryButton');
removeButton.simulate('click');
expect(deleteSpeciesAndSave).toHaveBeenCalledWith(
selectedSpecies.genome_id
);
expect(push).toHaveBeenCalledWith(urlFor.speciesSelector());
});
});
/**
* 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, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { push } from 'connected-react-router';
import classNames from 'classnames';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { getActiveGenomeId } from 'src/content/app/species/state/general/speciesGeneralSelectors';
import { getCommittedSpeciesById } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import {
toggleSpeciesUseAndSave,
deleteSpeciesAndSave
} from 'src/content/app/species-selector/state/speciesSelectorActions';
import SlideToggle from 'src/shared/components/slide-toggle/SlideToggle';
import { PrimaryButton } from 'src/shared/components/button/Button';
import QuestionButton from 'src/shared/components/question-button/QuestionButton';
import { RootState } from 'src/store';
import styles from './SpeciesSelectionControls.scss';
type SpeciesUseToggle = {
isUsed: boolean;
onChange: (isUsed: boolean) => void;
};
type LabelProps = {
className?: string;
onClick?: () => void;
};
type SpeciesRemovalConfirmationProps = {
onConfirm: () => void;
onReject: () => void;
};
const SpeciesSelectionControls = () => {
const [isRemoving, setIsRemoving] = useState(false);
const genomeId = useSelector(getActiveGenomeId);
const species = useSelector((state: RootState) =>
getCommittedSpeciesById(state, genomeId || '')
);
const dispatch = useDispatch();
if (!genomeId || !species) {
return null;
}
const onToggleUse = () => {
dispatch(toggleSpeciesUseAndSave(genomeId));
};
const toggleRemovalDialog = () => {
setIsRemoving(!isRemoving);
};
const onRemove = () => {
dispatch(push(urlFor.speciesSelector()));
dispatch(deleteSpeciesAndSave(genomeId));
};
const removeLabelStyles = classNames(styles.remove, styles.clickable);
return (
<div className={styles.speciesSelectionControls}>
<SpeciesUseToggle isUsed={species.isEnabled} onChange={onToggleUse} />
<div className={styles.removalContainer}>
{isRemoving ? (
<SpeciesRemovalConfirmation
onConfirm={onRemove}
onReject={toggleRemovalDialog}
/>
) : (
<span className={removeLabelStyles} onClick={toggleRemovalDialog}>
Remove
</span>
)}
</div>
</div>
);
};
const speciesUseToggleHelpMessage = `When 'Use' is selected, this species will appear in the species list in all apps.
'Don't use' will disable this species in other apps, but will not remove it from your list in Species selector.`;
const SpeciesUseToggle = (props: SpeciesUseToggle) => {
const doNotUseLabelProps: LabelProps = {};
const useLabelProps: LabelProps = {};
if (props.isUsed) {
doNotUseLabelProps.className = styles.clickable;
doNotUseLabelProps.onClick = () => props.onChange(!props.isUsed);
} else {
useLabelProps.className = styles.clickable;
useLabelProps.onClick = () => props.onChange(!props.isUsed);
}
return (
<div className={styles.speciesUseToggle}>
<span {...doNotUseLabelProps}>Don't use</span>
<SlideToggle
className={styles.toggle}
isOn={props.isUsed}
onChange={props.onChange}
/>
<span {...useLabelProps}>Use</span>
<QuestionButton helpText={speciesUseToggleHelpMessage} />
</div>
);
};
export const speciesRemovalConfirmationMessage =
'If you remove this species, any views you have configured will be lost — do you wish to continue?';
const SpeciesRemovalConfirmation = (props: SpeciesRemovalConfirmationProps) => {
return (
<div className={styles.speciesRemovalConfirmation}>
<span className={styles.speciesRemovalWarning}>
{speciesRemovalConfirmationMessage}
</span>
<PrimaryButton onClick={props.onConfirm}>Remove</PrimaryButton>
<span className={styles.clickable} onClick={props.onReject}>
Do not remove
</span>
</div>
);
};
export default SpeciesSelectionControls;
/**
* 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.
*/