Unverified Commit c981fe33 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub

Add SpeciesPageTitleArea (#360)

parent f667b98c
Pipeline #103164 passed with stages
in 8 minutes and 56 seconds
@mixin narrow-top-grid {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.speciesMainViewTop {
display: grid;
grid-template-columns: max-content auto;
align-items: center;
padding-top: 20px;
&Narrow {
@media (max-width: 1200px) {
@include narrow-top-grid;
}
}
}
.speciesLabelBlock {
......
......@@ -16,12 +16,12 @@
import React from 'react';
import SpeciesMainViewTop from './SpeciesMainViewTop';
import SpeciesTitleArea from 'src/content/app/species/components/species-title-area/SpeciesTitleArea';
const SpeciesMainView = () => {
return (
<div>
<SpeciesMainViewTop />
<SpeciesTitleArea />
</div>
);
};
......
/**
* 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';
$container-padding-right: 20px;
@mixin narrow-grid {
grid-template-areas:
'icon species-name species-name species-name'
'icon use-dont-use . remove';
grid-template-columns: 60px 150px 20px 1fr;
padding: 20px $container-padding-right 12px 0;
height: auto;
.speciesIcon {
align-self: start;
}
.speciesNameWrapper {
min-height: 60px;
display: flex;
align-items: center;
}
.speciesToggle {
align-self: start;
padding: 0;
}
.remove {
align-self: start;
}
}
.speciesTitleArea {
display: grid;
grid-template-areas:
'icon species-name usage-toggle remove';
grid-template-columns: 60px fit-content(60%) 200px minmax(280px, 1fr);
grid-column-gap: 20px;
align-items: center;
height: 90px;
margin-left: 60px;
padding-right: $container-padding-right;
@media (max-width: 999px) {
@include narrow-grid;
}
&Narrow {
@media (max-width: 1199px) {
@include narrow-grid;
}
}
}
.speciesIcon {
grid-area: icon;
width: 60px;
height: 60px;
border: 1px solid $blue;
padding: 6px;
[class*='text'] {
fill: white;
}
}
.speciesNameWrapper {
grid-area: species-name;
}
.speciesName {
display: inline;
font-size: 16px;
font-weight: $bold;
line-height: 1;
margin: 0 8px 0 0;
}
.assemblyName {
font-size: 11px;
line-height: 1;
}
.speciesToggle {
padding-left: 25px;
}
.speciesRemove {
grid-area: remove;
}
/**
* 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, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import classNames from 'classnames';
import { getDisplayName } from 'src/shared/components/new-selected-species/selectedSpeciesHelpers';
import { isSidebarOpen as getSidebarStatus } from 'src/content/app/species/state/sidebar/speciesSidebarSelectors';
import { getCommittedSpeciesById } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import { getActiveGenomeId } from 'src/content/app/species/state/general/speciesGeneralSelectors';
import { getPopularSpecies } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import { fetchPopularSpecies } from 'src/content/app/species-selector/state/speciesSelectorActions';
import SpeciesUsageToggle from './species-usage-toggle/SpeciesUsageToggle';
import SpeciesRemove from './species-remove/SpeciesRemove';
import InlineSVG from 'src/shared/components/inline-svg/InlineSvg';
import { RootState } from 'src/store';
import styles from './SpeciesTitleArea.scss';
const useSpecies = () => {
const activeGenomeId = useSelector(getActiveGenomeId) || '';
const popularSpecies = useSelector(getPopularSpecies);
const committedSpecies = useSelector((state: RootState) =>
getCommittedSpeciesById(state, activeGenomeId)
);
const dispatch = useDispatch();
useEffect(() => {
if (!popularSpecies.length) {
dispatch(fetchPopularSpecies());
}
}, []);
const iconUrl = popularSpecies.find(
(species) => species.genome_id === activeGenomeId
)?.image;
return committedSpecies && iconUrl
? {
species: committedSpecies,
iconUrl
}
: null;
};
const SpeciesTitleArea = () => {
const isSidebarOpen = useSelector(getSidebarStatus);
const { species, iconUrl } = useSpecies() || {};
const blockClasses = classNames(styles.speciesTitleArea, {
[styles.speciesTitleAreaNarrow]: isSidebarOpen
});
return species && iconUrl ? (
<div className={blockClasses}>
<div className={styles.speciesIcon}>
<InlineSVG src={iconUrl} />
</div>
<div className={styles.speciesNameWrapper}>
<h1 className={styles.speciesName}>{getDisplayName(species)}</h1>
<span className={styles.assemblyName}>{species.assembly_name}</span>
</div>
<div className={styles.speciesToggle}>
<SpeciesUsageToggle />
</div>
<div className={styles.speciesRemove}>
<SpeciesRemove />
</div>
</div>
) : (
<div className={blockClasses} />
);
};
export default SpeciesTitleArea;
@import 'src/styles/common';
// these two variables are used to emulate the unavailable flexbox-gap property
$fake-top-margin: 6px;
$fake-left-margin: 60px;
.speciesRemovalConfirmation {
/*
FIXME when Safari implements gap for flexbox
or when it becomes possible to test support for flexbox gap.
Will be something as simple as:
gap: 25px;
*/
margin-top: -#{$fake-top-margin};
margin-left: -#{$fake-left-margin};
& > * {
margin-top: $fake-top-margin;
margin-left: $fake-left-margin;
}
}
.speciesRemovalWarning {
display: inline-block;
color: $red;
line-height: 1.2;
}
.speciesRemovalConfirmationControls {
margin-left: $fake-left-margin;
display: inline-block;
button {
margin-right: 35px;
}
}
.clickable {
color: $blue;
cursor: pointer;
}
/**
* 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 { deleteSpeciesAndSave } from 'src/content/app/species-selector/state/speciesSelectorActions';
import { createSelectedSpecies } from 'tests/fixtures/selected-species';
import SpeciesRemove, { confirmationMessage } from './SpeciesRemove';
jest.mock('connected-react-router', () => ({
push: jest.fn(() => ({ type: 'push' }))
}));
jest.mock(
'src/content/app/species-selector/state/speciesSelectorActions',
() => ({
deleteSpeciesAndSave: jest.fn(() => ({ type: 'deleteSpeciesAndSave' }))
})
);
const selectedSpecies = createSelectedSpecies();
const mockState = {
speciesPage: {
general: {
activeGenomeId: selectedSpecies.genome_id
}
},
speciesSelector: {
committedItems: [selectedSpecies]
}
};
const mockStore = configureMockStore([thunk]);
const wrapInRedux = (state: typeof mockState = mockState) => {
return mount(
<Provider store={mockStore(state)}>
<SpeciesRemove />
</Provider>
);
};
describe('SpeciesSelectionControls', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('correctly toggles removal confirmation dialog', () => {
const wrapper = wrapInRedux();
const removeLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === 'Remove')
.first();
removeLabel.simulate('click');
expect(wrapper.text()).toContain(confirmationMessage);
const doNotRemoveLabel = wrapper
.find('span')
.filterWhere((wrapper) => wrapper.text() === 'Do not remove')
.first();
doNotRemoveLabel.simulate('click');
expect(wrapper.text()).not.toContain(confirmationMessage);
});
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 * 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 { deleteSpeciesAndSave } from 'src/content/app/species-selector/state/speciesSelectorActions';
import { PrimaryButton } from 'src/shared/components/button/Button';
import { RootState } from 'src/store';
import styles from './SpeciesRemove.scss';
const SpeciesRemove = () => {
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 toggleRemovalDialog = () => {
setIsRemoving(!isRemoving);
};
const onRemove = () => {
dispatch(push(urlFor.speciesSelector()));
dispatch(deleteSpeciesAndSave(genomeId));
};
return (
<div>
{isRemoving ? (
<div className={styles.speciesRemovalConfirmation}>
<span className={styles.speciesRemovalWarning}>
{confirmationMessage}
</span>
<div className={styles.speciesRemovalConfirmationControls}>
<PrimaryButton onClick={onRemove}>Remove</PrimaryButton>
<span className={styles.clickable} onClick={toggleRemovalDialog}>
Do not remove
</span>
</div>
</div>
) : (
<span className={styles.clickable} onClick={toggleRemovalDialog}>
Remove
</span>
)}
</div>
);
};
export const confirmationMessage =
'If you remove this species, any views you have configured will be lost — do you wish to continue?';
export default SpeciesRemove;
@import 'src/styles/common';
.speciesSelectionControls {
.speciesUsageToggle {
display: flex;
align-items: center;
font-size: 12px;
line-height: 1;
}
.speciesUseToggle {
display: flex;
align-items: center;
span {
white-space: nowrap;
......@@ -24,21 +18,7 @@
margin: 0 12px;
}
.removalContainer {
margin-left: 60px;
}
.clickable {
color: $blue;
cursor: pointer;
}
.speciesRemovalConfirmation {
button {
margin: 0 25px 0 35px;
}
}
.speciesRemovalWarning {
color: $red;
}
......@@ -17,31 +17,19 @@
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 { toggleSpeciesUseAndSave } from 'src/content/app/species-selector/state/speciesSelectorActions';
import { createSelectedSpecies } from 'tests/fixtures/selected-species';
import SpeciesSelectionControls, {
speciesRemovalConfirmationMessage
} from './SpeciesSelectionControls';
import SpeciesUsageToggle from './SpeciesUsageToggle';
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'
}))
......@@ -79,7 +67,7 @@ const wrapInRedux = (
) => {
return mount(
<Provider store={mockStore(state)}>
<SpeciesSelectionControls />
<SpeciesUsageToggle />
</Provider>
);
};
......@@ -166,43 +154,4 @@ describe('SpeciesSelectionControls', () => {