Skip to content

Commit

Permalink
Merge pull request #499 from mlibrary/LIBSEARCH-801-part-4
Browse files Browse the repository at this point in the history
[LIBSEARCH-801] Implement "clear active filters" designs for advanced search (Part 4)
  • Loading branch information
erinesullivan authored Oct 29, 2024
2 parents e2b4cbf + defb099 commit bc34606
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 219 deletions.
95 changes: 0 additions & 95 deletions src/modules/advanced/components/AdvancedFilter/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Checkbox } from '../../../reusable';
import { DateRangeInput } from '../../../core';
import NarrowSearchTo from '../NarrowSearchTo';
import PropTypes from 'prop-types';
import React from 'react';
Expand All @@ -18,82 +17,6 @@ const getIsCheckboxFilterChecked = ({ advancedFilter }) => {
return false;
};

const getDateRangeValue = ({ beginDateQuery, endDateQuery, selectedRange }) => {
switch (selectedRange) {
case 'Before':
if (endDateQuery) {
return `before ${endDateQuery}`;
}
return null;
case 'After':
if (beginDateQuery) {
return `after ${beginDateQuery}`;
}
return null;
case 'Between':
if (beginDateQuery && endDateQuery) {
return `${beginDateQuery} to ${endDateQuery}`;
}
return null;
case 'In':
if (beginDateQuery) {
return beginDateQuery;
}
return null;
default:
return null;
}
};

const getStateDateRangeValues = ({ advancedFilter }) => {
if (advancedFilter.activeFilters?.length > 0) {
const [filterValue] = advancedFilter.activeFilters;

// Before
if (filterValue.indexOf('before') !== -1) {
const values = filterValue.split('before');

return {
stateEndQuery: values[1],
stateSelectedRangeOption: 0
};
}

// After
if (filterValue.indexOf('after') !== -1) {
const values = filterValue.split('after');

return {
stateBeginQuery: values[1],
stateSelectedRangeOption: 1
};
}

// Between
if (filterValue.indexOf('to') !== -1) {
const values = filterValue.split('to');

return {
stateBeginQuery: values[0],
stateEndQuery: values[1],
stateSelectedRangeOption: 2
};
}

// In or other
return {
stateBeginQuery: filterValue,
stateSelectedRangeOption: 3
};
}

return {
stateBeginQuery: '',
stateEndQuery: '',
stateSelectedRangeOption: 0
};
};

