Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ This issue has been fixed by adding additional requests to delete the appropriat
* [OSDEV-1482](https://opensupplyhub.atlassian.net/browse/OSDEV-1482) - The `GET api/v1/moderation-events/{moderation_id}` endpoint returns a single response instead of an array containing one item.

### What's new
* [OSDEV-1373](https://opensupplyhub.atlassian.net/browse/OSDEV-1373) - The tab `Search by Name and Address.` on the Production Location Search screen has been implemented. There are three required properties (name, address, country). The "Search" button becomes clickable after filling out inputs, creates a link with parameters, and allows users to proceed to the results screen.
* [OSDEV-1175](https://opensupplyhub.atlassian.net/browse/OSDEV-1175) - New Moderation Queue Page was integrated with `GET api/v1/moderation-events/` endpoint that include pagination, sorting and filtering.

### Release instructions:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import renderWithProviders from '../../util/testUtils/renderWithProviders';
jest.mock('../../components/Contribute/SearchByOsIdTab', () => () => <div>Mocked SearchByOsIdTab</div>);

describe('ContributeProductionLocation component', () => {
const renderComponent = (initialEntries = ['/']) =>
const renderComponent = (initialEntries = ['/']) =>
renderWithProviders(
<MemoryRouter initialEntries={initialEntries}>
<ContributeProductionLocation />
Expand All @@ -34,23 +34,23 @@ describe('ContributeProductionLocation component', () => {
it('changes the tab when clicked and updates the URL', () => {
const { getByRole, getByText } = renderComponent();

const nameAddressTab = getByRole('tab', { name: /Search by Name and Address/i });
fireEvent.click(nameAddressTab);
const osIdTab = getByRole('tab', { name: /Search by OS ID/i });
fireEvent.click(osIdTab);

expect(nameAddressTab).toHaveAttribute('aria-selected', 'true');
expect(getByRole('tab', { name: /Search by OS ID/i })).toHaveAttribute('aria-selected', 'false');
expect(getByText('Search by Name and Address Tab')).toBeInTheDocument();
expect(osIdTab).toHaveAttribute('aria-selected', 'true');
expect(getByRole('tab', { name: /Search by Name and Address/i })).toHaveAttribute('aria-selected', 'false');
expect(getByText('Search by OS ID')).toBeInTheDocument();
});

it('renders SearchByOsIdTab when OS ID tab is selected', () => {
const { getByText } = renderComponent();
expect(getByText('Mocked SearchByOsIdTab')).toBeInTheDocument();
});
});

it('handles invalid tab and defaults to OS ID tab', () => {
const { getByRole } = renderComponent(['contribute/production-location?tab=invalid-tab']);
const osIdTab = getByRole('tab', { name: /Search by OS ID/i });

expect(osIdTab).toHaveAttribute('aria-selected', 'true');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
import SearchByOsIdTab from './SearchByOsIdTab';
import SearchByNameAndAddressTab from './SearchByNameAndAddressTab';
import { makeContributeProductionLocationStyles } from '../../util/styles';

const TAB_OS_ID = 'os-id';
Expand Down Expand Up @@ -55,22 +56,22 @@ const ContributeProductionLocation = ({ classes }) => {
selected: classes.tabSelectedStyles,
labelContainer: classes.tabLabelContainerStyles,
}}
label="Search by OS ID"
value={TAB_OS_ID}
label="Search by Name and Address"
value={TAB_NAME_ADDRESS}
/>
<Tab
classes={{
root: classes.tabRootStyles,
selected: classes.tabSelectedStyles,
labelContainer: classes.tabLabelContainerStyles,
}}
label="Search by Name and Address"
value={TAB_NAME_ADDRESS}
label="Search by OS ID"
value={TAB_OS_ID}
/>
</Tabs>
{selectedTab === TAB_OS_ID && <SearchByOsIdTab />}
{selectedTab === TAB_NAME_ADDRESS && (
<div>Search by Name and Address Tab</div>
<SearchByNameAndAddressTab />
)}
</div>
</div>
Expand Down
242 changes: 242 additions & 0 deletions src/react/src/components/Contribute/SearchByNameAndAddressTab.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import React, { useState, useEffect } from 'react';
import { bool, string, func, object } from 'prop-types';
import { useHistory } from 'react-router-dom';
import { withStyles } from '@material-ui/core/styles';
import { connect } from 'react-redux';
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';
import CircularProgress from '@material-ui/core/CircularProgress';
import StyledSelect from '../Filters/StyledSelect';

import { makeSearchByNameAddressTabStyles } from '../../util/styles';

import { countryOptionsPropType } from '../../util/propTypes';
import { fetchCountryOptions } from '../../actions/filterOptions';

const InputHelperText = ({ classes }) => (
<span className={classes.helperTextWrapStyles}>
<InfoOutlinedIcon className={classes.iconInfoStyles} />
<Typography component="span" className={classes.inputHelperTextStyles}>
This field is required.
</Typography>
</span>
);

const defaultCountryOption = {
label: "What's the country?",
value: '',
};

const selectStyles = {
control: provided => ({
...provided,
height: '56px',
}),
};

const SearchByNameAndAddressTab = ({
classes,
countriesData,
fetchCountries,
fetching,
error,
}) => {
const [inputName, setInputName] = useState('');
const [inputAddress, setInputAddress] = useState('');
const [inputCountry, setInputCountry] = useState(defaultCountryOption);
const [nameTouched, setNameTouched] = useState(false);
const [addressTouched, setAddressTouched] = useState(false);

const history = useHistory();
const validate = val => val.length > 0;
const handleNameChange = event => {
setNameTouched(true);
setInputName(event.target.value);
};
const handleAddressChange = event => {
setAddressTouched(true);
setInputAddress(event.target.value);
};
const handleCountryChange = event => {
setInputCountry(event || defaultCountryOption);
};

const handleSearch = () => {
const baseUrl = '/contribute/production-location/search/';
const params = new URLSearchParams({
name: inputName,
address: inputAddress,
country: inputCountry.value ?? '',
});
const url = `${baseUrl}?${params.toString()}`;

history.push(url);
};
const isFormValid =
validate(inputName) &&
validate(inputAddress) &&
validate(inputCountry.value);

useEffect(() => {
if (!countriesData) {
fetchCountries();
}
}, [countriesData, fetchCountries]);

if (fetching) {
return <CircularProgress />;
}

if (error) {
return (
<Typography variant="body2" className={classes.errorStyle}>
{error}
</Typography>
);
}

return (
<>
<Typography className={classes.instructionStyles}>
Check if the production location is already on OS Hub. Enter the
production location’s name, address and country in the fields
below and click “Search”.
</Typography>
<Paper className={classes.searchWrapStyles}>
<Typography component="h2" className={classes.titleStyles}>
Production Location Details
</Typography>
<Typography component="h4" className={classes.subTitleStyles}>
Enter the Name
</Typography>
<TextField
id="name"
className={classes.textInputStyles}
value={inputName}
onChange={handleNameChange}
placeholder="Type a name"
variant="outlined"
aria-label="Type a name"
InputProps={{
classes: {
input: `${classes.searchInputStyles}
${
nameTouched &&
!validate(inputName) &&
classes.errorStyle
}`,
notchedOutline: classes.notchedOutlineStyles,
},
inputProps: {
type: 'text',
},
}}
helperText={
nameTouched &&
!validate(inputName) && (
<InputHelperText classes={classes} />
)
}
error={nameTouched && !validate(inputName)}
/>
<Typography component="h4" className={classes.subTitleStyles}>
Enter the Address
</Typography>
<TextField
id="address"
className={classes.textInputStyles}
value={inputAddress}
onChange={handleAddressChange}
placeholder="Address"
variant="outlined"
aria-label="Address"
InputProps={{
classes: {
input: `${classes.searchInputStyles}
${
addressTouched &&
!validate(inputAddress) &&
classes.errorStyle
}`,
notchedOutline: classes.notchedOutlineStyles,
},
inputProps: {
type: 'text',
},
}}
helperText={
addressTouched &&
!validate(inputAddress) && (
<InputHelperText classes={classes} />
)
}
error={addressTouched && !validate(inputAddress)}
/>
<Typography component="h4" className={classes.subTitleStyles}>
Select the Country
</Typography>
<StyledSelect
id="countries"
name="What's the country?"
aria-label="Select country"
label={null}
options={countriesData || []}
value={inputCountry}
onChange={handleCountryChange}
className={`basic-multi-select notranslate ${classes.selectStyles}`}
styles={selectStyles}
placeholder="What's the country?"
isMulti={false}
/>

<Button
color="secondary"
variant="contained"
onClick={handleSearch}
className={classes.searchButtonStyles}
classes={{
label: classes.buttonLabel,
}}
disabled={!isFormValid}
>
Search
</Button>
</Paper>
</>
);
};

SearchByNameAndAddressTab.defaultProps = {
countriesData: null,
error: null,
};

SearchByNameAndAddressTab.propTypes = {
countriesData: countryOptionsPropType,
fetching: bool.isRequired,
error: string,
fetchCountries: func.isRequired,
classes: object.isRequired,
};

const mapStateToProps = ({
filterOptions: {
countries: { data: countriesData, error, fetching },
},
}) => ({
countriesData,
fetching,
error,
});

const mapDispatchToProps = dispatch => ({
fetchCountries: () => dispatch(fetchCountryOptions()),
});

export default connect(
mapStateToProps,
mapDispatchToProps,
)(withStyles(makeSearchByNameAddressTabStyles)(SearchByNameAndAddressTab));
19 changes: 11 additions & 8 deletions src/react/src/components/Filters/StyledSelect.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ function StyledSelect({
);
return (
<>
<InputLabel
shrink={false}
htmlFor={name}
className={classes.inputLabelStyle}
>
{label} {renderIcon()}
</InputLabel>
{label && (
<InputLabel
shrink={false}
htmlFor={name}
className={classes.inputLabelStyle}
>
{label} {renderIcon()}
</InputLabel>
)}
{(() => {
if (creatable)
return (
Expand Down Expand Up @@ -97,11 +99,12 @@ StyledSelect.defaultProps = {
creatable: false,
renderIcon: () => {},
origin: null,
label: '',
};

StyledSelect.propTypes = {
name: string.isRequired,
label: string.isRequired,
label: string,
creatable: bool,
renderIcon: func,
origin: string,
Expand Down
3 changes: 2 additions & 1 deletion src/react/src/util/COLOURS.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ export default {

// Reds
LIGHT_RED: '#FFEAEA',
RED: '#F44336',

// Grays and Neutrals
WHITE: '#fff',
WHITE: '#FFF',
GREY: '#D2D2D2',
LIGHT_GREY: '#F9F7F7',
DARK_GREY: '#6E707E',
Expand Down
Loading