Skip to content

Commit

Permalink
Merge pull request #1153 from MetaPhase-Consulting/feature/new-result…
Browse files Browse the repository at this point in the history
…s-highlight

Highlight new search results from a saved search notification
  • Loading branch information
elizabeth-jimenez committed Oct 14, 2020
2 parents e42b04a + 0ae3b75 commit 88a2d4b
Show file tree
Hide file tree
Showing 11 changed files with 78 additions and 19 deletions.
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

0 comments on commit 88a2d4b

Please sign in to comment.