const AdvancedFilter = ({ advancedFilter, changeAdvancedFilter }) => {
if (advancedFilter.type === 'scope_down') {
return (
Expand Down Expand Up @@ -127,24 +50,6 @@ const AdvancedFilter = ({ advancedFilter, changeAdvancedFilter }) => {
/>
);
}
if (advancedFilter.type === 'date_range_input') {
const { stateSelectedRangeOption, stateBeginQuery, stateEndQuery } = getStateDateRangeValues({ advancedFilter });

return (
<DateRangeInput
selectedRangeOption={stateSelectedRangeOption}
beginQuery={stateBeginQuery}
endQuery={stateEndQuery}
handleSelection={({ beginDateQuery, endDateQuery, selectedRange }) => {
return changeAdvancedFilter({
filterGroupUid: advancedFilter.uid,
filterType: advancedFilter.type,
filterValue: getDateRangeValue({ beginDateQuery, endDateQuery, selectedRange })
});
}}
/>
);
}
return null;
};

Expand Down
25 changes: 19 additions & 6 deletions src/modules/advanced/components/FiltersContainer/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { AdvancedSearchSubmit, setAdvancedFilter } from '../../../advanced';
import { DateRangeInput, Multiselect } from '../../../core';
import { useDispatch, useSelector } from 'react-redux';
import ActiveAdvancedFilters from '../ActiveAdvancedFilters';
import AdvancedFilter from '../AdvancedFilter';
import getFilters from './getFilters';
import { Multiselect } from '../../../core';
import PropTypes from 'prop-types';
import React from 'react';

const FiltersContainer = ({ datastoreUid }) => {
const dispatch = useDispatch();
const { activeFilters, filters: filterGroups } = useSelector((state) => {
const { [datastoreUid]: urlFilters = {} } = useSelector((state) => {
return state.filters.active;
});
const { activeFilters = {}, filters: filterGroups } = useSelector((state) => {
return state.advanced[datastoreUid] || {};
});
const advancedDatastoreFilters = getFilters({ activeFilters, filterGroups });
Expand Down Expand Up @@ -43,7 +46,6 @@ const FiltersContainer = ({ datastoreUid }) => {
}));
break;
case 'checkbox':
case 'date_range_input':
dispatch(setAdvancedFilter({
datastoreUid,
filterGroupUid,
Expand Down Expand Up @@ -74,13 +76,24 @@ const FiltersContainer = ({ datastoreUid }) => {
? (
advancedDatastoreFilters[filterGroup].map((advancedFilter, index) => {
const { filters, name, type, uid } = advancedFilter;
const currentAdvancedFilters = activeFilters[uid] || [];
const currentURLFilters = urlFilters[uid] || [];
// Make sure the URL filters and the advanced filters match on load
const currentFilters = [
...currentURLFilters.filter((currentURLFilter) => {
return !currentAdvancedFilters.includes(currentURLFilter);
}),
...currentAdvancedFilters.filter((currentAdvancedFilter) => {
return !currentURLFilters.includes(currentAdvancedFilter);
})
];
return (
<div key={index} className='advanced-filter-container'>
<h2 className='advanced-filter-label-text'>{name}</h2>
<div className='advanced-filter-inner-container'>
{type === 'multiple_select'
? <Multiselect {...{ datastoreUid, filterGroupUid: uid, filters, name }} />
: <AdvancedFilter {...{ advancedFilter, changeAdvancedFilter }} />}
{type === 'multiple_select' && <Multiselect {...{ currentFilters, datastoreUid, filterGroupUid: uid, filters, name }} />}
{type === 'date_range_input' && <DateRangeInput {...{ currentFilter: currentURLFilters[0], datastoreUid, filterGroupUid: uid }} />}
{!['multiple_select', 'date_range_input'].includes(type) && <AdvancedFilter {...{ advancedFilter, changeAdvancedFilter }} />}
</div>
</div>
);
Expand Down
170 changes: 95 additions & 75 deletions src/modules/core/components/DateRangeInput/index.js
Original file line number Diff line number Diff line change
@@ -1,114 +1,134 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { setAdvancedFilter } from '../../../advanced';
import { useDispatch } from 'react-redux';

const YearInput = ({ point = 'start', query, setQuery }) => {
return (
<div>
<label htmlFor={`date-range-${point}-date`}>{point.charAt(0).toUpperCase() + point.slice(1)} date</label>
<input
className='date-range-input-text'
id={`date-range-${point}-date`}
aria-describedby={`date-range-${point}-date-description`}
type='text'
value={query}
onChange={setQuery}
autoComplete='on'
pattern='[0-9]{4}'
/>
<small id={`date-range-${point}-date-description`}>Please enter this format: YYYY</small>
</div>
);
const options = ['before', 'after', 'between', 'in'];

const extractYears = (dateString) => {
return dateString.match(/\d+/gu) || [''];
};

YearInput.propTypes = {
point: PropTypes.string,
query: PropTypes.string,
setQuery: PropTypes.func
const extractRange = (dateString) => {
const years = extractYears(dateString);
if (!years[0]) {
return 'before';
}
if (years.length > 1) {
return 'between';
}
return ['before', 'after'].find((prefix) => {
return dateString.startsWith(prefix);
}) || 'in';
};

const dateRangeOptions = ['Before', 'After', 'Between', 'In'];
const minValue = 1000;
const maxValue = new Date().getFullYear();
const minValues = [minValue, minValue + 1];
const maxValues = [maxValue - 1, maxValue];

const DateRangeInput = ({ beginQuery, endQuery, selectedRangeOption, handleSelection }) => {
const [beginQueryState, setBeginQuery] = useState(beginQuery || '');
const [endQueryState, setEndQuery] = useState(endQuery || '');
const [selectedRangeOptionState, setSelectedRangeOption] = useState(selectedRangeOption || 0);
const DateRangeInput = ({ currentFilter = '', datastoreUid, filterGroupUid }) => {
const dispatch = useDispatch();
const [range, setRange] = useState(extractRange(currentFilter));
const [years, setYears] = useState(extractYears(currentFilter));
const [min, setMin] = useState(minValues);
const [max, setMax] = useState(maxValues);

const handleStateChange = (beginQueryVal, endQueryVal, selectedRange) => {
handleSelection({
beginDateQuery: beginQueryVal,
endDateQuery: endQueryVal,
selectedRange
});
};
const updateFilter = useCallback((filterValue) => {
dispatch(setAdvancedFilter({ datastoreUid, filterGroupUid, filterValue, onlyOneFilterValue: true }));
}, [dispatch, datastoreUid, filterGroupUid]);

useEffect(() => {
const selectedRange = dateRangeOptions[selectedRangeOptionState];
handleStateChange(beginQueryState, endQueryState, selectedRange);
}, [beginQueryState, endQueryState, selectedRangeOptionState]);

const handleBeginQueryChange = (query) => {
setBeginQuery(query);
};
updateFilter(currentFilter);
}, [currentFilter, updateFilter]);

const handleEndQueryChange = (query) => {
setEndQuery(query);
};
useEffect(() => {
let filterValue = '';
if (years.some(Boolean)) {
if (range === 'between') {
filterValue = years.filter(Number).join(' to ');
} else {
filterValue = ['before', 'after'].includes(range) ? `${range} ${years[0]}` : years[0];
}
}
updateFilter(filterValue);
}, [range, years, updateFilter]);

const rangeOption = dateRangeOptions[selectedRangeOptionState];
const handleYearChange = useCallback((index, value) => {
if (range === 'between') {
const newYears = [...years];
newYears[index] = value;
const newMin = [...min];
const newMax = [...max];
if (index === 0) {
newYears[1] = value ? newYears[1] : '';
newMin[1] = value && value < maxValues[1] - 1 ? Math.max(minValues[1], Number(value) + 1) : minValues[1];
} else {
newMax[0] = value && value > minValues[0] + 1 ? Math.min(maxValues[0], Number(value) - 1) : maxValues[0];
}
setYears(newYears);
setMin(newMin);
setMax(newMax);
} else {
setYears([value]);
}
}, [range, years, min, max]);

return (
<div className='date-range-input'>
<fieldset className='flex__responsive'>
<legend className='visually-hidden'>Select the type of date range to search on</legend>
{dateRangeOptions.map((option, index) => {
{options.map((option, index) => {
return (
<label key={index}>
<input
type='radio'
name='date-range-input'
value={option}
checked={selectedRangeOptionState === index}
checked={option === range}
onChange={() => {
return setSelectedRangeOption(index);
return setRange(option);
}}
/>
{option}
{option.charAt(0).toUpperCase() + option.slice(1)}
</label>
);
})}
</fieldset>
<div className='date-range-container'>
{
rangeOption !== 'Before' && (
<YearInput
query={beginQueryState}
setQuery={(event) => {
return handleBeginQueryChange(event.target.value);
}}
/>
)
}
{
['Before', 'Between'].includes(rangeOption) && (
<YearInput
query={endQueryState}
setQuery={(event) => {
return handleEndQueryChange(event.target.value);
}}
point='end'
/>
)
}
<div className='flex__responsive margin-top__xs'>
{Array(range === 'between' ? 2 : 1).fill('').map((input, index) => {
const label = (index === 1 || range === 'before') ? 'End' : 'Start';
const id = `date-range-${label.toLowerCase()}-date`;
return (
<div key={index}>
<label htmlFor={id}>{label} date</label>
<input
className='date-range-input-number'
id={id}
aria-describedby={`${id}-description`}
type='number'
value={years[index] || ''}
disabled={index > 0 && !years[index - 1]}
min={range === 'between' ? min[index] : minValue}
max={range === 'between' ? max[index] : maxValue}
onChange={(event) => {
return handleYearChange(index, event.target.value);
}}
autoComplete='on'
/>
<small id={`${id}-description`}>Please enter this format: YYYY</small>
</div>
);
})}
</div>
</div>
);
};

DateRangeInput.propTypes = {
beginQuery: PropTypes.string,
endQuery: PropTypes.string,
handleSelection: PropTypes.func,
selectedRangeOption: PropTypes.number
currentFilter: PropTypes.string,
datastoreUid: PropTypes.string.isRequired,
filterGroupUid: PropTypes.string.isRequired
};

export default DateRangeInput;
Loading

0 comments on commit bc34606

Please sign in to comment.