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

[LIBSEARCH-801] Implement "clear active filters" designs for advanced search (Part 4) #499

Merged
merged 9 commits into from
Oct 29, 2024
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