Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Highlight new search results from a saved search notification #1153

Merged
merged 3 commits into from
Oct 14, 2020
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
36 changes: 32 additions & 4 deletions src/Components/Notifications/NotificationRow/NotificationRow.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import FA from 'react-fontawesome';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { push } from 'connected-react-router';
import { formQueryString } from 'utilities';
import { EMPTY_FUNCTION } from '../../../Constants/PropTypes';
import { Column, Row } from '../../Layout';
import NotificationItem from '../../ProfileDashboard/Notifications/NotificationItem';
import LinkButton from '../../LinkButton';
import CheckBox from '../../CheckBox';

const NotificationRow = ({ id, message, tags, deleteOne, date, isRead, onCheck, checked }) => {
export const NotificationRow = ({ id, message, tags, deleteOne, date, isRead, onCheck, checked,
meta, onNavigateTo }) => {
let link;
let buttonTitle;
let buttonTitle2;
let icon = 'globe';
const tags$ = new Set(tags);
if (tags$.has('bidding')) {
Expand All @@ -21,16 +27,26 @@ const NotificationRow = ({ id, message, tags, deleteOne, date, isRead, onCheck,
link = '/profile/searches';
buttonTitle = 'Go to Saved Searches';
icon = 'clock-o';
if (meta.count && meta.search && meta.search.endpoint !== '/api/v1/fsbid/projected_vacancies/' && meta.search.endpoint !== '/api/v1/fsbid/projected_vacancies/tandem/') {
buttonTitle2 = 'View New Results';
}
}
const title = (
<div>
<div><FA name={icon} /> {message}</div>
</div>
);
const renderButton = () => !!link && !!title && <LinkButton toLink={link} className="usa-button">{buttonTitle}</LinkButton>;

const goToSavedSearch = () => {
const q = { ...meta.search.filters, ordering: '-posted_date', count: meta.count };
const stringifiedQuery = formQueryString(q);
onNavigateTo(`/results?${stringifiedQuery}`);
};

return (
<Row className={`usa-grid-full notification-row ${isRead ? 'notification-row--read' : ''}`}>
<Column columns={9} style={{ display: 'flex' }}>
<Column columns={8} style={{ display: 'flex' }}>
<CheckBox
_id={id}
id={`notification-checkbox-${id}`}
Expand All @@ -41,7 +57,8 @@ const NotificationRow = ({ id, message, tags, deleteOne, date, isRead, onCheck,
/>
<NotificationItem content={title} notificationTime={date} />
</Column>
<Column columns={3} className="notification-button">
<Column columns={4} className="notification-button">
{buttonTitle2 && <button className="usa-button" onClick={goToSavedSearch}>{buttonTitle2}</button>}
{renderButton()}
<button id="delete-notification-button" title="Delete this notification" onClick={() => deleteOne(id)} className="usa-button-secondary delete-button"><FA name="trash-o" /></button>
</Column>
Expand All @@ -58,6 +75,11 @@ NotificationRow.propTypes = {
isRead: PropTypes.bool,
onCheck: PropTypes.func,
checked: PropTypes.bool,
meta: PropTypes.shape({
count: PropTypes.number,
search: PropTypes.shape({ endpoint: PropTypes.string, filters: PropTypes.shape({}) }),
}),
onNavigateTo: PropTypes.func,
};

NotificationRow.defaultProps = {
Expand All @@ -67,6 +89,12 @@ NotificationRow.defaultProps = {
isRead: false,
onCheck: EMPTY_FUNCTION,
checked: false,
meta: {},
onNavigateTo: EMPTY_FUNCTION,
};

export default NotificationRow;
export const mapDispatchToProps = dispatch => ({
onNavigateTo: dest => dispatch(push(dest)),
});

export default connect(null, mapDispatchToProps)(withRouter(NotificationRow));
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { shallow } from 'enzyme';
import React from 'react';
import toJSON from 'enzyme-to-json';
import sinon from 'sinon';
import NotificationRow from './NotificationRow';
import { NotificationRow } from './NotificationRow';

describe('NotificationRowComponent', () => {
const props = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ exports[`NotificationRowComponent matches snapshot 1`] = `
<Column
as="div"
className=""
columns={9}
columns={8}
style={
Object {
"display": "flex",
Expand Down Expand Up @@ -49,7 +49,7 @@ exports[`NotificationRowComponent matches snapshot 1`] = `
<Column
as="div"
className="notification-button"
columns={3}
columns={4}
>
<LinkButton
className="usa-button"
Expand Down
1 change: 1 addition & 0 deletions src/Components/Notifications/Notifications.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const Notifications = ({ notifications, isLoading, hasErrored, deleteOne, page,
isRead={n.is_read}
onCheck={onCheck}
checked={checked}
meta={n.meta}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,12 @@ exports[`NotificationsComponent matches snapshot 1`] = `
className=""
columns={12}
>
<NotificationRow
checked={false}
date=""
<Connect(withRouter(NotificationRow))
deleteOne={[Function]}
id={1}
isRead={false}
key="1"
message="message"
onCheck={[Function]}
tags={Array []}
/>
</Column>
<Column
Expand Down
4 changes: 4 additions & 0 deletions src/Components/ResultsCard/ResultsCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class ResultsCard extends Component {
favoritesTandem,
favoritesPVTandem,
isGroupEnd,
isNew,
} = this.props;
const { isProjectedVacancy, isClient } = this.context;

Expand Down Expand Up @@ -205,6 +206,7 @@ class ResultsCard extends Component {
if (isTandem) cardClassArray.push('results-card--tandem');
if (isTandem2) cardClassArray.push('results-card--tandem-two');
if (isGroupEnd) cardClassArray.push('results-card--group-end');
if (isNew) cardClassArray.push('results-card--new');
const cardClass = cardClassArray.join(' ');

const headingTop =
Expand Down Expand Up @@ -359,6 +361,7 @@ ResultsCard.propTypes = {
favoritesTandem: FAVORITE_POSITIONS_ARRAY,
favoritesPVTandem: FAVORITE_POSITIONS_ARRAY,
isGroupEnd: PropTypes.bool,
isNew: PropTypes.bool,
};

ResultsCard.defaultProps = {
Expand All @@ -367,6 +370,7 @@ ResultsCard.defaultProps = {
favoritesTandem: [],
favoritesPVTandem: [],
isGroupEnd: false,
isNew: false,
};

export default ResultsCard;
5 changes: 4 additions & 1 deletion src/Components/ResultsList/ResultsList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ export const getIsGroupEnd = (results, i) => {
};

const ResultsList = ({ results, isLoading, favorites, favoritesPV,
favoritesTandem, favoritesPVTandem, bidList }, { isTandemSearch }) => {
favoritesTandem, favoritesPVTandem, bidList }, { isTandemSearch, newResultsCount }) => {
const mapResults = results.results || [];
return (
<div className={isLoading ? 'results-loading' : null}>
{ mapResults.map((result, i) => {
const key = shortid.generate();
const useGroupEnd = getIsGroupEnd(mapResults, i) && isTandemSearch;
const isNew = newResultsCount > i;
return (
<ResultsCard
id={key}
Expand All @@ -47,6 +48,7 @@ const ResultsList = ({ results, isLoading, favorites, favoritesPV,
result={result}
bidList={bidList}
isGroupEnd={useGroupEnd}
isNew={isNew}
/>
);
})}
Expand All @@ -56,6 +58,7 @@ const ResultsList = ({ results, isLoading, favorites, favoritesPV,

ResultsList.contextTypes = {
isTandemSearch: PropTypes.bool,
newResultsCount: PropTypes.number,
};

ResultsList.propTypes = {
Expand Down
24 changes: 21 additions & 3 deletions src/Containers/Results/Results.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { push } from 'connected-react-router';
import { withRouter } from 'react-router';
import queryString from 'query-string';
import { debounce, get, keys, isString, omit, pickBy, has } from 'lodash';
import { toastInfo } from 'actions/toast';
import queryParamUpdate from '../queryParams';
import { scrollToTop, cleanQueryParams, cleanTandemQueryParams, getAssetPath } from '../../utilities';
import { resultsFetchData } from '../../actions/results';
Expand Down Expand Up @@ -50,13 +51,15 @@ class Results extends Component {
getChildContext() {
const { tandem } = queryString.parse(get(this.state, 'query.value', ''));
const isTandemSearch = tandem === 'tandem';
const newResultsCount = this.getNewResultsCount();
return {
isTandemSearch,
isTandemSearch, newResultsCount,
};
}

UNSAFE_componentWillMount() {
const { isAuthorized, onNavigateTo } = this.props;
const { isAuthorized, onNavigateTo, showNewResultsToast } = this.props;
const { count } = queryString.parse(get(this.state, 'query.value', ''));
// store default search
this.storeSearch();
// check auth
Expand All @@ -66,6 +69,9 @@ class Results extends Component {
this.createQueryParams();
this.props.bidListFetchData();
}
if (count) {
showNewResultsToast(`There are ${count} new positions for your saved search. They have been automatically sorted by posted date.`, 'New Results');
}
}

UNSAFE_componentWillReceiveProps(nextProps) {
Expand Down Expand Up @@ -132,6 +138,12 @@ class Results extends Component {
}
};

getNewResultsCount = () => {
const q = queryString.parse(this.state.query.value);
const count = get(q, 'count');
return isNaN(count) ? 0 : +count;
}

// check if there are filters selected so that the clear filters button can be displayed or hidden
getQueryExists = () => {
const { query: { value } } = this.state;
Expand Down Expand Up @@ -205,10 +217,12 @@ class Results extends Component {
// updates the history by passing a string of query params
updateHistory(q) {
let q$ = q;
q$ = omit(queryString.parse(q$), ['count']);
q$ = queryString.stringify(q$);

// check if the keyword changed
if (this.resultsPageRef) {
const q$$ = this.getStringifiedQuery(q);
const q$$ = this.getStringifiedQuery(q$);
if (q$$) {
q$ = q$$;
}
Expand Down Expand Up @@ -308,6 +322,7 @@ Results.contextTypes = {

Results.childContextTypes = {
isTandemSearch: PropTypes.bool,
newResultsCount: PropTypes.number,
};

Results.propTypes = {
Expand Down Expand Up @@ -342,6 +357,7 @@ Results.propTypes = {
client: BIDDER_OBJECT,
clientIsLoading: PropTypes.bool,
clientHasErrored: PropTypes.bool,
showNewResultsToast: PropTypes.func,
};

Results.defaultProps = {
Expand All @@ -368,6 +384,7 @@ Results.defaultProps = {
client: {},
clientIsLoading: false,
clientHasErrored: false,
showNewResultsToast: EMPTY_FUNCTION,
};

const mapStateToProps = state => ({
Expand Down Expand Up @@ -405,6 +422,7 @@ export const mapDispatchToProps = dispatch => ({
toggleSearchBarVisibility: bool => dispatch(toggleSearchBar(bool)),
bidListFetchData: () => dispatch(bidListFetchData()),
storeSearch: obj => dispatch(storeCurrentSearch(obj)),
showNewResultsToast: (message, title) => dispatch(toastInfo(message, title)),
});

export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Results));
4 changes: 4 additions & 0 deletions src/sass/_notifications.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
border-bottom: 1px solid $color-gray;
}

.usa-button {
margin-bottom: 0;
}

.notification-button {
display: flex;
justify-content: flex-end;
Expand Down
4 changes: 4 additions & 0 deletions src/sass/_results.scss
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ $results-container-width: 100% - $filter-container-width;
}
}

&.results-card--new {
box-shadow: 0 0 15px $color-dodger-blue;
}

.tandem-identifier {
font-size: 1.5rem;
font-weight: 500;
Expand Down
7 changes: 4 additions & 3 deletions src/utilities.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Scroll from 'react-scroll';
import { distanceInWords, format } from 'date-fns';
import { cloneDeep, get, has, includes, intersection, isArray, isEmpty, isEqual, isFunction,
isNumber, isObject, isString, keys, lowerCase, merge as merge$, orderBy, padStart, pick, split,
startCase, take, toLower, toString, transform, uniqBy } from 'lodash';
isNumber, isObject, isString, keys, lowerCase, merge as merge$, omit, orderBy, padStart, pick,
split, startCase, take, toLower, toString, transform, uniqBy } from 'lodash';
import numeral from 'numeral';
import queryString from 'query-string';
import shortid from 'shortid';
Expand Down Expand Up @@ -268,7 +268,8 @@ export const existsInNestedObject = (ref, array, prop = 'position', nestedProp =
// we also want to get rid of page and limit,
// since those aren't valid params in the saved search endpoint
export const cleanQueryParams = (q) => {
const object = Object.assign({}, q);
let object = Object.assign({}, q);
object = omit(object, ['count']);
Object.keys(object).forEach((key) => {
if (VALID_PARAMS.indexOf(key) <= -1) {
delete object[key];
Expand Down