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

[CPCN-131] Feature(ui): Advanced search panel for Wire #375

Merged
merged 1 commit into from
May 23, 2023
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
18 changes: 18 additions & 0 deletions assets/search/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,24 @@ export function resetSearchParams() {
return {type: RESET_SEARCH_PARAMS};
}

export const TOGGLE_ADVANCED_SEARCH_FIELD = 'TOGGLE_ADVANCED_SEARCH_FIELD';
export function toggleAdvancedSearchField(field) {
return {type: TOGGLE_ADVANCED_SEARCH_FIELD, payload: field};
}

export const SET_ADVANCED_SEARCH_KEYWORDS = 'SET_ADVANCED_SEARCH_KEYWORDS';
export function setAdvancedSearchKeywords(field, keywords) {
return {type: SET_ADVANCED_SEARCH_KEYWORDS, payload: {
field: field,
keywords: keywords,
}};
}

export const CLEAR_ADVANCED_SEARCH_PARAMS = 'CLEAR_ADVANCED_SEARCH_PARAMS';
export function clearAdvanedSearchParams() {
return {type: CLEAR_ADVANCED_SEARCH_PARAMS};
}

export function setParams(params) {
return function(dispatch) {
if (get(params, 'created')) {
Expand Down
161 changes: 161 additions & 0 deletions assets/search/components/AdvancedSearchPanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';

import {gettext} from 'utils';
import {advancedSearchParamsSelector} from '../selectors';
import {toggleAdvancedSearchField, setAdvancedSearchKeywords, clearAdvanedSearchParams} from '../actions';

import CheckboxInput from 'components/CheckboxInput';
import InputWrapper from 'components/InputWrapper';

function AdvancedSearchPanelComponent({
params,
fetchItems,
toggleAdvancedSearchPanel,
toggleField,
setKeywords,
clearParams
}) {
return (
<div className="advanced-search__wrapper">
<div className="advanced-search__header">
<h3 className="a11y-only">{gettext('Advanced Search dialog')}</h3>
<nav className="content-bar navbar">
<h3>{gettext('Advanced Search')}</h3>
<div className="btn-group">
<div className="mx-2">
<button
className="icon-button icon-button icon-button--bordered"
aria-label={gettext('Close')}
onClick={toggleAdvancedSearchPanel}
>
<i className="icon--close-thin" />
</button>
</div>
</div>
</nav>
</div>
<div className="advanced-search__content-wrapper">
<div className="advanced-search__content">
<div className="advanced-search__content-block">
<p className="advanced-search__content-block-text advanced-search__content-block-text--bold">
{gettext('All of these:')}
</p>
<textarea
name="all"
placeholder={gettext('Add words here...')}
rows="2"
className="form-control"
value={params.all || ''}
onChange={(event) => setKeywords('all', event.target.value)}
autoFocus={true}
/>
<p className="advanced-search__content-description-text">
{gettext('Finds every item that contains keyword1 AND keyword2 etc.')}
</p>
</div>
<div className="advanced-search__content-block">
<p className="advanced-search__content-block-text advanced-search__content-block-text--bold">
{gettext('Any of these:')}
</p>
<textarea
name="any"
placeholder={gettext('Add words here...')}
rows="2"
className="form-control"
value={params.any || ''}
onChange={(event) => setKeywords('any', event.target.value)}
/>
<p className="advanced-search__content-description-text">
{gettext('And every item contain keyword1 OR keyword 2 etc.')}
</p>
</div>
<div className="advanced-search__content-block">
<p className="advanced-search__content-block-text advanced-search__content-block-text--bold">
{gettext('None of these:')}
</p>
<textarea
name="exclude"
placeholder={gettext('Add words here...')}
rows="2"
className="form-control"
value={params.exclude || ''}
onChange={(event) => setKeywords('exclude', event.target.value)}
/>
<p className="advanced-search__content-description-text">
{gettext('And every item will NOT contain keyword1 nor keyword2 etc.')}
</p>
</div>
<hr className="dashed" />
<div className="advanced-search__content-bottom">
<p>{gettext('Apply these keyword rules to these text fields:')}</p>
<InputWrapper>
<CheckboxInput
name="headline"
label={gettext('Headline')}
onChange={() => toggleField('headline')}
value={params.fields.includes('headline')}
/>
<CheckboxInput
name="slugline"
label={gettext('Slugline')}
onChange={() => toggleField('slugline')}
value={params.fields.includes('slugline')}
/>
<CheckboxInput
name="body_html"
label={gettext('Body')}
onChange={() => toggleField('body_html')}
value={params.fields.includes('body_html')}
/>
</InputWrapper>
</div>
</div>
</div>
<div className="advanced-search__footer">
<button
className="btn btn-outline-secondary"
onClick={clearParams}
>
{gettext('Clear all')}
</button>
<button
className="btn btn-primary"
onClick={() => {
fetchItems();
toggleAdvancedSearchPanel();
}}
>
{gettext('Search')}</button>
</div>
</div>
);
}

AdvancedSearchPanelComponent.propTypes = {
params: PropTypes.shape({
all: PropTypes.string,
any: PropTypes.string,
exclude: PropTypes.string,
fields: PropTypes.arrayOf(PropTypes.string)
}),
fetchItems: PropTypes.func,
toggleAdvancedSearchPanel: PropTypes.func,
toggleField: PropTypes.func,
setKeywords: PropTypes.func,
clearParams: PropTypes.func,
};

const mapStateToProps = (state) => ({
params: advancedSearchParamsSelector(state),
});

const mapDispatchToProps = (dispatch) => ({
toggleField: (field) => dispatch(toggleAdvancedSearchField(field)),
setKeywords: (field, keywords) => dispatch(setAdvancedSearchKeywords(field, keywords)),
clearParams: () => dispatch(clearAdvanedSearchParams()),
});


export const AdvancedSearchPanel = connect(mapStateToProps, mapDispatchToProps)(AdvancedSearchPanelComponent);
79 changes: 46 additions & 33 deletions assets/search/components/SearchBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,40 +50,52 @@ class SearchBar extends React.Component {

render() {
return (
<div className="search d-flex align-items-center">
<span className="search__icon">
<i className="icon--search icon--gray" />
</span>
<div className={classNames('search__form input-group', {
'searchForm--active': !!this.state.query,
})}>
<form className='d-flex align-items-center' role="search" aria-label={gettext('search')} onSubmit={this.onSubmit}>
<input
type='text'
name='q'
className='search__input form-control'
placeholder={gettext('Search for...')}
aria-label={gettext('Search for...')}
value={this.state.query || ''}
onChange={this.onChange}
onFocus={this.onFocus}
/>
<div className='search__form__buttons'>
<button
className='btn search__clear'
aria-label={gettext('Search clear')}
onClick={this.onClear}
type="reset"
>
<img src='/static/search_clear.png' width='16' height='16'/>
</button>
<button className='btn btn-outline-secondary' type='submit'>
{gettext('Search')}
</button>
</div>
</form>
<React.Fragment>
<div className="search d-flex align-items-center">
<span className="search__icon">
<i className="icon--search icon--gray" />
</span>
<div className={classNames('search__form input-group', {
'searchForm--active': !!this.state.query,
})}>
<form className='d-flex align-items-center' role="search" aria-label={gettext('search')} onSubmit={this.onSubmit}>
<input
type='text'
name='q'
className='search__input form-control'
placeholder={gettext('Search for...')}
aria-label={gettext('Search for...')}
value={this.state.query || ''}
onChange={this.onChange}
onFocus={this.onFocus}
/>
<div className='search__form__buttons'>
<button
className='btn search__clear'
aria-label={gettext('Search clear')}
onClick={this.onClear}
type="reset"
>
<img src='/static/search_clear.png' width='16' height='16'/>
</button>
<button className='btn btn-outline-secondary' type='submit'>
{gettext('Search')}
</button>
</div>
</form>
</div>
</div>
</div>
{this.props.toggleAdvancedSearchPanel == null ? this.props.toggleAdvancedSearchPanel : (
<div className="mx-2 d-flex gap-2">
<button
className="btn btn-primary"
onClick={this.props.toggleAdvancedSearchPanel}
>
{gettext('Advanced Search')}
</button>
</div>
)}
</React.Fragment>
);
}
}
Expand All @@ -93,6 +105,7 @@ SearchBar.propTypes = {
setQuery: PropTypes.func,
fetchItems: PropTypes.func,
enableQueryAction: PropTypes.bool,
toggleAdvancedSearchPanel: PropTypes.func,
};

const mapStateToProps = (state) => ({
Expand Down
47 changes: 47 additions & 0 deletions assets/search/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
SET_SEARCH_CREATED,
SET_SEARCH_PRODUCT,
RESET_SEARCH_PARAMS,
TOGGLE_ADVANCED_SEARCH_FIELD,
SET_ADVANCED_SEARCH_KEYWORDS,
CLEAR_ADVANCED_SEARCH_PARAMS,
} from './actions';

import {EXTENDED_VIEW} from 'wire/defaults';
Expand All @@ -32,6 +35,13 @@ const INITIAL_STATE = {
products: [],

activeView: EXTENDED_VIEW,

advanced: {
all: '',
any: '',
exclude: '',
fields: [],
},
};

export function searchReducer(state=INITIAL_STATE, action) {
Expand Down Expand Up @@ -142,6 +152,43 @@ export function searchReducer(state=INITIAL_STATE, action) {
activeFilter: INITIAL_STATE.activeFilter,
createdFilter: INITIAL_STATE.createdFilter,
productId: INITIAL_STATE.productId,
advanced: {
all: '',
any: '',
exclude: '',
fields: [],
},
};

case TOGGLE_ADVANCED_SEARCH_FIELD:
return {
...state,
advanced: {
...state.advanced,
fields: state.advanced.fields.includes(action.payload) ?
state.advanced.fields.filter((field) => field !== action.payload) :
[...state.advanced.fields, action.payload],
},
};

case SET_ADVANCED_SEARCH_KEYWORDS:
return {
...state,
advanced: {
...state.advanced,
[action.payload.field]: action.payload.keywords,
},
};

case CLEAR_ADVANCED_SEARCH_PARAMS:
return {
...state,
advanced: {
all: '',
any: '',
exclude: '',
fields: [],
},
};

default:
Expand Down
26 changes: 24 additions & 2 deletions assets/search/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ export const searchNavigationSelector = (state) => get(state, 'search.activeNavi
export const searchTopicIdSelector = (state) => get(state, 'search.activeTopic') || null;
export const searchProductSelector = (state) => get(state, 'search.productId') || null;

const DEFAULT_ADVANCED_SEARCH_PARAMS = {
all: '',
any: '',
exclude: '',
fields: [],
};
export const advancedSearchParamsSelector = (state) => get(state, 'search.advanced') || DEFAULT_ADVANCED_SEARCH_PARAMS;

export const activeViewSelector = (state) => get(state, 'search.activeView');
export const navigationsSelector = (state) => get(state, 'search.navigations') || [];

Expand Down Expand Up @@ -47,8 +55,8 @@ export const activeProductSelector = createSelector(
export const resultsFilteredSelector = (state) => state.resultsFiltered;

export const searchParamsSelector = createSelector(
[searchQuerySelector, searchCreatedSelector, searchNavigationSelector, searchFilterSelector, searchProductSelector],
(query, created, navigation, filter, product) => {
[searchQuerySelector, searchCreatedSelector, searchNavigationSelector, searchFilterSelector, searchProductSelector, advancedSearchParamsSelector],
(query, created, navigation, filter, product, advancedSearchParams) => {
const params = {};

if (!isEmpty(query)) {
Expand All @@ -67,6 +75,20 @@ export const searchParamsSelector = createSelector(
params.product = product;
}

if (advancedSearchParams.all || advancedSearchParams.any || advancedSearchParams.exclude) {
params.advancedSearch = {fields: advancedSearchParams.fields};

if (advancedSearchParams.all) {
params.advancedSearch.all = advancedSearchParams.all;
}
if (advancedSearchParams.any) {
params.advancedSearch.any = advancedSearchParams.any;
}
if (advancedSearchParams.exclude) {
params.advancedSearch.exclude = advancedSearchParams.exclude;
}
}

if (filter && Object.keys(filter).length > 0) {
params.filter = {};
Object.keys(filter).forEach((key) => {
Expand Down
Loading